Tiled Parser and Scene & Palette Management in Studio
@ -1,19 +1,19 @@
|
||||
{"type":"meta","next_id":{"DSC":30,"AGD":32,"DEC":28,"PLN":56,"LSN":42,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":30,"AGD":32,"DEC":29,"PLN":59,"LSN":46,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||
{"type":"discussion","id":"DSC-0028","status":"open","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-17","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[{"id":"AGD-0030","file":"AGD-0030-tiled-parser-and-scene-asset-type.md","status":"accepted","created_at":"2026-04-15","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0027","file":"DEC-0027-tiled-parser-and-scene-bank-asset-ownership.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0030"}],"plans":[{"id":"PLN-0053","file":"PLN-0053-scene-bank-asset-contract-and-studio-metadata-foundations.md","status":"open","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0027"]},{"id":"PLN-0054","file":"PLN-0054-tiled-xml-io-and-scene-bank-file-orchestration.md","status":"open","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0027"]},{"id":"PLN-0055","file":"PLN-0055-assets-workspace-scene-bank-validation-and-acceptance-flow.md","status":"open","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0027"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]}
|
||||
{"type":"discussion","id":"DSC-0027","status":"abandoned","ticket":"studio-scene-workspace","title":"Scene Workspace for SCENE Authoring","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["studio","workspace","scene","tilemap","asset","runtime-alignment"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted agenda and its downstream work."}],"decisions":[{"id":"DEC-0026","file":"DEC-0026-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0029","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted decision and stop using it as normative guidance."}],"plans":[{"id":"PLN-0049","file":"PLN-0049-scene-workspace-spec-and-boundary-propagation.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0050","file":"PLN-0050-scene-workspace-shell-and-project-state-foundations.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0051","file":"PLN-0051-scene-artifact-and-assets-handoff-contract.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0052","file":"PLN-0052-scene-workspace-wave-1-tilemap-editor.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0026","status":"done","ticket":"glyph-bank-naming-alignment-with-runtime","title":"Glyph Bank Naming Alignment with Runtime DEC-0006","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["packer","studio","naming","asset-contract","runtime-alignment","glyph-bank"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0026-glyph-bank-naming-alignment-with-runtime/LSN-0040-glyph-bank-artifact-naming-alignment.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||
{"type":"discussion","id":"DSC-0025","status":"done","ticket":"packer-pipeline-metadata-ownership","title":"Pipeline Metadata Ownership and Runtime Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["packer","metadata","runtime-contract","tooling","studio"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0025-packer-pipeline-metadata-ownership/LSN-0039-runtime-header-boundary-and-tooling-owned-pipeline-metadata.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"jacoco-reports-consolidation","title":"JaCoCo Reports Consolidation in Gradle","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["infra","gradle","jacoco","coverage","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0038-jacoco-reports-consolidation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"jacoco-reports-consolidation","title":"JaCoCo Reports Consolidation in Gradle","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["infra","gradle","jacoco","coverage","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0038-jacoco-reports-consolidation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"},{"id":"LSN-0044","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0044-jenkins-jacoco-alignment.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0002","status":"done","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-04-23","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0002-palette-management-in-studio/LSN-0042-schema-driven-palette-authoring-and-local-metadata-events.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]}
|
||||
{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0006","status":"done","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-30","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0006-pbs-game-facing-asset-refs-and-call-result-discard/LSN-0024-addressable-surface-host-metadata-and-ignored-value-discipline.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]}
|
||||
{"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||
{"type":"discussion","id":"DSC-0009","status":"done","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-04-07","tags":["studio","debugger","workspace","integration","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0022-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0009","status":"done","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-04-07","tags":["studio","debugger","workspace","integration","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
||||
{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]}
|
||||
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
||||
@ -26,5 +26,5 @@
|
||||
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"studio-project-local-setup-separate-from-main-studio-state","title":"Separate project-local setup from the main Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","setup","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"discussion/lessons/DSC-0019-studio-project-local-setup-separate-from-main-studio-state/LSN-0032-separate-project-config-from-session-restoration-state.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
||||
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"studio-editor-indentation-policy-and-project-setup","title":"Indentation Policy, Status-Bar Semantics, and Project-Local Editor Setup","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","indentation","tabs","setup","dot-studio","configuration","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
||||
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"studio-frontend-editor-write-and-save-wave","title":"Enable Frontend Editing and Save in the Studio Code Editor","created_at":"2026-04-04","updated_at":"2026-04-06","tags":["studio","editor","frontend","write","save","vfs","lsp","pbs","access-policy"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]}
|
||||
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"studio-play-stop-cartridge-flow","title":"Wire the Studio Play/Stop button to shipper phase 1 cartridge preparation and runtime execution","created_at":"2026-04-06","updated_at":"2026-04-07","tags":["studio","play-stop","shipper","runtime","cartridge","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0022-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"studio-play-stop-cartridge-flow","title":"Wire the Studio Play/Stop button to shipper phase 1 cartridge preparation and runtime execution","created_at":"2026-04-06","updated_at":"2026-04-07","tags":["studio","play-stop","shipper","runtime","cartridge","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0043-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"gradle-version-catalog-migration","title":"Move dependency version management to Version Catalog (TOML)","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["gradle","buildSrc","dependency-management","version-catalog"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0023-gradle-version-catalog-migration/LSN-0037-gradle-version-catalog-adoption.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
---
|
||||
id: LSN-0042
|
||||
ticket: palette-management-in-studio
|
||||
title: Schema-Driven Palette Authoring and Local Metadata Events
|
||||
created: 2026-04-23
|
||||
tags:
|
||||
- studio
|
||||
- glyph-bank
|
||||
- palette-management
|
||||
- color-schema
|
||||
- local-events
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The glyph-bank palette workflow originally treated palette candidates as extracted tile artifacts. That made grouping, recoloring, and final bank palette selection compete in the same surface.
|
||||
|
||||
The completed palette-management work split the workflow into two authored layers:
|
||||
|
||||
1. `Color Schema Groups`, where selected tiles are grouped into a shared index structure.
|
||||
2. `Palette Overhauling`, where palettes are authored and selected against already-ready schemas.
|
||||
|
||||
The durable implementation lesson is that schema edits, palette edits, and build selection are all local glyph-material metadata operations first. They should update local metadata projections directly instead of forcing a full asset-details reload.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Schema Owns Index Semantics
|
||||
|
||||
**What:** A schema is the authored index structure for a group of selected glyph-bank tiles. It owns `material_indices`, artifact bindings, fingerprint snapshots, version, and status.
|
||||
|
||||
**Why:** The source PNG stays unchanged, while Studio records how tile colors map into stable bank-local indices. This keeps tile grouping separate from recoloring.
|
||||
|
||||
**Trade-offs:** Schema invalidation must be explicit. When selected tile fingerprints drift, the schema can become inconsistent and dependent palettes become invalid or stale.
|
||||
|
||||
### Palettes Are Schema-Scoped
|
||||
|
||||
**What:** A palette always belongs to exactly one schema. It stores concrete `#RRGGBB` colors for that schema's visible indices and carries the schema version it was authored against.
|
||||
|
||||
**Why:** Multiple recolors can safely exist for one index structure without duplicating tile grouping metadata.
|
||||
|
||||
**Trade-offs:** Duplicate detection must use the tuple `schemaId + colors + order`, not display name or source tile identity.
|
||||
|
||||
### Original Palettes Are Synthetic and Read-Only
|
||||
|
||||
**What:** The original palette is the synthetic `schema-<schemaId>-original` projection derived from the current schema colors. It is not a persisted user palette.
|
||||
|
||||
**Why:** Updating the schema should update the original palette automatically. The original represents the schema's source color meaning, not a user-authored recolor.
|
||||
|
||||
**Trade-offs:** Users cannot edit or delete the original palette directly. If they want a changed palette, they create or edit a separate persisted palette for the same schema.
|
||||
|
||||
### Local Metadata Events Beat Global Reloads
|
||||
|
||||
**What:** Schema and palette metadata mutations should publish `StudioGlyphMaterialMetadataChangedEvent` and let interested sections rebind local state.
|
||||
|
||||
**Why:** Global asset refresh and selection events can close edit mode, reset form state, and make small metadata edits feel destructive.
|
||||
|
||||
**Trade-offs:** Controls must keep their local coordinators robust: stale button handlers should read the current view model at click time, and metadata replacement must preserve edit-mode drafts where possible.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
### `glyph-material.json`
|
||||
|
||||
`glyph-material.json` is the Studio-owned support file for glyph-bank material authoring. It persists schemas and user-authored palettes, while derived original palettes remain a view projection.
|
||||
|
||||
Schema records store:
|
||||
|
||||
- stable `schema_id`
|
||||
- `status`
|
||||
- `version`
|
||||
- dense visible `material_indices`
|
||||
- artifact bindings with fingerprint snapshots
|
||||
|
||||
Palette records store:
|
||||
|
||||
- stable `palette_id`
|
||||
- persisted `name`
|
||||
- parent `schema_id`
|
||||
- schema `version`
|
||||
- concrete `colors`
|
||||
|
||||
### Palette Names
|
||||
|
||||
Palette names are persisted even when the user leaves the name field blank. The blank-name path resolves to a deterministic, simple generated name such as `snow white`, `fog twist`, or `ember bloom`.
|
||||
|
||||
The generated name is a convenience label only. Palette identity remains `palette_id`, and duplicate detection remains schema plus ordered colors.
|
||||
|
||||
### Original Palette Projection
|
||||
|
||||
Every ready schema projects a synthetic original palette from the schema's current `material_indices`.
|
||||
|
||||
This projection must always exist in the palette view, even if a user-authored palette has the same colors. A saved palette with the same schema and colors is still user-authored and can be deleted.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not treat same colors as proof that a palette is original. Original means "synthetic schema projection", not "matches current schema colors".
|
||||
- Do not publish global asset refresh events for local schema or palette metadata changes. Use local metadata events unless pack/runtime state truly changed.
|
||||
- Do not let button handlers capture stale preview objects. Fetch the current coordinator view model at click time for destructive actions.
|
||||
- Do not let blank palette names remain blank on disk. Resolve the generated name before persisting.
|
||||
- Do not open the palette wizard for synthetic original palettes. They are read-only projections.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Schema formation and palette authoring are separate lifecycle stages.
|
||||
- Original palettes are derived views, not persisted editable records.
|
||||
- User-created palettes remain editable and removable even when they share a schema or colors with an original.
|
||||
- Local metadata events preserve form continuity better than full asset refreshes.
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
id: LSN-0022
|
||||
id: LSN-0043
|
||||
ticket: studio-play-stop-cartridge-flow
|
||||
title: Studio Play/Stop and Debugger Workspace Integration
|
||||
status: done
|
||||
@ -1,16 +0,0 @@
|
||||
# LSN-0039: Jenkins + JaCoCo Integration with Multi-Module Gradle
|
||||
|
||||
- **Problem:** O Jenkinsfile estava desatualizado, tentando rodar uma task de cobertura inexistente (`testCodeCoverageReport`) em um subprojeto específico, ignorando a cobertura global e causando falhas no pipeline.
|
||||
- **Decision:** Alinhar o `Jenkinsfile` com a task de agregação customizada no root project (`jacocoTestReport`), removendo a dependência de subprojetos específicos para métricas globais.
|
||||
- **Implementation:**
|
||||
- Atualizado o comando de execução no Jenkins: `./gradlew clean test jacocoTestReport`.
|
||||
- Ajustado o `recordCoverage` para ler o XML consolidado em `build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml`.
|
||||
- Ajustado o `publishHTML` para exibir o relatório consolidado em `build/reports/jacoco/jacocoTestReport/html`.
|
||||
- **Example:**
|
||||
- Antes: `SRS_APP/build/reports/...` (específico por app).
|
||||
- Depois: `build/reports/...` (global do projeto).
|
||||
- **Pitfalls:**
|
||||
- Ao usar relatórios consolidados no root, deve-se garantir que o Gradle rode a task de agregação *após* os testes de todos os subprojetos.
|
||||
- O Jenkins precisa de acesso ao workspace root para encontrar esses arquivos; caminhos relativos no `Jenkinsfile` geralmente partem do root do checkout.
|
||||
- **References:**
|
||||
- `DSC-0024`, `DEC-0025`, `PLN-0048`.
|
||||
@ -0,0 +1,45 @@
|
||||
---
|
||||
id: LSN-0044
|
||||
ticket: jacoco-reports-consolidation
|
||||
title: Jenkins Alignment with Consolidated JaCoCo Reports
|
||||
created: 2026-04-07
|
||||
tags:
|
||||
- infra
|
||||
- gradle
|
||||
- jacoco
|
||||
- jenkins
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The Jenkins pipeline was still wired to an outdated coverage task and module-specific report paths. That made CI brittle after the project moved coverage reporting to the root-level consolidated `jacocoTestReport` task.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Jenkins Reads Root-Level Coverage
|
||||
|
||||
**What:** Jenkins should run `./gradlew clean test jacocoTestReport` and publish coverage from the root project report directory.
|
||||
|
||||
**Why:** Coverage is aggregated across modules, so CI should not depend on a single application subproject path.
|
||||
|
||||
**Trade-offs:** The root task must run after all relevant tests. Jenkins paths must be rooted at the checkout workspace rather than a submodule directory.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
The canonical report locations are:
|
||||
|
||||
- XML: `build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml`
|
||||
- HTML: `build/reports/jacoco/jacocoTestReport/html`
|
||||
|
||||
Jenkins `recordCoverage` should consume the XML report. Jenkins `publishHTML` should expose the HTML directory.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not point Jenkins at `SRS_APP/build/reports/...` or another module-local report when the project expects consolidated root coverage.
|
||||
- Do not run only the reporting task without ensuring tests have executed first.
|
||||
- Do not assume Jenkins relative paths start inside a Gradle subproject; treat the checkout root as the base.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Root-level coverage aggregation needs root-level CI report paths.
|
||||
- The Jenkinsfile and Gradle coverage task names must move together.
|
||||
@ -0,0 +1,103 @@
|
||||
---
|
||||
id: LSN-0045
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Asset-Owned Scene Bank and Tiled XML Workflow
|
||||
created: 2026-04-23
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene-bank
|
||||
- tiled
|
||||
- tmx
|
||||
- tsx
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The abandoned scene-workspace direction was replaced by an asset-first model. Scene authoring and validation now belong to the `Assets` workspace, while heavy map editing remains external in Tiled.
|
||||
|
||||
The final model keeps runtime asset families disciplined:
|
||||
|
||||
- `Scene Bank` is the scene-facing asset managed by Studio.
|
||||
- `Glyph Bank` remains the base family for tilesets.
|
||||
- `Tileset`, `Sprites`, and `UI` are Studio-only glyph-bank specializations, not new runtime families.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Scene Banks Are Assets, Not Workspaces
|
||||
|
||||
**What:** Scene-bank lifecycle is owned by the `Assets` workspace: creation, metadata, layer configuration, validation, acceptance, and diagnostics.
|
||||
|
||||
**Why:** The product direction explicitly discarded a dedicated scene workspace. Asset ownership keeps scene work aligned with existing asset actions, summaries, diagnostics, and refresh flows.
|
||||
|
||||
**Trade-offs:** Studio does not provide a heavy map editor in this wave. Users edit `TMX` and `TSX` externally in Tiled, then return to Studio for validation and acceptance.
|
||||
|
||||
### TMX and TSX Are the Wave-1 XML Surfaces
|
||||
|
||||
**What:** Wave 1 supports XML read/write for `TMX` map documents and `TSX` tileset documents.
|
||||
|
||||
**Why:** Tiled is the interoperability surface. Keeping `TMX` as the maintained scene document avoids inventing a parallel normalized scene schema too early.
|
||||
|
||||
**Trade-offs:** Unsupported Tiled features must fail explicitly rather than being silently normalized away.
|
||||
|
||||
### Tileset Specialization Stays Studio-Only
|
||||
|
||||
**What:** A `Glyph Bank / Tileset` specialization generates the Tiled-facing `TSX` document for that glyph bank.
|
||||
|
||||
**Why:** Tileset generation is editorial Studio behavior over a glyph bank. It should not leak into runtime family identity.
|
||||
|
||||
**Trade-offs:** The asset summary must show both the base type and specialization so users understand the role without confusing it for a new runtime asset family.
|
||||
|
||||
### Scene Validation Requires Explicit Acceptance
|
||||
|
||||
**What:** A scene bank is not ready merely because files exist. Studio validates the current `TMX`/`TSX` state and requires explicit acceptance of the converged result.
|
||||
|
||||
**Why:** External editing can change files behind Studio's back. Acceptance makes the boundary deliberate and auditable.
|
||||
|
||||
**Trade-offs:** Users must perform an extra validation/accept step after external edits, but Studio avoids silently blessing invalid or unsupported scene state.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
### Scene-Bank Support Metadata
|
||||
|
||||
Scene-bank support metadata lives with the scene asset and records the wave-1 layer model:
|
||||
|
||||
- fixed `1..4` scene layers;
|
||||
- per-layer enabled state;
|
||||
- per-layer `tilemap` filename;
|
||||
- per-layer linked `tileset_asset_root`;
|
||||
- map dimensions and explicit layer-to-tilemap binding.
|
||||
|
||||
This support file is the Studio-owned convention that lets a scene bank contain multiple maintained `TMX` documents while preserving deterministic layer mapping.
|
||||
|
||||
### Tiled File Generation
|
||||
|
||||
`Tileset` glyph banks generate `TSX` files. Scene banks generate or maintain layer-specific `TMX` files that reference those generated `TSX` files.
|
||||
|
||||
Each wave-1 tilemap references exactly one generated `TSX`. Multi-tileset maps remain outside this wave.
|
||||
|
||||
### Validation and Acceptance Files
|
||||
|
||||
Scene validation records the inspected file state and diagnostics. Acceptance records the validated state that Studio has explicitly approved.
|
||||
|
||||
This creates a useful external-edit loop:
|
||||
|
||||
1. generate or edit `TMX`/`TSX`;
|
||||
2. validate in Studio;
|
||||
3. inspect diagnostics;
|
||||
4. accept only after validation succeeds.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not reintroduce a dedicated scene workspace for wave 1.
|
||||
- Do not turn `Tileset` specialization into a runtime asset family.
|
||||
- Do not silently auto-accept external Tiled edits after refresh.
|
||||
- Do not treat `TMX`/`TSX` generation as pack publication. Runtime scene packing remains a separate later concern.
|
||||
- Do not allow unsupported Tiled constructs to disappear quietly. They must surface as diagnostics.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Scene Bank belongs in Assets, not in a separate authoring workspace.
|
||||
- `TMX` remains the maintained scene document for wave 1.
|
||||
- `TSX` generation belongs to `Glyph Bank / Tileset`.
|
||||
- Validation plus explicit acceptance is the core safety gate for external Tiled editing.
|
||||
@ -1,116 +0,0 @@
|
||||
---
|
||||
id: AGD-0002
|
||||
ticket: palette-management-in-studio
|
||||
title: Palette Management in Studio
|
||||
status: open
|
||||
created: 2026-03-26
|
||||
resolved:
|
||||
decision:
|
||||
tags:
|
||||
- studio
|
||||
- legacy-import
|
||||
- palette-management
|
||||
- tile-bank
|
||||
- packer-boundary
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Studio already has a retained agenda for palette management in the `glyph bank` form session, but that agenda exists only under `docs/studio/agendas/` and therefore sits outside the discussion-framework lifecycle.
|
||||
|
||||
Without importing it into `discussion/`, the canonical discussion surface would lose the still-open Studio question about:
|
||||
|
||||
- how candidate palettes are presented;
|
||||
- how `1..64` palette capacity is enforced and explained;
|
||||
- how preview and selection behave inside the asset workflow;
|
||||
- how Studio UX stays separate from packer-owned extraction, validation, identity, and persistence.
|
||||
|
||||
## Context
|
||||
|
||||
Legacy source: `docs/studio/agendas/Palette Management in Studio Agenda.md`
|
||||
|
||||
Domain owner: `docs/studio`
|
||||
Cross-domain impact: `docs/packer`
|
||||
|
||||
The retained agenda is intentionally scoped to `glyph bank`.
|
||||
|
||||
The imported context keeps the same baseline:
|
||||
|
||||
- every `*.png` input of a glyph bank is one tile;
|
||||
- each tile yields one extracted palette during cache construction;
|
||||
- developers may also work with favorite palettes and asset palettes;
|
||||
- each bank may integrate at most `64` palettes;
|
||||
- each palette contains `16` colors with a default color-key baseline;
|
||||
- Studio owns curation UX, while packer remains authoritative for extraction, identity, validation, and persistence.
|
||||
|
||||
The current UI shape already converged on:
|
||||
|
||||
- a left-side transfer list for candidate and selected palettes;
|
||||
- custom palette rendering with swatches and source indicators;
|
||||
- a right-side preview panel with tile/file selector, tile preview, and applied palette strip.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] How should Studio detect and present duplicate or near-equivalent palettes across tile-derived, favorite, and asset-level sources?
|
||||
- [ ] Are favorites project-scoped references to existing palettes, or can they diverge into editable copies before bank admission?
|
||||
- [ ] What is the canonical user-facing identity of a candidate palette in Studio: source bucket plus logical name, file path, extraction origin tile, stable id, or a combination?
|
||||
- [ ] Should extracted tile palettes always enter the candidate set automatically, or can the developer suppress some of them before curation?
|
||||
- [ ] Should clicking a palette on the `selected` side be the only interaction needed to change the previewed palette in the first wave?
|
||||
- [ ] Which palette operations are read-only in the first wave, and which require staged preview/apply treatment?
|
||||
- [ ] Which parts of this interaction model should remain explicitly `glyph bank`-specific, and which parts are reusable Studio form-session primitives?
|
||||
- [ ] Does the first release need only an asset-scoped `glyph bank` section, or also a project-level palette browser for discovery and favorites management?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Automatic Integration
|
||||
- **Approach:** Treat extracted tile palettes as implicit inputs and integrate them automatically without explicit user curation.
|
||||
- **Pro:** Lowest immediate UX cost.
|
||||
- **Con:** Hides palette-budget reasoning, weakens deliberate composition, and makes source distinctions harder to understand.
|
||||
- **Maintainability:** Weak. It pushes important bank rules into implicit behavior.
|
||||
|
||||
### Option B - Asset-Scoped Curated Selection
|
||||
- **Approach:** Treat bank palettes as an asset-scoped curated selection built from tile-derived palettes, favorites, and asset palettes, with Studio owning the curation UX and packer owning persistence.
|
||||
- **Pro:** Matches the concrete `glyph bank` workflow and keeps authority boundaries clear.
|
||||
- **Con:** Requires explicit UX for capacity, identity, deduplication, and preview semantics.
|
||||
- **Maintainability:** Strong. It makes responsibilities explicit and keeps family-specific rules close to the workflow that needs them.
|
||||
|
||||
### Option C - Studio-Owned Palette Library
|
||||
- **Approach:** Treat palettes as a broader Studio-owned library/workspace with direct local editing and persistence behavior.
|
||||
- **Pro:** Potentially flexible for future palette-heavy features.
|
||||
- **Con:** Duplicates packer ownership and risks creating a second authority for bank composition.
|
||||
- **Maintainability:** Weak. It expands Studio responsibility before the persistence contract is closed.
|
||||
|
||||
## Discussion
|
||||
|
||||
The imported agenda keeps the original recommendation intact: palette management should be first-class for `glyph bank`, but Studio must not become the persistence authority.
|
||||
|
||||
That means the key open problem is not whether palettes exist. It is how Studio explains and curates them safely:
|
||||
|
||||
- candidate aggregation from multiple sources;
|
||||
- clear source and identity display;
|
||||
- explicit `1..64` capacity feedback;
|
||||
- a preview model that stays tied to the selected bank palettes;
|
||||
- a boundary between `glyph bank`-specific UX and reusable Studio primitives.
|
||||
|
||||
The retained source already converged on a pragmatic first wave:
|
||||
|
||||
- transfer-list selection on the left;
|
||||
- palette-focused preview on the right;
|
||||
- first selected palette as default preview;
|
||||
- clicking on a selected palette changes the previewed palette;
|
||||
- removing the current preview palette falls back to the first remaining selected palette.
|
||||
|
||||
## Resolution
|
||||
|
||||
Recommended direction: adopt **Option B**.
|
||||
|
||||
The imported agenda preserves the retained Studio direction:
|
||||
|
||||
1. palette management belongs to the `glyph bank` asset workflow;
|
||||
2. Studio aggregates candidate palettes from tile-derived extraction, favorites, and asset palettes;
|
||||
3. the first wave uses a custom-rendered transfer list with an explicit `1..64` selected range;
|
||||
4. the right-side preview panel stays tied to the current selected palette set;
|
||||
5. packer remains authoritative for extraction, identity, validation, and persistence;
|
||||
6. broader standalone palette-authoring workflows remain deferred.
|
||||
|
||||
Next step suggestion: convert this imported agenda into a decision that closes the ownership boundary, candidate-source model, first-wave transfer-list UX, preview behavior, and reusable-vs-family-specific primitive boundary.
|
||||
@ -1,329 +0,0 @@
|
||||
---
|
||||
id: AGD-0030
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Tiled Parser and Scene Asset-Type Ownership in Assets Workspace
|
||||
status: accepted
|
||||
created: 2026-04-15
|
||||
resolved: 2026-04-17
|
||||
decision: DEC-0027
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene
|
||||
- tiled
|
||||
- parser
|
||||
- asset-type
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
The current abandoned direction assumed a dedicated `Scene Workspace` as the primary authoring owner for scenes.
|
||||
|
||||
That no longer matches the intended product direction.
|
||||
|
||||
Without a new direction:
|
||||
|
||||
- the Studio has no agreed path for scene authoring;
|
||||
- the `Assets` workspace still lacks a defined ownership model for scene-like content;
|
||||
- there is no agreed contract for importing or interpreting `Tiled` data;
|
||||
- implementation risks rebuilding a custom scene editor model that the team has already decided to discard.
|
||||
|
||||
## Context
|
||||
|
||||
Domain owner: `studio`
|
||||
|
||||
Expected documentation owner:
|
||||
|
||||
- `docs/studio/specs`
|
||||
|
||||
Cross-domain impact:
|
||||
|
||||
- asset typing and creation flows in the `Assets` workspace;
|
||||
- Studio-side parsing/import rules for `Tiled`;
|
||||
- possible downstream alignment with runtime-facing `SCENE` publication, depending on the chosen import boundary.
|
||||
- Studio-only specialization of glyph-bank assets into map, sprite, and UI authoring roles.
|
||||
|
||||
User direction explicitly provided on 2026-04-15:
|
||||
|
||||
- abandon `DEC-0026` and its derived plans;
|
||||
- do not proceed with a dedicated `Scene Workspace`;
|
||||
- instead, write a parser for `Tiled`;
|
||||
- create a new asset type managed from the `Assets` workspace.
|
||||
|
||||
Additional direction explicitly provided on 2026-04-15:
|
||||
|
||||
- Studio-specific asset roles such as `Tileset`, `Sprites`, and `UI` remain glyph banks internally;
|
||||
- those roles are editorial specializations in Studio rather than distinct runtime asset families;
|
||||
- the specialization is stored as a Studio-only flag attached to glyph-bank asset creation and management;
|
||||
- specialization does not need to lock general glyph-bank editing fields in this wave;
|
||||
- specialization should instead enable role-specific Studio actions and flows;
|
||||
- the asset summary should continue to show the base bank type and add the specialization as a complementary chip, such as `Type: Glyph Bank / Tileset`;
|
||||
- Studio will expose role-specific commands, such as building a `tileset.tsx` from a `Tileset` glyph bank;
|
||||
- validation of map/tileset consistency should remain centered on the `Scene Bank` rather than on hard-locking the `Glyph Bank`.
|
||||
- specialization-specific actions should be stacked with the existing asset actions instead of introducing a separate action region between summary and actions.
|
||||
|
||||
This agenda exists to turn that direction into a disciplined technical recommendation rather than jumping directly into implementation with undefined ownership and file-contract assumptions.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [x] Which `Tiled` surface is the primary input contract: TMX XML, JSON export, TSX, or a constrained subset of those?
|
||||
- [x] Is the new scene-facing asset a raw imported `Tiled` document, a normalized Studio-owned intermediate asset, or a published runtime-facing `SCENE` derivative?
|
||||
- [x] Should `Assets` own only creation/import and metadata management, or also the main editing/reimport flow for `Tiled`-backed scene assets?
|
||||
- [x] How should `Tiled` tileset references map onto Studio-owned `Tileset` glyph banks and their identity rules?
|
||||
- [x] What parts of `Tiled` are intentionally out of scope for the first wave: object layers, properties, flips, collisions, infinite maps, templates, Wang sets, animations?
|
||||
- [x] Does the parser need to preserve full round-trip fidelity with `Tiled`, or only import the supported subset into Studio-owned structures?
|
||||
- [x] What is the canonical failure model when a `Tiled` document references unsupported constructs or broken external assets?
|
||||
- [x] How should Studio persist the editorial role flag of a glyph bank (`Tileset`, `Sprites`, `UI`) without inventing a separate runtime family?
|
||||
- [x] Which defaults become mandatory or locked for each Studio specialization at creation time?
|
||||
- [x] Where do Studio role-specific commands such as `build tileset.tsx` live in the `Assets` UX?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Import `Tiled` into a Studio-owned scene asset model linked to flagged glyph banks
|
||||
- **Approach:** Keep `Glyph Bank` as the canonical asset type, add a Studio-only specialization flag such as `Tileset`, `Sprites`, or `UI`, and parse supported `Tiled` files into a Studio-owned scene asset managed from `Assets`.
|
||||
- **Pro:** Aligns with the requested ownership shift, preserves the existing glyph-bank runtime model, and lets Studio attach role-specific rules, chips, defaults, and commands without multiplying runtime asset families.
|
||||
- **Con:** Requires a precise Studio metadata model for the specialization flag and a clear mapping from `Tiled` tilesets to flagged glyph-bank assets.
|
||||
- **Maintainability:** Strong, if the supported subset and ownership boundary are made explicit early.
|
||||
|
||||
### Option B - Treat `Tiled` files as external source documents and keep Studio as a thin importer/viewer
|
||||
- **Approach:** Keep `Tiled` as the primary authoring format and let Studio mostly import, validate, and publish from it.
|
||||
- **Pro:** Minimizes Studio-owned scene-model invention and stays close to existing `Tiled` authoring workflows.
|
||||
- **Con:** Couples Studio behavior heavily to `Tiled`'s full document model and makes partial support/error handling harder to reason about.
|
||||
- **Maintainability:** Medium, because unsupported `Tiled` breadth can leak into every implementation phase.
|
||||
|
||||
### Option C - Create a new Studio asset type unrelated to `Tiled` and only offer one-time conversion
|
||||
- **Approach:** Use `Tiled` only as an optional bootstrap/import tool, then move fully into a separate Studio-native asset format.
|
||||
- **Pro:** Gives Studio maximum control over the final asset model and future editing behavior.
|
||||
- **Con:** Moves away from the user's stated direction of writing a `Tiled` parser as a central path and may reintroduce the same custom-model burden the previous direction had.
|
||||
- **Maintainability:** Medium, but only if the conversion boundary is very clearly justified.
|
||||
|
||||
## Discussion
|
||||
|
||||
The key architectural change here is not only "use `Tiled`".
|
||||
|
||||
It is also a change in ownership:
|
||||
|
||||
- scene-related authoring/import should no longer justify a dedicated `Scene Workspace`;
|
||||
- the `Assets` workspace becomes the management surface for the relevant asset type;
|
||||
- any parser choice must therefore fit an asset-first workflow instead of a separate editor-first workflow.
|
||||
- asset specialization such as `Tileset`, `Sprites`, and `UI` is a Studio concern layered over the existing glyph-bank runtime family.
|
||||
|
||||
That creates several decisions that should be made deliberately:
|
||||
|
||||
1. what exact `Tiled` contract Studio supports in wave 1;
|
||||
2. whether Studio stores `Tiled` data directly or through a normalized asset form;
|
||||
3. how `Tileset` glyph banks map to `Tiled` tilesets without creating identity ambiguity;
|
||||
4. how Studio persists the specialization flag and summary-chip presentation without changing the runtime family model;
|
||||
5. which creation defaults become locked by each specialization;
|
||||
6. how much of `Tiled` fidelity is preserved versus normalized away;
|
||||
7. whether editing is a reimport/update flow, a direct managed-asset flow, or some hybrid.
|
||||
|
||||
There is already one strong constraint from the user:
|
||||
|
||||
- the previous `Scene Workspace` split should be considered discarded;
|
||||
- the new work should be organized around `Assets` plus a `Tiled` parser.
|
||||
|
||||
The current direction also suggests an important simplification:
|
||||
|
||||
- Prometeu can assume exactly one tileset per tilemap in its first wave;
|
||||
- a `Scene Bank` may contain more than one tilemap asset in the same bank scope;
|
||||
- each scene may contain up to `4` layers in wave 1;
|
||||
- each scene layer is associated with one tilemap, so a scene bank may keep more than one `TMX` file in the same asset directory;
|
||||
- the parser still needs to understand `Tiled` identifiers correctly, but the Studio-side scene model can intentionally narrow to one linked `Tileset` glyph bank per map;
|
||||
- this keeps the runtime/editor model simpler while still using `Tiled` as the interchange and tooling format.
|
||||
|
||||
Another important clarification is now explicit:
|
||||
|
||||
- the `Assets` workspace remains the owner of asset management;
|
||||
- `Glyph Bank` remains the base type shown in the asset summary;
|
||||
- the Studio-only specialization appears as a complementary suffix or chip such as `Glyph Bank / Tileset`;
|
||||
- specialization-specific behavior such as locked dimensions or generated `TSX` output belongs strictly to Studio behavior rather than the runtime asset contract.
|
||||
- specialization-specific actions should reuse the existing actions surface rather than creating a new intermediate panel between summary and actions.
|
||||
|
||||
### Convergence pass - accepted Tiled surfaces and tileset handoff
|
||||
|
||||
#### 1. Supported Tiled surfaces for wave 1
|
||||
|
||||
Direction: the Studio integration surface for wave 1 uses both `TMX` and `TSX`.
|
||||
|
||||
Implications:
|
||||
|
||||
- `TMX` is the map-side surface used for tilemap generation and interchange;
|
||||
- `TSX` is the tileset-side surface generated from a Studio `Glyph Bank` specialized as `Tileset`;
|
||||
- JSON export is not the primary interoperability surface for this wave.
|
||||
|
||||
#### 2. Mapping from `Tileset` glyph banks to Tiled tilesets
|
||||
|
||||
Direction: a `Glyph Bank` flagged as `Tileset` generates the `TSX` representation for that bank.
|
||||
|
||||
Implications:
|
||||
|
||||
- the generated `TSX` is the Tiled-facing tileset surface for that glyph bank;
|
||||
- the new `Scene Bank` may point to that generated `TSX` when generating a tilemap;
|
||||
- this coupling is scoped to tilemap generation and consumption, not to a runtime asset-family split;
|
||||
- `TSX` generation remains owned by the `Assets` workspace as Studio behavior over a `Glyph Bank`.
|
||||
|
||||
### Convergence pass - accepted Scene Bank and parser direction
|
||||
|
||||
#### 1. Persistence model of the new `Scene Bank`
|
||||
|
||||
Direction: the new `Scene Bank` MUST persist full `TMX` documents as its maintained Studio asset format, plus any additional support files needed by the asset.
|
||||
|
||||
Implications:
|
||||
|
||||
- the `Scene Bank` keeps `TMX` as the authoritative map-side document format rather than normalizing into a separate Studio-owned scene schema;
|
||||
- one scene-facing asset directory may contain more than one `TMX` file, because a scene may bind distinct tilemaps to distinct layers;
|
||||
- wave 1 limits each scene to at most `4` layers;
|
||||
- the asset may include additional Studio support files alongside the maintained `TMX`;
|
||||
- those support files own the Studio-side convention that maps scene layers to the corresponding tilemaps;
|
||||
- the `Assets` workspace may expose a dedicated area to inspect and edit that scene-bank support data;
|
||||
- `TMX` generation remains part of the Studio asset flow, but the generated `TMX` is also the canonical maintained map document for the asset bank;
|
||||
- final pack materialization into a dedicated `assets.pa` representation is explicitly outside this wave.
|
||||
|
||||
Implementation note accepted during agenda convergence:
|
||||
|
||||
- the exact shape of the support files is intentionally deferred and may be discovered during implementation, as long as the final direction preserves an explicit layer-to-tilemap mapping owned by the scene-facing asset.
|
||||
|
||||
#### 2. Ownership of scene-asset management in `Assets`
|
||||
|
||||
Direction: the `Assets` workspace MUST fully manage the scene-facing asset lifecycle.
|
||||
|
||||
Implications:
|
||||
|
||||
- `Assets` owns creation, metadata, validation, consistency checks, and asset-facing operations for the `Scene Bank`;
|
||||
- the heavy map-editing interaction is expected to occur in `Tiled`;
|
||||
- Studio-side asset management remains responsible for consistency and integration around that external editing loop.
|
||||
|
||||
#### 3. Wave-1 parser requirement
|
||||
|
||||
Direction: wave 1 MUST include an XML parser capable of both reading and writing `TMX` and `TSX`.
|
||||
|
||||
Implications:
|
||||
|
||||
- XML is the required implementation surface for this wave;
|
||||
- the Studio must be able to ingest existing `TMX` and `TSX` documents;
|
||||
- the Studio must also emit `TMX` and `TSX` documents from its owned asset state.
|
||||
|
||||
#### 3a. Asset-file placement and reference model
|
||||
|
||||
Direction: `TSX` files are generated by the `Glyph Bank` asset specialized as `Tileset`, and `TMX` files are generated inside the scene-facing asset and must reference those generated `TSX` files in the tileset asset directories.
|
||||
|
||||
Implications:
|
||||
|
||||
- a `Tileset` specialization on a `Glyph Bank` owns the generation of the Tiled-facing `TSX` files for that asset;
|
||||
- the scene-facing asset owns the generation and maintenance of the corresponding `TMX` files;
|
||||
- each tilemap inside the scene-facing asset must reference exactly one generated `TSX` in wave 1;
|
||||
- the reference path model between `TMX` and `TSX` must remain deterministic so the asset bank can be regenerated and validated safely.
|
||||
|
||||
Open precision still to be normalized in the future:
|
||||
|
||||
- the exact canonical naming convention for multiple `TMX` files in the same scene-bank directory is not yet fixed, but it must be canonical and must remain compatible with the layer-to-tilemap support-file convention.
|
||||
|
||||
#### 4. Pack boundary for the current wave
|
||||
|
||||
Direction: pack integration is NOT part of the current wave.
|
||||
|
||||
Implications:
|
||||
|
||||
- the current work should stay inside Studio until the asset model and Tiled interop are stabilized;
|
||||
- no packer-facing publication or final asset-pack contract is required to move this agenda forward;
|
||||
- later pack work will materialize the `TMX`/`TSX` asset mass plus support files into a dedicated binary representation.
|
||||
|
||||
#### 5. Persistence of the Studio-only specialization flag
|
||||
|
||||
Direction: the editorial specialization should be represented internally as an enum and stored on the Studio-controlled asset entity that already owns asset metadata.
|
||||
|
||||
Implications:
|
||||
|
||||
- the flag remains Studio-only and does not redefine the runtime asset family;
|
||||
- the enum should encode membership such as `Tileset (GlyphBank)`, making filtering and applicability rules explicit;
|
||||
- persistence should reuse the existing Studio/asset metadata home rather than inventing a separate document just for the specialization flag;
|
||||
- the current working assumption is that this persistence may belong alongside the asset entity already controlled by Studio tooling, pending code-level confirmation of the exact home.
|
||||
|
||||
#### 6. Validation ownership and specialization limits in wave 1
|
||||
|
||||
Direction: specialization should unlock Studio-specific actions, but it should NOT hard-lock general glyph-bank editing in this wave.
|
||||
|
||||
Implications:
|
||||
|
||||
- a specialization such as `Tileset`, `Sprites`, or `UI` primarily controls which Studio actions and flows are available for that glyph bank;
|
||||
- the base glyph-bank runtime/editor model remains flexible in wave 1;
|
||||
- consistency checks that depend on how the glyph bank is used in a map should be enforced by the `Scene Bank`;
|
||||
- this keeps wave-1 constraints aligned with the runtime flexibility instead of prematurely freezing the glyph-bank editor surface.
|
||||
|
||||
#### 7. Diagnostic and blocking model
|
||||
|
||||
Direction: scene and Tiled consistency issues should use the same diagnostic pattern already used by glyph assets.
|
||||
|
||||
Implications:
|
||||
|
||||
- invalid or unsupported references should surface as Studio diagnostics;
|
||||
- the same diagnostic discipline should block pack when inconsistencies remain;
|
||||
- this keeps the failure model aligned with the existing asset-validation experience rather than introducing a second error system.
|
||||
|
||||
#### 7a. External-edit acceptance workflow
|
||||
|
||||
Direction: wave 1 MUST use an explicit acceptance workflow between Studio-owned scene assets and external `Tiled` edits.
|
||||
|
||||
Implications:
|
||||
|
||||
- the initial workflow is: create the scene asset in Studio, edit the generated `TMX`/`TSX` documents in `Tiled`, then return to Studio to validate and explicitly accept the resulting changes;
|
||||
- a scene bank is considered ready only after Studio validation succeeds and the user explicitly accepts the converged state;
|
||||
- Studio remains the place where external edits are reviewed and accepted, rather than auto-merging them silently.
|
||||
|
||||
#### 8. Supported Tiled feature subset in wave 1
|
||||
|
||||
Direction: wave 1 should support `Object Layers`, properties, flips, and collisions from the start.
|
||||
|
||||
Implications:
|
||||
|
||||
- collisions should be supported in both places that Tiled can represent them for this workflow;
|
||||
- tile-owned collision may live in the `TSX` side of a specialized `Tileset` glyph bank;
|
||||
- map-owned collision must be represented through `Object Layer` content in the `TMX` side of the `Scene Bank`;
|
||||
- map-side `Object Group` support is useful because the pipeline will gradually absorb additional features such as collision, camera boundaries, lights, and similar scene annotations;
|
||||
- the remaining unsupported Tiled features for this wave are the broader feature set outside this accepted subset, such as infinite maps, templates, Wang sets, and animations, unless later narrowed by a follow-up decision.
|
||||
|
||||
### Future note - possible collision-pipeline consolidation
|
||||
|
||||
This agenda also records a possible future pipeline direction that is explicitly outside the current wave:
|
||||
|
||||
- once collision support becomes mature enough, the pipeline may generate a canonical map-side `objectgroup` dedicated to collision, such as `collision`;
|
||||
- that generated layer could read or derive effective collision information from tile-owned collision definitions and map-authored collision content;
|
||||
- the pack pipeline could then consume that consolidated collision layer as the immediate source for collision materialization in the packed asset output.
|
||||
|
||||
Important boundary:
|
||||
|
||||
- this is a future optimization and publication-direction note, not a wave-1 authoring rule;
|
||||
- authored collision and generated collision layers must remain conceptually separate until a later decision formalizes that pipeline stage.
|
||||
|
||||
### Convergence pass - accepted local UX direction
|
||||
|
||||
#### 1. Placement of specialization-specific actions in `Assets`
|
||||
|
||||
Recommendation: specialization-specific actions should be stacked together with the existing asset actions.
|
||||
|
||||
Reasoning:
|
||||
|
||||
- the current `Assets` workspace already has an established actions surface;
|
||||
- introducing a second action region between summary and actions would fragment the UX without adding a clearer ownership model;
|
||||
- specialization-specific commands remain asset actions and do not need a separate structural host.
|
||||
|
||||
Accepted local direction for this agenda:
|
||||
|
||||
- the `Assets` workspace MUST NOT introduce a separate action region between summary and actions for specialization-specific commands;
|
||||
- commands such as `build tileset.tsx` SHOULD appear in the same action stack as the existing asset actions;
|
||||
- visibility and enablement of those commands SHOULD be driven by the Studio-only specialization flag of the glyph bank;
|
||||
- the summary remains responsible for identity and type presentation, including the complementary specialization chip.
|
||||
|
||||
That makes Option A the current best fit for convergence, and the latest discussion now also closes the first-wave one-tileset-per-tilemap rule, `TMX`/`TSX` asset ownership, and the explicit Studio-side acceptance workflow for external `Tiled` edits. The remaining work is to turn those accepted directions into a precise decision text while marking the support-file shape and multi-`TMX` naming convention as intentionally deferred implementation details.
|
||||
|
||||
## Resolution
|
||||
|
||||
Recommended next step:
|
||||
|
||||
- write a replacement decision for `Scene Bank` ownership in `Assets`;
|
||||
- codify `TMX` as the maintained map document format and `TSX` as the tileset-facing generated format;
|
||||
- codify the first-wave rule of exactly one tileset per tilemap, while allowing more than one tilemap inside a `Scene Bank`;
|
||||
- codify the Studio-only glyph-bank specialization-flag model for `Tileset`, `Sprites`, and `UI`;
|
||||
- codify the wave-1 supported `Tiled` subset, validation model, and explicit acceptance flow for external edits;
|
||||
- carry forward the support-file shape and multi-`TMX` naming convention as deferred implementation details rather than pretending they are already closed.
|
||||
@ -1,139 +0,0 @@
|
||||
---
|
||||
id: DEC-0027
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Tiled Parser and Scene Bank Asset Ownership in Assets Workspace
|
||||
status: accepted
|
||||
created: 2026-04-17
|
||||
accepted: 2026-04-17
|
||||
agenda: AGD-0030
|
||||
plans: []
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene
|
||||
- tiled
|
||||
- parser
|
||||
- asset-type
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The previous `Scene Workspace` direction was explicitly abandoned and MUST NOT be reused as the basis for new planning or implementation.
|
||||
|
||||
The replacement direction is asset-first:
|
||||
|
||||
- scene-facing ownership belongs to the `Assets` workspace;
|
||||
- `Tiled` is the external authoring and interchange surface for wave 1;
|
||||
- `Glyph Bank` remains the base runtime asset family for bank assets;
|
||||
- Studio-specific roles such as `Tileset`, `Sprites`, and `UI` are editorial specializations only and MUST NOT create new runtime asset families.
|
||||
|
||||
This decision closes the ownership, persistence, interoperability, and validation baseline needed to plan and implement the first wave.
|
||||
|
||||
## Decision
|
||||
|
||||
The Studio SHALL adopt an asset-first `Scene Bank` model managed from the `Assets` workspace.
|
||||
|
||||
Wave 1 SHALL use `TMX` and `TSX` as the Tiled-facing asset formats:
|
||||
|
||||
- a `Scene Bank` MUST maintain full `TMX` documents as its canonical map-side asset documents;
|
||||
- a `Glyph Bank` specialized as `Tileset` MUST generate the `TSX` files used by scene tilemaps;
|
||||
- each tilemap MUST reference exactly one generated `TSX` file in wave 1;
|
||||
- a `Scene Bank` MAY contain more than one tilemap and therefore more than one `TMX` file in the same asset directory;
|
||||
- each scene MAY contain up to `4` layers in wave 1;
|
||||
- each scene layer MUST map to exactly one tilemap;
|
||||
- the layer-to-tilemap convention MUST be owned by Studio support files that live with the scene-facing asset.
|
||||
|
||||
The Studio SHALL own creation, metadata, validation, consistency checks, and asset-facing operations for `Scene Bank` assets.
|
||||
|
||||
Heavy map editing for wave 1 SHALL occur in `Tiled`, not in a dedicated Studio scene editor workspace.
|
||||
|
||||
## Rationale
|
||||
|
||||
This direction preserves the explicit user request to replace the discarded scene-workspace model with:
|
||||
|
||||
- a parser for `Tiled`;
|
||||
- a new scene-facing asset managed from `Assets`;
|
||||
- Studio-only specialization over existing `Glyph Bank` assets instead of runtime-family multiplication.
|
||||
|
||||
Keeping `TMX` as the maintained scene document avoids inventing a parallel normalized scene schema before the Tiled integration boundary is proven in practice.
|
||||
|
||||
Keeping `TSX` generation attached to `Tileset`-specialized glyph banks preserves a clear ownership split:
|
||||
|
||||
- tileset-facing generation belongs to the specialized glyph-bank asset;
|
||||
- scene-facing tilemap maintenance belongs to the scene asset;
|
||||
- runtime publication remains deferred until the Studio-side asset model stabilizes.
|
||||
|
||||
The explicit acceptance workflow also prevents silent drift between Studio-owned asset state and externally edited Tiled documents.
|
||||
|
||||
## Technical Specification
|
||||
|
||||
### 1. Asset ownership and specialization
|
||||
|
||||
- The `Assets` workspace MUST remain the management surface for `Scene Bank` assets.
|
||||
- `Glyph Bank` MUST remain the base asset type shown in summaries.
|
||||
- The Studio-only specialization flag MUST be stored on Studio-controlled asset metadata.
|
||||
- The specialization flag MUST be represented as an internal enum or equivalent closed set.
|
||||
- The specialization flag MUST NOT redefine the runtime asset family.
|
||||
- Specialization-specific actions MUST appear in the existing asset actions surface.
|
||||
- The summary SHOULD present the base type plus specialization chip, for example `Glyph Bank / Tileset`.
|
||||
|
||||
### 2. Scene Bank persistence
|
||||
|
||||
- A scene-facing asset directory MAY contain multiple `TMX` files.
|
||||
- The maintained scene-bank format MUST keep `TMX` in full rather than translating the asset into a separate normalized Studio scene format.
|
||||
- The asset MAY contain additional support files owned by Studio.
|
||||
- Those support files MUST encode the mapping between scene layers and tilemaps.
|
||||
- The exact support-file schema is intentionally deferred to implementation and MAY be discovered on the fly, but the resulting contract MUST remain explicit and deterministic.
|
||||
|
||||
### 3. Tiled surfaces and parser contract
|
||||
|
||||
- Wave 1 MUST support XML read/write for both `TMX` and `TSX`.
|
||||
- JSON export MUST NOT be treated as the primary interoperability surface in wave 1.
|
||||
- Studio MUST be able to ingest supported `TMX` and `TSX` documents.
|
||||
- Studio MUST be able to emit supported `TMX` and `TSX` documents from its owned asset state.
|
||||
|
||||
### 4. Tilemap, tileset, and layer rules
|
||||
|
||||
- Each tilemap MUST reference exactly one `Tileset` glyph-bank generated `TSX`.
|
||||
- One scene MAY contain multiple tilemaps.
|
||||
- Each scene MAY contain at most `4` layers in wave 1.
|
||||
- Each layer MUST point to one tilemap through the scene-bank support files.
|
||||
- The naming convention for multiple `TMX` files in one scene-bank directory is deferred, but the final convention MUST be canonical and MUST remain compatible with the layer-to-tilemap mapping.
|
||||
|
||||
### 5. Supported Tiled subset in wave 1
|
||||
|
||||
- Wave 1 MUST support `Object Layers`, properties, flips, and collisions.
|
||||
- Tile-owned collision MAY be represented on the `TSX` side of a `Tileset` glyph bank.
|
||||
- Map-owned collision MUST be represented through `Object Layer` content on the `TMX` side of the `Scene Bank`.
|
||||
- Infinite maps, templates, Wang sets, and animations are outside wave 1 unless revised by a later decision.
|
||||
|
||||
### 6. Validation and acceptance workflow
|
||||
|
||||
- The initial workflow SHALL be:
|
||||
1. create the scene-facing asset in Studio;
|
||||
2. edit the generated `TMX` and `TSX` documents in `Tiled`;
|
||||
3. return to Studio;
|
||||
4. validate the resulting state;
|
||||
5. explicitly accept the converged changes.
|
||||
- Studio MUST surface invalid, unsupported, or broken references as diagnostics using the same general diagnostic discipline already used for glyph assets.
|
||||
- Studio MUST NOT silently auto-accept external `Tiled` edits.
|
||||
- A `Scene Bank` is ready only after Studio validation succeeds and the user explicitly accepts the resulting state.
|
||||
- The same diagnostic discipline SHOULD block later pack publication when inconsistencies remain.
|
||||
|
||||
### 7. Pack boundary
|
||||
|
||||
- Pack integration is explicitly outside this wave.
|
||||
- Wave-1 work MUST stay inside Studio asset management and Tiled interoperability.
|
||||
- Later pack work MAY materialize the `TMX`/`TSX` asset mass plus support files into a dedicated binary representation.
|
||||
|
||||
## Constraints
|
||||
|
||||
- This decision supersedes the abandoned `Scene Workspace` direction and MUST be treated as the normative replacement for wave-1 planning.
|
||||
- Plans derived from this decision MUST NOT reintroduce a dedicated scene workspace or a separate normalized Studio-owned scene format unless the user explicitly requests a revised decision.
|
||||
- Plans derived from this decision MUST preserve the external-edit acceptance step as a required gate.
|
||||
- Plans derived from this decision MUST treat the support-file schema and multi-`TMX` naming convention as deferred implementation details, not as permission to weaken the layer-to-tilemap contract.
|
||||
- Any future move toward automatic convergence, multi-tileset tilemaps, or pack-facing publication contracts requires a new decision or explicit revision of this one.
|
||||
|
||||
## Revision Log
|
||||
|
||||
- 2026-04-17: Initial accepted decision from AGD-0030.
|
||||
@ -1,121 +0,0 @@
|
||||
---
|
||||
id: PLN-0053
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Scene Bank Asset Contract and Studio Metadata Foundations
|
||||
status: open
|
||||
created: 2026-04-17
|
||||
completed:
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene
|
||||
- tiled
|
||||
- metadata
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Establish the asset contract and Studio-owned metadata foundations required by `DEC-0027` so `Scene Bank` assets and specialized `Glyph Bank` assets can exist in the `Assets` workspace without reintroducing a dedicated scene workspace or a separate normalized scene schema.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0027` locks the following requirements that must be reflected in the asset contract before parser and UX work can be implemented safely:
|
||||
|
||||
- `Scene Bank` is an asset owned by the `Assets` workspace.
|
||||
- `Glyph Bank` remains the base runtime asset family, with Studio-only specialization flags such as `Tileset`, `Sprites`, and `UI`.
|
||||
- `TMX` remains the maintained scene document format.
|
||||
- `Scene Bank` support files own the explicit `layer -> tilemap` mapping.
|
||||
- one scene may contain multiple tilemaps, with up to `4` layers in wave 1.
|
||||
|
||||
The current repo already has the asset creation and detail surfaces under `prometeu-studio/src/main/java/p/studio/workspaces/assets` and the asset manifest/runtime contract under `prometeu-packer/prometeu-packer-v1`.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Add `Scene Bank` as a first-class asset option in the Studio asset creation and details pipeline.
|
||||
- Add the Studio-only specialization model for `Glyph Bank` assets, including `Tileset`, `Sprites`, and `UI`.
|
||||
- Define where Studio-owned scene support files and specialization metadata live relative to existing asset roots.
|
||||
- Add summary/details projection support so the `Assets` workspace can display base family plus specialization chip.
|
||||
- Define the wave-1 metadata contract for scene-layer mapping and scene-bank limits.
|
||||
|
||||
### Excluded
|
||||
- TMX/TSX XML parsing and serialization logic.
|
||||
- External-edit acceptance UI or file-diff validation flows.
|
||||
- Pack publication of scene assets into runtime binary formats.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Extend asset family and metadata models
|
||||
|
||||
**What:** Introduce the data models needed for `Scene Bank` assets and Studio-only glyph-bank specialization.
|
||||
|
||||
**How:** Update the packer/studio shared asset declarations and detail projection pipeline so `Scene Bank` can be created, read, and surfaced by the `Assets` workspace while keeping `Glyph Bank` as the underlying runtime family for specializations. Define the Studio-only specialization enum and the scene-bank support metadata surface that will later carry `layer -> tilemap` mapping and wave-1 constraints.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDeclarationParser.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/PackerAssetDetailsService.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetSummary.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java`
|
||||
- additional shared DTOs or catalogs discovered from the asset declaration flow
|
||||
|
||||
### Step 2 - Add creation and details support in Assets workspace
|
||||
|
||||
**What:** Make `Scene Bank` and glyph-bank specialization available in the actual Studio asset workflow.
|
||||
|
||||
**How:** Extend the add-asset wizard to expose `Scene Bank` as an asset type and add the minimum specialization capture or follow-up action path for `Glyph Bank`. Update details/summary rendering to show `Glyph Bank / Tileset`-style presentation and to surface scene-bank metadata ownership in the details view without inventing a separate workspace region.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/wizards/AddAssetWizard.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/summary/AssetDetailsSummaryControl.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java`
|
||||
- `prometeu-studio/src/main/resources/i18n/messages.properties`
|
||||
|
||||
### Step 3 - Define on-disk scene-bank support-file baseline
|
||||
|
||||
**What:** Establish the first implementation-owned support-file baseline for scene banks.
|
||||
|
||||
**How:** Introduce a concrete but implementation-scoped support-file format that records scene layer count, `layer -> tilemap` mapping, and references needed by the `Assets` workspace. The plan MUST preserve the decision rule that the exact shape can be discovered during implementation, but the result MUST be explicit, deterministic, and colocated with the scene asset root.
|
||||
|
||||
**File(s):**
|
||||
- asset-root support files under `test-projects/main/assets/...` fixtures
|
||||
- implementation targets discovered while wiring Studio asset persistence
|
||||
- related tests in `prometeu-studio/src/test/java/p/studio/workspaces/assets/...` and `prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/...`
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Add parser/service tests proving `Scene Bank` assets can be created and read through the asset declaration pipeline.
|
||||
- Add Studio projection tests proving summary/details surfaces can represent base family plus specialization.
|
||||
- Add tests for the support-file metadata model, including `1..4` layer constraints and explicit `layer -> tilemap` mapping requirements.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Extend asset workspace service tests to verify end-to-end creation of a `Scene Bank` asset root and detail retrieval.
|
||||
- Add or update test fixtures under `test-projects/main/assets` to cover specialized glyph banks and scene-bank roots with multiple tilemaps.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Create a new `Scene Bank` from the Assets workspace and confirm the asset appears in the list/details flow.
|
||||
- Create or mark a `Glyph Bank` as `Tileset` and confirm the summary shows base family plus specialization chip.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `Scene Bank` exists as a first-class asset choice in the Studio asset flow.
|
||||
- [ ] Studio-only glyph-bank specialization is represented by a closed metadata set and does not redefine the runtime asset family.
|
||||
- [ ] The Assets workspace can display base family and specialization together in the summary/details flow.
|
||||
- [ ] Scene-bank support metadata explicitly records `layer -> tilemap` mapping and enforces the wave-1 `4`-layer limit.
|
||||
- [ ] No plan step introduces a dedicated scene workspace or a normalized Studio-owned scene format.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `DEC-0027`.
|
||||
- Unblocks `PLN-0054` and `PLN-0055`.
|
||||
|
||||
## Risks
|
||||
|
||||
- The current asset declaration pipeline may assume existing family/output combinations too rigidly, which can force deeper changes in packer services than the UI suggests.
|
||||
- If specialization metadata is embedded in the wrong layer, Studio-only editorial flags may leak into runtime-facing contracts.
|
||||
- If the support-file baseline is too vague, later parser and validation work will drift on naming and ownership.
|
||||
@ -1,120 +0,0 @@
|
||||
---
|
||||
id: PLN-0054
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Tiled XML IO and Scene Bank File Orchestration
|
||||
status: open
|
||||
created: 2026-04-17
|
||||
completed:
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene
|
||||
- tiled
|
||||
- parser
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the wave-1 XML read/write pipeline for `TMX` and `TSX`, including deterministic file placement and reference orchestration between `Scene Bank` tilemaps and `Tileset`-specialized glyph-bank assets.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0027` requires wave-1 XML read/write support for both `TMX` and `TSX`, keeps `TMX` as the maintained scene document, and requires each tilemap to reference exactly one generated `TSX`. It also keeps naming details implementation-owned, but requires the resulting convention to be canonical and compatible with explicit `layer -> tilemap` mapping.
|
||||
|
||||
The repository already contains example `TMX` and `TSX` files under `test-projects/main/assets/scenes` and `test-projects/main/assets/Zelda3`, which can be promoted into fixture coverage.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Add XML read/write support for supported `TMX` and `TSX` surfaces.
|
||||
- Generate `TSX` from `Tileset`-specialized glyph-bank assets.
|
||||
- Generate and maintain one or more `TMX` files inside a `Scene Bank` asset root.
|
||||
- Define and implement a canonical naming/reference convention for multiple `TMX` files compatible with scene support metadata.
|
||||
- Preserve supported wave-1 Tiled features: object layers, properties, flips, and collisions.
|
||||
|
||||
### Excluded
|
||||
- Studio UX for validating and accepting external edits.
|
||||
- Pack/runtime materialization of scene data.
|
||||
- Support for infinite maps, templates, Wang sets, or animations.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Introduce Tiled XML domain models and codecs
|
||||
|
||||
**What:** Create the XML-side models and serialization services for supported `TMX` and `TSX`.
|
||||
|
||||
**How:** Add parser/writer services that cover the accepted wave-1 subset, preserving round-trip stability for supported fields and producing explicit diagnostics for unsupported constructs. Keep the implementation aligned to XML as the primary interoperability surface and avoid JSON-export dependencies.
|
||||
|
||||
**File(s):**
|
||||
- new `prometeu-studio/src/main/java/p/studio/.../tiled/...` package(s) discovered during implementation
|
||||
- related tests under `prometeu-studio/src/test/java/p/studio/.../tiled/...`
|
||||
|
||||
### Step 2 - Generate TSX from Tileset-specialized glyph banks
|
||||
|
||||
**What:** Add the generation path that emits `TSX` for `Glyph Bank` assets specialized as `Tileset`.
|
||||
|
||||
**How:** Extend the asset action/details flow so a `Tileset` glyph bank can materialize its Tiled-facing `TSX` file in the asset root. The generated `TSX` MUST include the supported tileset-side collision/properties surface needed by wave 1 and MUST remain deterministic for later scene references.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/...`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetAction.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java`
|
||||
- implementation packages created for Tiled file generation
|
||||
|
||||
### Step 3 - Generate and maintain TMX files inside Scene Bank assets
|
||||
|
||||
**What:** Add the file orchestration path for one scene with up to `4` layer-linked tilemaps.
|
||||
|
||||
**How:** Use the scene-bank support metadata from `PLN-0053` to generate and maintain one or more canonical `TMX` files in the scene asset root. Each `TMX` MUST reference exactly one generated `TSX` from a `Tileset` glyph-bank asset, and the chosen naming convention MUST remain stable across regenerate/validate cycles.
|
||||
|
||||
**File(s):**
|
||||
- scene-bank persistence/orchestration services added under `prometeu-studio/src/main/java/p/studio/...`
|
||||
- test fixtures under `test-projects/main/assets/scenes` and related asset roots
|
||||
|
||||
### Step 4 - Enforce supported subset and failure behavior
|
||||
|
||||
**What:** Formalize the wave-1 supported subset and rejection behavior for unsupported input.
|
||||
|
||||
**How:** Add diagnostic-producing validation around XML ingest so unsupported Tiled constructs fail explicitly and do not silently normalize away unsupported content. Cover tile-owned collision in `TSX` and map-owned collision via `Object Layer` in `TMX`.
|
||||
|
||||
**File(s):**
|
||||
- Tiled parser/validator packages introduced in this plan
|
||||
- diagnostics surfaces reused by the Assets workspace
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Add TMX parser/writer round-trip tests for supported map metadata, layers, object layers, properties, flips, and collision cases.
|
||||
- Add TSX parser/writer round-trip tests for supported tileset metadata and tile-owned collision/properties.
|
||||
- Add tests for rejection/diagnostics on unsupported features such as infinite maps, templates, Wang sets, and animations.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Add end-to-end fixture tests proving a `Tileset` glyph bank can generate `TSX` and a `Scene Bank` can generate one or more `TMX` files referencing it.
|
||||
- Add tests proving canonical `TMX` naming remains stable across regeneration using the same support metadata.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Generate `TSX` from a `Tileset` glyph bank and open it in Tiled.
|
||||
- Generate a multi-tilemap scene-bank root and confirm each `TMX` opens in Tiled and points to the expected `TSX`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Studio can read and write the supported XML subset for both `TMX` and `TSX`.
|
||||
- [ ] `Tileset`-specialized glyph banks can generate deterministic `TSX` files.
|
||||
- [ ] `Scene Bank` assets can maintain multiple canonical `TMX` files in one asset root.
|
||||
- [ ] Every generated tilemap references exactly one generated `TSX` in wave 1.
|
||||
- [ ] Unsupported Tiled features fail with explicit diagnostics instead of silent degradation.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `DEC-0027`.
|
||||
- Depends on `PLN-0053` for asset metadata and support-file ownership.
|
||||
- Unblocks `PLN-0055`.
|
||||
|
||||
## Risks
|
||||
|
||||
- XML round-trip fidelity can drift if parser and writer models are not designed from the same canonical structure.
|
||||
- Canonical naming and path resolution may become unstable if they are derived from mutable UI labels instead of explicit metadata.
|
||||
- TSX generation can accidentally encode Studio-only specialization state into runtime-facing data if the boundary is not kept strict.
|
||||
@ -1,120 +0,0 @@
|
||||
---
|
||||
id: PLN-0055
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Assets Workspace Scene Bank Validation and Acceptance Flow
|
||||
status: open
|
||||
created: 2026-04-17
|
||||
completed:
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene
|
||||
- validation
|
||||
- ux
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the `Assets` workspace flow that validates external `Tiled` edits, surfaces diagnostics, and requires explicit Studio-side acceptance before a `Scene Bank` is considered ready.
|
||||
|
||||
## Background
|
||||
|
||||
`DEC-0027` requires the initial workflow to be:
|
||||
|
||||
1. create the scene asset in Studio;
|
||||
2. edit generated `TMX`/`TSX` in `Tiled`;
|
||||
3. return to Studio;
|
||||
4. validate;
|
||||
5. explicitly accept the converged changes.
|
||||
|
||||
The decision explicitly forbids silent auto-acceptance and requires the same general diagnostic discipline already used for glyph assets. The current `Assets` workspace already contains refresh, detail, action, and diagnostics surfaces that can host this flow without creating a dedicated scene workspace.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Add scene-bank validation state to the `Assets` workspace projections.
|
||||
- Add explicit actions to validate and accept external scene-bank changes.
|
||||
- Surface scene/Tiled diagnostics in the existing asset details/logging patterns.
|
||||
- Ensure readiness/build participation reflects acceptance state for scene banks.
|
||||
|
||||
### Excluded
|
||||
- Automatic merge/convergence of external edits.
|
||||
- A dedicated scene-editing workspace.
|
||||
- Pack/runtime publication.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Extend asset details state for scene-bank validation
|
||||
|
||||
**What:** Add the projection/state needed to represent scene-bank validation and acceptance status in the assets UI.
|
||||
|
||||
**How:** Extend the asset workspace message and detail-state models so scene-bank assets can expose pending external changes, validation result, acceptance readiness, and scene-specific actions while reusing the current details/action host rather than adding a separate workspace.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceDetailsViewState.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetAction.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/dialogs/AssetDiagnosticsDialog.java`
|
||||
|
||||
### Step 2 - Add validate/accept actions to existing assets action surface
|
||||
|
||||
**What:** Add the concrete user actions for scene-bank validation and explicit acceptance.
|
||||
|
||||
**How:** Extend the existing asset actions surface so scene-bank assets can trigger validation against current `TMX`/`TSX` and then explicitly accept the converged result. Keep these actions stacked with existing asset actions, in line with `DEC-0027`, and ensure availability is driven by asset family/state rather than a new intermediate panel.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/summary/AssetDetailsSummaryControl.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsUiSupport.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/...` action-host controls discovered during implementation
|
||||
- `prometeu-studio/src/main/resources/i18n/messages.properties`
|
||||
|
||||
### Step 3 - Connect validation results to diagnostics and readiness gates
|
||||
|
||||
**What:** Make validation outcome meaningful to the workflow.
|
||||
|
||||
**How:** Reuse the current diagnostic/log/event patterns so unsupported references, broken paths, invalid tilemap-to-layer mapping, and unsupported Tiled constructs surface as diagnostics. Mark the scene bank as not ready until validation succeeds and explicit acceptance is recorded. Reflect this status in build participation or equivalent readiness signals without implementing pack publication itself.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/events/...`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetLogsPane.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java`
|
||||
- any validation coordinator/services created for scene-bank acceptance
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Add tests for scene-bank details state transitions: created, externally changed, validation failed, validation passed, accepted.
|
||||
- Add tests proving accept is unavailable when validation diagnostics remain.
|
||||
- Add tests proving scene-bank actions are hosted in the existing asset actions surface.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Add assets workspace tests covering refresh after external file edits, validation, diagnostics surfacing, and explicit acceptance.
|
||||
- Add fixture-backed tests covering invalid TSX references, unsupported TMX features, and broken layer-to-tilemap mappings.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Create a scene bank in Studio, edit its `TMX`/`TSX` externally in Tiled, return to Studio, validate, and explicitly accept.
|
||||
- Confirm the asset stays unready when diagnostics remain and becomes ready only after successful validation plus explicit acceptance.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Scene-bank assets expose validate/accept actions in the existing asset action surface.
|
||||
- [ ] Studio surfaces scene-bank diagnostics using the existing general diagnostic discipline.
|
||||
- [ ] Studio does not silently auto-accept external `Tiled` edits.
|
||||
- [ ] A scene bank is not considered ready until validation succeeds and the user explicitly accepts the result.
|
||||
- [ ] No part of the workflow introduces a dedicated scene workspace.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `DEC-0027`.
|
||||
- Depends on `PLN-0053` for asset/state foundations.
|
||||
- Depends on `PLN-0054` for TMX/TSX ingest and validation inputs.
|
||||
|
||||
## Risks
|
||||
|
||||
- If refresh and validation state are coupled too loosely, the UI can show stale readiness after external edits.
|
||||
- If acceptance is represented only in transient UI state, Studio may lose the required gate after reload.
|
||||
- If diagnostics are not normalized with existing asset flows, scene-bank errors will feel like a separate subsystem.
|
||||
@ -5,7 +5,7 @@ import java.util.Locale;
|
||||
|
||||
public enum OutputFormatCatalog {
|
||||
GLYPH_INDEXED_V1(AssetFamilyCatalog.GLYPH_BANK, "GLYPH/indexed_v1", "GLYPH/indexed_v1"),
|
||||
SCENE_TILED_V1(AssetFamilyCatalog.SCENE_BANK, "SCENE/tiled_v1", "SCENE/tiled_v1"),
|
||||
SCENE_TILED_V1(AssetFamilyCatalog.SCENE_BANK, "SCENE/tilemap_v1", "SCENE/tilemap_v1"),
|
||||
SOUND_V1(AssetFamilyCatalog.SOUND_BANK, "SOUND/v1", "SOUND/v1"),
|
||||
UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown");
|
||||
|
||||
@ -54,6 +54,9 @@ public enum OutputFormatCatalog {
|
||||
return UNKNOWN;
|
||||
}
|
||||
final String normalized = manifestValue.trim().toLowerCase(Locale.ROOT);
|
||||
if ("scene/tiled_v1".equals(normalized)) {
|
||||
return SCENE_TILED_V1;
|
||||
}
|
||||
if ("sound/bank_v1".equals(normalized)) {
|
||||
return SOUND_V1;
|
||||
}
|
||||
|
||||
@ -3,11 +3,15 @@ package p.packer.models;
|
||||
import java.util.Objects;
|
||||
|
||||
public record PackerAssetArtifactSelection(
|
||||
String artifactId,
|
||||
String file,
|
||||
int index) {
|
||||
int index,
|
||||
String fingerprint) {
|
||||
|
||||
public PackerAssetArtifactSelection {
|
||||
artifactId = artifactId == null || artifactId.isBlank() ? null : artifactId.trim();
|
||||
file = Objects.requireNonNull(file, "file").trim();
|
||||
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint.trim();
|
||||
if (file.isBlank()) {
|
||||
throw new IllegalArgumentException("file must not be blank");
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import p.packer.PackerWorkspacePaths;
|
||||
import p.packer.PackerWorkspaceService;
|
||||
import p.packer.events.PackerEventKind;
|
||||
@ -49,6 +50,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
private final FileSystemPackerCacheRepository cacheRepository;
|
||||
private final PackerProjectWriteCoordinator writeCoordinator;
|
||||
private final PackerEventSink eventSink;
|
||||
private final PackerGlyphMaterialSupport glyphMaterialSupport;
|
||||
|
||||
public FileSystemPackerWorkspaceService(
|
||||
ObjectMapper mapper,
|
||||
@ -69,6 +71,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
|
||||
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
|
||||
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
|
||||
this.glyphMaterialSupport = new PackerGlyphMaterialSupport(mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -82,7 +85,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
public ListAssetsResult listAssets(ListAssetsRequest request) {
|
||||
final var project = Objects.requireNonNull(request, "request").project();
|
||||
final var snapshot = request.deepSync()
|
||||
? runtimeRegistry.refresh(project).snapshot()
|
||||
? writeCoordinator.execute(project, () -> refreshAfterArtifactIdentityBackfill(project))
|
||||
: runtimeRegistry.getOrLoad(project).snapshot();
|
||||
final var events = new PackerOperationEventEmitter(project, eventSink);
|
||||
final var registry = snapshot.registry();
|
||||
@ -146,7 +149,11 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
|
||||
@Override
|
||||
public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) {
|
||||
return detailsService.getAssetDetails(request);
|
||||
final GetAssetDetailsRequest safeRequest = Objects.requireNonNull(request, "request");
|
||||
return writeCoordinator.execute(safeRequest.project(), () -> {
|
||||
backfillLoadedArtifactIdentity(safeRequest.project());
|
||||
return detailsService.getAssetDetails(safeRequest);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -172,7 +179,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
@Override
|
||||
public ValidatePackWorkspaceResult validatePackWorkspace(ValidatePackWorkspaceRequest request) {
|
||||
final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
|
||||
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
||||
final PackerRuntimeSnapshot snapshot = writeCoordinator.execute(project, () -> refreshAfterArtifactIdentityBackfill(project));
|
||||
final List<PackerPackValidationAssetDTO> blockedAssets = snapshot.assets().stream()
|
||||
.filter(this::isEffectivelyIncludedInPackSet)
|
||||
.map(this::toPackValidationAssetDTO)
|
||||
@ -322,7 +329,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
final long startedAt = System.currentTimeMillis();
|
||||
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
|
||||
|
||||
final PackerRuntimeSnapshot refreshedSnapshot = runtimeRegistry.refresh(project).snapshot();
|
||||
final PackerRuntimeSnapshot refreshedSnapshot = refreshAfterArtifactIdentityBackfill(project);
|
||||
final PackerRuntimeSnapshot frozenSnapshot = createFrozenPackingSnapshot(refreshedSnapshot);
|
||||
final List<PackerPackValidationAssetDTO> blockedAssets = frozenSnapshot.assets().stream()
|
||||
.filter(this::isEffectivelyIncludedInPackSet)
|
||||
@ -500,6 +507,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
}
|
||||
|
||||
try {
|
||||
if (request.includedInBuild() && !sceneBankCanEnterBuild(assetRoot)) {
|
||||
return new UpdateAssetBuildParticipationResult(
|
||||
PackerOperationStatus.FAILED,
|
||||
"Scene Bank must enable at least one layer before entering the build.",
|
||||
null);
|
||||
}
|
||||
final PackerRegistryEntry currentEntry = evaluation.resolved().registryEntry().orElseThrow();
|
||||
final PackerRegistryEntry updatedEntry = new PackerRegistryEntry(
|
||||
currentEntry.assetId(),
|
||||
@ -677,17 +690,49 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
supportFile.put("schema_version", 1);
|
||||
supportFile.put("map_width", 16);
|
||||
supportFile.put("map_height", 16);
|
||||
supportFile.put("layer_count", 1);
|
||||
supportFile.put("layers", List.of(Map.of(
|
||||
"index", 1,
|
||||
"name", "Layer 1",
|
||||
"tilemap", "layer-1.tmx",
|
||||
"tileset_asset_root", "")));
|
||||
supportFile.put("layer_count", 4);
|
||||
final List<Map<String, Object>> layers = new ArrayList<>();
|
||||
for (int index = 1; index <= 4; index += 1) {
|
||||
layers.add(Map.of(
|
||||
"index", index,
|
||||
"enabled", false,
|
||||
"name", "Layer " + index,
|
||||
"width", 16,
|
||||
"height", 16,
|
||||
"tilemap", "layer-" + index + ".tmx",
|
||||
"tileset_asset_root", ""));
|
||||
}
|
||||
supportFile.put("layers", layers);
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
assetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
|
||||
supportFile);
|
||||
}
|
||||
|
||||
private boolean sceneBankCanEnterBuild(Path assetRoot) {
|
||||
final Path supportFilePath = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFilePath)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(supportFilePath.toFile());
|
||||
if (!(root instanceof ObjectNode objectNode)) {
|
||||
return false;
|
||||
}
|
||||
final JsonNode layersNode = objectNode.get("layers");
|
||||
if (!(layersNode instanceof ArrayNode layers)) {
|
||||
return false;
|
||||
}
|
||||
for (JsonNode layer : layers) {
|
||||
if (layer.path("enabled").asBoolean(false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (IOException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeRelativeAssetRoot(String candidate) {
|
||||
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
||||
if (raw.isBlank()) {
|
||||
@ -802,6 +847,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
parsed.diagnostics(),
|
||||
runtimeAsset.walkDiagnostics(),
|
||||
collectPackRelevantWalkFileDiagnostics(runtimeAsset),
|
||||
glyphMaterialSupport.validate(runtimeAsset),
|
||||
packExecutionSupportDiagnostics(parsed.declaration(), runtimeAsset.manifestPath()),
|
||||
identityMismatchDiagnostics(registryEntry, parsed, runtimeAsset.manifestPath()))
|
||||
.flatMap(Collection::stream)
|
||||
@ -852,6 +898,84 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
cacheRepository.save(project, snapshot.cacheState());
|
||||
}
|
||||
|
||||
private PackerRuntimeSnapshot refreshAfterArtifactIdentityBackfill(PackerProjectContext project) {
|
||||
final PackerRuntimeSnapshot snapshot = runtimeRegistry.refresh(project).snapshot();
|
||||
return backfillArtifactIdentity(snapshot)
|
||||
? runtimeRegistry.refresh(project).snapshot()
|
||||
: snapshot;
|
||||
}
|
||||
|
||||
private PackerRuntimeSnapshot backfillLoadedArtifactIdentity(PackerProjectContext project) {
|
||||
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
||||
return backfillArtifactIdentity(snapshot)
|
||||
? runtimeRegistry.refresh(project).snapshot()
|
||||
: snapshot;
|
||||
}
|
||||
|
||||
private boolean backfillArtifactIdentity(PackerRuntimeSnapshot snapshot) {
|
||||
boolean changed = false;
|
||||
for (PackerRuntimeAsset runtimeAsset : snapshot.assets()) {
|
||||
try {
|
||||
changed = backfillArtifactIdentity(runtimeAsset) || changed;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Unable to update artifact fingerprints for " + runtimeAsset.manifestPath(), exception);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean backfillArtifactIdentity(PackerRuntimeAsset runtimeAsset) throws IOException {
|
||||
if (runtimeAsset.parsedDeclaration().declaration() == null || runtimeAsset.parsedDeclaration().declaration().artifacts().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
final Path manifestPath = runtimeAsset.manifestPath();
|
||||
if (!Files.isRegularFile(manifestPath)) {
|
||||
return false;
|
||||
}
|
||||
final JsonNode rawManifest = mapper.readTree(manifestPath.toFile());
|
||||
if (!(rawManifest instanceof ObjectNode manifest) || !(manifest.path("artifacts") instanceof ArrayNode artifactsNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
|
||||
runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file));
|
||||
final Set<String> artifactIds = new HashSet<>();
|
||||
for (JsonNode artifactNode : artifactsNode) {
|
||||
if (artifactNode.isObject()) {
|
||||
final String artifactId = artifactNode.path("artifact_id").asText("").trim();
|
||||
if (!artifactId.isBlank()) {
|
||||
artifactIds.add(artifactId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
for (JsonNode artifactNode : artifactsNode) {
|
||||
if (!(artifactNode instanceof ObjectNode artifactObject)) {
|
||||
continue;
|
||||
}
|
||||
final String file = artifactObject.path("file").asText("").trim();
|
||||
if (file.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (artifactObject.path("artifact_id").asText("").isBlank()) {
|
||||
artifactObject.put("artifact_id", nextArtifactId(artifactIds));
|
||||
changed = true;
|
||||
}
|
||||
final String fingerprint = Optional.ofNullable(walkFilesByPath.get(file))
|
||||
.map(PackerRuntimeWalkFile::fingerprint)
|
||||
.orElse(null);
|
||||
if (fingerprint != null && !fingerprint.isBlank() && !fingerprint.equals(artifactObject.path("fingerprint").asText(null))) {
|
||||
artifactObject.put("fingerprint", fingerprint);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private PackerRuntimeSnapshot createFrozenPackingSnapshot(PackerRuntimeSnapshot snapshot) {
|
||||
final PackerRuntimeAssetMaterializer materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(mapper));
|
||||
final List<PackerRuntimeAsset> assets = snapshot.assets().stream()
|
||||
@ -1277,7 +1401,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
}
|
||||
|
||||
try {
|
||||
patchManifestArtifacts(manifest, request);
|
||||
patchManifestArtifacts(manifest, request, evaluation.resolved().runtimeAsset());
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
|
||||
currentSnapshot,
|
||||
@ -1388,21 +1512,81 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
});
|
||||
}
|
||||
|
||||
private void patchManifestArtifacts(ObjectNode manifest, ApplyBankCompositionRequest request) {
|
||||
private void patchManifestArtifacts(
|
||||
ObjectNode manifest,
|
||||
ApplyBankCompositionRequest request,
|
||||
Optional<PackerRuntimeAsset> runtimeAsset) {
|
||||
manifest.remove("inputs");
|
||||
|
||||
final Map<String, ExistingArtifactEntry> existingArtifactsByFile = existingArtifactsByFile(manifest.path("artifacts"));
|
||||
final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
|
||||
runtimeAsset.ifPresent(asset -> asset.walkProjection().buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file)));
|
||||
final var artifactsNode = manifest.putArray("artifacts");
|
||||
for (int index = 0; index < request.selectedFiles().size(); index += 1) {
|
||||
final String file = Objects.requireNonNullElse(request.selectedFiles().get(index), "").trim();
|
||||
if (file.isBlank() || !isTrustedRelativePath(file)) {
|
||||
throw new IllegalArgumentException("Selected artifact file paths must stay inside the asset root.");
|
||||
}
|
||||
final ExistingArtifactEntry existing = existingArtifactsByFile.get(file);
|
||||
final var artifactNode = artifactsNode.addObject();
|
||||
artifactNode.put("artifact_id", existing != null && existing.artifactId() != null ? existing.artifactId() : nextArtifactId());
|
||||
artifactNode.put("file", file);
|
||||
artifactNode.put("index", index);
|
||||
final String fingerprint = Optional.ofNullable(walkFilesByPath.get(file))
|
||||
.map(PackerRuntimeWalkFile::fingerprint)
|
||||
.orElse(existing == null ? null : existing.fingerprint());
|
||||
if (fingerprint != null && !fingerprint.isBlank()) {
|
||||
artifactNode.put("fingerprint", fingerprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, ExistingArtifactEntry> existingArtifactsByFile(JsonNode artifactsNode) {
|
||||
if (!(artifactsNode instanceof ArrayNode artifactsArray)) {
|
||||
return Map.of();
|
||||
}
|
||||
final Map<String, ExistingArtifactEntry> existing = new LinkedHashMap<>();
|
||||
for (JsonNode entry : artifactsArray) {
|
||||
if (!entry.isObject()) {
|
||||
continue;
|
||||
}
|
||||
final String file = entry.path("file").asText("").trim();
|
||||
if (file.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
existing.put(file, new ExistingArtifactEntry(
|
||||
blankToNull(entry.path("artifact_id").asText(null)),
|
||||
blankToNull(entry.path("fingerprint").asText(null))));
|
||||
}
|
||||
return Map.copyOf(existing);
|
||||
}
|
||||
|
||||
private String nextArtifactId() {
|
||||
return "art-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||
}
|
||||
|
||||
private String nextArtifactId(Set<String> existingArtifactIds) {
|
||||
String artifactId = nextArtifactId();
|
||||
while (existingArtifactIds.contains(artifactId)) {
|
||||
artifactId = nextArtifactId();
|
||||
}
|
||||
existingArtifactIds.add(artifactId);
|
||||
return artifactId;
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final String trimmed = value.trim();
|
||||
return trimmed.isBlank() ? null : trimmed;
|
||||
}
|
||||
|
||||
private record ExistingArtifactEntry(
|
||||
String artifactId,
|
||||
String fingerprint) {
|
||||
}
|
||||
|
||||
private void patchManifestPipelinePalettes(ObjectNode manifest, ApplyPaletteOverhaulingRequest request) {
|
||||
final ObjectNode outputNode = mutableObject(manifest, "output");
|
||||
final ObjectNode pipelineNode = mutableObject(outputNode, "pipeline");
|
||||
|
||||
@ -404,8 +404,10 @@ public final class PackerAssetDeclarationParser {
|
||||
true));
|
||||
continue;
|
||||
}
|
||||
final JsonNode artifactIdNode = entry.path("artifact_id");
|
||||
final JsonNode fileNode = entry.path("file");
|
||||
final JsonNode indexNode = entry.path("index");
|
||||
final JsonNode fingerprintNode = entry.path("fingerprint");
|
||||
if (!fileNode.isTextual() || fileNode.asText().isBlank() || !indexNode.isInt() || indexNode.asInt() < 0) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
@ -425,7 +427,13 @@ public final class PackerAssetDeclarationParser {
|
||||
true));
|
||||
continue;
|
||||
}
|
||||
result.add(new PackerAssetArtifactSelection(relativePath, indexNode.asInt()));
|
||||
final String artifactId = artifactIdNode.isTextual() && !artifactIdNode.asText().isBlank()
|
||||
? artifactIdNode.asText().trim()
|
||||
: null;
|
||||
final String fingerprint = fingerprintNode.isTextual() && !fingerprintNode.asText().isBlank()
|
||||
? fingerprintNode.asText().trim()
|
||||
: null;
|
||||
result.add(new PackerAssetArtifactSelection(artifactId, relativePath, indexNode.asInt(), fingerprint));
|
||||
}
|
||||
result.sort(Comparator.comparingInt(PackerAssetArtifactSelection::index));
|
||||
return List.copyOf(result);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package p.packer.services;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
@ -16,6 +17,8 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public final class PackerAssetDetailsService {
|
||||
private static final ObjectMapper GLYPH_MATERIAL_MAPPER = new ObjectMapper();
|
||||
|
||||
private final PackerRuntimeRegistry runtimeRegistry;
|
||||
private final PackerAssetReferenceResolver assetReferenceResolver;
|
||||
|
||||
@ -48,6 +51,7 @@ public final class PackerAssetDetailsService {
|
||||
diagnostics.addAll(parsed.diagnostics());
|
||||
diagnostics.addAll(runtimeAsset.walkDiagnostics());
|
||||
appendWalkFileDiagnostics(runtimeAsset, diagnostics);
|
||||
diagnostics.addAll(new PackerGlyphMaterialSupport(GLYPH_MATERIAL_MAPPER).validate(runtimeAsset));
|
||||
if (!parsed.valid()) {
|
||||
return failureResult(project, request.assetReference(), resolved, diagnostics);
|
||||
}
|
||||
@ -149,8 +153,9 @@ public final class PackerAssetDetailsService {
|
||||
.map(this::toBankCompositionFile)
|
||||
.toList();
|
||||
|
||||
final List<PackerBankCompositionFile> selectedFiles = selectedPathsFor(declaration).stream()
|
||||
.map(path -> resolveSelectedBankFile(runtimeAsset.assetRoot(), path, walkFilesByPath, assetCacheEntry))
|
||||
final List<PackerBankCompositionFile> selectedFiles = declaration.artifacts().stream()
|
||||
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
|
||||
.map(artifact -> resolveSelectedBankFile(runtimeAsset.assetRoot(), artifact, walkFilesByPath, assetCacheEntry))
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
|
||||
@ -160,13 +165,6 @@ public final class PackerAssetDetailsService {
|
||||
walkProjection.measuredBankSizeBytes());
|
||||
}
|
||||
|
||||
private List<String> selectedPathsFor(PackerAssetDeclaration declaration) {
|
||||
return declaration.artifacts().stream()
|
||||
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
|
||||
.map(PackerAssetArtifactSelection::file)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> resolvePipelinePalettes(PackerAssetDeclaration declaration) {
|
||||
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
|
||||
if (!(palettesNode instanceof ArrayNode palettesArray)) {
|
||||
@ -219,16 +217,17 @@ public final class PackerAssetDetailsService {
|
||||
|
||||
private Optional<PackerBankCompositionFile> resolveSelectedBankFile(
|
||||
Path assetRoot,
|
||||
String relativePath,
|
||||
PackerAssetArtifactSelection artifact,
|
||||
Map<String, PackerRuntimeWalkFile> walkFilesByPath,
|
||||
Optional<PackerAssetCacheEntry> assetCacheEntry) {
|
||||
final String relativePath = artifact.file();
|
||||
final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath);
|
||||
if (runtimeWalkFile != null) {
|
||||
return Optional.of(toBankCompositionFile(runtimeWalkFile));
|
||||
return Optional.of(toSelectedBankCompositionFile(runtimeWalkFile, artifact));
|
||||
}
|
||||
final Optional<PackerFileCacheEntry> cachedFile = assetCacheEntry.flatMap(entry -> entry.findFile(relativePath));
|
||||
if (cachedFile.isPresent()) {
|
||||
return Optional.of(toBankCompositionFile(cachedFile.get()));
|
||||
return Optional.of(toSelectedBankCompositionFile(cachedFile.get(), artifact));
|
||||
}
|
||||
|
||||
final Path filePath = assetRoot.resolve(relativePath).toAbsolutePath().normalize();
|
||||
@ -242,8 +241,8 @@ public final class PackerAssetDetailsService {
|
||||
filePath.getFileName().toString(),
|
||||
Files.size(filePath),
|
||||
Files.getLastModifiedTime(filePath).toMillis(),
|
||||
null,
|
||||
Map.of()));
|
||||
artifact.fingerprint(),
|
||||
selectedArtifactMetadata(artifact, Map.of())));
|
||||
} catch (Exception exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@ -269,6 +268,43 @@ public final class PackerAssetDetailsService {
|
||||
file.metadata());
|
||||
}
|
||||
|
||||
private PackerBankCompositionFile toSelectedBankCompositionFile(
|
||||
PackerRuntimeWalkFile file,
|
||||
PackerAssetArtifactSelection artifact) {
|
||||
return new PackerBankCompositionFile(
|
||||
file.relativePath(),
|
||||
Path.of(file.relativePath()).getFileName().toString(),
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
selectedArtifactMetadata(artifact, file.metadata()));
|
||||
}
|
||||
|
||||
private PackerBankCompositionFile toSelectedBankCompositionFile(
|
||||
PackerFileCacheEntry file,
|
||||
PackerAssetArtifactSelection artifact) {
|
||||
return new PackerBankCompositionFile(
|
||||
file.relativePath(),
|
||||
Path.of(file.relativePath()).getFileName().toString(),
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
selectedArtifactMetadata(artifact, file.metadata()));
|
||||
}
|
||||
|
||||
private Map<String, Object> selectedArtifactMetadata(
|
||||
PackerAssetArtifactSelection artifact,
|
||||
Map<String, Object> metadata) {
|
||||
final Map<String, Object> decorated = new LinkedHashMap<>(metadata);
|
||||
if (artifact.artifactId() != null) {
|
||||
decorated.put("artifactId", artifact.artifactId());
|
||||
}
|
||||
if (artifact.fingerprint() != null) {
|
||||
decorated.put("selectedFingerprint", artifact.fingerprint());
|
||||
}
|
||||
return Map.copyOf(decorated);
|
||||
}
|
||||
|
||||
private List<PackerCodecConfigurationField> metadataFields(
|
||||
List<PackerCodecConfigurationField> definitions,
|
||||
Map<String, String> outputMetadata) {
|
||||
|
||||
@ -0,0 +1,333 @@
|
||||
package p.packer.services;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||
import p.packer.models.PackerAssetArtifactSelection;
|
||||
import p.packer.models.PackerAssetDeclaration;
|
||||
import p.packer.models.PackerDiagnostic;
|
||||
import p.packer.models.PackerRuntimeAsset;
|
||||
import p.packer.models.PackerRuntimeWalkFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
final class PackerGlyphMaterialSupport {
|
||||
static final String GLYPH_MATERIAL_SUPPORT_FILE = "glyph-material.json";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
PackerGlyphMaterialSupport(ObjectMapper mapper) {
|
||||
this.mapper = Objects.requireNonNull(mapper, "mapper");
|
||||
}
|
||||
|
||||
List<PackerDiagnostic> validate(PackerRuntimeAsset runtimeAsset) {
|
||||
final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
|
||||
if (declaration == null || declaration.assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return List.of();
|
||||
}
|
||||
final Path supportFile = runtimeAsset.assetRoot().resolve(GLYPH_MATERIAL_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFile)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||
final Optional<GlyphMaterialFile> parsed = readSupportFile(supportFile, diagnostics);
|
||||
if (parsed.isEmpty()) {
|
||||
return List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
final Map<String, PackerAssetArtifactSelection> selectedArtifactsById = new LinkedHashMap<>();
|
||||
for (PackerAssetArtifactSelection artifact : declaration.artifacts()) {
|
||||
if (artifact.artifactId() != null) {
|
||||
selectedArtifactsById.put(artifact.artifactId(), artifact);
|
||||
}
|
||||
}
|
||||
final Map<String, PackerRuntimeWalkFile> selectedFilesByPath = new LinkedHashMap<>();
|
||||
runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> selectedFilesByPath.put(file.relativePath(), file));
|
||||
|
||||
final Map<String, SchemaEvaluation> schemaEvaluations = new LinkedHashMap<>();
|
||||
for (GlyphMaterialSchema schema : parsed.get().schemas()) {
|
||||
final boolean declaredInconsistent = schema.status() == GlyphMaterialSchemaStatus.INCONSISTENT;
|
||||
boolean drifted = false;
|
||||
for (GlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
|
||||
final PackerAssetArtifactSelection artifact = selectedArtifactsById.get(binding.artifactId());
|
||||
if (artifact == null) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material schema '" + schema.schemaId() + "' references unknown selected artifact id '" + binding.artifactId() + "'.",
|
||||
supportFile));
|
||||
drifted = true;
|
||||
continue;
|
||||
}
|
||||
final PackerRuntimeWalkFile currentFile = selectedFilesByPath.get(artifact.file());
|
||||
final String currentFingerprint = currentFile != null
|
||||
? currentFile.fingerprint()
|
||||
: artifact.fingerprint();
|
||||
if (currentFingerprint == null || !currentFingerprint.equals(binding.fingerprint())) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material schema '" + schema.schemaId() + "' is inconsistent because artifact '" + binding.artifactId() + "' drifted.",
|
||||
supportFile));
|
||||
drifted = true;
|
||||
}
|
||||
}
|
||||
if (declaredInconsistent) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material schema '" + schema.schemaId() + "' is marked inconsistent.",
|
||||
supportFile));
|
||||
}
|
||||
schemaEvaluations.put(schema.schemaId(), new SchemaEvaluation(schema, declaredInconsistent || drifted));
|
||||
}
|
||||
|
||||
for (GlyphMaterialPalette palette : parsed.get().palettes()) {
|
||||
final SchemaEvaluation schema = schemaEvaluations.get(palette.schemaId());
|
||||
if (schema == null) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' references unknown schema '" + palette.schemaId() + "'.",
|
||||
supportFile));
|
||||
continue;
|
||||
}
|
||||
if (palette.colors().size() != schema.schema().materialIndices().size()) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' must define exactly "
|
||||
+ schema.schema().materialIndices().size() + " colors for schema '" + palette.schemaId() + "'.",
|
||||
supportFile));
|
||||
}
|
||||
if (palette.version() < schema.schema().version()) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' is stale because version "
|
||||
+ palette.version() + " is older than schema '" + palette.schemaId() + "' version "
|
||||
+ schema.schema().version() + ".",
|
||||
supportFile));
|
||||
}
|
||||
if (schema.inconsistent()) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' is invalid because schema '"
|
||||
+ palette.schemaId() + "' is inconsistent.",
|
||||
supportFile));
|
||||
}
|
||||
}
|
||||
|
||||
return List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
private Optional<GlyphMaterialFile> readSupportFile(Path supportFile, List<PackerDiagnostic> diagnostics) {
|
||||
final JsonNode root;
|
||||
try {
|
||||
root = mapper.readTree(supportFile.toFile());
|
||||
} catch (IOException exception) {
|
||||
diagnostics.add(blocking("Unable to parse glyph-material.json: " + exception.getMessage(), supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (root == null || !root.isObject()) {
|
||||
diagnostics.add(blocking("glyph-material.json must contain a JSON object at the root.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(root.path("schemas") instanceof ArrayNode schemasNode) || !(root.path("palettes") instanceof ArrayNode palettesNode)) {
|
||||
diagnostics.add(blocking("glyph-material.json must define array fields 'schemas' and 'palettes'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final List<GlyphMaterialSchema> schemas = new ArrayList<>();
|
||||
final Set<String> schemaIds = new HashSet<>();
|
||||
for (JsonNode schemaNode : schemasNode) {
|
||||
final Optional<GlyphMaterialSchema> schema = parseSchema(schemaNode, supportFile, diagnostics);
|
||||
if (schema.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!schemaIds.add(schema.get().schemaId())) {
|
||||
diagnostics.add(blocking("glyph-material.json contains duplicate schema_id '" + schema.get().schemaId() + "'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
schemas.add(schema.get());
|
||||
}
|
||||
|
||||
final List<GlyphMaterialPalette> palettes = new ArrayList<>();
|
||||
final Set<String> paletteIds = new HashSet<>();
|
||||
for (JsonNode paletteNode : palettesNode) {
|
||||
final Optional<GlyphMaterialPalette> palette = parsePalette(paletteNode, schemaIds, supportFile, diagnostics);
|
||||
if (palette.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!paletteIds.add(palette.get().paletteId())) {
|
||||
diagnostics.add(blocking("glyph-material.json contains duplicate palette_id '" + palette.get().paletteId() + "'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
palettes.add(palette.get());
|
||||
}
|
||||
return Optional.of(new GlyphMaterialFile(List.copyOf(schemas), List.copyOf(palettes)));
|
||||
}
|
||||
|
||||
private Optional<GlyphMaterialSchema> parseSchema(
|
||||
JsonNode schemaNode,
|
||||
Path supportFile,
|
||||
List<PackerDiagnostic> diagnostics) {
|
||||
if (!schemaNode.isObject()) {
|
||||
diagnostics.add(blocking("Each glyph material schema must be an object.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final String schemaId = schemaNode.path("schema_id").asText("").trim();
|
||||
final GlyphMaterialSchemaStatus status = GlyphMaterialSchemaStatus.fromManifestValue(schemaNode.path("status").asText(null));
|
||||
final int version = schemaNode.path("version").asInt(0);
|
||||
if (schemaId.isBlank() || status == null || version <= 0) {
|
||||
diagnostics.add(blocking("Each glyph material schema must define non-blank 'schema_id', valid 'status', and positive 'version'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final JsonNode materialIndicesNode = schemaNode.path("material_indices");
|
||||
if (!materialIndicesNode.isObject()) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define object 'material_indices'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final Map<Integer, String> materialIndices = new TreeMap<>();
|
||||
materialIndicesNode.fields().forEachRemaining(entry -> {
|
||||
try {
|
||||
final int index = Integer.parseInt(entry.getKey());
|
||||
final String color = normalizeHtmlColor(entry.getValue().asText(null));
|
||||
if (index >= 1 && index <= 15 && color != null) {
|
||||
materialIndices.put(index, color);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// validated below
|
||||
}
|
||||
});
|
||||
if (materialIndices.isEmpty()
|
||||
|| materialIndices.size() != materialIndicesNode.size()
|
||||
|| !denseIndices(materialIndices.keySet())) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define dense material_indices in range 1..15 using #RRGGBB values.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(schemaNode.path("artifact_bindings") instanceof ArrayNode bindingsNode) || bindingsNode.isEmpty()) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define non-empty 'artifact_bindings'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final List<GlyphMaterialArtifactBinding> bindings = new ArrayList<>();
|
||||
final Set<String> artifactIds = new HashSet<>();
|
||||
for (JsonNode bindingNode : bindingsNode) {
|
||||
final String artifactId = bindingNode.path("artifact_id").asText("").trim();
|
||||
final String fingerprint = bindingNode.path("fingerprint").asText("").trim();
|
||||
if (artifactId.isBlank() || fingerprint.isBlank() || !artifactIds.add(artifactId)) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define unique artifact bindings with 'artifact_id' and 'fingerprint'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
bindings.add(new GlyphMaterialArtifactBinding(artifactId, fingerprint));
|
||||
}
|
||||
return Optional.of(new GlyphMaterialSchema(schemaId, status, version, Map.copyOf(materialIndices), List.copyOf(bindings)));
|
||||
}
|
||||
|
||||
private Optional<GlyphMaterialPalette> parsePalette(
|
||||
JsonNode paletteNode,
|
||||
Set<String> schemaIds,
|
||||
Path supportFile,
|
||||
List<PackerDiagnostic> diagnostics) {
|
||||
if (!paletteNode.isObject()) {
|
||||
diagnostics.add(blocking("Each glyph material palette must be an object.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final String paletteId = paletteNode.path("palette_id").asText("").trim();
|
||||
final String schemaId = paletteNode.path("schema_id").asText("").trim();
|
||||
final int version = paletteNode.path("version").asInt(0);
|
||||
if (paletteId.isBlank() || schemaId.isBlank() || version <= 0 || !schemaIds.contains(schemaId)) {
|
||||
diagnostics.add(blocking("Each glyph material palette must define non-blank 'palette_id', valid 'schema_id', and positive 'version'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(paletteNode.path("colors") instanceof ArrayNode colorsNode)) {
|
||||
diagnostics.add(blocking("Palette '" + paletteId + "' must define array 'colors'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (JsonNode colorNode : colorsNode) {
|
||||
final String color = normalizeHtmlColor(colorNode.asText(null));
|
||||
if (color == null) {
|
||||
diagnostics.add(blocking("Palette '" + paletteId + "' must use #RRGGBB colors.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
colors.add(color);
|
||||
}
|
||||
return Optional.of(new GlyphMaterialPalette(paletteId, schemaId, version, List.copyOf(colors)));
|
||||
}
|
||||
|
||||
private static PackerDiagnostic blocking(String message, Path path) {
|
||||
return new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
message,
|
||||
path,
|
||||
true);
|
||||
}
|
||||
|
||||
private boolean denseIndices(Set<Integer> indices) {
|
||||
int expected = 1;
|
||||
for (Integer index : new TreeSet<>(indices)) {
|
||||
if (index == null || index != expected) {
|
||||
return false;
|
||||
}
|
||||
expected += 1;
|
||||
}
|
||||
return !indices.isEmpty();
|
||||
}
|
||||
|
||||
private String normalizeHtmlColor(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final String normalized = value.trim().toUpperCase();
|
||||
return normalized.matches("^#[0-9A-F]{6}$") ? normalized : null;
|
||||
}
|
||||
|
||||
private record GlyphMaterialFile(
|
||||
List<GlyphMaterialSchema> schemas,
|
||||
List<GlyphMaterialPalette> palettes) {
|
||||
}
|
||||
|
||||
private record GlyphMaterialSchema(
|
||||
String schemaId,
|
||||
GlyphMaterialSchemaStatus status,
|
||||
int version,
|
||||
Map<Integer, String> materialIndices,
|
||||
List<GlyphMaterialArtifactBinding> artifactBindings) {
|
||||
}
|
||||
|
||||
private record GlyphMaterialPalette(
|
||||
String paletteId,
|
||||
String schemaId,
|
||||
int version,
|
||||
List<String> colors) {
|
||||
}
|
||||
|
||||
private record GlyphMaterialArtifactBinding(
|
||||
String artifactId,
|
||||
String fingerprint) {
|
||||
}
|
||||
|
||||
private record SchemaEvaluation(
|
||||
GlyphMaterialSchema schema,
|
||||
boolean inconsistent) {
|
||||
}
|
||||
|
||||
private enum GlyphMaterialSchemaStatus {
|
||||
OK("ok"),
|
||||
INCONSISTENT("inconsistent");
|
||||
|
||||
private final String manifestValue;
|
||||
|
||||
GlyphMaterialSchemaStatus(String manifestValue) {
|
||||
this.manifestValue = manifestValue;
|
||||
}
|
||||
|
||||
static GlyphMaterialSchemaStatus fromManifestValue(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (GlyphMaterialSchemaStatus candidate : values()) {
|
||||
if (candidate.manifestValue.equalsIgnoreCase(value.trim())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,7 +81,7 @@ final class PackerRuntimeAssetMaterializerTest {
|
||||
|
||||
final PackerAssetDeclaration declaration = declaration(
|
||||
Map.of("tile_size", "16x16"),
|
||||
List.of(new PackerAssetArtifactSelection("selected.png", 0)),
|
||||
List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
|
||||
palettePipeline());
|
||||
final var materialized = materializer.materialize(
|
||||
assetRoot,
|
||||
@ -106,7 +106,7 @@ final class PackerRuntimeAssetMaterializerTest {
|
||||
|
||||
final PackerAssetDeclaration declaration = declaration(
|
||||
Map.of("tile_size", "16x16"),
|
||||
List.of(new PackerAssetArtifactSelection("selected.png", 0)),
|
||||
List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
|
||||
palettePipeline());
|
||||
final var materialized = materializer.materialize(
|
||||
assetRoot,
|
||||
|
||||
@ -533,8 +533,11 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
final var supportFile = MAPPER.readTree(assetRoot.resolve("scene-bank.studio.json").toFile());
|
||||
assertEquals(16, supportFile.path("map_width").asInt());
|
||||
assertEquals(16, supportFile.path("map_height").asInt());
|
||||
assertEquals(1, supportFile.path("layer_count").asInt());
|
||||
assertEquals(4, supportFile.path("layer_count").asInt());
|
||||
assertEquals(false, supportFile.path("layers").get(0).path("enabled").asBoolean());
|
||||
assertEquals("Layer 1", supportFile.path("layers").get(0).path("name").asText());
|
||||
assertEquals(16, supportFile.path("layers").get(0).path("width").asInt());
|
||||
assertEquals(16, supportFile.path("layers").get(0).path("height").asInt());
|
||||
assertEquals("layer-1.tmx", supportFile.path("layers").get(0).path("tilemap").asText());
|
||||
}
|
||||
|
||||
@ -722,6 +725,31 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertFalse(registry.path("assets").get(0).path("included_in_build").asBoolean(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAssetBuildParticipationBlocksSceneBankWithoutEnabledLayers() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("update-build-participation-scene");
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
final CreateAssetResult createResult = service.createAsset(new CreateAssetRequest(
|
||||
project(projectRoot),
|
||||
"scenes/overworld",
|
||||
"overworld",
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
false));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, createResult.status());
|
||||
|
||||
final var result = service.updateAssetBuildParticipation(new UpdateAssetBuildParticipationRequest(
|
||||
project(projectRoot),
|
||||
createResult.assetReference(),
|
||||
true));
|
||||
|
||||
assertEquals(PackerOperationStatus.FAILED, result.status());
|
||||
assertTrue(result.summary().contains("enable at least one layer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAssetContractDropsPersistedBankCompositionWhenContractFingerprintChanges() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-drops-bank"));
|
||||
@ -760,8 +788,8 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
void applyBankCompositionWritesArtifactsAndRefreshesSnapshotWithoutWholeProjectReload() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-bank-composition"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
Files.writeString(assetRoot.resolve("confirm.png"), "fixture");
|
||||
Files.writeString(assetRoot.resolve("cancel.png"), "fixture");
|
||||
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
writeTilePng(assetRoot.resolve("cancel.png"), 16);
|
||||
final CountingLoader loader = countingLoader();
|
||||
final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader);
|
||||
|
||||
@ -779,10 +807,14 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
|
||||
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
assertTrue(manifest.path("inputs").isMissingNode());
|
||||
assertTrue(manifest.path("artifacts").get(0).hasNonNull("artifact_id"));
|
||||
assertEquals("cancel.png", manifest.path("artifacts").get(0).path("file").asText());
|
||||
assertEquals(0, manifest.path("artifacts").get(0).path("index").asInt());
|
||||
assertTrue(manifest.path("artifacts").get(0).hasNonNull("fingerprint"));
|
||||
assertTrue(manifest.path("artifacts").get(1).hasNonNull("artifact_id"));
|
||||
assertEquals("confirm.png", manifest.path("artifacts").get(1).path("file").asText());
|
||||
assertEquals(1, manifest.path("artifacts").get(1).path("index").asInt());
|
||||
assertTrue(manifest.path("artifacts").get(1).hasNonNull("fingerprint"));
|
||||
|
||||
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
|
||||
project(projectRoot),
|
||||
@ -793,9 +825,154 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertEquals(
|
||||
List.of("cancel.png", "confirm.png"),
|
||||
detailsResult.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList());
|
||||
assertNotNull(detailsResult.details().bankComposition().selectedFiles().getFirst().metadata().get("artifactId"));
|
||||
assertEquals(1, loader.loadCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAssetDetailsBackfillsLegacyArtifactIdsAndFingerprints() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("legacy-artifact-identity"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var artifacts = manifest.putArray("artifacts");
|
||||
artifacts.addObject()
|
||||
.put("file", "confirm.png")
|
||||
.put("index", 0);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(assetRoot.resolve("asset.json").toFile(), manifest);
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1)));
|
||||
|
||||
final var migrated = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var migratedArtifact = migrated.path("artifacts").get(0);
|
||||
assertTrue(migratedArtifact.hasNonNull("artifact_id"));
|
||||
assertTrue(migratedArtifact.hasNonNull("fingerprint"));
|
||||
final var selectedFile = detailsResult.details().bankComposition().selectedFiles().getFirst();
|
||||
assertEquals(migratedArtifact.path("artifact_id").asText(), selectedFile.metadata().get("artifactId"));
|
||||
assertEquals(migratedArtifact.path("fingerprint").asText(), selectedFile.metadata().get("selectedFingerprint"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void packValidationBlocksGlyphMaterialPaletteWhenSchemaVersionIsNewer() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("glyph-material-stale-palette"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
assertTrue(service.applyBankComposition(new ApplyBankCompositionRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of("confirm.png"))).success());
|
||||
assertTrue(service.applyPaletteOverhauling(new ApplyPaletteOverhaulingRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFFFF0000),
|
||||
"convertedRgb565", List.of(0xF800))))).success());
|
||||
|
||||
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var selectedArtifact = manifest.path("artifacts").get(0);
|
||||
writeGlyphMaterialJson(
|
||||
assetRoot,
|
||||
"""
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schemas": [
|
||||
{
|
||||
"schema_id": "schema-1",
|
||||
"status": "ok",
|
||||
"version": 2,
|
||||
"material_indices": { "1": "#FF0000" },
|
||||
"artifact_bindings": [
|
||||
{ "artifact_id": "%s", "fingerprint": "%s" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"palettes": [
|
||||
{
|
||||
"palette_id": "palette-1",
|
||||
"schema_id": "schema-1",
|
||||
"version": 1,
|
||||
"colors": ["#FF0000"]
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(
|
||||
selectedArtifact.path("artifact_id").asText(),
|
||||
selectedArtifact.path("fingerprint").asText()));
|
||||
|
||||
final var result = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertFalse(result.canPack());
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("is stale because version 1 is older than schema 'schema-1' version 2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void packValidationBlocksGlyphMaterialSchemaWhenArtifactFingerprintDrifts() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("glyph-material-drift"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
final Path tilePath = assetRoot.resolve("confirm.png");
|
||||
writeTilePng(tilePath, 16);
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
assertTrue(service.applyBankComposition(new ApplyBankCompositionRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of("confirm.png"))).success());
|
||||
assertTrue(service.applyPaletteOverhauling(new ApplyPaletteOverhaulingRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFFFF0000),
|
||||
"convertedRgb565", List.of(0xF800))))).success());
|
||||
|
||||
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var selectedArtifact = manifest.path("artifacts").get(0);
|
||||
writeGlyphMaterialJson(
|
||||
assetRoot,
|
||||
"""
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schemas": [
|
||||
{
|
||||
"schema_id": "schema-1",
|
||||
"status": "ok",
|
||||
"version": 1,
|
||||
"material_indices": { "1": "#FF0000" },
|
||||
"artifact_bindings": [
|
||||
{ "artifact_id": "%s", "fingerprint": "%s" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"palettes": [
|
||||
{
|
||||
"palette_id": "palette-1",
|
||||
"schema_id": "schema-1",
|
||||
"version": 1,
|
||||
"colors": ["#FF0000"]
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(
|
||||
selectedArtifact.path("artifact_id").asText(),
|
||||
selectedArtifact.path("fingerprint").asText()));
|
||||
writeSolidTilePng(tilePath, 16, 0xFF00FF00);
|
||||
|
||||
final var result = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertFalse(result.canPack());
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("artifact '" + selectedArtifact.path("artifact_id").asText() + "' drifted")));
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("palette 'palette-1' is invalid because schema 'schema-1' is inconsistent")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyPaletteOverhaulingWritesPipelinePalettesInOrder() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-palette-overhauling"));
|
||||
@ -1376,11 +1553,15 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
}
|
||||
|
||||
private void writeTilePng(Path path, int tileSize) throws Exception {
|
||||
writeSolidTilePng(path, tileSize, 0xFFFF0000);
|
||||
}
|
||||
|
||||
private void writeSolidTilePng(Path path, int tileSize, int argb) throws Exception {
|
||||
Files.createDirectories(path.getParent());
|
||||
final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < tileSize; y += 1) {
|
||||
for (int x = 0; x < tileSize; x += 1) {
|
||||
image.setRGB(x, y, 0xFFFF0000);
|
||||
image.setRGB(x, y, argb);
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", path.toFile());
|
||||
@ -1397,6 +1578,10 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
ImageIO.write(image, "png", path.toFile());
|
||||
}
|
||||
|
||||
private void writeGlyphMaterialJson(Path assetRoot, String content) throws Exception {
|
||||
Files.writeString(assetRoot.resolve("glyph-material.json"), content);
|
||||
}
|
||||
|
||||
private static int readLeInt(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xFF)
|
||||
| ((bytes[offset + 1] & 0xFF) << 8)
|
||||
|
||||
@ -76,6 +76,31 @@ final class PackerAssetDeclarationParserTest {
|
||||
"asset_uuid": "uuid-scene",
|
||||
"name": "overworld_scene",
|
||||
"type": "scene_bank",
|
||||
"output": {
|
||||
"format": "SCENE/tilemap_v1",
|
||||
"codec": "NONE"
|
||||
},
|
||||
"preload": { "enabled": false }
|
||||
}
|
||||
""");
|
||||
|
||||
final var result = parser.parse(manifest);
|
||||
|
||||
assertTrue(result.valid());
|
||||
assertEquals(AssetFamilyCatalog.SCENE_BANK, result.declaration().assetFamily());
|
||||
assertEquals("SCENE/tilemap_v1", result.declaration().outputFormat().displayName());
|
||||
assertEquals(OutputCodecCatalog.NONE, result.declaration().outputCodec());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesLegacySceneBankDeclarationFormatAsTilemapContract() throws Exception {
|
||||
final Path manifest = tempDir.resolve("legacy-scene-asset.json");
|
||||
Files.writeString(manifest, """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"asset_uuid": "uuid-scene-legacy",
|
||||
"name": "overworld_scene",
|
||||
"type": "scene_bank",
|
||||
"output": {
|
||||
"format": "SCENE/tiled_v1",
|
||||
"codec": "NONE"
|
||||
@ -88,7 +113,7 @@ final class PackerAssetDeclarationParserTest {
|
||||
|
||||
assertTrue(result.valid());
|
||||
assertEquals(AssetFamilyCatalog.SCENE_BANK, result.declaration().assetFamily());
|
||||
assertEquals("SCENE/tiled_v1", result.declaration().outputFormat().displayName());
|
||||
assertEquals("SCENE/tilemap_v1", result.declaration().outputFormat().displayName());
|
||||
assertEquals(OutputCodecCatalog.NONE, result.declaration().outputCodec());
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -4,6 +4,7 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
@ -35,6 +36,8 @@ public abstract class StudioDualListView<T> extends HBox {
|
||||
private Consumer<List<T>> onMoveToLeft = ignored -> { };
|
||||
private Consumer<T> onLeftSelectionChanged = ignored -> { };
|
||||
private Consumer<T> onRightSelectionChanged = ignored -> { };
|
||||
private Consumer<T> onLeftItemActivated = ignored -> { };
|
||||
private Consumer<T> onRightItemActivated = ignored -> { };
|
||||
private IntConsumer onMoveUp = ignored -> { };
|
||||
private IntConsumer onMoveDown = ignored -> { };
|
||||
private boolean interactionEnabled = true;
|
||||
@ -145,6 +148,14 @@ public abstract class StudioDualListView<T> extends HBox {
|
||||
this.onRightSelectionChanged = Objects.requireNonNull(onRightSelectionChanged, "onRightSelectionChanged");
|
||||
}
|
||||
|
||||
public void setOnLeftItemActivated(Consumer<T> onLeftItemActivated) {
|
||||
this.onLeftItemActivated = Objects.requireNonNull(onLeftItemActivated, "onLeftItemActivated");
|
||||
}
|
||||
|
||||
public void setOnRightItemActivated(Consumer<T> onRightItemActivated) {
|
||||
this.onRightItemActivated = Objects.requireNonNull(onRightItemActivated, "onRightItemActivated");
|
||||
}
|
||||
|
||||
public void setOnMoveUp(IntConsumer onMoveUp) {
|
||||
this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
|
||||
}
|
||||
@ -188,6 +199,19 @@ public abstract class StudioDualListView<T> extends HBox {
|
||||
|
||||
private ListCell<T> createCell(boolean indexed) {
|
||||
return new ListCell<>() {
|
||||
{
|
||||
setOnMouseClicked(event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY || event.getClickCount() != 2 || getItem() == null) {
|
||||
return;
|
||||
}
|
||||
if (indexed) {
|
||||
onRightItemActivated.accept(getItem());
|
||||
} else {
|
||||
onLeftItemActivated.accept(getItem());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
@ -145,6 +145,9 @@ public enum I18n {
|
||||
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
||||
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
|
||||
ASSETS_SECTION_BANK_COMPOSITION("assets.section.bankComposition"),
|
||||
ASSETS_SECTION_SCENE_LAYERS("assets.section.sceneLayers"),
|
||||
ASSETS_SECTION_COLOR_SCHEMA_GROUPS("assets.section.colorSchemaGroups"),
|
||||
ASSETS_SECTION_LAYER_CONFIGURATION("assets.section.layerConfiguration"),
|
||||
ASSETS_SECTION_PALETTE_OVERHAULING("assets.section.paletteOverhauling"),
|
||||
ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"),
|
||||
ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"),
|
||||
@ -158,10 +161,13 @@ public enum I18n {
|
||||
ASSETS_ACTION_GENERATE_TMX("assets.action.generateTmx"),
|
||||
ASSETS_ACTION_VALIDATE_SCENE_BANK("assets.action.validateSceneBank"),
|
||||
ASSETS_ACTION_ACCEPT_SCENE_BANK("assets.action.acceptSceneBank"),
|
||||
ASSETS_ACTION_VALIDATE_LAYER("assets.action.validateLayer"),
|
||||
ASSETS_ACTION_ACCEPT_LAYER("assets.action.acceptLayer"),
|
||||
ASSETS_ACTION_DELETE("assets.action.delete"),
|
||||
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||
ASSETS_ACTION_RELOCATE("assets.action.relocate"),
|
||||
ASSETS_ACTION_REMOVE("assets.action.remove"),
|
||||
ASSETS_DELETE_DIALOG_TITLE("assets.deleteDialog.title"),
|
||||
ASSETS_DELETE_DIALOG_DESCRIPTION("assets.deleteDialog.description"),
|
||||
ASSETS_DELETE_DIALOG_PROMPT("assets.deleteDialog.prompt"),
|
||||
@ -197,8 +203,16 @@ public enum I18n {
|
||||
ASSETS_LABEL_STUDIO_ROLE("assets.label.studioRole"),
|
||||
ASSETS_LABEL_SCENE_STATUS("assets.label.sceneStatus"),
|
||||
ASSETS_LABEL_SCENE_LAYERS("assets.label.sceneLayers"),
|
||||
ASSETS_LABEL_SCENE_DIMENSIONS("assets.label.sceneDimensions"),
|
||||
ASSETS_LABEL_LAYER("assets.label.layer"),
|
||||
ASSETS_LABEL_LAYER_OVERVIEW("assets.label.layerOverview"),
|
||||
ASSETS_LABEL_TILEMAPS("assets.label.tilemaps"),
|
||||
ASSETS_LABEL_TILEMAP("assets.label.tilemap"),
|
||||
ASSETS_LABEL_TILESET("assets.label.tileset"),
|
||||
ASSETS_LABEL_SUPPORT_FILE("assets.label.supportFile"),
|
||||
ASSETS_LABEL_USE_LAYER("assets.label.useLayer"),
|
||||
ASSETS_VALUE_NOT_CONFIGURED("assets.value.notConfigured"),
|
||||
ASSETS_VALUE_LAYER_OVERVIEW_PENDING("assets.value.layerOverviewPending"),
|
||||
ASSETS_TYPE_GLYPH_BANK("assets.type.glyphBank"),
|
||||
ASSETS_TYPE_SCENE_BANK("assets.type.sceneBank"),
|
||||
ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"),
|
||||
@ -259,6 +273,22 @@ public enum I18n {
|
||||
ASSETS_PALETTE_OVERHAULING_TILE_SELECTOR("assets.paletteOverhauling.tileSelector"),
|
||||
ASSETS_PALETTE_OVERHAULING_APPLIED_PALETTE("assets.paletteOverhauling.appliedPalette"),
|
||||
ASSETS_PALETTE_OVERHAULING_PREVIEW_EMPTY("assets.paletteOverhauling.previewEmpty"),
|
||||
ASSETS_PALETTE_WIZARD_CREATE_TITLE("assets.paletteWizard.createTitle"),
|
||||
ASSETS_PALETTE_WIZARD_UPDATE_TITLE("assets.paletteWizard.updateTitle"),
|
||||
ASSETS_PALETTE_WIZARD_NAME("assets.paletteWizard.name"),
|
||||
ASSETS_PALETTE_WIZARD_SCHEMA("assets.paletteWizard.schema"),
|
||||
ASSETS_PALETTE_WIZARD_ORIGINAL("assets.paletteWizard.original"),
|
||||
ASSETS_PALETTE_WIZARD_EDITABLE("assets.paletteWizard.editable"),
|
||||
ASSETS_PALETTE_WIZARD_APPLY_COLOR("assets.paletteWizard.applyColor"),
|
||||
ASSETS_PALETTE_WIZARD_DUPLICATE("assets.paletteWizard.duplicate"),
|
||||
ASSETS_PALETTE_WIZARD_READY("assets.paletteWizard.ready"),
|
||||
ASSETS_COLOR_SCHEMA_EMPTY("assets.colorSchema.empty"),
|
||||
ASSETS_COLOR_SCHEMA_SUMMARY("assets.colorSchema.summary"),
|
||||
ASSETS_COLOR_SCHEMA_REMOVE_BLOCKED("assets.colorSchema.removeBlocked"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_AVAILABLE_TILES("assets.colorSchema.dialog.availableTiles"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_ASSIGNED_TILES("assets.colorSchema.dialog.assignedTiles"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT("assets.colorSchema.dialog.colorCount"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS("assets.colorSchema.dialog.tooManyColors"),
|
||||
ASSETS_DETAILS_CODEC_CONFIGURATION_EMPTY("assets.details.codecConfiguration.empty"),
|
||||
ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"),
|
||||
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),
|
||||
@ -290,6 +320,7 @@ public enum I18n {
|
||||
ASSETS_ADD_WIZARD_ERROR_ROOT_OUTSIDE_ASSETS("assets.addWizard.error.rootOutsideAssets"),
|
||||
ASSETS_ADD_WIZARD_ERROR_ROOT_ALREADY_ASSET("assets.addWizard.error.rootAlreadyAsset"),
|
||||
ASSETS_ADD_WIZARD_ERROR_TYPE("assets.addWizard.error.type"),
|
||||
ASSETS_ADD_WIZARD_ERROR_SPECIALIZATION("assets.addWizard.error.specialization"),
|
||||
ASSETS_ADD_WIZARD_ERROR_FORMAT("assets.addWizard.error.format"),
|
||||
ASSETS_ADD_WIZARD_ERROR_CODEC("assets.addWizard.error.codec"),
|
||||
ASSETS_ADD_WIZARD_ERROR_UNSUPPORTED_COMBINATION("assets.addWizard.error.unsupportedCombination"),
|
||||
|
||||
@ -12,6 +12,7 @@ import p.packer.dtos.PackerAssetActionAvailabilityDTO;
|
||||
import p.packer.dtos.PackerAssetDetailsDTO;
|
||||
import p.packer.dtos.PackerDiagnosticDTO;
|
||||
import p.packer.messages.*;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||
import p.studio.controls.forms.StudioSection;
|
||||
@ -22,6 +23,8 @@ import p.studio.workspaces.assets.dialogs.AssetDiagnosticsDialog;
|
||||
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
|
||||
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
|
||||
import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhaulingControl;
|
||||
import p.studio.workspaces.assets.details.schema.AssetDetailsColorSchemaGroupsControl;
|
||||
import p.studio.workspaces.assets.details.scene.AssetDetailsSceneLayersControl;
|
||||
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataSnapshot;
|
||||
@ -62,7 +65,9 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
private final AssetDetailsSummaryControl summaryControl;
|
||||
private final AssetDetailsContractControl contractControl;
|
||||
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
||||
private final AssetDetailsColorSchemaGroupsControl colorSchemaGroupsControl;
|
||||
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
|
||||
private final AssetDetailsSceneLayersControl sceneLayersControl;
|
||||
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
|
||||
private final TiledAssetGenerationService tiledGenerationService = new TiledAssetGenerationService();
|
||||
private final SceneBankWorkflowService sceneBankWorkflowService = new SceneBankWorkflowService();
|
||||
@ -85,7 +90,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
|
||||
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
|
||||
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
|
||||
this.colorSchemaGroupsControl = new AssetDetailsColorSchemaGroupsControl(projectReference, workspaceBus);
|
||||
this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(projectReference, workspaceBus);
|
||||
this.sceneLayersControl = new AssetDetailsSceneLayersControl(projectReference, workspaceBus);
|
||||
this.sceneLayersControl.setGenerateLayerAction(this::generateTmx);
|
||||
this.sceneLayersControl.setValidateLayerAction(this::validateSceneBank);
|
||||
this.sceneLayersControl.setAcceptLayerAction(this::acceptSceneBank);
|
||||
this.actionsSection = createActionsSection();
|
||||
|
||||
getStyleClass().add("assets-workspace-pane");
|
||||
@ -269,13 +279,43 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
renderActions();
|
||||
if (!readyMounted) {
|
||||
readyMounted = true;
|
||||
detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl, paletteOverhaulingControl);
|
||||
detailsContent.getChildren().setAll(buildDetailsSections());
|
||||
}
|
||||
syncActionsSectionHeight(summaryControl.getHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Node[] buildDetailsSections() {
|
||||
final java.util.List<Node> sections = new java.util.ArrayList<>();
|
||||
sections.add(primarySectionsRow);
|
||||
sections.add(contractControl);
|
||||
if (showsSceneLayers(viewState.selectedAssetDetails())) {
|
||||
sections.add(sceneLayersControl);
|
||||
} else if (!hidesBankComposition(viewState.selectedAssetDetails())) {
|
||||
sections.add(bankCompositionControl);
|
||||
}
|
||||
if (showsColorSchemaGroups(viewState.selectedAssetDetails())) {
|
||||
sections.add(colorSchemaGroupsControl);
|
||||
}
|
||||
sections.add(paletteOverhaulingControl);
|
||||
return sections.toArray(Node[]::new);
|
||||
}
|
||||
|
||||
private boolean hidesBankComposition(AssetWorkspaceAssetDetails details) {
|
||||
return details != null && details.summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK;
|
||||
}
|
||||
|
||||
private boolean showsSceneLayers(AssetWorkspaceAssetDetails details) {
|
||||
return details != null
|
||||
&& details.summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
|
||||
&& details.sceneBankMetadata() != null;
|
||||
}
|
||||
|
||||
private boolean showsColorSchemaGroups(AssetWorkspaceAssetDetails details) {
|
||||
return details != null && details.summary().assetFamily() == AssetFamilyCatalog.GLYPH_BANK;
|
||||
}
|
||||
|
||||
private VBox createActionsSection() {
|
||||
actionsContent.getStyleClass().add("assets-details-actions-content");
|
||||
actionsScroll.setContent(actionsContent);
|
||||
@ -316,9 +356,6 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
|
||||
private java.util.List<Node> buildActionNodes() {
|
||||
final java.util.List<Node> nodes = new java.util.ArrayList<>();
|
||||
if (actionFeedbackMessage != null && !actionFeedbackMessage.isBlank()) {
|
||||
nodes.add(AssetDetailsUiSupport.createSectionMessage(actionFeedbackMessage));
|
||||
}
|
||||
if (viewState.selectedAssetDetails() == null) {
|
||||
nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
|
||||
return nodes;
|
||||
@ -343,24 +380,6 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
generateTsxButton.setOnAction(ignored -> generateTsx());
|
||||
nodes.add(generateTsxButton);
|
||||
}
|
||||
if (canGenerateTmx()) {
|
||||
final Button generateTmxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_GENERATE_TMX));
|
||||
generateTmxButton.setDisable(actionRunning);
|
||||
generateTmxButton.setOnAction(ignored -> generateTmx());
|
||||
nodes.add(generateTmxButton);
|
||||
}
|
||||
if (canValidateSceneBank()) {
|
||||
final Button validateSceneButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_VALIDATE_SCENE_BANK));
|
||||
validateSceneButton.setDisable(actionRunning);
|
||||
validateSceneButton.setOnAction(ignored -> validateSceneBank());
|
||||
nodes.add(validateSceneButton);
|
||||
|
||||
final Button acceptSceneButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_ACCEPT_SCENE_BANK));
|
||||
acceptSceneButton.setDisable(actionRunning || !viewState.selectedAssetDetails().sceneBankValidation().canAccept());
|
||||
acceptSceneButton.setOnAction(ignored -> acceptSceneBank());
|
||||
nodes.add(acceptSceneButton);
|
||||
}
|
||||
|
||||
final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
||||
AssetDetailsUiSupport.applyActionTone(
|
||||
buildParticipationButton,
|
||||
@ -449,7 +468,16 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
|
||||
private boolean canToggleBuildParticipation() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().state() == p.studio.workspaces.assets.messages.AssetWorkspaceAssetState.REGISTERED;
|
||||
&& viewState.selectedAssetDetails().summary().state() == p.studio.workspaces.assets.messages.AssetWorkspaceAssetState.REGISTERED
|
||||
&& (!isSceneBankAwaitingActiveLayerForBuild());
|
||||
}
|
||||
|
||||
private boolean isSceneBankAwaitingActiveLayerForBuild() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
|
||||
&& currentBuildParticipation() != AssetWorkspaceBuildParticipation.INCLUDED
|
||||
&& (viewState.selectedAssetDetails().sceneBankMetadata() == null
|
||||
|| viewState.selectedAssetDetails().sceneBankMetadata().enabledLayerCount() == 0);
|
||||
}
|
||||
|
||||
private AssetWorkspaceBuildParticipation currentBuildParticipation() {
|
||||
@ -492,13 +520,11 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
}
|
||||
|
||||
private boolean canGenerateTmx() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK;
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean canValidateSceneBank() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void generateTsx() {
|
||||
@ -511,11 +537,11 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.generateTilesetTsx(details);
|
||||
Platform.runLater(() -> applyTiledGenerationResult(result));
|
||||
Platform.runLater(() -> applyTiledGenerationResult("glyph-bank-tileset", result));
|
||||
});
|
||||
}
|
||||
|
||||
private void generateTmx() {
|
||||
private void generateTmx(int layerIndex) {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
@ -524,15 +550,15 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.generateSceneBankTilemaps(projectReference, details);
|
||||
Platform.runLater(() -> applyTiledGenerationResult(result));
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.generateSceneBankTilemap(projectReference, details, layerIndex);
|
||||
Platform.runLater(() -> applyTiledGenerationResult("scene-bank-layer-" + layerIndex, result));
|
||||
});
|
||||
}
|
||||
|
||||
private void applyTiledGenerationResult(TiledAssetGenerationResult result) {
|
||||
private void applyTiledGenerationResult(String source, TiledAssetGenerationResult result) {
|
||||
actionRunning = false;
|
||||
actionFeedbackMessage = result.message();
|
||||
workspaceBus.publish(new StudioAssetLogEvent("scene-bank", result.message()));
|
||||
workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
|
||||
renderActions();
|
||||
if (result.success() && viewState.selectedAssetReference() != null) {
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||
@ -540,7 +566,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSceneBank() {
|
||||
private void validateSceneBank(int layerIndex) {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
@ -549,12 +575,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final SceneBankWorkflowResult result = sceneBankWorkflowService.validate(projectReference, details);
|
||||
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-validate", result));
|
||||
final SceneBankWorkflowResult result = sceneBankWorkflowService.validateLayer(projectReference, details, layerIndex);
|
||||
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-validate-layer-" + layerIndex, result));
|
||||
});
|
||||
}
|
||||
|
||||
private void acceptSceneBank() {
|
||||
private void acceptSceneBank(int layerIndex) {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
@ -563,8 +589,8 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final SceneBankWorkflowResult result = sceneBankWorkflowService.accept(projectReference, details);
|
||||
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-accept", result));
|
||||
final SceneBankWorkflowResult result = sceneBankWorkflowService.acceptLayer(projectReference, details, layerIndex);
|
||||
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-accept-layer-" + layerIndex, result));
|
||||
});
|
||||
}
|
||||
|
||||
@ -699,6 +725,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
details.outputPipeline(),
|
||||
bankComposition,
|
||||
details.pipelinePalettes(),
|
||||
studioMetadata.glyphMaterialMetadata(),
|
||||
studioMetadata.sceneBankMetadata(),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
@ -725,6 +752,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
details.outputPipeline(),
|
||||
bankComposition,
|
||||
details.pipelinePalettes(),
|
||||
studioMetadata.glyphMaterialMetadata(),
|
||||
studioMetadata.sceneBankMetadata(),
|
||||
sceneBankWorkflow.validation(),
|
||||
mergedDiagnostics);
|
||||
|
||||
@ -31,6 +31,10 @@ public final class AssetDetailsUiSupport {
|
||||
return StudioSection.createSection(title, content);
|
||||
}
|
||||
|
||||
public static VBox createSectionWithoutHeader(Node content) {
|
||||
return StudioSection.createSectionWithoutHeader(content);
|
||||
}
|
||||
|
||||
public static VBox createSection(String title, Node headerActions, Node content) {
|
||||
return StudioFormSection.createEditableSection(title, headerActions, content, StudioFormMode.READ_ONLY);
|
||||
}
|
||||
@ -76,6 +80,14 @@ public final class AssetDetailsUiSupport {
|
||||
return chip;
|
||||
}
|
||||
|
||||
public static Label createChip(String toneClass, String text, String... extraClasses) {
|
||||
final Label chip = createChip(toneClass, text);
|
||||
if (extraClasses != null && extraClasses.length > 0) {
|
||||
chip.getStyleClass().addAll(extraClasses);
|
||||
}
|
||||
return chip;
|
||||
}
|
||||
|
||||
public static Button createActionButton(String text) {
|
||||
final Button button = new Button(text);
|
||||
button.getStyleClass().addAll("studio-button", "studio-button-primary", "assets-details-action-button");
|
||||
@ -125,7 +137,7 @@ public final class AssetDetailsUiSupport {
|
||||
public static String typeChipTone(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-details-chip-image";
|
||||
case SCENE_BANK -> "assets-details-chip-generic";
|
||||
case SCENE_BANK -> "assets-details-chip-scene";
|
||||
case SOUND_BANK -> "assets-details-chip-audio";
|
||||
case UNKNOWN -> "assets-details-chip-generic";
|
||||
};
|
||||
|
||||
@ -5,7 +5,10 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.packer.dtos.PackerBankCompositionDetailsDTO;
|
||||
import p.packer.dtos.PackerBankCompositionFileDTO;
|
||||
import p.packer.messages.ApplyBankCompositionRequest;
|
||||
import p.packer.messages.GetAssetDetailsRequest;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.banks.StudioAssetCapacityMeter;
|
||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||
@ -15,12 +18,14 @@ import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionAppliedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyFailedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionCapacityChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionDraftChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioBankCompositionChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
|
||||
@ -76,6 +81,19 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
||||
}));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioBankCompositionChangedEvent.class, event -> {
|
||||
if (viewState == null || !Objects.equals(viewState.selectedAssetReference(), event.assetReference())) {
|
||||
return;
|
||||
}
|
||||
viewState = new AssetWorkspaceDetailsViewState(
|
||||
viewState.detailsStatus(),
|
||||
viewState.selectedAssetReference(),
|
||||
viewState.selectedAssetDetails().withBankComposition(event.bankComposition()),
|
||||
viewState.detailsErrorMessage());
|
||||
coordinator.replaceDetails(viewState.selectedAssetDetails());
|
||||
renderSection();
|
||||
publishStateNotifications();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -181,12 +199,9 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
||||
final var response = Container.packer().workspaceService().applyBankComposition(request);
|
||||
Platform.runLater(() -> {
|
||||
if (response.success()) {
|
||||
coordinator.apply();
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
renderSection();
|
||||
publishStateNotifications();
|
||||
workspaceBus.publish(new StudioAssetBankCompositionAppliedEvent(assetReference));
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
|
||||
Container.backgroundTasks().submit(() -> publishAppliedBankComposition(assetReference));
|
||||
} else {
|
||||
workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent(
|
||||
assetReference,
|
||||
@ -201,6 +216,37 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
||||
});
|
||||
}
|
||||
|
||||
private void publishAppliedBankComposition(p.packer.messages.AssetReference assetReference) {
|
||||
try {
|
||||
final var detailsResult = Container.packer().workspaceService().getAssetDetails(new GetAssetDetailsRequest(
|
||||
projectReference.toPackerProjectContext(),
|
||||
assetReference));
|
||||
final AssetWorkspaceBankCompositionDetails bankComposition = mapBankComposition(detailsResult.details().bankComposition());
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioBankCompositionChangedEvent(assetReference, bankComposition)));
|
||||
} catch (Exception exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent(
|
||||
assetReference,
|
||||
exception.getMessage())));
|
||||
}
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionDetails mapBankComposition(PackerBankCompositionDetailsDTO details) {
|
||||
return new AssetWorkspaceBankCompositionDetails(
|
||||
details.availableFiles().stream().map(this::mapBankCompositionFile).toList(),
|
||||
details.selectedFiles().stream().map(this::mapBankCompositionFile).toList(),
|
||||
details.measuredBankSizeBytes());
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile mapBankCompositionFile(PackerBankCompositionFileDTO file) {
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
file.path(),
|
||||
file.displayName(),
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
file.metadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset() {
|
||||
coordinator.reset();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.workspaces.assets.details.palette;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
@ -18,8 +19,12 @@ import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioGlyphMaterialMetadataChangedEvent;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@ -31,9 +36,12 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||
private final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
private final AssetDetailsPaletteOverhaulingDualListView dualListView = new AssetDetailsPaletteOverhaulingDualListView();
|
||||
private final AssetDetailsPaletteOverhaulingPreviewPane previewPane = new AssetDetailsPaletteOverhaulingPreviewPane();
|
||||
private final HBox body = new HBox(16, dualListView, previewPane);
|
||||
private final VBox paletteActions = new VBox(6);
|
||||
private final HBox listWithActions = new HBox(8, paletteActions, dualListView);
|
||||
private final HBox body = new HBox(16, listWithActions, previewPane);
|
||||
private final VBox content = new VBox(12, body);
|
||||
|
||||
private AssetWorkspaceDetailsViewState viewState;
|
||||
@ -42,9 +50,12 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||
body.getStyleClass().add("assets-details-palette-overhauling-body");
|
||||
paletteActions.getStyleClass().add("assets-details-palette-overhauling-toolbar");
|
||||
listWithActions.getStyleClass().add("assets-details-list-with-actions");
|
||||
content.setFillWidth(true);
|
||||
HBox.setHgrow(dualListView, Priority.ALWAYS);
|
||||
HBox.setHgrow(listWithActions, Priority.ALWAYS);
|
||||
HBox.setHgrow(previewPane, Priority.ALWAYS);
|
||||
HBox.setHgrow(dualListView, Priority.ALWAYS);
|
||||
dualListView.setMaxWidth(Double.MAX_VALUE);
|
||||
previewPane.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
@ -61,6 +72,13 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
}));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioGlyphMaterialMetadataChangedEvent.class, event -> {
|
||||
if (viewState == null || !Objects.equals(viewState.selectedAssetReference(), event.assetReference())) {
|
||||
return;
|
||||
}
|
||||
coordinator.replaceMaterialMetadata(event.metadata());
|
||||
renderSection();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -110,6 +128,8 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
coordinator.viewModel().renderableFileCount());
|
||||
}
|
||||
});
|
||||
dualListView.setOnLeftItemActivated(item -> openPaletteWizard(item));
|
||||
dualListView.setOnRightItemActivated(item -> openPaletteWizard(item));
|
||||
if (viewModel.previewPaletteFile() != null) {
|
||||
if (viewModel.selectedFiles().contains(viewModel.previewPaletteFile())) {
|
||||
dualListView.selectRightItem(viewModel.previewPaletteFile());
|
||||
@ -134,6 +154,17 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
viewModel.previewPaletteFile(),
|
||||
viewModel.renderableFileCount());
|
||||
|
||||
final Button addButton = new Button("+");
|
||||
addButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
addButton.setDisable(!viewModel.editing() || viewState == null || viewState.selectedAssetDetails() == null);
|
||||
addButton.setOnAction(ignored -> openPaletteWizard(null));
|
||||
|
||||
final Button removeButton = new Button("-");
|
||||
removeButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
removeButton.setDisable(!viewModel.editing() || viewModel.previewPaletteFile() == null);
|
||||
removeButton.setOnAction(ignored -> removePalette(coordinator.viewModel().previewPaletteFile()));
|
||||
paletteActions.getChildren().setAll(addButton, removeButton);
|
||||
|
||||
renderFormSection(content);
|
||||
}
|
||||
|
||||
@ -232,6 +263,67 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
: "asset-details:" + viewState.selectedAssetReference();
|
||||
}
|
||||
|
||||
private void openPaletteWizard(p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
if (getScene() == null || !coordinator.ready() || viewState == null || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
if (AssetDetailsPaletteOverhaulingCoordinator.paletteIsOriginal(paletteFile)) {
|
||||
return;
|
||||
}
|
||||
final var result = AssetDetailsPaletteWizardDialog.showAndWait(
|
||||
getScene().getWindow(),
|
||||
coordinator,
|
||||
paletteFile);
|
||||
result.ifPresent(edit -> Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.upsertPalette(
|
||||
edit.paletteId(),
|
||||
edit.displayName(),
|
||||
edit.schemaId(),
|
||||
edit.colors());
|
||||
metadataService.writeGlyphMaterialMetadata(viewState.selectedAssetDetails().summary().assetRoot(), metadata);
|
||||
Platform.runLater(() -> {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("palette-overhauling", "Palette saved: " + edit.paletteId()));
|
||||
workspaceBus.publish(new StudioGlyphMaterialMetadataChangedEvent(viewState.selectedAssetReference(), metadata));
|
||||
});
|
||||
} catch (IOException exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"palette-overhauling",
|
||||
"Unable to save palette: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void removePalette(p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
if (paletteFile == null || viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetReference() == null) {
|
||||
return;
|
||||
}
|
||||
if (AssetDetailsPaletteOverhaulingCoordinator.paletteIsOriginal(paletteFile)) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"palette-overhauling",
|
||||
"Original schema palettes cannot be removed."));
|
||||
return;
|
||||
}
|
||||
final Object paletteIdValue = paletteFile.metadata().get("paletteId");
|
||||
if (!(paletteIdValue instanceof String paletteId) || paletteId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.removePalette(paletteId);
|
||||
metadataService.writeGlyphMaterialMetadata(viewState.selectedAssetDetails().summary().assetRoot(), metadata);
|
||||
Platform.runLater(() -> {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("palette-overhauling", "Palette removed: " + paletteId));
|
||||
workspaceBus.publish(new StudioGlyphMaterialMetadataChangedEvent(viewState.selectedAssetReference(), metadata));
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"palette-overhauling",
|
||||
"Unable to remove palette: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> palettePayload(Map<String, Object> metadata) {
|
||||
final Object palette = metadata.get("palette");
|
||||
|
||||
@ -5,24 +5,40 @@ import p.studio.controls.forms.StudioFormMode;
|
||||
import p.studio.controls.forms.StudioFormSession;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialPalette;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
|
||||
import p.studio.workspaces.assets.details.schema.AssetDetailsColorSchemaCoordinator;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
private StudioFormSession<AssetDetailsPaletteOverhaulingDraft> formSession;
|
||||
private AssetWorkspaceAssetDetails details;
|
||||
private AssetStudioGlyphMaterialMetadata localMaterialMetadata;
|
||||
|
||||
public void replaceDetails(AssetWorkspaceAssetDetails details) {
|
||||
this.details = details;
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
formSession = null;
|
||||
localMaterialMetadata = null;
|
||||
return;
|
||||
}
|
||||
localMaterialMetadata = details.glyphMaterialMetadata();
|
||||
rebuildSession();
|
||||
}
|
||||
|
||||
public void replaceMaterialMetadata(AssetStudioGlyphMaterialMetadata metadata) {
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return;
|
||||
}
|
||||
localMaterialMetadata = Objects.requireNonNull(metadata, "metadata");
|
||||
rebuildSession();
|
||||
}
|
||||
|
||||
private void rebuildSession() {
|
||||
final List<AssetWorkspaceBankCompositionFile> allFiles = paletteFiles(details);
|
||||
final List<AssetWorkspaceBankCompositionFile> previewTileFiles = details.bankComposition().selectedFiles().stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::supportsPalettePreview)
|
||||
@ -37,12 +53,21 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
available,
|
||||
initialSelected,
|
||||
previewTileFiles.isEmpty() ? null : previewTileFiles.getFirst().path(),
|
||||
null);
|
||||
initialSelected.isEmpty()
|
||||
? allFiles.isEmpty() ? null : allFiles.getFirst().path()
|
||||
: initialSelected.getFirst().path());
|
||||
if (formSession == null) {
|
||||
formSession = new StudioFormSession<>(source);
|
||||
return;
|
||||
}
|
||||
if (!Objects.equals(formSession.source(), source)) {
|
||||
if (formSession.mode() == StudioFormMode.EDITING) {
|
||||
final AssetDetailsPaletteOverhaulingDraft currentDraft = formSession.draft();
|
||||
formSession = new StudioFormSession<>(source);
|
||||
formSession.beginEdit();
|
||||
formSession.setDraft(rebindDraft(currentDraft, source));
|
||||
return;
|
||||
}
|
||||
formSession.replaceSource(source);
|
||||
}
|
||||
}
|
||||
@ -51,6 +76,10 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
return formSession != null;
|
||||
}
|
||||
|
||||
public AssetWorkspaceAssetDetails details() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void beginEdit() {
|
||||
if (ready()) {
|
||||
formSession.beginEdit();
|
||||
@ -77,7 +106,10 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
|
||||
public void moveToSelected(List<AssetWorkspaceBankCompositionFile> files) {
|
||||
if (ready() && formSession.mode() == StudioFormMode.EDITING) {
|
||||
formSession.updateDraft(current -> current.moveToSelected(files));
|
||||
final List<AssetWorkspaceBankCompositionFile> validFiles = files.stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsValid)
|
||||
.toList();
|
||||
formSession.updateDraft(current -> current.moveToSelected(validFiles));
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,27 +137,118 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
}
|
||||
|
||||
final AssetDetailsPaletteOverhaulingDraft draft = formSession.draft();
|
||||
final AssetWorkspaceBankCompositionFile previewPaletteFile = fileByPath(draft.allFiles(), draft.previewPalettePath());
|
||||
final List<AssetWorkspaceBankCompositionFile> schemaPreviewTileFiles = previewTileFilesForPalette(
|
||||
draft.previewTileFiles(),
|
||||
previewPaletteFile);
|
||||
final AssetWorkspaceBankCompositionFile previewTileFile = Optional
|
||||
.ofNullable(fileByPath(schemaPreviewTileFiles, draft.previewTilePath()))
|
||||
.orElse(schemaPreviewTileFiles.isEmpty() ? null : schemaPreviewTileFiles.getFirst());
|
||||
return new AssetDetailsPaletteOverhaulingViewModel(
|
||||
formSession.mode() == StudioFormMode.EDITING,
|
||||
formSession.isDirty(),
|
||||
draft.previewTileFiles(),
|
||||
schemaPreviewTileFiles,
|
||||
draft.allFiles(),
|
||||
draft.availableFiles(),
|
||||
draft.selectedFiles(),
|
||||
fileByPath(draft.previewTileFiles(), draft.previewTilePath()),
|
||||
fileByPath(draft.allFiles(), draft.previewPalettePath()),
|
||||
previewTileFile,
|
||||
previewPaletteFile,
|
||||
draft.allFiles().size());
|
||||
}
|
||||
|
||||
static boolean supportsPalettePreview(AssetWorkspaceBankCompositionFile file) {
|
||||
public List<AssetStudioGlyphMaterialSchema> readySchemas() {
|
||||
return materialMetadata().schemas().stream()
|
||||
.filter(schema -> schema.status() == AssetStudioGlyphMaterialSchemaStatus.OK)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public String nextPaletteId() {
|
||||
int next = 1;
|
||||
final Set<String> ids = materialMetadata().palettes().stream()
|
||||
.map(AssetStudioGlyphMaterialPalette::paletteId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
while (ids.contains("palette-" + next)) {
|
||||
next += 1;
|
||||
}
|
||||
return "palette-" + next;
|
||||
}
|
||||
|
||||
public boolean duplicatePaletteExists(String paletteId, String schemaId, List<String> colors) {
|
||||
final List<String> normalized = colors.stream().map(AssetDetailsPaletteOverhaulingCoordinator::normalizeHtmlColor).toList();
|
||||
return allPaletteRecords().stream()
|
||||
.filter(record -> !Objects.equals(record.paletteId(), paletteId))
|
||||
.anyMatch(record -> record.schemaId().equals(schemaId) && record.colors().equals(normalized));
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata upsertPalette(String paletteId, String displayName, String schemaId, List<String> colors) {
|
||||
final AssetStudioGlyphMaterialMetadata current = materialMetadata();
|
||||
final List<String> normalized = colors.stream().map(AssetDetailsPaletteOverhaulingCoordinator::normalizeHtmlColor).toList();
|
||||
final int schemaVersion = current.findSchema(schemaId).map(AssetStudioGlyphMaterialSchema::version).orElse(1);
|
||||
final String resolvedDisplayName = displayName == null || displayName.trim().isBlank()
|
||||
? generatedDisplayName(normalized)
|
||||
: displayName.trim();
|
||||
final AssetStudioGlyphMaterialPalette next = new AssetStudioGlyphMaterialPalette(
|
||||
paletteId,
|
||||
resolvedDisplayName,
|
||||
schemaId,
|
||||
schemaVersion,
|
||||
normalized);
|
||||
final List<AssetStudioGlyphMaterialPalette> palettes = new ArrayList<>();
|
||||
boolean replaced = false;
|
||||
for (AssetStudioGlyphMaterialPalette palette : current.palettes()) {
|
||||
if (palette.paletteId().equals(paletteId)) {
|
||||
palettes.add(next);
|
||||
replaced = true;
|
||||
} else {
|
||||
palettes.add(palette);
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
palettes.add(next);
|
||||
}
|
||||
palettes.sort(Comparator.comparing(AssetStudioGlyphMaterialPalette::paletteId, String.CASE_INSENSITIVE_ORDER));
|
||||
return new AssetStudioGlyphMaterialMetadata(current.schemas(), palettes, current.supportFile());
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata removePalette(String paletteId) {
|
||||
final AssetWorkspaceBankCompositionFile paletteFile = paletteFileById(paletteId);
|
||||
if (paletteFile != null && Boolean.TRUE.equals(paletteFile.metadata().get("derivedOriginal"))) {
|
||||
throw new IllegalStateException("Original schema palettes are derived and cannot be removed.");
|
||||
}
|
||||
final AssetStudioGlyphMaterialMetadata current = materialMetadata();
|
||||
final List<AssetStudioGlyphMaterialPalette> palettes = current.palettes().stream()
|
||||
.filter(palette -> !palette.paletteId().equals(paletteId))
|
||||
.toList();
|
||||
return new AssetStudioGlyphMaterialMetadata(current.schemas(), palettes, current.supportFile());
|
||||
}
|
||||
|
||||
public AssetWorkspaceBankCompositionFile paletteFileById(String paletteId) {
|
||||
if (paletteId == null || !ready()) {
|
||||
return null;
|
||||
}
|
||||
return paletteFiles(details).stream()
|
||||
.filter(file -> paletteId.equals(file.metadata().get("paletteId")))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static boolean supportsPalettePreview(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
return nestedMap(file.metadata(), "tile") != null && nestedMap(file.metadata(), "palette") != null;
|
||||
}
|
||||
|
||||
public static boolean paletteIsValid(AssetWorkspaceBankCompositionFile file) {
|
||||
return !Boolean.FALSE.equals(file.metadata().get("valid"));
|
||||
}
|
||||
|
||||
public static boolean paletteIsOriginal(AssetWorkspaceBankCompositionFile file) {
|
||||
return file != null && Boolean.TRUE.equals(file.metadata().get("derivedOriginal"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
|
||||
static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
|
||||
final Object value = metadata.get(key);
|
||||
if (value instanceof Map<?, ?> nested) {
|
||||
return (Map<String, Object>) nested;
|
||||
@ -133,37 +256,236 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<AssetWorkspaceBankCompositionFile> paletteFiles(AssetWorkspaceAssetDetails details) {
|
||||
final Map<String, AssetWorkspaceBankCompositionFile> ordered = new LinkedHashMap<>();
|
||||
// `bankComposition` files are already projected by the packer read model from the current cache/walk state.
|
||||
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().availableFiles()) {
|
||||
if (supportsPalettePreview(file)) {
|
||||
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
|
||||
}
|
||||
private AssetStudioGlyphMaterialMetadata materialMetadata() {
|
||||
if (details == null) {
|
||||
return new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null);
|
||||
}
|
||||
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().selectedFiles()) {
|
||||
if (supportsPalettePreview(file)) {
|
||||
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
|
||||
}
|
||||
return localMaterialMetadata == null
|
||||
? new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)
|
||||
: localMaterialMetadata;
|
||||
}
|
||||
|
||||
private List<AssetWorkspaceBankCompositionFile> paletteFiles(AssetWorkspaceAssetDetails details) {
|
||||
final Map<String, AssetWorkspaceBankCompositionFile> ordered = new LinkedHashMap<>();
|
||||
for (PaletteRecord record : allPaletteRecords()) {
|
||||
final AssetWorkspaceBankCompositionFile file = toPaletteFile(record);
|
||||
ordered.putIfAbsent(file.path(), file);
|
||||
}
|
||||
return List.copyOf(ordered.values());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static AssetWorkspaceBankCompositionFile canonicalPaletteFile(AssetWorkspaceBankCompositionFile file) {
|
||||
final String hash = paletteHash(file);
|
||||
final Map<String, Object> metadata = new LinkedHashMap<>(file.metadata());
|
||||
metadata.put("paletteHash", hash);
|
||||
private List<PaletteRecord> allPaletteRecords() {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = materialMetadata();
|
||||
final List<PaletteRecord> records = new ArrayList<>();
|
||||
for (AssetStudioGlyphMaterialSchema schema : metadata.schemas()) {
|
||||
final AssetStudioGlyphMaterialSchemaStatus effectiveSchemaStatus = AssetDetailsColorSchemaCoordinator.effectiveStatus(
|
||||
schema,
|
||||
details.bankComposition().selectedFiles());
|
||||
final List<String> originalColors = schema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
records.add(new PaletteRecord(
|
||||
"schema-" + schema.schemaId() + "-original",
|
||||
null,
|
||||
schema.schemaId(),
|
||||
schema.version(),
|
||||
originalColors,
|
||||
schema.materialIndices(),
|
||||
schemaArtifactIds(schema),
|
||||
true,
|
||||
effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK
|
||||
? null
|
||||
: "Schema is inconsistent."));
|
||||
}
|
||||
for (AssetStudioGlyphMaterialPalette palette : metadata.palettes()) {
|
||||
final AssetStudioGlyphMaterialSchema schema = metadata.findSchema(palette.schemaId()).orElse(null);
|
||||
final AssetStudioGlyphMaterialSchemaStatus effectiveSchemaStatus = schema == null
|
||||
? AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT
|
||||
: AssetDetailsColorSchemaCoordinator.effectiveStatus(schema, details.bankComposition().selectedFiles());
|
||||
final boolean valid = schema != null
|
||||
&& effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK
|
||||
&& palette.version() >= schema.version();
|
||||
final String invalidReason;
|
||||
if (schema == null) {
|
||||
invalidReason = "Schema is missing.";
|
||||
} else if (effectiveSchemaStatus != AssetStudioGlyphMaterialSchemaStatus.OK) {
|
||||
invalidReason = "Schema is inconsistent.";
|
||||
} else if (palette.version() < schema.version()) {
|
||||
invalidReason = "Palette is stale and must be updated.";
|
||||
} else {
|
||||
invalidReason = null;
|
||||
}
|
||||
records.add(new PaletteRecord(
|
||||
palette.paletteId(),
|
||||
palette.displayName(),
|
||||
palette.schemaId(),
|
||||
palette.version(),
|
||||
palette.colors(),
|
||||
schema == null ? Map.of() : schema.materialIndices(),
|
||||
schema == null ? Set.of() : schemaArtifactIds(schema),
|
||||
false,
|
||||
valid,
|
||||
invalidReason));
|
||||
}
|
||||
records.sort(Comparator.comparing(PaletteRecord::paletteId, String.CASE_INSENSITIVE_ORDER));
|
||||
return List.copyOf(records);
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile toPaletteFile(PaletteRecord record) {
|
||||
final List<Integer> argb = record.colors().stream()
|
||||
.map(AssetDetailsPaletteOverhaulingCoordinator::htmlToArgb8888)
|
||||
.toList();
|
||||
final List<Integer> rgb565 = record.colors().stream()
|
||||
.map(AssetDetailsPaletteOverhaulingCoordinator::htmlToRgb565)
|
||||
.toList();
|
||||
final Map<String, Object> palette = Map.of(
|
||||
"originalArgb8888", argb,
|
||||
"convertedRgb565", rgb565);
|
||||
final Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put("palette", palette);
|
||||
metadata.put("paletteId", record.paletteId());
|
||||
metadata.put("paletteDisplayName", displayName(record));
|
||||
if (record.displayName() != null) {
|
||||
metadata.put("explicitPaletteDisplayName", record.displayName());
|
||||
}
|
||||
metadata.put("schemaId", record.schemaId());
|
||||
metadata.put("schemaMaterialIndices", record.materialIndices());
|
||||
metadata.put("schemaArtifactIds", List.copyOf(record.artifactIds()));
|
||||
metadata.put("paletteVersion", record.version());
|
||||
metadata.put("derivedOriginal", record.derivedOriginal());
|
||||
metadata.put("valid", record.valid());
|
||||
if (record.invalidReason() != null) {
|
||||
metadata.put("invalidReason", record.invalidReason());
|
||||
}
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
hash,
|
||||
hash,
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
record.paletteId(),
|
||||
displayName(record),
|
||||
0L,
|
||||
0L,
|
||||
null,
|
||||
metadata);
|
||||
}
|
||||
|
||||
private static List<AssetWorkspaceBankCompositionFile> initialSelectedPaletteFiles(
|
||||
static String generatedDisplayName(List<String> colors) {
|
||||
final List<ColorSample> samples = colors.stream()
|
||||
.map(AssetDetailsPaletteOverhaulingCoordinator::toColorSample)
|
||||
.toList();
|
||||
if (samples.isEmpty()) {
|
||||
return "quiet shade";
|
||||
}
|
||||
final ColorSample anchor = samples.stream()
|
||||
.min(Comparator.comparingDouble(sample -> sample.saturation < 0.16 ? sample.lightness : -sample.saturation))
|
||||
.orElse(samples.getFirst());
|
||||
final double averageLightness = samples.stream()
|
||||
.mapToDouble(sample -> sample.lightness)
|
||||
.average()
|
||||
.orElse(anchor.lightness);
|
||||
final double averageSaturation = samples.stream()
|
||||
.mapToDouble(sample -> sample.saturation)
|
||||
.average()
|
||||
.orElse(anchor.saturation);
|
||||
final double lightnessRange = samples.stream().mapToDouble(sample -> sample.lightness).max().orElse(anchor.lightness)
|
||||
- samples.stream().mapToDouble(sample -> sample.lightness).min().orElse(anchor.lightness);
|
||||
return baseName(anchor, averageLightness, averageSaturation) + " " + moodName(averageLightness, averageSaturation, lightnessRange, samples.size());
|
||||
}
|
||||
|
||||
private static String baseName(ColorSample sample, double averageLightness, double averageSaturation) {
|
||||
if (averageSaturation < 0.12) {
|
||||
if (averageLightness > 0.88) {
|
||||
return "snow";
|
||||
}
|
||||
if (averageLightness > 0.68) {
|
||||
return "fog";
|
||||
}
|
||||
if (averageLightness < 0.24) {
|
||||
return "ink";
|
||||
}
|
||||
return "ash";
|
||||
}
|
||||
final double hue = sample.hue;
|
||||
if (hue < 18 || hue >= 345) {
|
||||
return sample.lightness > 0.62 ? "rose" : "ember";
|
||||
}
|
||||
if (hue < 42) {
|
||||
return sample.lightness > 0.62 ? "honey" : "copper";
|
||||
}
|
||||
if (hue < 72) {
|
||||
return sample.lightness > 0.68 ? "sun" : "ochre";
|
||||
}
|
||||
if (hue < 155) {
|
||||
return sample.lightness > 0.58 ? "moss" : "pine";
|
||||
}
|
||||
if (hue < 195) {
|
||||
return sample.lightness > 0.62 ? "mint" : "jade";
|
||||
}
|
||||
if (hue < 245) {
|
||||
return sample.lightness > 0.62 ? "sky" : "cobalt";
|
||||
}
|
||||
if (hue < 290) {
|
||||
return sample.lightness > 0.62 ? "lilac" : "violet";
|
||||
}
|
||||
return sample.lightness > 0.62 ? "orchid" : "plum";
|
||||
}
|
||||
|
||||
private static String moodName(double averageLightness, double averageSaturation, double lightnessRange, int colorCount) {
|
||||
if (averageSaturation < 0.12 && averageLightness > 0.86) {
|
||||
return "white";
|
||||
}
|
||||
if (colorCount > 1 && lightnessRange > 0.42) {
|
||||
return "twist";
|
||||
}
|
||||
if (averageLightness > 0.78) {
|
||||
return averageSaturation > 0.45 ? "glow" : "veil";
|
||||
}
|
||||
if (averageLightness < 0.26) {
|
||||
return averageSaturation > 0.34 ? "deep" : "shade";
|
||||
}
|
||||
if (averageSaturation > 0.62) {
|
||||
return "bloom";
|
||||
}
|
||||
if (averageSaturation < 0.22) {
|
||||
return "haze";
|
||||
}
|
||||
return "drift";
|
||||
}
|
||||
|
||||
private static ColorSample toColorSample(String htmlColor) {
|
||||
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||
final double red = ((rgb >> 16) & 0xFF) / 255.0;
|
||||
final double green = ((rgb >> 8) & 0xFF) / 255.0;
|
||||
final double blue = (rgb & 0xFF) / 255.0;
|
||||
final double max = Math.max(red, Math.max(green, blue));
|
||||
final double min = Math.min(red, Math.min(green, blue));
|
||||
final double delta = max - min;
|
||||
final double lightness = (max + min) / 2.0;
|
||||
final double saturation = delta == 0.0
|
||||
? 0.0
|
||||
: delta / (1.0 - Math.abs((2.0 * lightness) - 1.0));
|
||||
final double hue;
|
||||
if (delta == 0.0) {
|
||||
hue = 0.0;
|
||||
} else if (max == red) {
|
||||
hue = 60.0 * (((green - blue) / delta) % 6.0);
|
||||
} else if (max == green) {
|
||||
hue = 60.0 * (((blue - red) / delta) + 2.0);
|
||||
} else {
|
||||
hue = 60.0 * (((red - green) / delta) + 4.0);
|
||||
}
|
||||
return new ColorSample(hue < 0.0 ? hue + 360.0 : hue, saturation, lightness);
|
||||
}
|
||||
|
||||
private static String displayName(PaletteRecord record) {
|
||||
if (record.derivedOriginal()) {
|
||||
return record.schemaId() + " / original";
|
||||
}
|
||||
return record.displayName() == null || record.displayName().isBlank()
|
||||
? generatedDisplayName(record.colors())
|
||||
: record.displayName();
|
||||
}
|
||||
|
||||
private List<AssetWorkspaceBankCompositionFile> initialSelectedPaletteFiles(
|
||||
AssetWorkspaceAssetDetails details,
|
||||
List<AssetWorkspaceBankCompositionFile> allFiles) {
|
||||
final List<AssetWorkspaceBankCompositionFile> selected = new ArrayList<>();
|
||||
@ -186,32 +508,21 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
&& Objects.equals(left.get("convertedRgb565"), right.get("convertedRgb565"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static String paletteHash(AssetWorkspaceBankCompositionFile file) {
|
||||
final Map<String, Object> palette = nestedMap(file.metadata(), "palette");
|
||||
final Object convertedValue = palette == null ? null : palette.get("convertedRgb565");
|
||||
final List<Integer> colors = convertedValue instanceof List<?> converted
|
||||
? (List<Integer>) converted
|
||||
: List.of();
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
for (int index = 0; index < 15; index++) {
|
||||
final int color = index < colors.size() && colors.get(index) != null ? colors.get(index) : 0;
|
||||
digest.update(ByteBuffer.allocate(4).putInt(color).array());
|
||||
}
|
||||
long value = 0L;
|
||||
final byte[] bytes = digest.digest();
|
||||
for (int index = 0; index < 8; index++) {
|
||||
value = (value << 8) | Byte.toUnsignedLong(bytes[index]);
|
||||
}
|
||||
value &= 0x0FFFFFFFFFFFFFFFL;
|
||||
final String base36 = Long.toUnsignedString(value, 36).toUpperCase();
|
||||
return base36.length() >= 10
|
||||
? base36.substring(0, 10)
|
||||
: "0".repeat(10 - base36.length()) + base36;
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to hash palette colors", exception);
|
||||
}
|
||||
private static int htmlToArgb8888(String htmlColor) {
|
||||
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||
return 0xFF000000 | rgb;
|
||||
}
|
||||
|
||||
private static int htmlToRgb565(String htmlColor) {
|
||||
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||
final int red = (rgb >> 16) & 0xFF;
|
||||
final int green = (rgb >> 8) & 0xFF;
|
||||
final int blue = rgb & 0xFF;
|
||||
return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
||||
}
|
||||
|
||||
private static String normalizeHtmlColor(String color) {
|
||||
return Objects.requireNonNull(color, "color").trim().toUpperCase();
|
||||
}
|
||||
|
||||
private static AssetWorkspaceBankCompositionFile fileByPath(List<AssetWorkspaceBankCompositionFile> files, String path) {
|
||||
@ -223,4 +534,93 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static AssetDetailsPaletteOverhaulingDraft rebindDraft(
|
||||
AssetDetailsPaletteOverhaulingDraft draft,
|
||||
AssetDetailsPaletteOverhaulingDraft source) {
|
||||
final Set<String> selectedPaths = draft.selectedFiles().stream()
|
||||
.map(AssetWorkspaceBankCompositionFile::path)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
final List<AssetWorkspaceBankCompositionFile> selected = source.allFiles().stream()
|
||||
.filter(file -> selectedPaths.contains(file.path()))
|
||||
.toList();
|
||||
final List<AssetWorkspaceBankCompositionFile> available = source.allFiles().stream()
|
||||
.filter(file -> !selectedPaths.contains(file.path()))
|
||||
.toList();
|
||||
final String previewPalettePath = fileByPath(source.allFiles(), draft.previewPalettePath()) == null
|
||||
? source.previewPalettePath()
|
||||
: draft.previewPalettePath();
|
||||
final String previewTilePath = fileByPath(source.previewTileFiles(), draft.previewTilePath()) == null
|
||||
? source.previewTilePath()
|
||||
: draft.previewTilePath();
|
||||
return new AssetDetailsPaletteOverhaulingDraft(
|
||||
source.previewTileFiles(),
|
||||
source.allFiles(),
|
||||
available,
|
||||
selected,
|
||||
previewTilePath,
|
||||
previewPalettePath);
|
||||
}
|
||||
|
||||
private static Set<String> schemaArtifactIds(AssetStudioGlyphMaterialSchema schema) {
|
||||
return schema.artifactBindings().stream()
|
||||
.map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private static List<AssetWorkspaceBankCompositionFile> previewTileFilesForPalette(
|
||||
List<AssetWorkspaceBankCompositionFile> previewTileFiles,
|
||||
AssetWorkspaceBankCompositionFile previewPaletteFile) {
|
||||
if (previewPaletteFile == null) {
|
||||
return previewTileFiles;
|
||||
}
|
||||
final Set<String> schemaArtifactIds = schemaArtifactIds(previewPaletteFile);
|
||||
if (schemaArtifactIds.isEmpty()) {
|
||||
return previewTileFiles;
|
||||
}
|
||||
return previewTileFiles.stream()
|
||||
.filter(file -> schemaArtifactIds.contains(artifactId(file)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static Set<String> schemaArtifactIds(AssetWorkspaceBankCompositionFile previewPaletteFile) {
|
||||
final Object artifactIds = previewPaletteFile.metadata().get("schemaArtifactIds");
|
||||
if (!(artifactIds instanceof Iterable<?> iterable)) {
|
||||
return Set.of();
|
||||
}
|
||||
final Set<String> ids = new LinkedHashSet<>();
|
||||
for (Object artifactId : iterable) {
|
||||
if (artifactId instanceof String value && !value.isBlank()) {
|
||||
ids.add(value.trim());
|
||||
}
|
||||
}
|
||||
return Set.copyOf(ids);
|
||||
}
|
||||
|
||||
private static String artifactId(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
final Object artifactId = file.metadata().get("artifactId");
|
||||
if (artifactId instanceof String value && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record PaletteRecord(
|
||||
String paletteId,
|
||||
String displayName,
|
||||
String schemaId,
|
||||
int version,
|
||||
List<String> colors,
|
||||
Map<Integer, String> materialIndices,
|
||||
Set<String> artifactIds,
|
||||
boolean derivedOriginal,
|
||||
boolean valid,
|
||||
String invalidReason) {
|
||||
}
|
||||
|
||||
private record ColorSample(double hue, double saturation, double lightness) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.banks.StudioDualListView;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
@ -28,8 +29,15 @@ public final class AssetDetailsPaletteOverhaulingDualListView extends StudioDual
|
||||
final Label hashLabel = new Label(item.displayName());
|
||||
hashLabel.getStyleClass().add("assets-details-palette-hash");
|
||||
final HBox strip = createPaletteStrip(item);
|
||||
final HBox root = new HBox(8, hashLabel, strip);
|
||||
root.getStyleClass().add("assets-details-palette-item");
|
||||
final HBox top = new HBox(8, hashLabel, strip);
|
||||
top.getStyleClass().add("assets-details-palette-item");
|
||||
final Object invalidReason = item.metadata().get("invalidReason");
|
||||
if (!(invalidReason instanceof String reason) || reason.isBlank()) {
|
||||
return top;
|
||||
}
|
||||
final Label invalidLabel = new Label(reason);
|
||||
invalidLabel.getStyleClass().add("assets-details-palette-invalid");
|
||||
final VBox root = new VBox(4, top, invalidLabel);
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
@ -146,6 +146,8 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
final Integer height = asInteger(tile.get("height"));
|
||||
final List<Integer> indices = paletteIndices(tile.get("paletteIndices"));
|
||||
final List<Integer> colors = paletteDisplayColors(paletteFile.metadata());
|
||||
final List<Integer> sourceColors = tileDisplayColors(tileFile.metadata());
|
||||
final Map<String, Integer> schemaIndicesByColor = schemaIndicesByColor(paletteFile.metadata());
|
||||
if (width == null || height == null || width <= 0 || height <= 0 || indices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -153,7 +155,7 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
final double pixelWidth = PREVIEW_SIZE / width;
|
||||
final double pixelHeight = PREVIEW_SIZE / height;
|
||||
for (int index = 0; index < indices.size(); index++) {
|
||||
final int paletteIndex = indices.get(index);
|
||||
final int paletteIndex = schemaPaletteIndex(indices.get(index), sourceColors, schemaIndicesByColor);
|
||||
final int x = index % width;
|
||||
final int y = index / width;
|
||||
graphics.setFill(toFxColor(paletteIndexToArgb(colors, paletteIndex)));
|
||||
@ -206,6 +208,35 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Integer> tileDisplayColors(Map<String, Object> metadata) {
|
||||
final Map<String, Object> palette = nestedMap(metadata, "palette");
|
||||
if (palette == null) {
|
||||
return List.of();
|
||||
}
|
||||
final Object original = palette.get("originalArgb8888");
|
||||
if (original instanceof List<?> list) {
|
||||
return (List<Integer>) list;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private Map<String, Integer> schemaIndicesByColor(Map<String, Object> metadata) {
|
||||
final Object materialIndices = metadata.get("schemaMaterialIndices");
|
||||
if (!(materialIndices instanceof Map<?, ?> map)) {
|
||||
return Map.of();
|
||||
}
|
||||
final Map<String, Integer> indicesByColor = new java.util.LinkedHashMap<>();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
final Integer index = asInteger(entry.getKey());
|
||||
if (index == null || index <= 0 || !(entry.getValue() instanceof String color) || color.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
indicesByColor.put(color.trim().toUpperCase(), index);
|
||||
}
|
||||
return Map.copyOf(indicesByColor);
|
||||
}
|
||||
|
||||
private Integer asInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
@ -220,6 +251,17 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
return colorAt(colors, paletteIndex - 1);
|
||||
}
|
||||
|
||||
private int schemaPaletteIndex(int tilePaletteIndex, List<Integer> sourceColors, Map<String, Integer> schemaIndicesByColor) {
|
||||
if (tilePaletteIndex <= 0 || sourceColors.isEmpty() || schemaIndicesByColor.isEmpty()) {
|
||||
return tilePaletteIndex;
|
||||
}
|
||||
final int sourceColorIndex = tilePaletteIndex - 1;
|
||||
if (sourceColorIndex < 0 || sourceColorIndex >= sourceColors.size()) {
|
||||
return 0;
|
||||
}
|
||||
return schemaIndicesByColor.getOrDefault(cssColor(sourceColors.get(sourceColorIndex)), 0);
|
||||
}
|
||||
|
||||
private int colorAt(List<Integer> colors, int index) {
|
||||
if (index < 0 || index >= colors.size()) {
|
||||
return 0x00000000;
|
||||
|
||||
@ -0,0 +1,281 @@
|
||||
package p.studio.workspaces.assets.details.palette;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import p.studio.Container;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class AssetDetailsPaletteWizardDialog {
|
||||
private final Stage stage = new Stage();
|
||||
private final ComboBox<AssetStudioGlyphMaterialSchema> schemaComboBox = new ComboBox<>();
|
||||
private final TextField nameField = new TextField();
|
||||
private final ObservableList<String> editableColors = FXCollections.observableArrayList();
|
||||
private final ObservableList<String> indexItems = FXCollections.observableArrayList();
|
||||
private final ListView<String> indexListView = new ListView<>(indexItems);
|
||||
private final ColorPicker colorPicker = new ColorPicker(Color.WHITE);
|
||||
private final Label summaryLabel = new Label();
|
||||
private final HBox originalStrip = new HBox(4);
|
||||
private final HBox editableStrip = new HBox(4);
|
||||
|
||||
private PaletteEditResult result;
|
||||
private final String paletteId;
|
||||
private final boolean editing;
|
||||
|
||||
private AssetDetailsPaletteWizardDialog(
|
||||
Window owner,
|
||||
AssetDetailsPaletteOverhaulingCoordinator coordinator,
|
||||
AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
this.editing = paletteFile != null;
|
||||
this.paletteId = editing && paletteFile.metadata().get("paletteId") instanceof String id && !id.isBlank()
|
||||
? id
|
||||
: coordinator.nextPaletteId();
|
||||
stage.initOwner(owner);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setTitle(Container.i18n().text(editing
|
||||
? I18n.ASSETS_PALETTE_WIZARD_UPDATE_TITLE
|
||||
: I18n.ASSETS_PALETTE_WIZARD_CREATE_TITLE));
|
||||
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = coordinator.readySchemas();
|
||||
schemaComboBox.getItems().setAll(schemas);
|
||||
schemaComboBox.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(AssetStudioGlyphMaterialSchema item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : item.schemaId());
|
||||
}
|
||||
});
|
||||
schemaComboBox.setButtonCell(schemaComboBox.getCellFactory().call(null));
|
||||
|
||||
final AssetStudioGlyphMaterialSchema initialSchema = resolveInitialSchema(coordinator, paletteFile, schemas);
|
||||
if (initialSchema != null) {
|
||||
schemaComboBox.getSelectionModel().select(initialSchema);
|
||||
}
|
||||
editableColors.setAll(initialEditableColors(paletteFile, initialSchema));
|
||||
nameField.setText(initialDisplayName(paletteFile));
|
||||
nameField.setPromptText(AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(editableColors));
|
||||
schemaComboBox.valueProperty().addListener((ignored, previous, current) -> {
|
||||
if (current == null) {
|
||||
editableColors.clear();
|
||||
} else if (!editing || previous == null || !Objects.equals(previous.schemaId(), current.schemaId())) {
|
||||
editableColors.setAll(current.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList());
|
||||
}
|
||||
refreshPreview(coordinator);
|
||||
});
|
||||
nameField.textProperty().addListener((ignored, previous, current) -> refreshPreview(coordinator));
|
||||
indexListView.getSelectionModel().selectedIndexProperty().addListener((ignored, previous, current) -> {
|
||||
final int selected = current == null ? -1 : current.intValue();
|
||||
if (selected >= 0 && selected < editableColors.size()) {
|
||||
colorPicker.setValue(Color.web(editableColors.get(selected)));
|
||||
}
|
||||
});
|
||||
|
||||
final Button applyColorButton = new Button(Container.i18n().text(I18n.ASSETS_PALETTE_WIZARD_APPLY_COLOR));
|
||||
applyColorButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
applyColorButton.setOnAction(ignored -> {
|
||||
final int index = indexListView.getSelectionModel().getSelectedIndex();
|
||||
if (index < 0 || index >= editableColors.size()) {
|
||||
return;
|
||||
}
|
||||
editableColors.set(index, toHtmlColor(colorPicker.getValue()));
|
||||
refreshPreview(coordinator);
|
||||
indexListView.getSelectionModel().select(index);
|
||||
});
|
||||
|
||||
final Button cancelButton = new Button(Container.i18n().text(I18n.FORM_ACTION_CANCEL));
|
||||
cancelButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
cancelButton.setOnAction(ignored -> {
|
||||
result = null;
|
||||
stage.close();
|
||||
});
|
||||
final Button applyButton = new Button(Container.i18n().text(I18n.FORM_ACTION_APPLY));
|
||||
applyButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||
applyButton.setOnAction(ignored -> {
|
||||
final AssetStudioGlyphMaterialSchema selectedSchema = schemaComboBox.getSelectionModel().getSelectedItem();
|
||||
if (selectedSchema == null || coordinator.duplicatePaletteExists(paletteId, selectedSchema.schemaId(), editableColors)) {
|
||||
return;
|
||||
}
|
||||
result = new PaletteEditResult(paletteId, normalizedDisplayName(), selectedSchema.schemaId(), List.copyOf(editableColors));
|
||||
stage.close();
|
||||
});
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
final HBox actions = new HBox(8, spacer, cancelButton, applyButton);
|
||||
actions.getStyleClass().add("assets-details-palette-wizard-actions");
|
||||
|
||||
final Label schemaLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_SCHEMA);
|
||||
final Label nameLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_NAME);
|
||||
final Label originalLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_ORIGINAL);
|
||||
final Label editableLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_EDITABLE);
|
||||
final VBox colorEditor = new VBox(8, colorPicker, applyColorButton);
|
||||
colorEditor.getStyleClass().add("assets-details-palette-wizard-color-editor");
|
||||
final HBox editorBody = new HBox(12, indexListView, colorEditor);
|
||||
editorBody.getStyleClass().add("assets-details-palette-wizard-editor");
|
||||
final VBox root = new VBox(
|
||||
12,
|
||||
nameLabel,
|
||||
nameField,
|
||||
schemaLabel,
|
||||
schemaComboBox,
|
||||
originalLabel,
|
||||
originalStrip,
|
||||
editableLabel,
|
||||
editableStrip,
|
||||
editorBody,
|
||||
summaryLabel,
|
||||
actions);
|
||||
root.setPadding(new Insets(16));
|
||||
root.getStyleClass().add("assets-details-palette-wizard");
|
||||
originalStrip.getStyleClass().add("assets-details-palette-wizard-strip");
|
||||
editableStrip.getStyleClass().add("assets-details-palette-wizard-strip");
|
||||
summaryLabel.getStyleClass().add("assets-details-palette-wizard-summary");
|
||||
HBox.setHgrow(indexListView, Priority.ALWAYS);
|
||||
HBox.setHgrow(schemaComboBox, Priority.ALWAYS);
|
||||
HBox.setHgrow(nameField, Priority.ALWAYS);
|
||||
refreshPreview(coordinator);
|
||||
final Scene scene = new Scene(root, 620, 570);
|
||||
scene.getStylesheets().add(Container.theme().getDefaultTheme());
|
||||
stage.setScene(scene);
|
||||
stage.setOnShown(ignored -> stage.centerOnScreen());
|
||||
}
|
||||
|
||||
public static Optional<PaletteEditResult> showAndWait(
|
||||
Window owner,
|
||||
AssetDetailsPaletteOverhaulingCoordinator coordinator,
|
||||
AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
final AssetDetailsPaletteWizardDialog dialog = new AssetDetailsPaletteWizardDialog(owner, coordinator, paletteFile);
|
||||
dialog.stage.showAndWait();
|
||||
return Optional.ofNullable(dialog.result);
|
||||
}
|
||||
|
||||
private Label fieldLabel(I18n label) {
|
||||
final Label node = new Label(Container.i18n().text(label));
|
||||
node.getStyleClass().add("assets-details-palette-wizard-label");
|
||||
return node;
|
||||
}
|
||||
|
||||
private AssetStudioGlyphMaterialSchema resolveInitialSchema(
|
||||
AssetDetailsPaletteOverhaulingCoordinator coordinator,
|
||||
AssetWorkspaceBankCompositionFile paletteFile,
|
||||
List<AssetStudioGlyphMaterialSchema> schemas) {
|
||||
if (paletteFile != null && paletteFile.metadata().get("schemaId") instanceof String schemaId) {
|
||||
return coordinator.readySchemas().stream()
|
||||
.filter(schema -> schema.schemaId().equals(schemaId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
return schemas.isEmpty() ? null : schemas.getFirst();
|
||||
}
|
||||
|
||||
private List<String> initialEditableColors(
|
||||
AssetWorkspaceBankCompositionFile paletteFile,
|
||||
AssetStudioGlyphMaterialSchema initialSchema) {
|
||||
if (paletteFile != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
final Map<String, Object> palette = (Map<String, Object>) paletteFile.metadata().get("palette");
|
||||
final Object original = palette == null ? null : palette.get("originalArgb8888");
|
||||
if (original instanceof Iterable<?> iterable) {
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (Object value : iterable) {
|
||||
if (value instanceof Number number) {
|
||||
colors.add(String.format("#%06X", number.intValue() & 0x00FFFFFF));
|
||||
}
|
||||
}
|
||||
return List.copyOf(colors);
|
||||
}
|
||||
}
|
||||
if (initialSchema == null) {
|
||||
return List.of();
|
||||
}
|
||||
return initialSchema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String initialDisplayName(AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
if (paletteFile == null) {
|
||||
return "";
|
||||
}
|
||||
final Object explicitName = paletteFile.metadata().get("explicitPaletteDisplayName");
|
||||
return explicitName instanceof String value ? value : "";
|
||||
}
|
||||
|
||||
private void refreshPreview(AssetDetailsPaletteOverhaulingCoordinator coordinator) {
|
||||
final AssetStudioGlyphMaterialSchema schema = schemaComboBox.getSelectionModel().getSelectedItem();
|
||||
final List<String> originalColors = schema == null
|
||||
? List.of()
|
||||
: schema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
originalStrip.getChildren().setAll(originalColors.stream().map(this::swatch).toList());
|
||||
editableStrip.getChildren().setAll(editableColors.stream().map(this::swatch).toList());
|
||||
nameField.setPromptText(AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(editableColors));
|
||||
indexItems.setAll(java.util.stream.IntStream.range(0, editableColors.size())
|
||||
.mapToObj(index -> (index + 1) + " -> " + editableColors.get(index))
|
||||
.toList());
|
||||
final boolean duplicate = schema != null && coordinator.duplicatePaletteExists(paletteId, schema.schemaId(), editableColors);
|
||||
summaryLabel.setText(duplicate
|
||||
? Container.i18n().text(I18n.ASSETS_PALETTE_WIZARD_DUPLICATE)
|
||||
: Container.i18n().text(I18n.ASSETS_PALETTE_WIZARD_READY));
|
||||
}
|
||||
|
||||
private String normalizedDisplayName() {
|
||||
final String value = nameField.getText();
|
||||
return value == null || value.trim().isBlank()
|
||||
? AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(editableColors)
|
||||
: value.trim();
|
||||
}
|
||||
|
||||
private Region swatch(String color) {
|
||||
final Region swatch = new Region();
|
||||
swatch.setMinSize(22, 22);
|
||||
swatch.setPrefSize(22, 22);
|
||||
swatch.setMaxSize(22, 22);
|
||||
swatch.getStyleClass().add("assets-details-palette-wizard-swatch");
|
||||
swatch.setStyle("-fx-background-color: " + color + ";");
|
||||
return swatch;
|
||||
}
|
||||
|
||||
private String toHtmlColor(Color color) {
|
||||
return String.format(
|
||||
"#%02X%02X%02X",
|
||||
(int) Math.round(color.getRed() * 255),
|
||||
(int) Math.round(color.getGreen() * 255),
|
||||
(int) Math.round(color.getBlue() * 255));
|
||||
}
|
||||
|
||||
public record PaletteEditResult(
|
||||
String paletteId,
|
||||
String displayName,
|
||||
String schemaId,
|
||||
List<String> colors) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,545 @@
|
||||
package p.studio.workspaces.assets.details.scene;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.forms.StudioFormMode;
|
||||
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
|
||||
import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
import p.packer.messages.ListAssetsRequest;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
public final class AssetDetailsSceneLayersControl extends VBox implements StudioControlLifecycle {
|
||||
private final ProjectReference projectReference;
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
|
||||
private AssetWorkspaceDetailsViewState viewState;
|
||||
private IntConsumer generateLayerAction = ignored -> {
|
||||
};
|
||||
private IntConsumer validateLayerAction = ignored -> {
|
||||
};
|
||||
private IntConsumer acceptLayerAction = ignored -> {
|
||||
};
|
||||
private Integer editingLayerIndex;
|
||||
private LayerDraft editingDraft;
|
||||
private int selectedLayerIndex = 1;
|
||||
|
||||
public AssetDetailsSceneLayersControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) {
|
||||
StudioControlLifecycleSupport.install(this, this);
|
||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe() {
|
||||
subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
|
||||
viewState = event.viewState();
|
||||
render();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe() {
|
||||
subscriptions.clear();
|
||||
}
|
||||
|
||||
public void setGenerateLayerAction(IntConsumer generateLayerAction) {
|
||||
this.generateLayerAction = Objects.requireNonNull(generateLayerAction, "generateLayerAction");
|
||||
}
|
||||
|
||||
public void setValidateLayerAction(IntConsumer validateLayerAction) {
|
||||
this.validateLayerAction = Objects.requireNonNull(validateLayerAction, "validateLayerAction");
|
||||
}
|
||||
|
||||
public void setAcceptLayerAction(IntConsumer acceptLayerAction) {
|
||||
this.acceptLayerAction = Objects.requireNonNull(acceptLayerAction, "acceptLayerAction");
|
||||
}
|
||||
|
||||
private void render() {
|
||||
final AssetStudioSceneBankMetadata metadata = viewState == null || viewState.selectedAssetDetails() == null
|
||||
? null
|
||||
: viewState.selectedAssetDetails().sceneBankMetadata();
|
||||
if (metadata == null || viewState.selectedAssetDetails() == null) {
|
||||
getChildren().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
final VBox content = new VBox(10);
|
||||
content.getChildren().add(createLayerSelector(
|
||||
viewState.selectedAssetDetails().summary().assetRoot(),
|
||||
metadata));
|
||||
content.getStyleClass().add("assets-scene-layer-shell");
|
||||
|
||||
getChildren().setAll(AssetDetailsUiSupport.createSectionWithoutHeader(content));
|
||||
}
|
||||
|
||||
private Node createLayerSelector(Path sceneAssetRoot, AssetStudioSceneBankMetadata metadata) {
|
||||
if (selectedLayerIndex < 1 || selectedLayerIndex > metadata.layerCount()) {
|
||||
selectedLayerIndex = 1;
|
||||
}
|
||||
final HBox selector = new HBox(8);
|
||||
selector.getStyleClass().add("assets-scene-layer-selector");
|
||||
for (int index = 1; index <= 4; index += 1) {
|
||||
final int layerIndex = index;
|
||||
final Button button = new Button(Container.i18n().text(I18n.ASSETS_LABEL_LAYER) + " " + layerIndex);
|
||||
button.getStyleClass().addAll("studio-button", "assets-scene-layer-selector-button");
|
||||
if (layerIndex == selectedLayerIndex) {
|
||||
button.getStyleClass().add("assets-scene-layer-selector-button-active");
|
||||
}
|
||||
button.setOnAction(ignored -> {
|
||||
selectedLayerIndex = layerIndex;
|
||||
render();
|
||||
});
|
||||
selector.getChildren().add(button);
|
||||
}
|
||||
|
||||
final AssetStudioSceneLayerBinding binding = metadata.layerBindings().stream()
|
||||
.filter(candidate -> candidate.index() == selectedLayerIndex)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
final VBox container = new VBox(10);
|
||||
container.getChildren().addAll(selector, createLayerCard(sceneAssetRoot, selectedLayerIndex, binding));
|
||||
return container;
|
||||
}
|
||||
|
||||
private Node createLayerCard(Path sceneAssetRoot, int layerIndex, AssetStudioSceneLayerBinding binding) {
|
||||
final boolean editing = Objects.equals(editingLayerIndex, layerIndex) && binding != null && editingDraft != null;
|
||||
final LayerDraft draft = editing ? editingDraft : LayerDraft.from(binding);
|
||||
final VBox formContent = buildLayerFormContent(sceneAssetRoot, binding, draft, editing);
|
||||
final VBox formSection = AssetDetailsUiSupport.createEditableSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_LAYER_CONFIGURATION),
|
||||
createLayerFormHeaderActions(layerIndex, binding, editing),
|
||||
formContent,
|
||||
editing ? StudioFormMode.EDITING : StudioFormMode.READ_ONLY);
|
||||
formSection.getStyleClass().add("assets-scene-layer-config-section");
|
||||
|
||||
final VBox leftColumn = new VBox(10);
|
||||
leftColumn.getStyleClass().add("assets-scene-layer-config-column");
|
||||
leftColumn.getChildren().addAll(
|
||||
formSection,
|
||||
createLayerActions(sceneAssetRoot, layerIndex, draft));
|
||||
|
||||
final VBox overview = createLayerOverviewPlaceholder();
|
||||
final HBox layout = new HBox(12, leftColumn, overview);
|
||||
layout.getStyleClass().add("assets-scene-layer-layout");
|
||||
HBox.setHgrow(leftColumn, Priority.ALWAYS);
|
||||
return layout;
|
||||
}
|
||||
|
||||
private VBox buildLayerFormContent(Path sceneAssetRoot, AssetStudioSceneLayerBinding binding, LayerDraft draft, boolean editing) {
|
||||
final VBox card = new VBox(8);
|
||||
card.getStyleClass().addAll("assets-scene-layer-card", "assets-scene-layer-config");
|
||||
|
||||
final CheckBox enabledCheckBox = new CheckBox(Container.i18n().text(I18n.ASSETS_LABEL_USE_LAYER));
|
||||
enabledCheckBox.setSelected(draft != null && draft.enabled());
|
||||
enabledCheckBox.getStyleClass().add("assets-scene-layer-enabled");
|
||||
enabledCheckBox.setDisable(!editing);
|
||||
enabledCheckBox.setOnAction(ignored -> {
|
||||
if (draft != null) {
|
||||
draft.setEnabled(enabledCheckBox.isSelected());
|
||||
}
|
||||
});
|
||||
card.getChildren().add(enabledCheckBox);
|
||||
|
||||
card.getChildren().addAll(
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_SCENE_DIMENSIONS),
|
||||
editing ? createLayerSizeEditor(draft) : new Label(draft.widthText() + " x " + draft.heightText())),
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TILEMAP),
|
||||
binding == null
|
||||
? Container.i18n().text(I18n.ASSETS_VALUE_NOT_CONFIGURED)
|
||||
: AssetDetailsUiSupport.projectRelativePath(projectReference, sceneAssetRoot.resolve(binding.tilemap()))),
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TILESET),
|
||||
editing ? createTilesetCombo(draft, true) : createTilesetReadOnlyValue(binding, draft)));
|
||||
return card;
|
||||
}
|
||||
|
||||
private VBox createLayerOverviewPlaceholder() {
|
||||
final VBox overview = new VBox(10);
|
||||
overview.getStyleClass().add("assets-scene-layer-overview");
|
||||
|
||||
final Label title = new Label(Container.i18n().text(I18n.ASSETS_LABEL_LAYER_OVERVIEW));
|
||||
title.getStyleClass().add("assets-scene-layer-overview-title");
|
||||
|
||||
final Region surface = new Region();
|
||||
surface.getStyleClass().add("assets-scene-layer-overview-surface");
|
||||
VBox.setVgrow(surface, Priority.ALWAYS);
|
||||
|
||||
overview.getChildren().addAll(
|
||||
title,
|
||||
surface,
|
||||
AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_VALUE_LAYER_OVERVIEW_PENDING)));
|
||||
return overview;
|
||||
}
|
||||
|
||||
private Node createLayerFormHeaderActions(int layerIndex, AssetStudioSceneLayerBinding binding, boolean editing) {
|
||||
final HBox actions = new HBox(8);
|
||||
actions.getStyleClass().add("assets-scene-layer-form-actions");
|
||||
if (!editing) {
|
||||
final Button changeButton = createFormActionButton(I18n.FORM_ACTION_CHANGE);
|
||||
changeButton.setDisable(binding == null);
|
||||
changeButton.setOnAction(ignored -> beginEdit(layerIndex, binding));
|
||||
actions.getChildren().add(changeButton);
|
||||
return actions;
|
||||
}
|
||||
|
||||
final Button applyButton = createFormActionButton(I18n.FORM_ACTION_APPLY);
|
||||
applyButton.setOnAction(ignored -> applyEdit(layerIndex, binding));
|
||||
final Button resetButton = createFormActionButton(I18n.FORM_ACTION_RESET);
|
||||
resetButton.setOnAction(ignored -> resetEdit(binding));
|
||||
final Button cancelButton = createFormActionButton(I18n.FORM_ACTION_CANCEL);
|
||||
cancelButton.setOnAction(ignored -> cancelEdit());
|
||||
actions.getChildren().addAll(applyButton, resetButton, cancelButton);
|
||||
return actions;
|
||||
}
|
||||
|
||||
private Node createLayerActions(Path sceneAssetRoot, int layerIndex, LayerDraft draft) {
|
||||
final HBox actions = new HBox(8);
|
||||
actions.setAlignment(Pos.CENTER_LEFT);
|
||||
actions.getStyleClass().add("assets-scene-layer-actions");
|
||||
final boolean enabled = draft != null && draft.enabled();
|
||||
final boolean hasTmx = hasLayerTilemap(sceneAssetRoot, layerIndex);
|
||||
|
||||
final Button generateButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_GENERATE_TMX));
|
||||
generateButton.getStyleClass().add("assets-scene-layer-action");
|
||||
generateButton.setDisable(!enabled || hasTmx);
|
||||
generateButton.setOnAction(ignored -> generateLayerAction.accept(layerIndex));
|
||||
|
||||
final Button validateButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_VALIDATE_LAYER));
|
||||
validateButton.getStyleClass().add("assets-scene-layer-action");
|
||||
validateButton.setDisable(!enabled || !hasTmx);
|
||||
validateButton.setOnAction(ignored -> validateLayerAction.accept(layerIndex));
|
||||
|
||||
final Button acceptButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_ACCEPT_LAYER));
|
||||
acceptButton.getStyleClass().add("assets-scene-layer-action");
|
||||
acceptButton.setDisable(!enabled || !hasTmx);
|
||||
acceptButton.setOnAction(ignored -> acceptLayerAction.accept(layerIndex));
|
||||
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
actions.getChildren().addAll(generateButton, validateButton, acceptButton, spacer);
|
||||
return actions;
|
||||
}
|
||||
|
||||
private Node createLayerSizeEditor(LayerDraft draft) {
|
||||
final HBox editor = new HBox(8);
|
||||
editor.getStyleClass().add("assets-scene-layer-size-editor");
|
||||
|
||||
final TextField widthField = createSizeField(draft.widthText(), draft::setWidthText);
|
||||
final Label separator = new Label("x");
|
||||
separator.getStyleClass().add("assets-scene-layer-size-separator");
|
||||
final TextField heightField = createSizeField(draft.heightText(), draft::setHeightText);
|
||||
|
||||
editor.getChildren().addAll(widthField, separator, heightField);
|
||||
return editor;
|
||||
}
|
||||
|
||||
private TextField createSizeField(String value, java.util.function.Consumer<String> updater) {
|
||||
final TextField textField = new TextField(value);
|
||||
textField.getStyleClass().add("assets-scene-layer-size-field");
|
||||
textField.textProperty().addListener((ignored, oldValue, newValue) -> updater.accept(newValue));
|
||||
return textField;
|
||||
}
|
||||
|
||||
private ComboBox<TilesetOption> createTilesetCombo(LayerDraft draft, boolean editing) {
|
||||
final ComboBox<TilesetOption> combo = new ComboBox<>();
|
||||
combo.setMaxWidth(Double.MAX_VALUE);
|
||||
combo.getStyleClass().add("assets-details-combo");
|
||||
final List<TilesetOption> options = loadTilesetOptions();
|
||||
combo.setItems(FXCollections.observableArrayList(options));
|
||||
combo.setCellFactory(ignored -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(TilesetOption item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : item.label());
|
||||
}
|
||||
});
|
||||
combo.setButtonCell(new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(TilesetOption item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : item.label());
|
||||
}
|
||||
});
|
||||
final String currentTileset = draft == null ? "" : draft.tilesetAssetRoot();
|
||||
options.stream()
|
||||
.filter(option -> option.relativeAssetRoot().equals(currentTileset))
|
||||
.findFirst()
|
||||
.ifPresentOrElse(
|
||||
combo.getSelectionModel()::select,
|
||||
() -> combo.getSelectionModel().clearSelection());
|
||||
combo.setDisable(!editing || draft == null);
|
||||
if (draft == null) {
|
||||
combo.setDisable(true);
|
||||
return combo;
|
||||
}
|
||||
combo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
if (Objects.equals(oldValue, newValue)) {
|
||||
return;
|
||||
}
|
||||
draft.setTilesetAssetRoot(newValue == null ? "" : newValue.relativeAssetRoot());
|
||||
});
|
||||
return combo;
|
||||
}
|
||||
|
||||
private Node createTilesetReadOnlyValue(AssetStudioSceneLayerBinding binding, LayerDraft draft) {
|
||||
final String value = binding == null || draft == null || draft.tilesetAssetRoot().isBlank()
|
||||
? Container.i18n().text(I18n.ASSETS_VALUE_NOT_CONFIGURED)
|
||||
: tilesetPath(draft.tilesetAssetRoot());
|
||||
return new Label(value);
|
||||
}
|
||||
|
||||
private List<TilesetOption> loadTilesetOptions() {
|
||||
try {
|
||||
final var response = Container.packer().workspaceService().listAssets(new ListAssetsRequest(projectReference.toPackerProjectContext()));
|
||||
return response.assets().stream()
|
||||
.filter(asset -> asset.assetFamily() == AssetFamilyCatalog.GLYPH_BANK)
|
||||
.filter(asset -> metadataService.read(asset.identity().assetRoot(), AssetFamilyCatalog.GLYPH_BANK).glyphSpecialization()
|
||||
== p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization.TILESET)
|
||||
.map(asset -> {
|
||||
final String relativeAssetRoot = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.relativize(asset.identity().assetRoot())
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
return new TilesetOption(asset.identity().assetName(), relativeAssetRoot);
|
||||
})
|
||||
.sorted(Comparator.comparing(TilesetOption::label, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
} catch (RuntimeException exception) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLayerBinding(
|
||||
int layerIndex,
|
||||
java.util.function.Function<AssetStudioSceneLayerBinding, AssetStudioSceneLayerBinding> mutator,
|
||||
String successMessage) {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetDetails().sceneBankMetadata() == null) {
|
||||
return;
|
||||
}
|
||||
final AssetStudioSceneBankMetadata metadata = viewState.selectedAssetDetails().sceneBankMetadata();
|
||||
final java.util.List<AssetStudioSceneLayerBinding> updatedBindings = new ArrayList<>();
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (binding.index() == layerIndex) {
|
||||
updatedBindings.add(mutator.apply(binding));
|
||||
} else {
|
||||
updatedBindings.add(binding);
|
||||
}
|
||||
}
|
||||
final AssetStudioSceneLayerBinding firstBinding = updatedBindings.getFirst();
|
||||
final AssetStudioSceneBankMetadata updatedMetadata = new AssetStudioSceneBankMetadata(
|
||||
firstBinding.width(),
|
||||
firstBinding.height(),
|
||||
metadata.layerCount(),
|
||||
updatedBindings,
|
||||
metadata.supportFile());
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
metadataService.writeSceneBankMetadata(viewState.selectedAssetDetails().summary().assetRoot(), updatedMetadata);
|
||||
Platform.runLater(() -> {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("scene-bank-layer", successMessage));
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||
});
|
||||
} catch (IOException exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(
|
||||
new StudioAssetLogEvent("scene-bank-layer", "Unable to update layer " + layerIndex + ": " + exception.getMessage())));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String tilesetPath(AssetStudioSceneLayerBinding binding) {
|
||||
return tilesetPath(binding.tilesetAssetRoot());
|
||||
}
|
||||
|
||||
private String tilesetPath(String tilesetAssetRoot) {
|
||||
if (tilesetAssetRoot.isBlank()) {
|
||||
return "—";
|
||||
}
|
||||
return AssetDetailsUiSupport.projectRelativePath(
|
||||
projectReference,
|
||||
projectReference.rootPath().resolve("assets").resolve(tilesetAssetRoot));
|
||||
}
|
||||
|
||||
private void beginEdit(int layerIndex, AssetStudioSceneLayerBinding binding) {
|
||||
editingLayerIndex = layerIndex;
|
||||
editingDraft = LayerDraft.from(binding);
|
||||
render();
|
||||
}
|
||||
|
||||
private void applyEdit(int layerIndex, AssetStudioSceneLayerBinding binding) {
|
||||
if (binding == null || editingDraft == null || !editingDraft.hasValidDimensions()) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"scene-bank-layer",
|
||||
"Layer size must use positive integer values before applying changes."));
|
||||
return;
|
||||
}
|
||||
final LayerDraft draft = editingDraft.copy();
|
||||
editingLayerIndex = null;
|
||||
editingDraft = null;
|
||||
render();
|
||||
updateLayerBinding(layerIndex, current -> new AssetStudioSceneLayerBinding(
|
||||
draft.enabled(),
|
||||
current.index(),
|
||||
current.layerName(),
|
||||
draft.widthValue(),
|
||||
draft.heightValue(),
|
||||
current.tilemap(),
|
||||
draft.tilesetAssetRoot()),
|
||||
"Updated layer " + layerIndex + " configuration.");
|
||||
}
|
||||
|
||||
private void resetEdit(AssetStudioSceneLayerBinding binding) {
|
||||
editingDraft = LayerDraft.from(binding);
|
||||
render();
|
||||
}
|
||||
|
||||
private void cancelEdit() {
|
||||
editingLayerIndex = null;
|
||||
editingDraft = null;
|
||||
render();
|
||||
}
|
||||
|
||||
private Button createFormActionButton(I18n textKey) {
|
||||
final Button button = new Button(Container.i18n().text(textKey));
|
||||
button.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
return button;
|
||||
}
|
||||
|
||||
private boolean hasLayerTilemap(Path sceneAssetRoot, int layerIndex) {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetDetails().sceneBankMetadata() == null) {
|
||||
return false;
|
||||
}
|
||||
return viewState.selectedAssetDetails().sceneBankMetadata().layerBindings().stream()
|
||||
.filter(binding -> binding.index() == layerIndex)
|
||||
.findFirst()
|
||||
.map(binding -> Files.isRegularFile(sceneAssetRoot.resolve(binding.tilemap())))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private record TilesetOption(String assetName, String relativeAssetRoot) {
|
||||
String label() {
|
||||
return assetName + " [" + relativeAssetRoot + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LayerDraft {
|
||||
private boolean enabled;
|
||||
private String widthText;
|
||||
private String heightText;
|
||||
private String tilesetAssetRoot;
|
||||
|
||||
private LayerDraft(boolean enabled, String widthText, String heightText, String tilesetAssetRoot) {
|
||||
this.enabled = enabled;
|
||||
this.widthText = widthText;
|
||||
this.heightText = heightText;
|
||||
this.tilesetAssetRoot = Objects.requireNonNullElse(tilesetAssetRoot, "").trim();
|
||||
}
|
||||
|
||||
static LayerDraft from(AssetStudioSceneLayerBinding binding) {
|
||||
if (binding == null) {
|
||||
return new LayerDraft(false, "16", "16", "");
|
||||
}
|
||||
return new LayerDraft(
|
||||
binding.enabled(),
|
||||
Integer.toString(binding.width()),
|
||||
Integer.toString(binding.height()),
|
||||
binding.tilesetAssetRoot());
|
||||
}
|
||||
|
||||
LayerDraft copy() {
|
||||
return new LayerDraft(enabled, widthText, heightText, tilesetAssetRoot);
|
||||
}
|
||||
|
||||
boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
String widthText() {
|
||||
return widthText;
|
||||
}
|
||||
|
||||
void setWidthText(String widthText) {
|
||||
this.widthText = Objects.requireNonNullElse(widthText, "").trim();
|
||||
}
|
||||
|
||||
String heightText() {
|
||||
return heightText;
|
||||
}
|
||||
|
||||
void setHeightText(String heightText) {
|
||||
this.heightText = Objects.requireNonNullElse(heightText, "").trim();
|
||||
}
|
||||
|
||||
String tilesetAssetRoot() {
|
||||
return tilesetAssetRoot;
|
||||
}
|
||||
|
||||
void setTilesetAssetRoot(String tilesetAssetRoot) {
|
||||
this.tilesetAssetRoot = Objects.requireNonNullElse(tilesetAssetRoot, "").trim();
|
||||
}
|
||||
|
||||
boolean hasValidDimensions() {
|
||||
return parsePositiveInt(widthText) > 0 && parsePositiveInt(heightText) > 0;
|
||||
}
|
||||
|
||||
int widthValue() {
|
||||
return parsePositiveInt(widthText);
|
||||
}
|
||||
|
||||
int heightValue() {
|
||||
return parsePositiveInt(heightText);
|
||||
}
|
||||
|
||||
private static int parsePositiveInt(String value) {
|
||||
try {
|
||||
return Integer.parseInt(Objects.requireNonNullElse(value, "").trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialPalette;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class AssetDetailsColorSchemaCoordinator {
|
||||
private AssetWorkspaceAssetDetails details;
|
||||
private AssetStudioGlyphMaterialMetadata localMaterialMetadata;
|
||||
|
||||
public void replaceDetails(AssetWorkspaceAssetDetails details) {
|
||||
this.details = details;
|
||||
this.localMaterialMetadata = details == null ? null : details.glyphMaterialMetadata();
|
||||
}
|
||||
|
||||
public void replaceMaterialMetadata(AssetStudioGlyphMaterialMetadata metadata) {
|
||||
this.localMaterialMetadata = Objects.requireNonNull(metadata, "metadata");
|
||||
}
|
||||
|
||||
public boolean ready() {
|
||||
return details != null && details.summary().assetFamily() == AssetFamilyCatalog.GLYPH_BANK;
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata metadata() {
|
||||
if (!ready() || localMaterialMetadata == null) {
|
||||
return new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null);
|
||||
}
|
||||
return localMaterialMetadata;
|
||||
}
|
||||
|
||||
public List<AssetStudioGlyphMaterialSchema> schemas() {
|
||||
return metadata().schemas();
|
||||
}
|
||||
|
||||
public List<AssetWorkspaceBankCompositionFile> selectedTiles() {
|
||||
if (!ready()) {
|
||||
return List.of();
|
||||
}
|
||||
return details.bankComposition().selectedFiles().stream()
|
||||
.filter(AssetDetailsColorSchemaCoordinator::supportsSchemaAuthoring)
|
||||
.filter(file -> artifactId(file) != null)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<AssetWorkspaceBankCompositionFile> availableTilesForCreate() {
|
||||
final Set<String> reservedArtifactIds = new HashSet<>();
|
||||
for (AssetStudioGlyphMaterialSchema schema : schemas()) {
|
||||
schema.artifactBindings().forEach(binding -> reservedArtifactIds.add(binding.artifactId()));
|
||||
}
|
||||
return selectedTiles().stream()
|
||||
.filter(file -> !reservedArtifactIds.contains(artifactId(file)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<AssetWorkspaceBankCompositionFile> availableTilesForEdit(String schemaId) {
|
||||
final AssetStudioGlyphMaterialSchema schema = findSchema(schemaId).orElse(null);
|
||||
if (schema == null) {
|
||||
return availableTilesForCreate();
|
||||
}
|
||||
final Set<String> current = schema.artifactBindings().stream()
|
||||
.map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
final Set<String> reservedElsewhere = new HashSet<>();
|
||||
for (AssetStudioGlyphMaterialSchema candidate : schemas()) {
|
||||
if (candidate.schemaId().equals(schemaId)) {
|
||||
continue;
|
||||
}
|
||||
candidate.artifactBindings().forEach(binding -> reservedElsewhere.add(binding.artifactId()));
|
||||
}
|
||||
return selectedTiles().stream()
|
||||
.filter(file -> current.contains(artifactId(file)) || !reservedElsewhere.contains(artifactId(file)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public boolean canRemove(String schemaId) {
|
||||
return metadata().palettes().stream().noneMatch(palette -> palette.schemaId().equals(schemaId));
|
||||
}
|
||||
|
||||
public Optional<AssetStudioGlyphMaterialSchema> findSchema(String schemaId) {
|
||||
return schemas().stream().filter(schema -> schema.schemaId().equals(schemaId)).findFirst();
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata upsertSchema(
|
||||
String schemaId,
|
||||
List<AssetWorkspaceBankCompositionFile> assignedTiles,
|
||||
List<String> orderedColors) {
|
||||
final AssetStudioGlyphMaterialMetadata current = metadata();
|
||||
final AssetStudioGlyphMaterialSchema existing = current.findSchema(schemaId).orElse(null);
|
||||
final AssetStudioGlyphMaterialSchema next = buildSchema(schemaId, existing, assignedTiles, orderedColors);
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = new ArrayList<>();
|
||||
boolean replaced = false;
|
||||
for (AssetStudioGlyphMaterialSchema schema : current.schemas()) {
|
||||
if (schema.schemaId().equals(schemaId)) {
|
||||
schemas.add(next);
|
||||
replaced = true;
|
||||
} else {
|
||||
schemas.add(schema);
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
schemas.add(next);
|
||||
}
|
||||
schemas.sort(Comparator.comparing(AssetStudioGlyphMaterialSchema::schemaId, String.CASE_INSENSITIVE_ORDER));
|
||||
return new AssetStudioGlyphMaterialMetadata(schemas, current.palettes(), current.supportFile());
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata removeSchema(String schemaId) {
|
||||
if (!canRemove(schemaId)) {
|
||||
throw new IllegalStateException("Schema is still referenced by one or more palettes.");
|
||||
}
|
||||
final AssetStudioGlyphMaterialMetadata current = metadata();
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = current.schemas().stream()
|
||||
.filter(schema -> !schema.schemaId().equals(schemaId))
|
||||
.toList();
|
||||
return new AssetStudioGlyphMaterialMetadata(schemas, current.palettes(), current.supportFile());
|
||||
}
|
||||
|
||||
public String nextSchemaId() {
|
||||
int next = 1;
|
||||
final Set<String> ids = schemas().stream().map(AssetStudioGlyphMaterialSchema::schemaId).collect(java.util.stream.Collectors.toSet());
|
||||
while (ids.contains("schema-" + next)) {
|
||||
next += 1;
|
||||
}
|
||||
return "schema-" + next;
|
||||
}
|
||||
|
||||
public int unassignedTileCount() {
|
||||
return availableTilesForCreate().size();
|
||||
}
|
||||
|
||||
public static AssetStudioGlyphMaterialSchemaStatus effectiveStatus(
|
||||
AssetStudioGlyphMaterialSchema schema,
|
||||
List<AssetWorkspaceBankCompositionFile> selectedTiles) {
|
||||
if (schema.status() == AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT) {
|
||||
return AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT;
|
||||
}
|
||||
final Map<String, AssetWorkspaceBankCompositionFile> selectedByArtifactId = new HashMap<>();
|
||||
for (AssetWorkspaceBankCompositionFile tile : selectedTiles) {
|
||||
final String artifactId = artifactId(tile);
|
||||
if (artifactId != null) {
|
||||
selectedByArtifactId.put(artifactId, tile);
|
||||
}
|
||||
}
|
||||
for (AssetStudioGlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
|
||||
final AssetWorkspaceBankCompositionFile selectedTile = selectedByArtifactId.get(binding.artifactId());
|
||||
if (selectedTile == null || !Objects.equals(binding.fingerprint(), fingerprint(selectedTile))) {
|
||||
return AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT;
|
||||
}
|
||||
}
|
||||
return AssetStudioGlyphMaterialSchemaStatus.OK;
|
||||
}
|
||||
|
||||
private AssetStudioGlyphMaterialSchema buildSchema(
|
||||
String schemaId,
|
||||
AssetStudioGlyphMaterialSchema existing,
|
||||
List<AssetWorkspaceBankCompositionFile> assignedTiles,
|
||||
List<String> orderedColors) {
|
||||
final Map<Integer, String> materialIndices = new LinkedHashMap<>();
|
||||
for (int index = 0; index < orderedColors.size(); index += 1) {
|
||||
materialIndices.put(index + 1, normalizeHtmlColor(orderedColors.get(index)));
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialArtifactBinding> bindings = assignedTiles.stream()
|
||||
.map(file -> new AssetStudioGlyphMaterialArtifactBinding(artifactId(file), fingerprint(file)))
|
||||
.toList();
|
||||
final boolean unchanged = existing != null
|
||||
&& existing.materialIndices().equals(materialIndices)
|
||||
&& existing.artifactBindings().equals(bindings);
|
||||
final int version = existing == null
|
||||
? 1
|
||||
: unchanged ? existing.version() : existing.version() + 1;
|
||||
final AssetStudioGlyphMaterialSchemaStatus status = existing == null || !unchanged
|
||||
? AssetStudioGlyphMaterialSchemaStatus.OK
|
||||
: existing.status();
|
||||
return new AssetStudioGlyphMaterialSchema(schemaId, status, version, materialIndices, bindings);
|
||||
}
|
||||
|
||||
static boolean supportsSchemaAuthoring(AssetWorkspaceBankCompositionFile file) {
|
||||
return file != null
|
||||
&& artifactId(file) != null
|
||||
&& fingerprint(file) != null;
|
||||
}
|
||||
|
||||
static String artifactId(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
final Object artifactId = file.metadata().get("artifactId");
|
||||
if (artifactId instanceof String value && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String fingerprint(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
if (file.fingerprint() != null && !file.fingerprint().isBlank()) {
|
||||
return file.fingerprint().trim();
|
||||
}
|
||||
final Object selectedFingerprint = file.metadata().get("selectedFingerprint");
|
||||
if (selectedFingerprint instanceof String value && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<String> tileColors(AssetWorkspaceBankCompositionFile file) {
|
||||
final Map<String, Object> palette = nestedMap(file.metadata(), "palette");
|
||||
if (palette == null) {
|
||||
return List.of();
|
||||
}
|
||||
final Object originalColors = palette.get("originalArgb8888");
|
||||
if (!(originalColors instanceof Iterable<?> iterable)) {
|
||||
return List.of();
|
||||
}
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (Object item : iterable) {
|
||||
if (!(item instanceof Number number)) {
|
||||
continue;
|
||||
}
|
||||
colors.add(String.format(
|
||||
"#%02X%02X%02X",
|
||||
(number.intValue() >> 16) & 0xFF,
|
||||
(number.intValue() >> 8) & 0xFF,
|
||||
number.intValue() & 0xFF));
|
||||
}
|
||||
return List.copyOf(colors);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
|
||||
final Object value = metadata.get(key);
|
||||
if (value instanceof Map<?, ?> nested) {
|
||||
return (Map<String, Object>) nested;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String normalizeHtmlColor(String color) {
|
||||
return Objects.requireNonNull(color, "color").trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
record AssetDetailsColorSchemaDraft(
|
||||
String schemaId,
|
||||
boolean existingSchema,
|
||||
boolean removeRequested,
|
||||
List<AssetWorkspaceBankCompositionFile> availableTiles,
|
||||
List<AssetWorkspaceBankCompositionFile> assignedTiles,
|
||||
List<String> preferredColors,
|
||||
boolean deriveColorsFromTiles) {
|
||||
|
||||
AssetDetailsColorSchemaDraft {
|
||||
availableTiles = List.copyOf(availableTiles);
|
||||
assignedTiles = List.copyOf(assignedTiles);
|
||||
preferredColors = normalize(preferredColors);
|
||||
}
|
||||
|
||||
static AssetDetailsColorSchemaDraft noSelection() {
|
||||
return new AssetDetailsColorSchemaDraft(null, false, false, List.of(), List.of(), List.of(), false);
|
||||
}
|
||||
|
||||
boolean selected() {
|
||||
return schemaId != null && !schemaId.isBlank();
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveToAssigned(List<AssetWorkspaceBankCompositionFile> files) {
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAvailable = new ArrayList<>(availableTiles);
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
for (AssetWorkspaceBankCompositionFile file : files) {
|
||||
if (nextAvailable.remove(file) && !nextAssigned.contains(file)) {
|
||||
nextAssigned.add(file);
|
||||
}
|
||||
}
|
||||
return withTiles(nextAvailable, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveToAvailable(List<AssetWorkspaceBankCompositionFile> files) {
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAvailable = new ArrayList<>(availableTiles);
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
for (AssetWorkspaceBankCompositionFile file : files) {
|
||||
if (nextAssigned.remove(file) && !nextAvailable.contains(file)) {
|
||||
nextAvailable.add(file);
|
||||
}
|
||||
}
|
||||
return withTiles(nextAvailable, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveAssignedUp(int index) {
|
||||
if (index <= 0 || index >= assignedTiles.size()) {
|
||||
return this;
|
||||
}
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
final AssetWorkspaceBankCompositionFile file = nextAssigned.remove(index);
|
||||
nextAssigned.add(index - 1, file);
|
||||
return withTiles(availableTiles, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveAssignedDown(int index) {
|
||||
if (index < 0 || index >= assignedTiles.size() - 1) {
|
||||
return this;
|
||||
}
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
final AssetWorkspaceBankCompositionFile file = nextAssigned.remove(index);
|
||||
nextAssigned.add(index + 1, file);
|
||||
return withTiles(availableTiles, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft markForRemoval() {
|
||||
if (!selected()) {
|
||||
return this;
|
||||
}
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, existingSchema, true, availableTiles, assignedTiles, preferredColors, deriveColorsFromTiles);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft asUpdateDraft() {
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, existingSchema, false, availableTiles, assignedTiles, orderedColors(), true);
|
||||
}
|
||||
|
||||
List<String> orderedColors() {
|
||||
if (!deriveColorsFromTiles) {
|
||||
return preferredColors;
|
||||
}
|
||||
final LinkedHashSet<String> active = new LinkedHashSet<>();
|
||||
for (AssetWorkspaceBankCompositionFile tile : assignedTiles) {
|
||||
AssetDetailsColorSchemaCoordinator.tileColors(tile).stream()
|
||||
.map(AssetDetailsColorSchemaDraft::normalize)
|
||||
.forEach(active::add);
|
||||
}
|
||||
final List<String> ordered = new ArrayList<>();
|
||||
for (String preferredColor : preferredColors) {
|
||||
if (active.contains(preferredColor) && !ordered.contains(preferredColor)) {
|
||||
ordered.add(preferredColor);
|
||||
}
|
||||
}
|
||||
for (String color : active) {
|
||||
if (!ordered.contains(color)) {
|
||||
ordered.add(color);
|
||||
}
|
||||
}
|
||||
return List.copyOf(ordered);
|
||||
}
|
||||
|
||||
private AssetDetailsColorSchemaDraft withTiles(
|
||||
List<AssetWorkspaceBankCompositionFile> nextAvailable,
|
||||
List<AssetWorkspaceBankCompositionFile> nextAssigned) {
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, existingSchema, false, nextAvailable, nextAssigned, orderedColors(), true);
|
||||
}
|
||||
|
||||
private static List<String> normalize(List<String> colors) {
|
||||
final List<String> normalized = new ArrayList<>();
|
||||
for (String color : colors) {
|
||||
if (color == null || color.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
normalized.add(normalize(color));
|
||||
}
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
private static String normalize(String color) {
|
||||
return color.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,521 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.banks.StudioDualListView;
|
||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||
import p.studio.controls.forms.StudioFormMode;
|
||||
import p.studio.controls.forms.StudioFormSection;
|
||||
import p.studio.controls.forms.StudioFormSession;
|
||||
import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioGlyphMaterialMetadataChangedEvent;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class AssetDetailsColorSchemaGroupsControl extends StudioFormSection {
|
||||
private static final String SECTION_ID = "asset-details.color-schema-groups";
|
||||
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
private final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
private final ListView<SchemaListItem> schemaListView = new ListView<>();
|
||||
private final SchemaTileDualListView tileDualListView = new SchemaTileDualListView();
|
||||
private final VBox schemaActions = new VBox(6);
|
||||
private final HBox schemaListPane = new HBox(8, schemaActions, schemaListView);
|
||||
private final HBox paletteStrip = new HBox(4);
|
||||
private final Label summaryLabel = new Label();
|
||||
private final Label schemaTitleLabel = new Label();
|
||||
private final Label paletteCounterLabel = new Label();
|
||||
private final Label warningLabel = new Label();
|
||||
private final VBox editorPane = new VBox(10);
|
||||
private final HBox body = new HBox(16);
|
||||
private final VBox content = new VBox(12, body, summaryLabel);
|
||||
|
||||
private AssetWorkspaceDetailsViewState viewState;
|
||||
private StudioFormSession<AssetDetailsColorSchemaDraft> formSession;
|
||||
private String selectedSchemaId;
|
||||
private boolean selectingSchema;
|
||||
|
||||
public AssetDetailsColorSchemaGroupsControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) {
|
||||
Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||
schemaListView.getStyleClass().add("assets-details-color-schema-list");
|
||||
schemaListView.setCellFactory(ignored -> createSchemaCell());
|
||||
schemaListView.getSelectionModel().selectedItemProperty().addListener((ignored, previous, current) -> {
|
||||
if (selectingSchema) {
|
||||
return;
|
||||
}
|
||||
selectedSchemaId = current == null ? null : current.schemaId();
|
||||
if (formMode() == StudioFormMode.EDITING) {
|
||||
formSession.setDraft(selectedSchemaId == null
|
||||
? AssetDetailsColorSchemaDraft.noSelection()
|
||||
: sourceDraft(selectedSchemaId).asUpdateDraft());
|
||||
} else {
|
||||
replaceFormSource();
|
||||
}
|
||||
renderSection();
|
||||
});
|
||||
schemaActions.getStyleClass().add("assets-details-color-schema-toolbar");
|
||||
schemaListPane.getStyleClass().add("assets-details-list-with-actions");
|
||||
content.getStyleClass().add("assets-details-color-schema-content");
|
||||
body.getStyleClass().add("assets-details-color-schema-body");
|
||||
paletteStrip.getStyleClass().add("assets-details-color-schema-strip");
|
||||
schemaTitleLabel.getStyleClass().add("assets-details-color-schema-title");
|
||||
paletteCounterLabel.getStyleClass().add("assets-details-color-schema-summary");
|
||||
summaryLabel.getStyleClass().add("assets-details-color-schema-summary");
|
||||
warningLabel.getStyleClass().add("assets-details-color-schema-warning");
|
||||
HBox.setHgrow(schemaListPane, Priority.ALWAYS);
|
||||
HBox.setHgrow(schemaListView, Priority.ALWAYS);
|
||||
HBox.setHgrow(editorPane, Priority.ALWAYS);
|
||||
schemaListView.setMaxWidth(Double.MAX_VALUE);
|
||||
editorPane.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe() {
|
||||
subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
|
||||
viewState = event.viewState();
|
||||
coordinator.replaceDetails(viewState == null ? null : viewState.selectedAssetDetails());
|
||||
normalizeSelectedSchema();
|
||||
replaceFormSource();
|
||||
if (viewState == null || viewState.selectedAssetReference() == null) {
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
}
|
||||
renderSection();
|
||||
}));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioGlyphMaterialMetadataChangedEvent.class, event -> {
|
||||
if (viewState == null || !Objects.equals(viewState.selectedAssetReference(), event.assetReference())) {
|
||||
return;
|
||||
}
|
||||
coordinator.replaceMaterialMetadata(event.metadata());
|
||||
if (formMode() != StudioFormMode.EDITING) {
|
||||
replaceFormSource();
|
||||
}
|
||||
renderSection();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe() {
|
||||
subscriptions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderSection() {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || !coordinator.ready()) {
|
||||
formSession = null;
|
||||
clearRenderedSection();
|
||||
return;
|
||||
}
|
||||
if (formSession == null) {
|
||||
replaceFormSource();
|
||||
}
|
||||
|
||||
final AssetDetailsColorSchemaDraft draft = formSession.draft();
|
||||
renderSchemaList();
|
||||
renderSchemaActions(draft);
|
||||
renderEditor(draft);
|
||||
summaryLabel.setText(Container.i18n().format(
|
||||
I18n.ASSETS_COLOR_SCHEMA_SUMMARY,
|
||||
coordinator.schemas().size(),
|
||||
coordinator.unassignedTileCount()));
|
||||
body.getChildren().setAll(schemaListPane, editorPane);
|
||||
renderFormSection(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String sectionTitle() {
|
||||
return Container.i18n().text(I18n.ASSETS_SECTION_COLOR_SCHEMA_GROUPS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StudioFormMode formMode() {
|
||||
return formSession == null ? StudioFormMode.READ_ONLY : formSession.mode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDirty() {
|
||||
return formSession != null && formSession.isDirty();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beginEdit() {
|
||||
if (formSession == null) {
|
||||
replaceFormSource();
|
||||
}
|
||||
selectedSchemaId = null;
|
||||
formSession.replaceSource(AssetDetailsColorSchemaDraft.noSelection());
|
||||
formSession.beginEdit();
|
||||
publishEditScope(workspaceBus, currentScopeKey(), SECTION_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply() {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetReference() == null || formSession == null) {
|
||||
return;
|
||||
}
|
||||
final AssetDetailsColorSchemaDraft draft = formSession.draft();
|
||||
if (!draft.selected()) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("color-schema-groups", "Apply failed: select or create a schema first."));
|
||||
return;
|
||||
}
|
||||
if (!draft.removeRequested()) {
|
||||
final List<String> orderedColors = draft.orderedColors();
|
||||
if (draft.assignedTiles().isEmpty() || orderedColors.isEmpty()) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("color-schema-groups", "Apply failed: schema must contain at least one tile."));
|
||||
return;
|
||||
}
|
||||
if (orderedColors.size() > 15) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"color-schema-groups",
|
||||
Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final var assetDetails = viewState.selectedAssetDetails();
|
||||
final var assetReference = viewState.selectedAssetReference();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = draft.removeRequested()
|
||||
? coordinator.removeSchema(draft.schemaId())
|
||||
: coordinator.upsertSchema(draft.schemaId(), draft.assignedTiles(), draft.orderedColors());
|
||||
metadataService.writeGlyphMaterialMetadata(assetDetails.summary().assetRoot(), metadata);
|
||||
Platform.runLater(() -> {
|
||||
formSession.apply();
|
||||
selectedSchemaId = draft.removeRequested() ? null : draft.schemaId();
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"color-schema-groups",
|
||||
draft.removeRequested() ? "Schema removed: " + draft.schemaId() : "Schema saved: " + draft.schemaId()));
|
||||
workspaceBus.publish(new StudioGlyphMaterialMetadataChangedEvent(assetReference, metadata));
|
||||
renderSection();
|
||||
});
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"color-schema-groups",
|
||||
"Unable to save schema: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset() {
|
||||
if (formSession == null) {
|
||||
return;
|
||||
}
|
||||
formSession.resetDraft();
|
||||
selectedSchemaId = formSession.draft().selected() ? formSession.draft().schemaId() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancel() {
|
||||
if (formSession == null) {
|
||||
return;
|
||||
}
|
||||
formSession.cancelEdit();
|
||||
selectedSchemaId = formSession.draft().selected() ? formSession.draft().schemaId() : null;
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String sectionStyleClass() {
|
||||
return "assets-details-color-schema-section";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String formSectionId() {
|
||||
return SECTION_ID;
|
||||
}
|
||||
|
||||
private void renderSchemaList() {
|
||||
selectingSchema = true;
|
||||
schemaListView.getItems().setAll(schemaListItems());
|
||||
schemaListView.setDisable(false);
|
||||
if (selectedSchemaId != null) {
|
||||
schemaListView.getItems().stream()
|
||||
.filter(schema -> schema.schemaId().equals(selectedSchemaId))
|
||||
.findFirst()
|
||||
.ifPresentOrElse(
|
||||
schema -> schemaListView.getSelectionModel().select(schema),
|
||||
() -> schemaListView.getSelectionModel().clearSelection());
|
||||
} else {
|
||||
schemaListView.getSelectionModel().clearSelection();
|
||||
}
|
||||
selectingSchema = false;
|
||||
}
|
||||
|
||||
private void renderSchemaActions(AssetDetailsColorSchemaDraft draft) {
|
||||
final Button newButton = new Button("+");
|
||||
newButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
newButton.setDisable(formMode() != StudioFormMode.EDITING);
|
||||
newButton.setOnAction(ignored -> {
|
||||
selectedSchemaId = coordinator.nextSchemaId();
|
||||
formSession.setDraft(sourceDraft(selectedSchemaId).asUpdateDraft());
|
||||
rerenderPreservingScrollPosition();
|
||||
});
|
||||
|
||||
final Button removeButton = new Button("-");
|
||||
removeButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
removeButton.setDisable(formMode() != StudioFormMode.EDITING || !draft.selected() || (draft.existingSchema() && !coordinator.canRemove(draft.schemaId())));
|
||||
removeButton.setOnAction(ignored -> {
|
||||
if (draft.existingSchema()) {
|
||||
formSession.updateDraft(AssetDetailsColorSchemaDraft::markForRemoval);
|
||||
} else {
|
||||
selectedSchemaId = null;
|
||||
formSession.setDraft(AssetDetailsColorSchemaDraft.noSelection());
|
||||
}
|
||||
rerenderPreservingScrollPosition();
|
||||
});
|
||||
if (draft.existingSchema() && !coordinator.canRemove(draft.schemaId())) {
|
||||
removeButton.setTooltip(new javafx.scene.control.Tooltip(
|
||||
Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_REMOVE_BLOCKED, draft.schemaId())));
|
||||
}
|
||||
|
||||
schemaActions.getChildren().setAll(newButton, removeButton);
|
||||
}
|
||||
|
||||
private void renderEditor(AssetDetailsColorSchemaDraft draft) {
|
||||
editorPane.getChildren().clear();
|
||||
if (!draft.selected()) {
|
||||
tileDualListView.setLeftItems(List.of());
|
||||
tileDualListView.setRightItems(List.of());
|
||||
tileDualListView.setInteractionEnabled(false);
|
||||
paletteStrip.getChildren().clear();
|
||||
paletteCounterLabel.setText(Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT, 0));
|
||||
editorPane.getChildren().addAll(
|
||||
emptyMessage("Select a schema or create a new one to edit its tile group."),
|
||||
tileDualListView,
|
||||
paletteCounterLabel,
|
||||
paletteStrip);
|
||||
return;
|
||||
}
|
||||
if (draft.removeRequested()) {
|
||||
editorPane.getChildren().add(emptyMessage("Schema " + draft.schemaId() + " will be removed when you apply this form."));
|
||||
return;
|
||||
}
|
||||
|
||||
final List<String> orderedColors = draft.orderedColors();
|
||||
schemaTitleLabel.setText(draft.schemaId() + (draft.existingSchema() ? "" : " (new)"));
|
||||
paletteCounterLabel.setText(Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT, orderedColors.size()));
|
||||
warningLabel.setText(orderedColors.size() > 15
|
||||
? Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS)
|
||||
: "");
|
||||
paletteStrip.getChildren().setAll(orderedColors.stream().map(this::swatch).toList());
|
||||
|
||||
tileDualListView.setLeftItems(draft.availableTiles());
|
||||
tileDualListView.setRightItems(draft.assignedTiles());
|
||||
tileDualListView.setInteractionEnabled(formMode() == StudioFormMode.EDITING && draft.selected());
|
||||
tileDualListView.setMoveToRightAllowed(true);
|
||||
tileDualListView.setOnMoveToRight(items -> updateDraft(current -> current.moveToAssigned(items)));
|
||||
tileDualListView.setOnMoveToLeft(items -> updateDraft(current -> current.moveToAvailable(items)));
|
||||
tileDualListView.setOnMoveUp(index -> updateDraft(current -> current.moveAssignedUp(index)));
|
||||
tileDualListView.setOnMoveDown(index -> updateDraft(current -> current.moveAssignedDown(index)));
|
||||
|
||||
editorPane.getChildren().addAll(schemaTitleLabel, tileDualListView, paletteCounterLabel, paletteStrip, warningLabel);
|
||||
VBox.setVgrow(tileDualListView, Priority.ALWAYS);
|
||||
}
|
||||
|
||||
private void updateDraft(java.util.function.UnaryOperator<AssetDetailsColorSchemaDraft> update) {
|
||||
if (formMode() != StudioFormMode.EDITING || formSession == null) {
|
||||
return;
|
||||
}
|
||||
formSession.updateDraft(update);
|
||||
rerenderPreservingScrollPosition();
|
||||
}
|
||||
|
||||
private AssetDetailsColorSchemaDraft sourceDraft(String schemaId) {
|
||||
final AssetStudioGlyphMaterialSchema schema = coordinator.findSchema(schemaId).orElse(null);
|
||||
final List<AssetWorkspaceBankCompositionFile> selectableTiles = schema == null
|
||||
? coordinator.availableTilesForCreate()
|
||||
: coordinator.availableTilesForEdit(schema.schemaId());
|
||||
final Set<String> assignedArtifactIds = schema == null
|
||||
? Set.of()
|
||||
: schema.artifactBindings().stream()
|
||||
.map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
final List<AssetWorkspaceBankCompositionFile> assignedTiles = selectableTiles.stream()
|
||||
.filter(file -> assignedArtifactIds.contains(AssetDetailsColorSchemaCoordinator.artifactId(file)))
|
||||
.toList();
|
||||
final List<AssetWorkspaceBankCompositionFile> availableTiles = selectableTiles.stream()
|
||||
.filter(file -> !assignedArtifactIds.contains(AssetDetailsColorSchemaCoordinator.artifactId(file)))
|
||||
.toList();
|
||||
final List<String> preferredColors = schema == null
|
||||
? List.of()
|
||||
: schema.materialIndices().entrySet().stream()
|
||||
.sorted(java.util.Map.Entry.comparingByKey())
|
||||
.map(java.util.Map.Entry::getValue)
|
||||
.toList();
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, schema != null, false, availableTiles, assignedTiles, preferredColors, schema == null);
|
||||
}
|
||||
|
||||
private void replaceFormSource() {
|
||||
if (!coordinator.ready()) {
|
||||
formSession = null;
|
||||
return;
|
||||
}
|
||||
if (formSession != null && formSession.mode() == StudioFormMode.EDITING) {
|
||||
return;
|
||||
}
|
||||
final String schemaId = selectedSchemaId != null ? selectedSchemaId : defaultSchemaId();
|
||||
selectedSchemaId = schemaId;
|
||||
final AssetDetailsColorSchemaDraft source = schemaId == null
|
||||
? AssetDetailsColorSchemaDraft.noSelection()
|
||||
: sourceDraft(schemaId);
|
||||
if (formSession == null) {
|
||||
formSession = new StudioFormSession<>(source);
|
||||
} else {
|
||||
formSession.replaceSource(source);
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeSelectedSchema() {
|
||||
if (selectedSchemaId == null || coordinator.findSchema(selectedSchemaId).isPresent()) {
|
||||
return;
|
||||
}
|
||||
selectedSchemaId = null;
|
||||
}
|
||||
|
||||
private String defaultSchemaId() {
|
||||
return coordinator.schemas().stream()
|
||||
.map(AssetStudioGlyphMaterialSchema::schemaId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private List<SchemaListItem> schemaListItems() {
|
||||
final List<SchemaListItem> items = new java.util.ArrayList<>(coordinator.schemas().stream()
|
||||
.map(schema -> SchemaListItem.fromSchema(schema, coordinator.selectedTiles()))
|
||||
.toList());
|
||||
if (formSession != null
|
||||
&& formSession.draft().selected()
|
||||
&& !formSession.draft().existingSchema()
|
||||
&& items.stream().noneMatch(item -> item.schemaId().equals(formSession.draft().schemaId()))) {
|
||||
items.add(SchemaListItem.fromDraft(formSession.draft()));
|
||||
}
|
||||
return List.copyOf(items);
|
||||
}
|
||||
|
||||
private ListCell<SchemaListItem> createSchemaCell() {
|
||||
return new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(SchemaListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
final Label title = new Label(item.schemaId());
|
||||
title.getStyleClass().add("assets-details-color-schema-title");
|
||||
final Label version = new Label(item.existing() ? "v" + item.version() : "new");
|
||||
version.getStyleClass().add("assets-details-color-schema-version");
|
||||
final Label status = new Label(item.status());
|
||||
status.getStyleClass().addAll("studio-chip", Objects.equals(item.status(), AssetStudioGlyphMaterialSchemaStatus.OK.manifestValue())
|
||||
? "assets-details-color-schema-status-ok"
|
||||
: "assets-details-color-schema-status-inconsistent");
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
final HBox top = new HBox(8, title, version, spacer, status);
|
||||
|
||||
final Label meta = new Label(item.tileCount() + " tiles · " + item.indexCount() + " indices");
|
||||
meta.getStyleClass().add("assets-details-color-schema-meta");
|
||||
final VBox root = new VBox(6, top, meta);
|
||||
root.getStyleClass().add("assets-details-color-schema-item");
|
||||
setGraphic(root);
|
||||
setText(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Region swatch(String color) {
|
||||
final Region swatch = new Region();
|
||||
swatch.getStyleClass().add("assets-details-color-schema-swatch");
|
||||
swatch.setMinSize(20, 20);
|
||||
swatch.setPrefSize(20, 20);
|
||||
swatch.setMaxSize(20, 20);
|
||||
swatch.setStyle("-fx-background-color: " + color + ";");
|
||||
return swatch;
|
||||
}
|
||||
|
||||
private Node emptyMessage(String text) {
|
||||
final Label label = new Label(text);
|
||||
label.getStyleClass().add("assets-details-color-schema-empty");
|
||||
return label;
|
||||
}
|
||||
|
||||
private String currentScopeKey() {
|
||||
return viewState == null || viewState.selectedAssetReference() == null
|
||||
? null
|
||||
: "asset-details:" + viewState.selectedAssetReference();
|
||||
}
|
||||
|
||||
private static final class SchemaTileDualListView extends StudioDualListView<AssetWorkspaceBankCompositionFile> {
|
||||
private SchemaTileDualListView() {
|
||||
setLeftTitle(Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_AVAILABLE_TILES));
|
||||
setRightTitle(Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_ASSIGNED_TILES));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String itemText(AssetWorkspaceBankCompositionFile item) {
|
||||
return item.displayName();
|
||||
}
|
||||
}
|
||||
|
||||
private record SchemaListItem(
|
||||
String schemaId,
|
||||
boolean existing,
|
||||
int version,
|
||||
String status,
|
||||
int tileCount,
|
||||
int indexCount) {
|
||||
|
||||
private static SchemaListItem fromSchema(AssetStudioGlyphMaterialSchema schema, List<AssetWorkspaceBankCompositionFile> selectedTiles) {
|
||||
return new SchemaListItem(
|
||||
schema.schemaId(),
|
||||
true,
|
||||
schema.version(),
|
||||
AssetDetailsColorSchemaCoordinator.effectiveStatus(schema, selectedTiles).manifestValue(),
|
||||
schema.artifactBindings().size(),
|
||||
schema.materialIndices().size());
|
||||
}
|
||||
|
||||
private static SchemaListItem fromDraft(AssetDetailsColorSchemaDraft draft) {
|
||||
return new SchemaListItem(
|
||||
draft.schemaId(),
|
||||
false,
|
||||
0,
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK.manifestValue(),
|
||||
draft.assignedTiles().size(),
|
||||
draft.orderedColors().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package p.studio.workspaces.assets.details.summary;
|
||||
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||
@ -10,7 +11,6 @@ import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
@ -56,43 +56,23 @@ public final class AssetDetailsSummaryControl extends VBox implements StudioCont
|
||||
}
|
||||
|
||||
final VBox content = new VBox(8);
|
||||
final AssetStudioSceneBankMetadata sceneBankMetadata = viewState.selectedAssetDetails().sceneBankMetadata();
|
||||
content.getChildren().setAll(
|
||||
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())),
|
||||
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot())));
|
||||
final VBox typeBox = new VBox(6);
|
||||
final HBox typeBox = new HBox(8);
|
||||
typeBox.getStyleClass().add("assets-details-chips");
|
||||
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
|
||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||
AssetDetailsUiSupport.typeLabel(summary.assetFamily())));
|
||||
if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) {
|
||||
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
|
||||
"assets-details-chip-generic",
|
||||
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization())));
|
||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization()),
|
||||
"assets-details-chip-fixed"));
|
||||
}
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TYPE),
|
||||
typeBox));
|
||||
if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) {
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_STUDIO_ROLE),
|
||||
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization())));
|
||||
}
|
||||
if (sceneBankMetadata != null) {
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_SCENE_STATUS),
|
||||
AssetDetailsUiSupport.sceneBankStatusLabel(viewState.selectedAssetDetails().sceneBankValidation().status())));
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_SCENE_LAYERS),
|
||||
String.valueOf(sceneBankMetadata.layerCount())));
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TILEMAPS),
|
||||
sceneBankMetadata.layerBindings().stream()
|
||||
.map(AssetStudioSceneLayerBinding::tilemap)
|
||||
.collect(java.util.stream.Collectors.joining(", "))));
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_SUPPORT_FILE),
|
||||
AssetDetailsUiSupport.projectRelativePath(projectReference, sceneBankMetadata.supportFile())));
|
||||
}
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION),
|
||||
AssetDetailsUiSupport.createChip(
|
||||
|
||||
@ -113,7 +113,7 @@ public final class AssetListItemControl extends VBox {
|
||||
private String assetRowToneClass(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-workspace-asset-row-tone-image";
|
||||
case SCENE_BANK -> "assets-workspace-asset-row-tone-generic";
|
||||
case SCENE_BANK -> "assets-workspace-asset-row-tone-scene";
|
||||
case SOUND_BANK -> "assets-workspace-asset-row-tone-audio";
|
||||
default -> "assets-workspace-asset-row-tone-generic";
|
||||
};
|
||||
@ -122,7 +122,7 @@ public final class AssetListItemControl extends VBox {
|
||||
private String assetNameToneClass(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-workspace-asset-name-tone-image";
|
||||
case SCENE_BANK -> "assets-workspace-asset-name-tone-generic";
|
||||
case SCENE_BANK -> "assets-workspace-asset-name-tone-scene";
|
||||
case SOUND_BANK -> "assets-workspace-asset-name-tone-audio";
|
||||
default -> "assets-workspace-asset-name-tone-generic";
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||
import p.packer.dtos.PackerDiagnosticDTO;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
|
||||
import java.util.List;
|
||||
@ -21,6 +22,7 @@ public record AssetWorkspaceAssetDetails(
|
||||
Map<String, Object> outputPipeline,
|
||||
AssetWorkspaceBankCompositionDetails bankComposition,
|
||||
List<Map<String, Object>> pipelinePalettes,
|
||||
AssetStudioGlyphMaterialMetadata glyphMaterialMetadata,
|
||||
AssetStudioSceneBankMetadata sceneBankMetadata,
|
||||
AssetWorkspaceSceneBankValidation sceneBankValidation,
|
||||
List<PackerDiagnosticDTO> diagnostics) {
|
||||
@ -39,4 +41,40 @@ public record AssetWorkspaceAssetDetails(
|
||||
sceneBankValidation = Objects.requireNonNullElse(sceneBankValidation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||
}
|
||||
|
||||
public AssetWorkspaceAssetDetails withBankComposition(AssetWorkspaceBankCompositionDetails bankComposition) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
summary,
|
||||
actions,
|
||||
outputFormat,
|
||||
outputCodec,
|
||||
availableOutputCodecs,
|
||||
codecConfigurationFieldsByCodec,
|
||||
metadataFields,
|
||||
outputPipeline,
|
||||
bankComposition,
|
||||
pipelinePalettes,
|
||||
glyphMaterialMetadata,
|
||||
sceneBankMetadata,
|
||||
sceneBankValidation,
|
||||
diagnostics);
|
||||
}
|
||||
|
||||
public AssetWorkspaceAssetDetails withGlyphMaterialMetadata(AssetStudioGlyphMaterialMetadata glyphMaterialMetadata) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
summary,
|
||||
actions,
|
||||
outputFormat,
|
||||
outputCodec,
|
||||
availableOutputCodecs,
|
||||
codecConfigurationFieldsByCodec,
|
||||
metadataFields,
|
||||
outputPipeline,
|
||||
bankComposition,
|
||||
pipelinePalettes,
|
||||
glyphMaterialMetadata,
|
||||
sceneBankMetadata,
|
||||
sceneBankValidation,
|
||||
diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.workspaces.assets.messages.events;
|
||||
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.studio.lsp.events.StudioEvent;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record StudioBankCompositionChangedEvent(
|
||||
AssetReference assetReference,
|
||||
AssetWorkspaceBankCompositionDetails bankComposition) implements StudioEvent {
|
||||
|
||||
public StudioBankCompositionChangedEvent {
|
||||
assetReference = Objects.requireNonNull(assetReference, "assetReference");
|
||||
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.workspaces.assets.messages.events;
|
||||
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.studio.lsp.events.StudioEvent;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record StudioGlyphMaterialMetadataChangedEvent(
|
||||
AssetReference assetReference,
|
||||
AssetStudioGlyphMaterialMetadata metadata) implements StudioEvent {
|
||||
|
||||
public StudioGlyphMaterialMetadataChangedEvent {
|
||||
assetReference = Objects.requireNonNull(assetReference, "assetReference");
|
||||
metadata = Objects.requireNonNull(metadata, "metadata");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioGlyphMaterialArtifactBinding(
|
||||
String artifactId,
|
||||
String fingerprint) {
|
||||
|
||||
public AssetStudioGlyphMaterialArtifactBinding {
|
||||
artifactId = Objects.requireNonNull(artifactId, "artifactId").trim();
|
||||
fingerprint = Objects.requireNonNull(Objects.requireNonNullElse(fingerprint, ""), "fingerprint").trim();
|
||||
if (artifactId.isBlank()) {
|
||||
throw new IllegalArgumentException("artifactId must not be blank");
|
||||
}
|
||||
if (fingerprint.isBlank()) {
|
||||
throw new IllegalArgumentException("fingerprint must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record AssetStudioGlyphMaterialMetadata(
|
||||
List<AssetStudioGlyphMaterialSchema> schemas,
|
||||
List<AssetStudioGlyphMaterialPalette> palettes,
|
||||
Path supportFile) {
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata {
|
||||
schemas = List.copyOf(Objects.requireNonNull(schemas, "schemas"));
|
||||
palettes = List.copyOf(Objects.requireNonNull(palettes, "palettes"));
|
||||
supportFile = supportFile == null ? null : supportFile.toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
public Optional<AssetStudioGlyphMaterialSchema> findSchema(String schemaId) {
|
||||
return schemas.stream().filter(schema -> schema.schemaId().equals(schemaId)).findFirst();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioGlyphMaterialPalette(
|
||||
String paletteId,
|
||||
String displayName,
|
||||
String schemaId,
|
||||
int version,
|
||||
List<String> colors) {
|
||||
|
||||
public AssetStudioGlyphMaterialPalette(String paletteId, String schemaId, int version, List<String> colors) {
|
||||
this(paletteId, null, schemaId, version, colors);
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialPalette {
|
||||
paletteId = Objects.requireNonNull(paletteId, "paletteId").trim();
|
||||
displayName = displayName == null || displayName.trim().isBlank() ? null : displayName.trim();
|
||||
schemaId = Objects.requireNonNull(schemaId, "schemaId").trim();
|
||||
if (paletteId.isBlank()) {
|
||||
throw new IllegalArgumentException("paletteId must not be blank");
|
||||
}
|
||||
if (schemaId.isBlank()) {
|
||||
throw new IllegalArgumentException("schemaId must not be blank");
|
||||
}
|
||||
if (version <= 0) {
|
||||
throw new IllegalArgumentException("version must be positive");
|
||||
}
|
||||
colors = List.copyOf(Objects.requireNonNull(colors, "colors"));
|
||||
}
|
||||
|
||||
public List<Integer> convertedRgb565Colors() {
|
||||
final List<Integer> converted = new ArrayList<>(colors.size());
|
||||
for (String color : colors) {
|
||||
converted.add(convertHtmlRgbToRgb565(color));
|
||||
}
|
||||
return List.copyOf(converted);
|
||||
}
|
||||
|
||||
static int convertHtmlRgbToRgb565(String htmlRgb) {
|
||||
final String value = Objects.requireNonNull(htmlRgb, "htmlRgb").trim();
|
||||
if (!value.matches("^#[0-9A-Fa-f]{6}$")) {
|
||||
throw new IllegalArgumentException("htmlRgb must use #RRGGBB format");
|
||||
}
|
||||
final int rgb = Integer.parseInt(value.substring(1), 16);
|
||||
final int red = (rgb >> 16) & 0xFF;
|
||||
final int green = (rgb >> 8) & 0xFF;
|
||||
final int blue = rgb & 0xFF;
|
||||
return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioGlyphMaterialSchema(
|
||||
String schemaId,
|
||||
AssetStudioGlyphMaterialSchemaStatus status,
|
||||
int version,
|
||||
Map<Integer, String> materialIndices,
|
||||
List<AssetStudioGlyphMaterialArtifactBinding> artifactBindings) {
|
||||
|
||||
public AssetStudioGlyphMaterialSchema {
|
||||
schemaId = Objects.requireNonNull(schemaId, "schemaId").trim();
|
||||
status = Objects.requireNonNull(status, "status");
|
||||
if (schemaId.isBlank()) {
|
||||
throw new IllegalArgumentException("schemaId must not be blank");
|
||||
}
|
||||
if (version <= 0) {
|
||||
throw new IllegalArgumentException("version must be positive");
|
||||
}
|
||||
materialIndices = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(materialIndices, "materialIndices")));
|
||||
artifactBindings = List.copyOf(Objects.requireNonNull(artifactBindings, "artifactBindings"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
public enum AssetStudioGlyphMaterialSchemaStatus {
|
||||
OK("ok"),
|
||||
INCONSISTENT("inconsistent");
|
||||
|
||||
private final String manifestValue;
|
||||
|
||||
AssetStudioGlyphMaterialSchemaStatus(String manifestValue) {
|
||||
this.manifestValue = manifestValue;
|
||||
}
|
||||
|
||||
public String manifestValue() {
|
||||
return manifestValue;
|
||||
}
|
||||
|
||||
public static AssetStudioGlyphMaterialSchemaStatus fromManifestValue(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (AssetStudioGlyphMaterialSchemaStatus candidate : values()) {
|
||||
if (candidate.manifestValue.equalsIgnoreCase(value.trim())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -12,12 +12,14 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class AssetStudioMetadataService {
|
||||
public static final String STUDIO_ASSET_METADATA_FILE = "studio.asset.json";
|
||||
public static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
|
||||
public static final String GLYPH_MATERIAL_SUPPORT_FILE = "glyph-material.json";
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@ -25,7 +27,32 @@ public final class AssetStudioMetadataService {
|
||||
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
return new AssetStudioMetadataSnapshot(
|
||||
readGlyphSpecialization(normalizedAssetRoot, assetFamily),
|
||||
readSceneBankMetadata(normalizedAssetRoot, assetFamily));
|
||||
readSceneBankMetadata(normalizedAssetRoot, assetFamily),
|
||||
readGlyphMaterialMetadata(normalizedAssetRoot, assetFamily));
|
||||
}
|
||||
|
||||
public void writeSceneBankMetadata(Path assetRoot, AssetStudioSceneBankMetadata metadata) throws IOException {
|
||||
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
final AssetStudioSceneBankMetadata normalized = Objects.requireNonNull(metadata, "metadata");
|
||||
final var root = mapper.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
root.put("map_width", normalized.mapWidth());
|
||||
root.put("map_height", normalized.mapHeight());
|
||||
root.put("layer_count", normalized.layerCount());
|
||||
final ArrayNode layers = root.putArray("layers");
|
||||
for (AssetStudioSceneLayerBinding binding : normalized.layerBindings()) {
|
||||
final var layer = layers.addObject();
|
||||
layer.put("index", binding.index());
|
||||
layer.put("enabled", binding.enabled());
|
||||
layer.put("name", binding.layerName());
|
||||
layer.put("width", binding.width());
|
||||
layer.put("height", binding.height());
|
||||
layer.put("tilemap", binding.tilemap());
|
||||
layer.put("tileset_asset_root", binding.tilesetAssetRoot());
|
||||
}
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
normalizedAssetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
|
||||
root);
|
||||
}
|
||||
|
||||
public void writeGlyphSpecialization(Path assetRoot, AssetStudioGlyphSpecialization specialization) throws IOException {
|
||||
@ -46,6 +73,45 @@ public final class AssetStudioMetadataService {
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(metadataPath.toFile(), root);
|
||||
}
|
||||
|
||||
public void writeGlyphMaterialMetadata(Path assetRoot, AssetStudioGlyphMaterialMetadata metadata) throws IOException {
|
||||
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
final AssetStudioGlyphMaterialMetadata normalized = Objects.requireNonNull(metadata, "metadata");
|
||||
final var root = mapper.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
final ArrayNode schemas = root.putArray("schemas");
|
||||
for (AssetStudioGlyphMaterialSchema schema : normalized.schemas()) {
|
||||
final var schemaNode = schemas.addObject();
|
||||
schemaNode.put("schema_id", schema.schemaId());
|
||||
schemaNode.put("status", schema.status().manifestValue());
|
||||
schemaNode.put("version", schema.version());
|
||||
final var materialIndicesNode = schemaNode.putObject("material_indices");
|
||||
schema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.forEach(entry -> materialIndicesNode.put(String.valueOf(entry.getKey()), entry.getValue()));
|
||||
final ArrayNode bindingsNode = schemaNode.putArray("artifact_bindings");
|
||||
for (AssetStudioGlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
|
||||
final var bindingNode = bindingsNode.addObject();
|
||||
bindingNode.put("artifact_id", binding.artifactId());
|
||||
bindingNode.put("fingerprint", binding.fingerprint());
|
||||
}
|
||||
}
|
||||
final ArrayNode palettes = root.putArray("palettes");
|
||||
for (AssetStudioGlyphMaterialPalette palette : normalized.palettes()) {
|
||||
final var paletteNode = palettes.addObject();
|
||||
paletteNode.put("palette_id", palette.paletteId());
|
||||
if (palette.displayName() != null) {
|
||||
paletteNode.put("name", palette.displayName());
|
||||
}
|
||||
paletteNode.put("schema_id", palette.schemaId());
|
||||
paletteNode.put("version", palette.version());
|
||||
final ArrayNode colorsNode = paletteNode.putArray("colors");
|
||||
palette.colors().forEach(colorsNode::add);
|
||||
}
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
normalizedAssetRoot.resolve(GLYPH_MATERIAL_SUPPORT_FILE).toFile(),
|
||||
root);
|
||||
}
|
||||
|
||||
private AssetStudioGlyphSpecialization readGlyphSpecialization(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||
if (assetFamily != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return AssetStudioGlyphSpecialization.NONE;
|
||||
@ -81,7 +147,7 @@ public final class AssetStudioMetadataService {
|
||||
final int mapWidth = root.path("map_width").asInt(0);
|
||||
final int mapHeight = root.path("map_height").asInt(0);
|
||||
final int layerCount = root.path("layer_count").asInt(0);
|
||||
if (mapWidth <= 0 || mapHeight <= 0 || layerCount < 1 || layerCount > 4) {
|
||||
if (mapWidth <= 0 || mapHeight <= 0 || layerCount != 4) {
|
||||
return null;
|
||||
}
|
||||
if (!(root.path("layers") instanceof ArrayNode layersNode) || layersNode.size() != layerCount) {
|
||||
@ -91,13 +157,16 @@ public final class AssetStudioMetadataService {
|
||||
final Set<Integer> indexes = new HashSet<>();
|
||||
for (JsonNode layerNode : layersNode) {
|
||||
final int index = layerNode.path("index").asInt(0);
|
||||
final boolean enabled = layerNode.path("enabled").asBoolean(false);
|
||||
final String layerName = layerNode.path("name").asText("").trim();
|
||||
final int width = layerNode.path("width").asInt(mapWidth);
|
||||
final int height = layerNode.path("height").asInt(mapHeight);
|
||||
final String tilemap = layerNode.path("tilemap").asText("").trim();
|
||||
final String tilesetAssetRoot = layerNode.path("tileset_asset_root").asText("").trim();
|
||||
if (index < 1 || index > layerCount || layerName.isBlank() || tilemap.isBlank() || !indexes.add(index)) {
|
||||
if (index < 1 || index > layerCount || layerName.isBlank() || width <= 0 || height <= 0 || tilemap.isBlank() || !indexes.add(index)) {
|
||||
return null;
|
||||
}
|
||||
bindings.add(new AssetStudioSceneLayerBinding(index, layerName, tilemap, tilesetAssetRoot));
|
||||
bindings.add(new AssetStudioSceneLayerBinding(enabled, index, layerName, width, height, tilemap, tilesetAssetRoot));
|
||||
}
|
||||
bindings.sort(Comparator.comparingInt(AssetStudioSceneLayerBinding::index));
|
||||
return new AssetStudioSceneBankMetadata(mapWidth, mapHeight, layerCount, bindings, supportFile);
|
||||
@ -105,4 +174,117 @@ public final class AssetStudioMetadataService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AssetStudioGlyphMaterialMetadata readGlyphMaterialMetadata(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||
if (assetFamily != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return null;
|
||||
}
|
||||
final Path supportFile = assetRoot.resolve(GLYPH_MATERIAL_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(supportFile.toFile());
|
||||
if (root == null || !root.isObject()) {
|
||||
return null;
|
||||
}
|
||||
if (!(root.path("schemas") instanceof ArrayNode schemasNode) || !(root.path("palettes") instanceof ArrayNode palettesNode)) {
|
||||
return null;
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = new ArrayList<>();
|
||||
final Set<String> schemaIds = new HashSet<>();
|
||||
for (JsonNode schemaNode : schemasNode) {
|
||||
final String schemaId = schemaNode.path("schema_id").asText("").trim();
|
||||
final AssetStudioGlyphMaterialSchemaStatus status = AssetStudioGlyphMaterialSchemaStatus.fromManifestValue(
|
||||
schemaNode.path("status").asText(null));
|
||||
final int version = schemaNode.path("version").asInt(0);
|
||||
if (schemaId.isBlank() || status == null || version <= 0 || !schemaIds.add(schemaId)) {
|
||||
return null;
|
||||
}
|
||||
final JsonNode materialIndicesNode = schemaNode.path("material_indices");
|
||||
if (!materialIndicesNode.isObject()) {
|
||||
return null;
|
||||
}
|
||||
final Map<Integer, String> materialIndices = new java.util.TreeMap<>();
|
||||
materialIndicesNode.fields().forEachRemaining(entry -> {
|
||||
try {
|
||||
final int index = Integer.parseInt(entry.getKey());
|
||||
final String color = normalizeHtmlColor(entry.getValue().asText(null));
|
||||
if (index >= 1 && index <= 15 && color != null) {
|
||||
materialIndices.put(index, color);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// handled below through validation
|
||||
}
|
||||
});
|
||||
if (materialIndices.isEmpty()
|
||||
|| materialIndices.size() != materialIndicesNode.size()
|
||||
|| !denseIndices(materialIndices.keySet())) {
|
||||
return null;
|
||||
}
|
||||
if (!(schemaNode.path("artifact_bindings") instanceof ArrayNode bindingsNode) || bindingsNode.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialArtifactBinding> bindings = new ArrayList<>();
|
||||
final Set<String> artifactIds = new HashSet<>();
|
||||
for (JsonNode bindingNode : bindingsNode) {
|
||||
final String artifactId = bindingNode.path("artifact_id").asText("").trim();
|
||||
final String fingerprint = bindingNode.path("fingerprint").asText("").trim();
|
||||
if (artifactId.isBlank() || fingerprint.isBlank() || !artifactIds.add(artifactId)) {
|
||||
return null;
|
||||
}
|
||||
bindings.add(new AssetStudioGlyphMaterialArtifactBinding(artifactId, fingerprint));
|
||||
}
|
||||
schemas.add(new AssetStudioGlyphMaterialSchema(schemaId, status, version, materialIndices, bindings));
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialPalette> palettes = new ArrayList<>();
|
||||
final Set<String> paletteIds = new HashSet<>();
|
||||
for (JsonNode paletteNode : palettesNode) {
|
||||
final String paletteId = paletteNode.path("palette_id").asText("").trim();
|
||||
final String displayName = paletteNode.path("name").asText(null);
|
||||
final String schemaId = paletteNode.path("schema_id").asText("").trim();
|
||||
final int version = paletteNode.path("version").asInt(0);
|
||||
if (paletteId.isBlank() || schemaId.isBlank() || version <= 0 || !paletteIds.add(paletteId) || !schemaIds.contains(schemaId)) {
|
||||
return null;
|
||||
}
|
||||
if (!(paletteNode.path("colors") instanceof ArrayNode colorsNode)) {
|
||||
return null;
|
||||
}
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (JsonNode colorNode : colorsNode) {
|
||||
final String color = normalizeHtmlColor(colorNode.asText(null));
|
||||
if (color == null) {
|
||||
return null;
|
||||
}
|
||||
colors.add(color);
|
||||
}
|
||||
palettes.add(new AssetStudioGlyphMaterialPalette(paletteId, displayName, schemaId, version, colors));
|
||||
}
|
||||
return new AssetStudioGlyphMaterialMetadata(schemas, palettes, supportFile);
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean denseIndices(Set<Integer> indices) {
|
||||
if (indices.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int expected = 1;
|
||||
for (Integer index : new java.util.TreeSet<>(indices)) {
|
||||
if (index == null || index != expected) {
|
||||
return false;
|
||||
}
|
||||
expected += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String normalizeHtmlColor(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final String normalized = value.trim().toUpperCase();
|
||||
return normalized.matches("^#[0-9A-F]{6}$") ? normalized : null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@ import java.util.Objects;
|
||||
|
||||
public record AssetStudioMetadataSnapshot(
|
||||
AssetStudioGlyphSpecialization glyphSpecialization,
|
||||
AssetStudioSceneBankMetadata sceneBankMetadata) {
|
||||
AssetStudioSceneBankMetadata sceneBankMetadata,
|
||||
AssetStudioGlyphMaterialMetadata glyphMaterialMetadata) {
|
||||
|
||||
public AssetStudioMetadataSnapshot {
|
||||
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
|
||||
|
||||
@ -17,11 +17,15 @@ public record AssetStudioSceneBankMetadata(
|
||||
if (mapWidth <= 0 || mapHeight <= 0) {
|
||||
throw new IllegalArgumentException("map dimensions must be positive");
|
||||
}
|
||||
if (layerCount < 1 || layerCount > 4) {
|
||||
throw new IllegalArgumentException("layerCount must stay between 1 and 4");
|
||||
if (layerCount != 4) {
|
||||
throw new IllegalArgumentException("layerCount must stay fixed at 4");
|
||||
}
|
||||
if (layerBindings.size() != layerCount) {
|
||||
throw new IllegalArgumentException("layerBindings must match layerCount");
|
||||
}
|
||||
}
|
||||
|
||||
public long enabledLayerCount() {
|
||||
return layerBindings.stream().filter(AssetStudioSceneLayerBinding::enabled).count();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,11 @@ package p.studio.workspaces.assets.metadata;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioSceneLayerBinding(
|
||||
boolean enabled,
|
||||
int index,
|
||||
String layerName,
|
||||
int width,
|
||||
int height,
|
||||
String tilemap,
|
||||
String tilesetAssetRoot) {
|
||||
public AssetStudioSceneLayerBinding {
|
||||
@ -17,6 +20,9 @@ public record AssetStudioSceneLayerBinding(
|
||||
if (layerName.isBlank()) {
|
||||
throw new IllegalArgumentException("layerName must not be blank");
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new IllegalArgumentException("layer dimensions must be positive");
|
||||
}
|
||||
if (tilemap.isBlank()) {
|
||||
throw new IllegalArgumentException("tilemap must not be blank");
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ public final class SceneBankWorkflowService {
|
||||
if (details == null || details.summary().assetFamily() != p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new SceneBankWorkflowResult(true, "", AssetWorkspaceSceneBankValidation.NOT_APPLICABLE, List.of());
|
||||
}
|
||||
final List<PackerDiagnosticDTO> diagnostics = validateDiagnostics(projectReference, details);
|
||||
final List<PackerDiagnosticDTO> diagnostics = validateDiagnostics(projectReference, details, null);
|
||||
final String currentFingerprint = diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)
|
||||
? ""
|
||||
: computeFingerprint(projectReference, details);
|
||||
@ -98,7 +98,70 @@ public final class SceneBankWorkflowService {
|
||||
return inspect(projectReference, details);
|
||||
}
|
||||
|
||||
private List<PackerDiagnosticDTO> validateDiagnostics(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
public SceneBankWorkflowResult inspectLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
if (details == null || details.summary().assetFamily() != p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new SceneBankWorkflowResult(true, "", AssetWorkspaceSceneBankValidation.NOT_APPLICABLE, List.of());
|
||||
}
|
||||
final List<PackerDiagnosticDTO> diagnostics = validateDiagnostics(projectReference, details, layerIndex);
|
||||
final String currentFingerprint = diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)
|
||||
? ""
|
||||
: computeFingerprint(projectReference, details, layerIndex);
|
||||
final String validatedFingerprint = readFingerprint(details.summary().assetRoot().resolve(layerValidationFile(layerIndex)));
|
||||
final String acceptedFingerprint = readFingerprint(details.summary().assetRoot().resolve(layerAcceptanceFile(layerIndex)));
|
||||
final AssetWorkspaceSceneBankValidation validation = validationState(
|
||||
diagnostics,
|
||||
currentFingerprint,
|
||||
validatedFingerprint,
|
||||
acceptedFingerprint);
|
||||
return new SceneBankWorkflowResult(
|
||||
diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking),
|
||||
messageFor(validation),
|
||||
validation,
|
||||
diagnostics);
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult validateLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
final SceneBankWorkflowResult inspection = inspectLayer(projectReference, details, layerIndex);
|
||||
final Path validationFile = details.summary().assetRoot().resolve(layerValidationFile(layerIndex));
|
||||
try {
|
||||
if (inspection.success() && !inspection.validation().currentFingerprint().isBlank()) {
|
||||
writeFingerprint(validationFile, inspection.validation().currentFingerprint());
|
||||
} else {
|
||||
Files.deleteIfExists(validationFile);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Unable to persist validation state: " + exception.getMessage(),
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
return inspectLayer(projectReference, details, layerIndex);
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult acceptLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
final SceneBankWorkflowResult inspection = inspectLayer(projectReference, details, layerIndex);
|
||||
if (!inspection.validation().canAccept()) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Scene layer must validate successfully before acceptance.",
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
try {
|
||||
writeFingerprint(details.summary().assetRoot().resolve(layerAcceptanceFile(layerIndex)), inspection.validation().currentFingerprint());
|
||||
writeFingerprint(details.summary().assetRoot().resolve(layerValidationFile(layerIndex)), inspection.validation().currentFingerprint());
|
||||
} catch (IOException exception) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Unable to persist acceptance state: " + exception.getMessage(),
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
return inspectLayer(projectReference, details, layerIndex);
|
||||
}
|
||||
|
||||
private List<PackerDiagnosticDTO> validateDiagnostics(ProjectReference projectReference, AssetWorkspaceAssetDetails details, Integer targetLayerIndex) {
|
||||
final List<PackerDiagnosticDTO> diagnostics = new ArrayList<>();
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
@ -106,6 +169,12 @@ public final class SceneBankWorkflowService {
|
||||
return diagnostics;
|
||||
}
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (!binding.enabled()) {
|
||||
continue;
|
||||
}
|
||||
if (targetLayerIndex != null && binding.index() != targetLayerIndex) {
|
||||
continue;
|
||||
}
|
||||
if (binding.tilesetAssetRoot().isBlank()) {
|
||||
diagnostics.add(error("Layer " + binding.index() + " is missing tileset_asset_root.", metadata.supportFile()));
|
||||
continue;
|
||||
@ -139,7 +208,7 @@ public final class SceneBankWorkflowService {
|
||||
} else if (!expectedSource.equals(map.tilesets().getFirst().source())) {
|
||||
diagnostics.add(error("TMX tileset reference does not match support metadata: " + binding.tilemap(), tmxPath));
|
||||
}
|
||||
if (map.width() != metadata.mapWidth() || map.height() != metadata.mapHeight()) {
|
||||
if (map.width() != binding.width() || map.height() != binding.height()) {
|
||||
diagnostics.add(error("TMX dimensions do not match scene-bank support metadata: " + binding.tilemap(), tmxPath));
|
||||
}
|
||||
if (map.tileWidth() != tileset.tileWidth() || map.tileHeight() != tileset.tileHeight()) {
|
||||
@ -167,6 +236,9 @@ public final class SceneBankWorkflowService {
|
||||
final List<Path> files = new ArrayList<>();
|
||||
files.add(metadata.supportFile());
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (!binding.enabled()) {
|
||||
continue;
|
||||
}
|
||||
files.add(details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize());
|
||||
if (!binding.tilesetAssetRoot().isBlank()) {
|
||||
files.add(projectReference.rootPath().resolve("assets").resolve(binding.tilesetAssetRoot()).resolve("tileset.tsx").toAbsolutePath().normalize());
|
||||
@ -182,6 +254,38 @@ public final class SceneBankWorkflowService {
|
||||
}
|
||||
}
|
||||
|
||||
private String computeFingerprint(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
final List<Path> files = new ArrayList<>();
|
||||
files.add(metadata.supportFile());
|
||||
metadata.layerBindings().stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.filter(binding -> binding.index() == layerIndex)
|
||||
.findFirst()
|
||||
.ifPresent(binding -> {
|
||||
files.add(details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize());
|
||||
if (!binding.tilesetAssetRoot().isBlank()) {
|
||||
files.add(projectReference.rootPath().resolve("assets").resolve(binding.tilesetAssetRoot()).resolve("tileset.tsx").toAbsolutePath().normalize());
|
||||
}
|
||||
});
|
||||
if (files.size() == 1) {
|
||||
return "";
|
||||
}
|
||||
files.stream()
|
||||
.distinct()
|
||||
.sorted(Comparator.comparing(Path::toString))
|
||||
.forEach(path -> updateDigest(digest, path));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (Exception exception) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDigest(MessageDigest digest, Path path) {
|
||||
try {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
@ -238,6 +342,14 @@ public final class SceneBankWorkflowService {
|
||||
acceptedFingerprint);
|
||||
}
|
||||
|
||||
private String layerValidationFile(int layerIndex) {
|
||||
return "scene-bank.layer-" + layerIndex + ".validation.json";
|
||||
}
|
||||
|
||||
private String layerAcceptanceFile(int layerIndex) {
|
||||
return "scene-bank.layer-" + layerIndex + ".acceptance.json";
|
||||
}
|
||||
|
||||
private String messageFor(AssetWorkspaceSceneBankValidation validation) {
|
||||
return switch (validation.status()) {
|
||||
case NOT_APPLICABLE -> "";
|
||||
|
||||
@ -28,7 +28,7 @@ public final class TiledAssetGenerationService {
|
||||
}
|
||||
final List<AssetWorkspaceBankCompositionFile> selectedFiles = details.bankComposition().selectedFiles();
|
||||
if (selectedFiles.isEmpty()) {
|
||||
return new TiledAssetGenerationResult(false, "Select at least one glyph artifact before generating TSX.", List.of());
|
||||
return new TiledAssetGenerationResult(false, "The selected tileset bank has no included glyph artifacts.", List.of());
|
||||
}
|
||||
final int tileSize = parseTileSize(details.metadataFields());
|
||||
final List<TiledTilesetTile> tiles = new ArrayList<>();
|
||||
@ -72,63 +72,98 @@ public final class TiledAssetGenerationService {
|
||||
final List<Path> writtenFiles = new ArrayList<>();
|
||||
try {
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
final String tilesetAssetRoot = Objects.requireNonNullElse(binding.tilesetAssetRoot(), "").trim();
|
||||
if (tilesetAssetRoot.isBlank()) {
|
||||
return new TiledAssetGenerationResult(
|
||||
false,
|
||||
"Layer " + binding.index() + " must declare a non-blank tileset_asset_root before TMX generation.",
|
||||
List.of());
|
||||
if (!binding.enabled()) {
|
||||
continue;
|
||||
}
|
||||
final Path tilesetPath = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.resolve(tilesetAssetRoot)
|
||||
.resolve(GENERATED_TSX_FILE)
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (!java.nio.file.Files.isRegularFile(tilesetPath)) {
|
||||
return new TiledAssetGenerationResult(
|
||||
false,
|
||||
"Referenced TSX was not found for layer " + binding.index() + ": " + tilesetAssetRoot,
|
||||
List.of());
|
||||
final TiledAssetGenerationResult result = generateSceneBankTilemap(projectReference, details, binding.index());
|
||||
if (!result.success()) {
|
||||
return result;
|
||||
}
|
||||
final TiledTilesetDocument tileset = codec.readTileset(tilesetPath);
|
||||
final String relativeTilesetPath = details.summary().assetRoot()
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.relativize(tilesetPath)
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
final int tileCount = metadata.mapWidth() * metadata.mapHeight();
|
||||
final List<Long> gids = new ArrayList<>(tileCount);
|
||||
for (int index = 0; index < tileCount; index += 1) {
|
||||
gids.add(0L);
|
||||
}
|
||||
final TiledMapDocument document = new TiledMapDocument(
|
||||
"1.10",
|
||||
"1.12.1",
|
||||
"orthogonal",
|
||||
"right-down",
|
||||
metadata.mapWidth(),
|
||||
metadata.mapHeight(),
|
||||
tileset.tileWidth(),
|
||||
tileset.tileHeight(),
|
||||
2,
|
||||
1,
|
||||
List.of(),
|
||||
List.of(new TiledTilesetReference(1, relativeTilesetPath)),
|
||||
List.of(new TiledTileLayer(
|
||||
1,
|
||||
binding.layerName(),
|
||||
metadata.mapWidth(),
|
||||
metadata.mapHeight(),
|
||||
gids,
|
||||
List.of())),
|
||||
List.of());
|
||||
final Path outputPath = details.summary().assetRoot().resolve(binding.tilemap());
|
||||
codec.writeMap(outputPath, document);
|
||||
writtenFiles.add(outputPath);
|
||||
writtenFiles.addAll(result.writtenFiles());
|
||||
}
|
||||
if (writtenFiles.isEmpty()) {
|
||||
return new TiledAssetGenerationResult(false, "Enable at least one layer before generating TMX.", List.of());
|
||||
}
|
||||
return new TiledAssetGenerationResult(true, "Generated TMX successfully.", writtenFiles);
|
||||
} catch (RuntimeException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to generate TMX: " + exception.getMessage(), List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult generateSceneBankTilemap(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new TiledAssetGenerationResult(false, "TMX generation is available only for Scene Bank assets.", List.of());
|
||||
}
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
return new TiledAssetGenerationResult(false, "Scene Bank support metadata is missing or invalid.", List.of());
|
||||
}
|
||||
final AssetStudioSceneLayerBinding binding = metadata.layerBindings().stream()
|
||||
.filter(candidate -> candidate.index() == layerIndex)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (binding == null) {
|
||||
return new TiledAssetGenerationResult(false, "Layer " + layerIndex + " is not defined in Scene Bank metadata.", List.of());
|
||||
}
|
||||
if (!binding.enabled()) {
|
||||
return new TiledAssetGenerationResult(false, "Layer " + layerIndex + " is disabled.", List.of());
|
||||
}
|
||||
try {
|
||||
final String tilesetAssetRoot = Objects.requireNonNullElse(binding.tilesetAssetRoot(), "").trim();
|
||||
if (tilesetAssetRoot.isBlank()) {
|
||||
return new TiledAssetGenerationResult(
|
||||
false,
|
||||
"Layer " + binding.index() + " must declare a non-blank tileset_asset_root before TMX generation.",
|
||||
List.of());
|
||||
}
|
||||
final Path tilesetPath = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.resolve(tilesetAssetRoot)
|
||||
.resolve(GENERATED_TSX_FILE)
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (!java.nio.file.Files.isRegularFile(tilesetPath)) {
|
||||
return new TiledAssetGenerationResult(
|
||||
false,
|
||||
"Referenced TSX was not found for layer " + binding.index() + ": " + tilesetAssetRoot,
|
||||
List.of());
|
||||
}
|
||||
final TiledTilesetDocument tileset = codec.readTileset(tilesetPath);
|
||||
final String relativeTilesetPath = details.summary().assetRoot()
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.relativize(tilesetPath)
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
final int tileCount = binding.width() * binding.height();
|
||||
final List<Long> gids = new ArrayList<>(tileCount);
|
||||
for (int index = 0; index < tileCount; index += 1) {
|
||||
gids.add(0L);
|
||||
}
|
||||
final TiledMapDocument document = new TiledMapDocument(
|
||||
"1.10",
|
||||
"1.12.1",
|
||||
"orthogonal",
|
||||
"right-down",
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
tileset.tileWidth(),
|
||||
tileset.tileHeight(),
|
||||
2,
|
||||
1,
|
||||
List.of(),
|
||||
List.of(new TiledTilesetReference(1, relativeTilesetPath)),
|
||||
List.of(new TiledTileLayer(
|
||||
1,
|
||||
binding.layerName(),
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
gids,
|
||||
List.of())),
|
||||
List.of());
|
||||
final Path outputPath = details.summary().assetRoot().resolve(binding.tilemap());
|
||||
codec.writeMap(outputPath, document);
|
||||
return new TiledAssetGenerationResult(true, "Generated TMX for layer " + binding.index() + ".", List.of(outputPath));
|
||||
} catch (TiledUnsupportedFeatureException | IOException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to generate TMX: " + exception.getMessage(), List.of());
|
||||
}
|
||||
|
||||
@ -137,7 +137,9 @@ public final class AddAssetWizard {
|
||||
assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
if (!Objects.equals(oldValue, newValue)) {
|
||||
if (newValue != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.NONE);
|
||||
glyphSpecializationCombo.getSelectionModel().clearSelection();
|
||||
} else if (glyphSpecializationCombo.getValue() == null) {
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.SPRITES);
|
||||
}
|
||||
refreshOutputFormats();
|
||||
if (stepIndex == 1) {
|
||||
@ -148,10 +150,13 @@ public final class AddAssetWizard {
|
||||
}
|
||||
|
||||
private void configureGlyphSpecializationCombo() {
|
||||
glyphSpecializationCombo.setItems(FXCollections.observableArrayList(AssetStudioGlyphSpecialization.values()));
|
||||
glyphSpecializationCombo.setItems(FXCollections.observableArrayList(
|
||||
java.util.Arrays.stream(AssetStudioGlyphSpecialization.values())
|
||||
.filter(candidate -> candidate != AssetStudioGlyphSpecialization.NONE)
|
||||
.toList()));
|
||||
glyphSpecializationCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
glyphSpecializationCombo.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION));
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.NONE);
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.SPRITES);
|
||||
glyphSpecializationCombo.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
|
||||
@ -331,6 +336,11 @@ public final class AddAssetWizard {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_TYPE));
|
||||
return false;
|
||||
}
|
||||
if (selectedFamily() == AssetFamilyCatalog.GLYPH_BANK
|
||||
&& (selectedGlyphSpecialization() == null || selectedGlyphSpecialization() == AssetStudioGlyphSpecialization.NONE)) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_SPECIALIZATION));
|
||||
return false;
|
||||
}
|
||||
if (selectedFormat() == null || selectedFormat() == OutputFormatCatalog.UNKNOWN) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_FORMAT));
|
||||
return false;
|
||||
|
||||
@ -135,6 +135,9 @@ assets.badge.diagnostics=Diagnostics
|
||||
assets.section.summary=Summary
|
||||
assets.section.runtimeContract=Runtime Contract
|
||||
assets.section.bankComposition=Bank Composition
|
||||
assets.section.sceneLayers=Scene Layers
|
||||
assets.section.colorSchemaGroups=Color Schema Groups
|
||||
assets.section.layerConfiguration=Layer Configuration
|
||||
assets.section.paletteOverhauling=Palette Overhauling
|
||||
assets.subsection.codecConfiguration=Codec Configuration
|
||||
assets.subsection.metadata=Metadata
|
||||
@ -148,6 +151,8 @@ assets.action.generateTsx=Generate TSX
|
||||
assets.action.generateTmx=Generate TMX
|
||||
assets.action.validateSceneBank=Validate Scene Bank
|
||||
assets.action.acceptSceneBank=Accept Scene Bank
|
||||
assets.action.validateLayer=Validate Layer
|
||||
assets.action.acceptLayer=Accept Layer
|
||||
assets.action.delete=Delete
|
||||
assets.deleteDialog.title=Delete Asset
|
||||
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
|
||||
@ -188,8 +193,14 @@ assets.label.type=Type
|
||||
assets.label.studioRole=Studio Role
|
||||
assets.label.sceneStatus=Scene Status
|
||||
assets.label.sceneLayers=Scene Layers
|
||||
assets.label.sceneDimensions=Layer Size
|
||||
assets.label.layer=Layer
|
||||
assets.label.layerOverview=Layer Overview
|
||||
assets.label.tilemaps=Tilemaps
|
||||
assets.label.tilemap=Tilemap
|
||||
assets.label.tileset=Tileset
|
||||
assets.label.supportFile=Support File
|
||||
assets.label.useLayer=Use this layer
|
||||
assets.type.glyphBank=Glyph Bank
|
||||
assets.type.sceneBank=Scene Bank
|
||||
assets.type.paletteBank=Palette Bank
|
||||
@ -215,6 +226,8 @@ assets.value.included=Included
|
||||
assets.value.excluded=Excluded
|
||||
assets.value.yes=Yes
|
||||
assets.value.no=No
|
||||
assets.value.notConfigured=Not configured
|
||||
assets.value.layerOverviewPending=Overview reserved for this layer. Preview data will appear here when the layer pipeline is connected.
|
||||
assets.progress.idle=Assets workspace idle.
|
||||
assets.progress.refreshing=Refreshing assets...
|
||||
assets.progress.loadingDetails=Loading selected asset details...
|
||||
@ -250,6 +263,22 @@ assets.paletteOverhauling.selected=Bank Palettes
|
||||
assets.paletteOverhauling.tileSelector=Preview Tile
|
||||
assets.paletteOverhauling.appliedPalette=Applied Palette
|
||||
assets.paletteOverhauling.previewEmpty=Select at least one palette to inspect it on a tile.
|
||||
assets.paletteWizard.createTitle=Create Palette
|
||||
assets.paletteWizard.updateTitle=Update Palette
|
||||
assets.paletteWizard.name=Name
|
||||
assets.paletteWizard.schema=Schema
|
||||
assets.paletteWizard.original=Original Palette
|
||||
assets.paletteWizard.editable=Editable Palette
|
||||
assets.paletteWizard.applyColor=Apply Color
|
||||
assets.paletteWizard.duplicate=An identical palette already exists for this schema.
|
||||
assets.paletteWizard.ready=Palette is ready to be saved.
|
||||
assets.colorSchema.empty=No schemas have been authored for this glyph bank yet.
|
||||
assets.colorSchema.summary={0} schema groups ready, {1} selected tiles still unassigned.
|
||||
assets.colorSchema.removeBlocked=Schema {0} cannot be removed while palettes still reference it.
|
||||
assets.colorSchema.dialog.availableTiles=Available Tiles
|
||||
assets.colorSchema.dialog.assignedTiles=Assigned Tiles
|
||||
assets.colorSchema.dialog.colorCount=Color count: {0}
|
||||
assets.colorSchema.dialog.tooManyColors=Color Schema Groups support at most 15 visible indices.
|
||||
assets.details.codecConfiguration.empty=This codec does not expose configuration fields yet.
|
||||
assets.details.metadata.empty=This asset does not expose metadata fields yet.
|
||||
assets.addWizard.title=Add Asset
|
||||
@ -281,6 +310,7 @@ assets.addWizard.error.rootRelative=Asset root must be a relative path inside as
|
||||
assets.addWizard.error.rootOutsideAssets=The selected directory must stay inside assets/.
|
||||
assets.addWizard.error.rootAlreadyAsset=The selected root already contains asset.json.
|
||||
assets.addWizard.error.type=Choose an asset type before continuing.
|
||||
assets.addWizard.error.specialization=Choose a Studio specialization before continuing.
|
||||
assets.addWizard.error.format=Choose an output format before continuing.
|
||||
assets.addWizard.error.codec=Choose an output codec before continuing.
|
||||
assets.addWizard.error.unsupportedCombination=The selected asset type, output format, and codec combination is not supported.
|
||||
|
||||
@ -114,6 +114,20 @@
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.studio-button.assets-details-mini-action-button {
|
||||
-fx-min-width: 26;
|
||||
-fx-pref-width: 26;
|
||||
-fx-max-width: 26;
|
||||
-fx-min-height: 24;
|
||||
-fx-pref-height: 24;
|
||||
-fx-max-height: 24;
|
||||
-fx-padding: 0;
|
||||
-fx-background-radius: 6;
|
||||
-fx-border-radius: 6;
|
||||
-fx-font-size: 12px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-primary {
|
||||
-fx-background-color: #27507a;
|
||||
-fx-border-color: #5ea0de;
|
||||
@ -1213,6 +1227,15 @@
|
||||
-fx-background-color: #162a20;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-scene {
|
||||
-fx-background-color: #1d1610;
|
||||
-fx-border-color: #b58931;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-scene:hover {
|
||||
-fx-background-color: #2a2015;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-generic {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-border-color: #334050;
|
||||
@ -1240,6 +1263,10 @@
|
||||
-fx-text-fill: #aee8c5;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-scene {
|
||||
-fx-text-fill: #f5d47a;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-generic {
|
||||
-fx-text-fill: #f6fbff;
|
||||
}
|
||||
@ -1457,6 +1484,12 @@
|
||||
-fx-max-height: 446;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-section {
|
||||
-fx-min-height: 446;
|
||||
-fx-pref-height: 446;
|
||||
-fx-max-height: 446;
|
||||
}
|
||||
|
||||
.assets-details-bank-composition-body {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
@ -1465,6 +1498,76 @@
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-palette-overhauling-toolbar {
|
||||
-fx-alignment: top-left;
|
||||
-fx-padding: 25 0 0 0;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-content {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-toolbar {
|
||||
-fx-alignment: top-left;
|
||||
-fx-padding: 25 0 0 0;
|
||||
}
|
||||
|
||||
.assets-details-list-with-actions {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-body {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-list {
|
||||
-fx-pref-width: 280;
|
||||
-fx-max-width: 320;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-item {
|
||||
-fx-padding: 8 10 8 10;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-title {
|
||||
-fx-font-size: 12px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: #f2f6fb;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-version {
|
||||
-fx-font-size: 11px;
|
||||
-fx-text-fill: #8fa4b8;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-meta,
|
||||
.assets-details-color-schema-summary,
|
||||
.assets-details-color-schema-empty,
|
||||
.assets-details-color-schema-warning,
|
||||
.assets-details-color-schema-preview-title {
|
||||
-fx-font-size: 11px;
|
||||
-fx-text-fill: #9bb0c3;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-status-ok {
|
||||
-fx-background-color: #203a2b;
|
||||
-fx-text-fill: #8ee0ac;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-status-inconsistent {
|
||||
-fx-background-color: #4a261f;
|
||||
-fx-text-fill: #ffb29d;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-strip {
|
||||
-fx-padding: 0 0 4 0;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-swatch {
|
||||
-fx-border-color: #334150;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-bank-file-cell {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
@ -1500,6 +1603,43 @@
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-details-palette-invalid {
|
||||
-fx-font-size: 10px;
|
||||
-fx-text-fill: #d9a58f;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard {
|
||||
-fx-background-color: #111821;
|
||||
-fx-border-color: #263545;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-label {
|
||||
-fx-font-size: 11px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: #9fc3e7;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-strip,
|
||||
.assets-details-palette-wizard-editor,
|
||||
.assets-details-palette-wizard-color-editor {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-summary {
|
||||
-fx-font-size: 11px;
|
||||
-fx-text-fill: #c0d0e0;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-actions {
|
||||
-fx-alignment: center-right;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-swatch {
|
||||
-fx-border-color: #334150;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-palette-hash {
|
||||
-fx-text-fill: #9fc3e7;
|
||||
-fx-font-size: 10px;
|
||||
@ -1594,6 +1734,124 @@
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-chip-fixed {
|
||||
-fx-alignment: center;
|
||||
-fx-min-width: 88;
|
||||
-fx-pref-width: 88;
|
||||
-fx-max-width: 88;
|
||||
}
|
||||
|
||||
.assets-scene-layer-shell {
|
||||
-fx-spacing: 0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-layout {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-scene-layer-config-column {
|
||||
-fx-spacing: 10;
|
||||
}
|
||||
|
||||
.assets-scene-layer-config {
|
||||
-fx-pref-width: 0;
|
||||
-fx-spacing: 8;
|
||||
}
|
||||
|
||||
.assets-scene-layer-config-section {
|
||||
-fx-pref-width: 0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-chip {
|
||||
-fx-min-width: 70;
|
||||
-fx-pref-width: 70;
|
||||
-fx-max-width: 70;
|
||||
-fx-alignment: center;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector-button {
|
||||
-fx-background-color: #17120d;
|
||||
-fx-border-color: #634d22;
|
||||
-fx-text-fill: #d9c28f;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 8 14 8 14;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector-button:hover {
|
||||
-fx-background-color: #221a10;
|
||||
-fx-border-color: #8f6f33;
|
||||
-fx-text-fill: #f5ddb0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector-button-active {
|
||||
-fx-background-color: #2a1f12;
|
||||
-fx-border-color: #d4a94a;
|
||||
-fx-text-fill: #ffe5a6;
|
||||
}
|
||||
|
||||
.assets-scene-layer-enabled {
|
||||
-fx-text-fill: #d6e0ea;
|
||||
}
|
||||
|
||||
.assets-scene-layer-form-actions {
|
||||
-fx-alignment: center-right;
|
||||
-fx-spacing: 8;
|
||||
}
|
||||
|
||||
.assets-scene-layer-actions {
|
||||
-fx-padding: 6 0 0 0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-action {
|
||||
-fx-max-width: 140;
|
||||
}
|
||||
|
||||
.assets-scene-layer-size-editor {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-scene-layer-size-field {
|
||||
-fx-pref-width: 88;
|
||||
}
|
||||
|
||||
.assets-scene-layer-size-separator {
|
||||
-fx-text-fill: #d6e0ea;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-scene-layer-overview {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-color: #26313d;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-width: 1;
|
||||
-fx-min-width: 260;
|
||||
-fx-pref-width: 260;
|
||||
-fx-max-width: 260;
|
||||
-fx-min-height: 220;
|
||||
-fx-padding: 10;
|
||||
-fx-spacing: 10;
|
||||
}
|
||||
|
||||
.assets-scene-layer-overview-title {
|
||||
-fx-text-fill: #d6e0ea;
|
||||
-fx-font-size: 12px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-scene-layer-overview-surface {
|
||||
-fx-background-color: #0f141a;
|
||||
-fx-background-radius: 8;
|
||||
-fx-border-color: #26313d;
|
||||
-fx-border-radius: 8;
|
||||
-fx-border-width: 1;
|
||||
-fx-min-height: 148;
|
||||
}
|
||||
|
||||
.assets-details-chip-image {
|
||||
-fx-background-color: #11283a;
|
||||
-fx-border-color: #5cb6ff;
|
||||
@ -1606,6 +1864,12 @@
|
||||
-fx-text-fill: #dde6ff;
|
||||
}
|
||||
|
||||
.assets-details-chip-scene {
|
||||
-fx-background-color: #2a1f12;
|
||||
-fx-border-color: #d4a94a;
|
||||
-fx-text-fill: #ffe5a6;
|
||||
}
|
||||
|
||||
.assets-details-chip-generic {
|
||||
-fx-background-color: #1e242c;
|
||||
-fx-border-color: #5f7388;
|
||||
|
||||
@ -116,6 +116,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
@ -140,6 +141,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.*;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@ -16,64 +16,388 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
@Test
|
||||
void defaultsToFirstTilePaletteWhenNoSelectedFilesExist() {
|
||||
void derivesOriginalPaletteFromReadySchemaWhenNoPaletteRecordExists() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(tileDetails(List.of(), files("tile", 3)));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertTrue(coordinator.ready());
|
||||
assertEquals(0, coordinator.viewModel().selectedFiles().size());
|
||||
assertNull(coordinator.viewModel().previewTileFile());
|
||||
assertNull(coordinator.viewModel().previewPaletteFile());
|
||||
assertEquals(1, coordinator.viewModel().allFiles().size());
|
||||
assertEquals("schema-1 / original", coordinator.viewModel().allFiles().getFirst().displayName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void clickingSelectedPaletteChangesOnlyPreviewPalette() {
|
||||
void initialSelectionStillMatchesBuildFacingPipelinePalettePayload() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(tileDetails(List.of(files("tile", 3).get(0), files("tile", 3).get(1)), List.of(files("tile", 3).get(2))));
|
||||
coordinator.beginEdit();
|
||||
|
||||
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
|
||||
coordinator.moveToSelected(List.of(first));
|
||||
coordinator.selectPreviewPalette(first);
|
||||
|
||||
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
|
||||
assertEquals(1, coordinator.viewModel().selectedFiles().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingPreviewedPaletteFallsBackToFirstRemainingSelectedPalette() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
final List<AssetWorkspaceBankCompositionFile> generated = files("tile", 3);
|
||||
coordinator.replaceDetails(tileDetails(List.of(generated.get(0), generated.get(1)), List.of(generated.get(2))));
|
||||
coordinator.beginEdit();
|
||||
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
|
||||
coordinator.moveToSelected(List.of(first));
|
||||
coordinator.selectPreviewPalette(first);
|
||||
|
||||
coordinator.moveToAvailable(List.of(first));
|
||||
final AssetStudioGlyphMaterialPalette persisted = new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(persisted),
|
||||
null),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFFFF0000, 0xFF00FF00),
|
||||
"convertedRgb565", List.of(0xF800, 0x07E0)))));
|
||||
|
||||
assertEquals(1, coordinator.viewModel().selectedFiles().size());
|
||||
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
|
||||
assertEquals("palette-1", coordinator.viewModel().selectedFiles().getFirst().metadata().get("paletteId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deduplicatesPalettesByColorHash() {
|
||||
void previewPaletteDefaultsToAvailablePaletteWhenBuildSelectionIsEmpty() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile first = file("tile-a.png", List.of(0xFFFF0000, 0xFF00FF00));
|
||||
final AssetWorkspaceBankCompositionFile duplicate = file("tile-b.png", List.of(0xFFFF0000, 0xFF00FF00));
|
||||
final AssetWorkspaceBankCompositionFile unique = file("tile-c.png", List.of(0xFF0000FF));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#112233", "#445566"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
coordinator.replaceDetails(tileDetails(List.of(first), List.of(duplicate, unique)));
|
||||
assertNotNull(coordinator.viewModel().previewPaletteFile());
|
||||
assertTrue(coordinator.viewModel().availableFiles().contains(coordinator.viewModel().previewPaletteFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void originalPaletteRemainsSyntheticEvenWhenUserPaletteHasSameColors() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertEquals(2, coordinator.viewModel().allFiles().size());
|
||||
assertEquals(10, coordinator.viewModel().allFiles().getFirst().displayName().length());
|
||||
assertEquals(coordinator.viewModel().allFiles().stream().map(AssetWorkspaceBankCompositionFile::displayName).distinct().count(),
|
||||
coordinator.viewModel().allFiles().size());
|
||||
final AssetWorkspaceBankCompositionFile original = coordinator.viewModel().allFiles().stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsOriginal)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
final AssetWorkspaceBankCompositionFile userPalette = coordinator.viewModel().allFiles().stream()
|
||||
.filter(file -> "palette-1".equals(file.metadata().get("paletteId")))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertEquals("schema-schema-1-original", original.metadata().get("paletteId"));
|
||||
assertFalse(AssetDetailsPaletteOverhaulingCoordinator.paletteIsOriginal(userPalette));
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails tileDetails(
|
||||
@Test
|
||||
void originalPaletteTracksSchemaMaterialColorsAfterMetadataUpdate() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
coordinator.replaceMaterialMetadata(new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
2,
|
||||
Map.of(1, "#112233", 2, "#445566"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile original = coordinator.viewModel().allFiles().stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsOriginal)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final Map<String, Object> palette = (Map<String, Object>) original.metadata().get("palette");
|
||||
assertEquals(List.of(0xFF112233, 0xFF445566), palette.get("originalArgb8888"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateDetectionUsesSameSchemaColorsAndOrder() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertTrue(coordinator.duplicatePaletteExists(null, "schema-1", List.of("#FF0000", "#00FF00")));
|
||||
assertFalse(coordinator.duplicatePaletteExists(null, "schema-1", List.of("#00FF00", "#FF0000")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void paletteDisplayNameUsesExplicitNameWhenPresent() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"Forest",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals("Forest", palette.displayName());
|
||||
assertEquals("Forest", palette.metadata().get("explicitPaletteDisplayName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void paletteDisplayNameFallsBackToColorsWhenNameIsBlank() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals("ember bloom", palette.displayName());
|
||||
assertFalse(palette.metadata().containsKey("explicitPaletteDisplayName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedPaletteDisplayNameUsesPoeticColorWords() {
|
||||
assertEquals("snow white", AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(List.of("#FFFFFF")));
|
||||
assertEquals("fog twist", AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(List.of("#F2F4F4", "#707878")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertPalettePersistsGeneratedDisplayNameWhenNameIsBlank() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#F2F4F4", 2, "#707878"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.upsertPalette(
|
||||
"palette-1",
|
||||
" ",
|
||||
"schema-1",
|
||||
List.of("#F2F4F4", "#707878"));
|
||||
|
||||
assertEquals("fog twist", metadata.palettes().getFirst().displayName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removePaletteDeletesUserPaletteEvenWhenSelectedForBuild() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#112233", "#445566"))),
|
||||
null),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFF112233, 0xFF445566),
|
||||
"convertedRgb565", List.of(0x1106, 0x42AC)))));
|
||||
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.removePalette("palette-1");
|
||||
|
||||
assertTrue(metadata.palettes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removePaletteRejectsSyntheticOriginalPalette() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> coordinator.removePalette("schema-schema-1-original"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stalePaletteIsProjectedAsInvalid() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
2,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals(Boolean.FALSE, palette.metadata().get("valid"));
|
||||
assertTrue(((String) palette.metadata().get("invalidReason")).contains("stale"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void paletteIsInvalidWhenSchemaArtifactFingerprintDrifts() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-old")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals(Boolean.FALSE, palette.metadata().get("valid"));
|
||||
assertTrue(((String) palette.metadata().get("invalidReason")).contains("inconsistent"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void previewTilesAreScopedToSelectedPaletteSchema() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a"), tile("tile-b.png", "artifact-b")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(
|
||||
new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a"))),
|
||||
new AssetStudioGlyphMaterialSchema(
|
||||
"schema-2",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#0000FF"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-b", "fp-b")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile schema2Palette = coordinator.viewModel().allFiles().stream()
|
||||
.filter(file -> "schema-2".equals(file.metadata().get("schemaId")))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
coordinator.selectPreviewPalette(schema2Palette);
|
||||
|
||||
assertEquals(List.of("tile-b.png"), coordinator.viewModel().previewTileFiles().stream().map(AssetWorkspaceBankCompositionFile::path).toList());
|
||||
assertEquals("schema-2", coordinator.viewModel().previewPaletteFile().metadata().get("schemaId"));
|
||||
assertEquals(Map.of(1, "#0000FF"), coordinator.viewModel().previewPaletteFile().metadata().get("schemaMaterialIndices"));
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails details(
|
||||
List<AssetWorkspaceBankCompositionFile> selectedFiles,
|
||||
List<AssetWorkspaceBankCompositionFile> availableFiles) {
|
||||
AssetStudioGlyphMaterialMetadata metadata,
|
||||
List<Map<String, Object>> pipelinePalettes) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(1),
|
||||
@ -82,7 +406,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
AssetFamilyCatalog.GLYPH_BANK,
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
AssetStudioGlyphSpecialization.SPRITES,
|
||||
Path.of("/tmp/bank"),
|
||||
false,
|
||||
false),
|
||||
@ -93,48 +417,29 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L),
|
||||
selectedFiles.stream().map(file -> (Map<String, Object>) file.metadata().get("palette")).toList(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), selectedFiles, 0L),
|
||||
pipelinePalettes,
|
||||
metadata,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
private List<AssetWorkspaceBankCompositionFile> files(String prefix, int count) {
|
||||
return java.util.stream.IntStream.range(0, count)
|
||||
.mapToObj(index -> file(
|
||||
prefix + "-" + index + ".png",
|
||||
List.of(
|
||||
0xFF000000 | ((index + 1) << 16),
|
||||
0xFF000000 | ((index + 1) << 8),
|
||||
0xFF000000 | (index + 1))))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile file(String path, List<Integer> colors) {
|
||||
private AssetWorkspaceBankCompositionFile tile(String path, String artifactId) {
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
path,
|
||||
path,
|
||||
256L,
|
||||
1L,
|
||||
null,
|
||||
"artifact-a".equals(artifactId) ? "fp-a" : "fp-b",
|
||||
Map.of(
|
||||
"artifactId", artifactId,
|
||||
"tile", Map.of(
|
||||
"width", 2,
|
||||
"height", 2,
|
||||
"paletteIndices", List.of(0, 1, 2, 0)),
|
||||
"palette", Map.of(
|
||||
"originalArgb8888", colors,
|
||||
"convertedRgb565", colors.stream().map(this::toRgb565).toList())));
|
||||
}
|
||||
|
||||
private int toRgb565(int argb8888) {
|
||||
final int red = (argb8888 >> 16) & 0xFF;
|
||||
final int green = (argb8888 >> 8) & 0xFF;
|
||||
final int blue = argb8888 & 0xFF;
|
||||
final int r5 = (red >> 3) & 0x1F;
|
||||
final int g6 = (green >> 2) & 0x3F;
|
||||
final int b5 = (blue >> 3) & 0x1F;
|
||||
return (r5 << 11) | (g6 << 5) | b5;
|
||||
"originalArgb8888", List.of(0xFFFF0000, 0xFF00FF00),
|
||||
"convertedRgb565", List.of(0xF800, 0x07E0))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,219 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.*;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class AssetDetailsColorSchemaCoordinatorTest {
|
||||
@Test
|
||||
void createFlowExcludesTilesAlreadyBoundToAnotherSchema() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tileA = tile("a.png", "artifact-a", "fp-a", List.of("#FF0000"));
|
||||
final AssetWorkspaceBankCompositionFile tileB = tile("b.png", "artifact-b", "fp-b", List.of("#00FF00"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tileA, tileB),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null)));
|
||||
|
||||
assertEquals(List.of(tileB), coordinator.availableTilesForCreate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFlowListsSelectedTilesBeforePaletteMetadataExists() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tile = selectedArtifactOnly("a.png", "artifact-a", "fp-a");
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile),
|
||||
new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)));
|
||||
|
||||
assertEquals(List.of(tile), coordinator.availableTilesForCreate());
|
||||
assertEquals(List.of(), AssetDetailsColorSchemaCoordinator.tileColors(tile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFlowUsesSelectedFingerprintWhenCurrentFileFingerprintIsMissing() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tile = new AssetWorkspaceBankCompositionFile(
|
||||
"a.png",
|
||||
"a.png",
|
||||
64L,
|
||||
1L,
|
||||
null,
|
||||
Map.of(
|
||||
"artifactId", "artifact-a",
|
||||
"selectedFingerprint", "fp-a"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile),
|
||||
new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)));
|
||||
|
||||
assertEquals(List.of(tile), coordinator.availableTilesForCreate());
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.upsertSchema(
|
||||
"schema-1",
|
||||
List.of(tile),
|
||||
List.of("#FF0000"));
|
||||
assertEquals("fp-a", metadata.schemas().getFirst().artifactBindings().getFirst().fingerprint());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateFlowKeepsCurrentTilesAndIncrementsVersionWhenSchemaChanges() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tileA = tile("a.png", "artifact-a", "fp-a", List.of("#FF0000"));
|
||||
final AssetWorkspaceBankCompositionFile tileB = tile("b.png", "artifact-b", "fp-b", List.of("#00FF00"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tileA, tileB),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null)));
|
||||
|
||||
final AssetStudioGlyphMaterialMetadata updated = coordinator.upsertSchema(
|
||||
"schema-1",
|
||||
List.of(tileA, tileB),
|
||||
List.of("#FF0000", "#00FF00"));
|
||||
|
||||
final AssetStudioGlyphMaterialSchema schema = updated.schemas().getFirst();
|
||||
assertEquals(2, schema.version());
|
||||
assertEquals(Map.of(1, "#FF0000", 2, "#00FF00"), schema.materialIndices());
|
||||
assertEquals(List.of("artifact-a", "artifact-b"), schema.artifactBindings().stream().map(AssetStudioGlyphMaterialArtifactBinding::artifactId).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeIsBlockedWhenPalettesStillReferenceSchema() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("a.png", "artifact-a", "fp-a", List.of("#FF0000"))),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette("palette-1", "schema-1", 1, List.of("#FF0000"))),
|
||||
null)));
|
||||
|
||||
assertFalse(coordinator.canRemove("schema-1"));
|
||||
assertThrows(IllegalStateException.class, () -> coordinator.removeSchema("schema-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void effectiveStatusBecomesInconsistentWhenArtifactFingerprintDrifts() {
|
||||
final AssetStudioGlyphMaterialSchema schema = new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-old")));
|
||||
|
||||
assertEquals(
|
||||
AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT,
|
||||
AssetDetailsColorSchemaCoordinator.effectiveStatus(
|
||||
schema,
|
||||
List.of(tile("a.png", "artifact-a", "fp-new", List.of("#FF0000", "#FF0001")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaUpdateCanRefreshOnlyFingerprintAndIncrementVersion() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile changedTile = tile("a.png", "artifact-a", "fp-new", List.of("#FF0000"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(changedTile),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-old")))),
|
||||
List.of(),
|
||||
null)));
|
||||
|
||||
final AssetStudioGlyphMaterialSchema updated = coordinator.upsertSchema(
|
||||
"schema-1",
|
||||
List.of(changedTile),
|
||||
List.of("#FF0000")).schemas().getFirst();
|
||||
|
||||
assertEquals(2, updated.version());
|
||||
assertEquals(AssetStudioGlyphMaterialSchemaStatus.OK, updated.status());
|
||||
assertEquals("fp-new", updated.artifactBindings().getFirst().fingerprint());
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails details(
|
||||
List<AssetWorkspaceBankCompositionFile> selectedTiles,
|
||||
AssetStudioGlyphMaterialMetadata metadata) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(1),
|
||||
"glyph_bank",
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
AssetFamilyCatalog.GLYPH_BANK,
|
||||
AssetStudioGlyphSpecialization.SPRITES,
|
||||
Path.of("/tmp/glyph"),
|
||||
false,
|
||||
false),
|
||||
List.of(),
|
||||
OutputFormatCatalog.GLYPH_INDEXED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
List.of(OutputCodecCatalog.NONE),
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), selectedTiles, 0L),
|
||||
List.of(),
|
||||
metadata,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile tile(String path, String artifactId, String fingerprint, List<String> colors) {
|
||||
final List<Integer> argb = colors.stream()
|
||||
.map(color -> Integer.parseUnsignedInt("FF" + color.substring(1), 16))
|
||||
.map(Integer::valueOf)
|
||||
.toList();
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
path,
|
||||
path,
|
||||
64L,
|
||||
1L,
|
||||
fingerprint,
|
||||
Map.of(
|
||||
"artifactId", artifactId,
|
||||
"tile", Map.of("width", 8, "height", 8, "paletteIndices", List.of(1, 1, 1)),
|
||||
"palette", Map.of("originalArgb8888", argb, "convertedRgb565", List.of())));
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile selectedArtifactOnly(String path, String artifactId, String fingerprint) {
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
path,
|
||||
path,
|
||||
64L,
|
||||
1L,
|
||||
fingerprint,
|
||||
Map.of("artifactId", artifactId));
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,8 @@ import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ -25,8 +27,67 @@ final class AssetStudioMetadataServiceTest {
|
||||
|
||||
assertEquals(AssetStudioGlyphSpecialization.TILESET, snapshot.glyphSpecialization());
|
||||
assertNull(snapshot.sceneBankMetadata());
|
||||
assertNull(snapshot.glyphMaterialMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsGlyphBankWithoutStudioMetadataAsNone() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("legacy-glyph");
|
||||
Files.createDirectories(assetRoot);
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.GLYPH_BANK);
|
||||
|
||||
assertEquals(AssetStudioGlyphSpecialization.NONE, snapshot.glyphSpecialization());
|
||||
assertNull(snapshot.sceneBankMetadata());
|
||||
assertNull(snapshot.glyphMaterialMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsAndWritesGlyphMaterialSupportMetadata() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("glyph");
|
||||
Files.createDirectories(assetRoot);
|
||||
final AssetStudioGlyphMaterialMetadata metadata = new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
2,
|
||||
Map.of(1, "#8B4513", 2, "#00AA00"),
|
||||
List.of(
|
||||
new AssetStudioGlyphMaterialArtifactBinding("artifact-1", "fp-1"),
|
||||
new AssetStudioGlyphMaterialArtifactBinding("artifact-2", "fp-2")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"Forest",
|
||||
"schema-1",
|
||||
2,
|
||||
List.of("#8B4513", "#00AA00"))),
|
||||
null);
|
||||
service.writeGlyphMaterialMetadata(assetRoot, metadata);
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.GLYPH_BANK);
|
||||
|
||||
assertNotNull(snapshot.glyphMaterialMetadata());
|
||||
assertEquals(1, snapshot.glyphMaterialMetadata().schemas().size());
|
||||
assertEquals("schema-1", snapshot.glyphMaterialMetadata().schemas().getFirst().schemaId());
|
||||
assertEquals("#8B4513", snapshot.glyphMaterialMetadata().schemas().getFirst().materialIndices().get(1));
|
||||
assertEquals(1, snapshot.glyphMaterialMetadata().palettes().size());
|
||||
assertEquals("Forest", snapshot.glyphMaterialMetadata().palettes().getFirst().displayName());
|
||||
assertEquals(List.of("#8B4513", "#00AA00"), snapshot.glyphMaterialMetadata().palettes().getFirst().colors());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsHtmlRgbAsCanonicalGlyphMaterialColorAndDerivesRgb565() {
|
||||
final AssetStudioGlyphMaterialPalette palette = new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00", "#0000FF"));
|
||||
|
||||
assertEquals(List.of("#FF0000", "#00FF00", "#0000FF"), palette.colors());
|
||||
assertEquals(List.of(0xF800, 0x07E0, 0x001F), palette.convertedRgb565Colors());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void readsSceneBankSupportMetadataWhenContractIsValid() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("scene");
|
||||
@ -36,10 +97,12 @@ final class AssetStudioMetadataServiceTest {
|
||||
"schema_version": 1,
|
||||
"map_width": 20,
|
||||
"map_height": 12,
|
||||
"layer_count": 2,
|
||||
"layer_count": 4,
|
||||
"layers": [
|
||||
{ "index": 1, "name": "Ground", "tilemap": "ground.tmx", "tileset_asset_root": "tilesets/ground" },
|
||||
{ "index": 2, "name": "Collision", "tilemap": "collision.tmx", "tileset_asset_root": "tilesets/ground" }
|
||||
{ "index": 1, "enabled": true, "name": "Ground", "width": 20, "height": 12, "tilemap": "ground.tmx", "tileset_asset_root": "tilesets/ground" },
|
||||
{ "index": 2, "enabled": true, "name": "Collision", "width": 10, "height": 8, "tilemap": "collision.tmx", "tileset_asset_root": "tilesets/ground" },
|
||||
{ "index": 3, "enabled": false, "name": "Layer 3", "width": 20, "height": 12, "tilemap": "layer-3.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 4, "enabled": false, "name": "Layer 4", "width": 20, "height": 12, "tilemap": "layer-4.tmx", "tileset_asset_root": "" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
@ -49,14 +112,19 @@ final class AssetStudioMetadataServiceTest {
|
||||
assertNotNull(snapshot.sceneBankMetadata());
|
||||
assertEquals(20, snapshot.sceneBankMetadata().mapWidth());
|
||||
assertEquals(12, snapshot.sceneBankMetadata().mapHeight());
|
||||
assertEquals(2, snapshot.sceneBankMetadata().layerCount());
|
||||
assertEquals(4, snapshot.sceneBankMetadata().layerCount());
|
||||
assertEquals(10, snapshot.sceneBankMetadata().layerBindings().get(1).width());
|
||||
assertEquals(8, snapshot.sceneBankMetadata().layerBindings().get(1).height());
|
||||
assertEquals(
|
||||
java.util.List.of("ground.tmx", "collision.tmx"),
|
||||
snapshot.sceneBankMetadata().layerBindings().stream().map(AssetStudioSceneLayerBinding::tilemap).toList());
|
||||
snapshot.sceneBankMetadata().layerBindings().stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.map(AssetStudioSceneLayerBinding::tilemap)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSceneBankSupportMetadataBeyondWaveOneLayerLimit() throws Exception {
|
||||
void rejectsSceneBankSupportMetadataWhenLayerCountIsNotFixedAtFour() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("scene-invalid");
|
||||
Files.createDirectories(assetRoot);
|
||||
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
|
||||
@ -64,13 +132,11 @@ final class AssetStudioMetadataServiceTest {
|
||||
"schema_version": 1,
|
||||
"map_width": 16,
|
||||
"map_height": 16,
|
||||
"layer_count": 5,
|
||||
"layer_count": 3,
|
||||
"layers": [
|
||||
{ "index": 1, "name": "A", "tilemap": "a.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 2, "name": "B", "tilemap": "b.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 3, "name": "C", "tilemap": "c.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 4, "name": "D", "tilemap": "d.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 5, "name": "E", "tilemap": "e.tmx", "tileset_asset_root": "" }
|
||||
{ "index": 1, "enabled": true, "name": "A", "width": 16, "height": 16, "tilemap": "a.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 2, "enabled": false, "name": "B", "width": 16, "height": 16, "tilemap": "b.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 3, "enabled": false, "name": "C", "width": 16, "height": 16, "tilemap": "c.tmx", "tileset_asset_root": "" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
@ -80,11 +80,16 @@ final class SceneBankWorkflowServiceTest {
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||
List.of(),
|
||||
null,
|
||||
new AssetStudioSceneBankMetadata(
|
||||
8,
|
||||
8,
|
||||
1,
|
||||
List.of(new AssetStudioSceneLayerBinding(1, "Ground", "ground.tmx", "tilesets/missing")),
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 8, 8, "ground.tmx", "tilesets/missing"),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 8, 8, "layer-2.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 8, 8, "layer-3.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 8, 8, "layer-4.tmx", "")),
|
||||
sceneRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
@ -155,6 +160,7 @@ final class SceneBankWorkflowServiceTest {
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
@ -181,11 +187,16 @@ final class SceneBankWorkflowServiceTest {
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||
List.of(),
|
||||
null,
|
||||
new AssetStudioSceneBankMetadata(
|
||||
16,
|
||||
12,
|
||||
1,
|
||||
List.of(new AssetStudioSceneLayerBinding(1, "Ground", "ground.tmx", "tilesets/overworld")),
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 16, 12, "ground.tmx", "tilesets/overworld"),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 12, "layer-2.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 12, "layer-3.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 12, "layer-4.tmx", "")),
|
||||
assetRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
|
||||
@ -105,6 +105,7 @@ final class TiledAssetGenerationServiceTest {
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
@ -131,11 +132,16 @@ final class TiledAssetGenerationServiceTest {
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||
List.of(),
|
||||
null,
|
||||
new AssetStudioSceneBankMetadata(
|
||||
16,
|
||||
12,
|
||||
1,
|
||||
List.of(new AssetStudioSceneLayerBinding(1, "Ground", "ground.tmx", "tilesets/overworld")),
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 16, 12, "ground.tmx", "tilesets/overworld"),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 12, "layer-2.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 12, "layer-3.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 12, "layer-4.tmx", "")),
|
||||
assetRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
|
||||
@ -1592,5 +1592,319 @@
|
||||
"blocking" : true
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"asset_id" : 16,
|
||||
"contract_fingerprint" : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"files" : [ ]
|
||||
}, {
|
||||
"asset_id" : 17,
|
||||
"contract_fingerprint" : "058b188cc68f9b9780825f8072d7ab316b042d66c80a76a344e06313cb49ad01",
|
||||
"files" : [ {
|
||||
"relative_path" : "tileset-001.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 92,
|
||||
"last_modified" : 1776880392312,
|
||||
"fingerprint" : "8771c3e294d0edbc8dd3aaa6995a6db32166ae6bfb3e063170cc79b298d17e71",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640 ],
|
||||
"convertedRgb565" : [ 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.001.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 184,
|
||||
"last_modified" : 1776880490117,
|
||||
"fingerprint" : "5d81eef8be13043330b42b546e10beed8ba00dfaa42df67477a8778c2b9484d7",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQEBAQEBAQEBAQICAgECAgICAgICAgMEBQUFBQQDBAUGBgYGBQMEBQYGBwcIAwQFCAYGCAgBAwUFCAgIBw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -12552128, -14129088, -14133200, -14145496, -7835584, -5730208, -10465232 ],
|
||||
"convertedRgb565" : [ 19657, 17352, 11080, 10950, 10565, -29816, -21364, 25222 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.002.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 172,
|
||||
"last_modified" : 1776962843559,
|
||||
"fingerprint" : "3250840c4fe08c134ea966ce7c8fcaccae2c2a45660d7173c5e262a2e5076548",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQECAwMBAQEBAgMEBQEBAQIDBAUFAQIDAwQEBAQDAwYFBQUFBQQGBQUGBgYGBQUFBgYFBQUFBgYGBQUGBg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12552128, -14133200, -14145496, -10465232, -5730208, -7835584 ],
|
||||
"convertedRgb565" : [ 17352, 10950, 10565, 25222, -21364, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.003.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 191,
|
||||
"last_modified" : 1776880575940,
|
||||
"fingerprint" : "fe3ab2b7eb3b10a042de14484d53e718e6b505edb1ad19053668565bfe3a8f25",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAwMDAwMEBQECAwMDAwQEBQECAwMDBQUFBQEBAgMEBAQEBAYBAQYGBgYEBAYFBAQEBgYEBAQGBgQEBgYGBA=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -14145496, -14133200, -12552128, -5730208, -10465232, -7835584 ],
|
||||
"convertedRgb565" : [ 10565, 10950, 17352, -21364, 25222, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.004.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 193,
|
||||
"last_modified" : 1776880597961,
|
||||
"fingerprint" : "7fcbf90e1e9a69d6ce3be80ea1901d66c9992f68dc72e4c636dcd2ad52fa2898",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQEBAQECAgIBAQEBAQICAgICAgIBAwQEBAQDBQIEBgYGBgQDBQcICAYGBAMFBwcGBgcEAwUIBwcHBAQFAQ=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -12552128, -14133200, -14145496, -14129088, -7835584, -10465232, -5730208 ],
|
||||
"convertedRgb565" : [ 19657, 17352, 10950, 10565, 11080, -29816, 25222, -21364 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.005.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 171,
|
||||
"last_modified" : 1776880624257,
|
||||
"fingerprint" : "451fc8d646049944d5aa7c7dc254636eec0151aaff6e2051059304b9d6ebc31e",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQIDBAQFBgYBAQIDBAYGBwEBAQMEBgcHAQEBBAQGBwcBAQQFBAYHBwEEBwUEBgcHBAcGBQQGBwcEBwYFBAYHBw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -14129088, -14133200, -14145496, -10465232, -5730208, -7835584 ],
|
||||
"convertedRgb565" : [ 19657, 11080, 10950, 10565, 25222, -21364, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.006.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 174,
|
||||
"last_modified" : 1776880647207,
|
||||
"fingerprint" : "aa757df4600823ec6655235a1081061777124ccee74b31481bbe402e53635f53",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAwMBAQEBAgIBAwMBAgICAQEBAwMCAgIBAQIDBAMCAgEBAgMEBAICAQECAwQEAgIBAQIDBAMCAgIBAwQDAg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ],
|
||||
"convertedRgb565" : [ -29816, -21364, 25222, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.007.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 171,
|
||||
"last_modified" : 1776880669446,
|
||||
"fingerprint" : "38162157f5685cce25b835d3a2caee3465fd2ec3ee5db2a5089d7d5ae5ea3bb7",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAgIDAQEDAQICAQMDAQMCAgEBAQMDAgQCAwEBAwMEBAIDAQEDAwQEAgMBAQMDAgQCAwEBAwMDAgQCAQMDAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -10465232, -5730208, -14145496 ],
|
||||
"convertedRgb565" : [ -29816, 25222, -21364, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.008.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 170,
|
||||
"last_modified" : 1776880691747,
|
||||
"fingerprint" : "8cab5b3d553af9ed371bd19fc624763accc90e5d926d300ba31dd8d9d6bab803",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAwMEBQYHAQEDBAUGBgcHAQMEBgYGBwcBAwMGBgYHBwEDAgMGBgcHAQMCBwMGBwcBAwIBBwMHBwEDAgEHAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -5730208, -10465232, -14145496, -14133200, -14129088, -12019640, -7835584 ],
|
||||
"convertedRgb565" : [ -21364, 25222, 10565, 10950, 11080, 19657, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.009.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 201,
|
||||
"last_modified" : 1776881019843,
|
||||
"fingerprint" : "d9ff5a6df0f65f5fcfb8f22376beeb266d429e2c6e9c82f51c12bc6ee4c3fcdc",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQIDAgIBBAMFAQECAgEEBAUGBQECAgEEBwYFBQECAQEHBwYGBQEBAgcHBwYFAQMDBwcHBwEBAwMHBwcFAQICAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -14145496, -10465232, -7835584, -5730208, -14133200, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ 10565, 25222, -29816, -21364, 10950, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.010.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 195,
|
||||
"last_modified" : 1776881039443,
|
||||
"fingerprint" : "b9ff45c77f7779a6acebcc537c2ddac0e27e91162b656a04dc4772ec5f24fc26",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQICAwMBAQIBAQMDAgIBAQIBAQEBAgIBAgICAgEBAgIEAwMCAgEBAQMEAwMCAgEBAQMEAwMDAgIBAwMEAwMDAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ],
|
||||
"convertedRgb565" : [ -29816, -21364, 25222, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.011.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 188,
|
||||
"last_modified" : 1776881059396,
|
||||
"fingerprint" : "43f370324fedccda888d49409e2a64f182f08b4143b4bdd5bafaf498feebd21a",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQICAwMBAQICAgEBAwMCAgIBAQICAgIBAQECAgEBAQECAgIBAQMDBAICAQEDAwQDAQEDAwMEAwIDAwMDBAMDAg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -5730208, -7835584, -10465232, -14145496 ],
|
||||
"convertedRgb565" : [ -21364, -29816, 25222, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.012.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 203,
|
||||
"last_modified" : 1776881081851,
|
||||
"fingerprint" : "cb047e655f4e196f22ca0048149df5dcb959f895faeaeb94920182d4a2d16b38",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQIDBAQBBAMCAgMEBAMDBQIDBAQDBQYFAwMEAwUFBgcEAwMFBgYHBwEBAwUGBwcHAQEDAwcHBwcBBAQDBQcHBw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -5730208, -14145496, -10465232, -14133200, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ -29816, -21364, 10565, 25222, 10950, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.013.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 186,
|
||||
"last_modified" : 1776881102592,
|
||||
"fingerprint" : "25eba17b67dd5159f5fb9d3edd531d54a44d1c76f997385840dac53517b98886",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAgMDBAQBAQIDAwMDBAEBAgMDAwMEAQECAwMEAgQBAQIEAgUFAgEBBQUGBQYFAQEBAQEGAQYBAQEBAQEBAQ=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -14145496, -7835584, -10465232, -14133200, -14129088 ],
|
||||
"convertedRgb565" : [ 19657, 10565, -29816, 25222, 10950, 11080 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.014.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 184,
|
||||
"last_modified" : 1776881122741,
|
||||
"fingerprint" : "32ca96c14290de8009fd2db623f55514bed2345b7d5881a80dd3fe6235012854",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAgEBAgICAQICAQIBAQICAwICAQEEAgMDAwIBAQQDAwUDAwIBAQUFBgUFAgEEBgYGBgYDAgEGBgYGBgYDAg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -10465232, -14145496, -14133200, -7835584, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ 25222, 10565, 10950, -29816, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.015.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 175,
|
||||
"last_modified" : 1776881143481,
|
||||
"fingerprint" : "7a093fe00c805bcf38d4facff14a6d4c2b8baeaa2d553fa8d3db9f2d76b32f3b",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAgECAgICAgECAQECAQMCAgEBBAEBAwICAQQEBAECAgEEBAUEBAMCAQUFBgUFAgEEBgYGBgYBBAYGBgYGBg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -14145496, -10465232, -7835584, -14133200, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ 10565, 25222, -29816, 10950, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.016.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 192,
|
||||
"last_modified" : 1776881170765,
|
||||
"fingerprint" : "77c5277220421226d16f58b1bcfb4eaf157fa4f6c740c5aaebefaf88fb91c22e",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAgMEBAQBAgICAgMEBAECAgICAwQEAQMBAgIDBAQDBQUDAQMEBAUGBQYFBQQEBgQGBAQEBAQEBAQEBAQEBA=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -10465232, -7835584, -14145496, -12019640, -14133200, -14129088 ],
|
||||
"convertedRgb565" : [ 25222, -29816, 10565, 19657, 10950, 11080 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"next_asset_id" : 16,
|
||||
"next_asset_id" : 18,
|
||||
"assets" : [ {
|
||||
"asset_id" : 3,
|
||||
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||
@ -41,5 +41,15 @@
|
||||
"asset_uuid" : "87396aab-337e-479e-b1f4-ec296678389e",
|
||||
"root" : "Zelda3",
|
||||
"included_in_build" : true
|
||||
}, {
|
||||
"asset_id" : 16,
|
||||
"asset_uuid" : "6f05bee1-f974-4b65-a43f-eacd46b8ec96",
|
||||
"root" : "tiled",
|
||||
"included_in_build" : false
|
||||
}, {
|
||||
"asset_id" : 17,
|
||||
"asset_uuid" : "c12f67ad-0471-4ab1-96dd-a816cfc66462",
|
||||
"root" : "tileset",
|
||||
"included_in_build" : false
|
||||
} ]
|
||||
}
|
||||
@ -25,27 +25,43 @@
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"file" : "link00.png",
|
||||
"index" : 0
|
||||
"index" : 0,
|
||||
"artifact_id" : "art-b82d5371eb9c",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"file" : "link01.png",
|
||||
"index" : 1
|
||||
"index" : 1,
|
||||
"artifact_id" : "art-acf4398779de",
|
||||
"fingerprint" : "e2130efbb9643b41a4fb62619f2713402f9622c7db040fe18d929609ef70b89a"
|
||||
}, {
|
||||
"file" : "link02.png",
|
||||
"index" : 2
|
||||
"index" : 2,
|
||||
"artifact_id" : "art-88d5de66ceea",
|
||||
"fingerprint" : "9216dfc6a6226fac398e9f781550d23fbfaa65bd377cdda69187149beca114ec"
|
||||
}, {
|
||||
"file" : "link03.png",
|
||||
"index" : 3
|
||||
"index" : 3,
|
||||
"artifact_id" : "art-18a3b61a9561",
|
||||
"fingerprint" : "ebc4b7ca7bb1455288681c5d71424f60f658a44cb343d0c3934329cec676c867"
|
||||
}, {
|
||||
"file" : "link04.png",
|
||||
"index" : 4
|
||||
"index" : 4,
|
||||
"artifact_id" : "art-231ecb85b204",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"file" : "link05.png",
|
||||
"index" : 5
|
||||
"index" : 5,
|
||||
"artifact_id" : "art-f36b8455db22",
|
||||
"fingerprint" : "1ba8ce75c445396334737143e0cfa56c87a1fb64cca8ad0962192594849249eb"
|
||||
}, {
|
||||
"file" : "link06.png",
|
||||
"index" : 6
|
||||
"index" : 6,
|
||||
"artifact_id" : "art-884d6b34d741",
|
||||
"fingerprint" : "5e0954447699c6d5eac7550f1a10cf520efcddad0b9fbd9a716434da9d9550d4"
|
||||
}, {
|
||||
"file" : "link07.png",
|
||||
"index" : 7
|
||||
"index" : 7,
|
||||
"artifact_id" : "art-ca5e39fb056c",
|
||||
"fingerprint" : "6104ba7f216a937994d478ed07766ec28f48ad72fdd6620508535a1f877b8ddb"
|
||||
} ]
|
||||
}
|
||||
47
test-projects/main/assets/Zelda3/glyph-material.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"schemas" : [ {
|
||||
"schema_id" : "schema-1",
|
||||
"status" : "ok",
|
||||
"version" : 1,
|
||||
"material_indices" : {
|
||||
"1" : "#202020",
|
||||
"2" : "#519210",
|
||||
"3" : "#71B220",
|
||||
"4" : "#F3C320",
|
||||
"5" : "#C38200",
|
||||
"6" : "#B26120",
|
||||
"7" : "#F3F3F3",
|
||||
"8" : "#F3A261",
|
||||
"9" : "#E39251",
|
||||
"10" : "#614120",
|
||||
"11" : "#B28251"
|
||||
},
|
||||
"artifact_bindings" : [ {
|
||||
"artifact_id" : "art-b82d5371eb9c",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"artifact_id" : "art-acf4398779de",
|
||||
"fingerprint" : "e2130efbb9643b41a4fb62619f2713402f9622c7db040fe18d929609ef70b89a"
|
||||
}, {
|
||||
"artifact_id" : "art-88d5de66ceea",
|
||||
"fingerprint" : "9216dfc6a6226fac398e9f781550d23fbfaa65bd377cdda69187149beca114ec"
|
||||
}, {
|
||||
"artifact_id" : "art-18a3b61a9561",
|
||||
"fingerprint" : "ebc4b7ca7bb1455288681c5d71424f60f658a44cb343d0c3934329cec676c867"
|
||||
}, {
|
||||
"artifact_id" : "art-231ecb85b204",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"artifact_id" : "art-f36b8455db22",
|
||||
"fingerprint" : "1ba8ce75c445396334737143e0cfa56c87a1fb64cca8ad0962192594849249eb"
|
||||
}, {
|
||||
"artifact_id" : "art-884d6b34d741",
|
||||
"fingerprint" : "5e0954447699c6d5eac7550f1a10cf520efcddad0b9fbd9a716434da9d9550d4"
|
||||
}, {
|
||||
"artifact_id" : "art-ca5e39fb056c",
|
||||
"fingerprint" : "6104ba7f216a937994d478ed07766ec28f48ad72fdd6620508535a1f877b8ddb"
|
||||
} ]
|
||||
} ],
|
||||
"palettes" : [ ]
|
||||
}
|
||||
4
test-projects/main/assets/Zelda3/studio.asset.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
4
test-projects/main/assets/bigode/studio.asset.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -16,6 +16,8 @@
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"file" : "confirm.png",
|
||||
"index" : 0
|
||||
"index" : 0,
|
||||
"artifact_id" : "art-8c6bdad24eb1",
|
||||
"fingerprint" : "aa7d241deabcebe29a6096e14eaf16fdc06cf06380c11a507620b00fc7bff094"
|
||||
} ]
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
14
test-projects/main/assets/tiled/asset.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"asset_uuid" : "6f05bee1-f974-4b65-a43f-eacd46b8ec96",
|
||||
"name" : "Tiled Test",
|
||||
"type" : "scene_bank",
|
||||
"output" : {
|
||||
"pipeline" : { },
|
||||
"codec" : "NONE",
|
||||
"format" : "SCENE/tilemap_v1"
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : false
|
||||
}
|
||||
}
|
||||
24
test-projects/main/assets/tiled/layer-1.tmx
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="8" tileheight="8" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../tileset/tileset.tsx"/>
|
||||
<layer id="1" name="Layer 1" width="16" height="16">
|
||||
<data encoding="csv">
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,
|
||||
1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
39
test-projects/main/assets/tiled/scene-bank.studio.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"map_width" : 50,
|
||||
"map_height" : 50,
|
||||
"layer_count" : 4,
|
||||
"layers" : [ {
|
||||
"index" : 1,
|
||||
"enabled" : true,
|
||||
"name" : "Layer 1",
|
||||
"width" : 50,
|
||||
"height" : 50,
|
||||
"tilemap" : "layer-1.tmx",
|
||||
"tileset_asset_root" : "tileset"
|
||||
}, {
|
||||
"index" : 2,
|
||||
"enabled" : false,
|
||||
"name" : "Layer 2",
|
||||
"width" : 16,
|
||||
"height" : 16,
|
||||
"tilemap" : "layer-2.tmx",
|
||||
"tileset_asset_root" : ""
|
||||
}, {
|
||||
"index" : 3,
|
||||
"enabled" : false,
|
||||
"name" : "Layer 3",
|
||||
"width" : 16,
|
||||
"height" : 16,
|
||||
"tilemap" : "layer-3.tmx",
|
||||
"tileset_asset_root" : ""
|
||||
}, {
|
||||
"index" : 4,
|
||||
"enabled" : false,
|
||||
"name" : "Layer 4",
|
||||
"width" : 16,
|
||||
"height" : 16,
|
||||
"tilemap" : "layer-4.tmx",
|
||||
"tileset_asset_root" : ""
|
||||
} ]
|
||||
}
|
||||
112
test-projects/main/assets/tileset/asset.json
Normal file
@ -0,0 +1,112 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"asset_uuid" : "c12f67ad-0471-4ab1-96dd-a816cfc66462",
|
||||
"name" : "tileset",
|
||||
"type" : "glyph_bank",
|
||||
"output" : {
|
||||
"codec" : "NONE",
|
||||
"pipeline" : {
|
||||
"palettes" : [ {
|
||||
"index" : 0,
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -12552128, -14129088, -14133200, -14145496, -7835584, -5730208, -10465232 ],
|
||||
"convertedRgb565" : [ 19657, 17352, 11080, 10950, 10565, 35720, 44172, 25222 ]
|
||||
}
|
||||
} ]
|
||||
},
|
||||
"format" : "GLYPH/indexed_v1",
|
||||
"codec_configuration" : { },
|
||||
"metadata" : {
|
||||
"tile_size" : "8x8"
|
||||
}
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : false
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"artifact_id" : "art-67db3cae2605",
|
||||
"file" : "tileset-001.png",
|
||||
"index" : 0,
|
||||
"fingerprint" : "8771c3e294d0edbc8dd3aaa6995a6db32166ae6bfb3e063170cc79b298d17e71"
|
||||
}, {
|
||||
"artifact_id" : "art-ee733c010b67",
|
||||
"file" : "tileset-002.001.png",
|
||||
"index" : 1,
|
||||
"fingerprint" : "5d81eef8be13043330b42b546e10beed8ba00dfaa42df67477a8778c2b9484d7"
|
||||
}, {
|
||||
"artifact_id" : "art-ab1efa5013f0",
|
||||
"file" : "tileset-002.002.png",
|
||||
"index" : 2,
|
||||
"fingerprint" : "3250840c4fe08c134ea966ce7c8fcaccae2c2a45660d7173c5e262a2e5076548"
|
||||
}, {
|
||||
"artifact_id" : "art-453fed2836ce",
|
||||
"file" : "tileset-002.003.png",
|
||||
"index" : 3,
|
||||
"fingerprint" : "fe3ab2b7eb3b10a042de14484d53e718e6b505edb1ad19053668565bfe3a8f25"
|
||||
}, {
|
||||
"artifact_id" : "art-52434bb77d67",
|
||||
"file" : "tileset-002.004.png",
|
||||
"index" : 4,
|
||||
"fingerprint" : "7fcbf90e1e9a69d6ce3be80ea1901d66c9992f68dc72e4c636dcd2ad52fa2898"
|
||||
}, {
|
||||
"artifact_id" : "art-edc19227b5c8",
|
||||
"file" : "tileset-002.005.png",
|
||||
"index" : 5,
|
||||
"fingerprint" : "451fc8d646049944d5aa7c7dc254636eec0151aaff6e2051059304b9d6ebc31e"
|
||||
}, {
|
||||
"artifact_id" : "art-a1131a305e06",
|
||||
"file" : "tileset-002.006.png",
|
||||
"index" : 6,
|
||||
"fingerprint" : "aa757df4600823ec6655235a1081061777124ccee74b31481bbe402e53635f53"
|
||||
}, {
|
||||
"artifact_id" : "art-e8a9cff3fb90",
|
||||
"file" : "tileset-002.007.png",
|
||||
"index" : 7,
|
||||
"fingerprint" : "38162157f5685cce25b835d3a2caee3465fd2ec3ee5db2a5089d7d5ae5ea3bb7"
|
||||
}, {
|
||||
"artifact_id" : "art-b1513b30e866",
|
||||
"file" : "tileset-002.008.png",
|
||||
"index" : 8,
|
||||
"fingerprint" : "8cab5b3d553af9ed371bd19fc624763accc90e5d926d300ba31dd8d9d6bab803"
|
||||
}, {
|
||||
"artifact_id" : "art-ff5ad76c4e5e",
|
||||
"file" : "tileset-002.009.png",
|
||||
"index" : 9,
|
||||
"fingerprint" : "d9ff5a6df0f65f5fcfb8f22376beeb266d429e2c6e9c82f51c12bc6ee4c3fcdc"
|
||||
}, {
|
||||
"artifact_id" : "art-704961ee43df",
|
||||
"file" : "tileset-002.010.png",
|
||||
"index" : 10,
|
||||
"fingerprint" : "b9ff45c77f7779a6acebcc537c2ddac0e27e91162b656a04dc4772ec5f24fc26"
|
||||
}, {
|
||||
"artifact_id" : "art-895e14841cc2",
|
||||
"file" : "tileset-002.011.png",
|
||||
"index" : 11,
|
||||
"fingerprint" : "43f370324fedccda888d49409e2a64f182f08b4143b4bdd5bafaf498feebd21a"
|
||||
}, {
|
||||
"artifact_id" : "art-21bacf6b47b9",
|
||||
"file" : "tileset-002.012.png",
|
||||
"index" : 12,
|
||||
"fingerprint" : "cb047e655f4e196f22ca0048149df5dcb959f895faeaeb94920182d4a2d16b38"
|
||||
}, {
|
||||
"artifact_id" : "art-4c80bb3fc4ba",
|
||||
"file" : "tileset-002.013.png",
|
||||
"index" : 13,
|
||||
"fingerprint" : "25eba17b67dd5159f5fb9d3edd531d54a44d1c76f997385840dac53517b98886"
|
||||
}, {
|
||||
"artifact_id" : "art-58970a0dffaf",
|
||||
"file" : "tileset-002.014.png",
|
||||
"index" : 14,
|
||||
"fingerprint" : "32ca96c14290de8009fd2db623f55514bed2345b7d5881a80dd3fe6235012854"
|
||||
}, {
|
||||
"artifact_id" : "art-8d83870d938b",
|
||||
"file" : "tileset-002.015.png",
|
||||
"index" : 15,
|
||||
"fingerprint" : "7a093fe00c805bcf38d4facff14a6d4c2b8baeaa2d553fa8d3db9f2d76b32f3b"
|
||||
}, {
|
||||
"artifact_id" : "art-b94f1f9218ea",
|
||||
"file" : "tileset-002.016.png",
|
||||
"index" : 16,
|
||||
"fingerprint" : "77c5277220421226d16f58b1bcfb4eaf157fa4f6c740c5aaebefaf88fb91c22e"
|
||||
} ]
|
||||
}
|
||||
76
test-projects/main/assets/tileset/glyph-material.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"schemas" : [ {
|
||||
"schema_id" : "schema-1",
|
||||
"status" : "ok",
|
||||
"version" : 5,
|
||||
"material_indices" : {
|
||||
"1" : "#489848",
|
||||
"2" : "#407840",
|
||||
"3" : "#286840",
|
||||
"4" : "#285830",
|
||||
"5" : "#282828",
|
||||
"6" : "#887040",
|
||||
"7" : "#A89060",
|
||||
"8" : "#605030"
|
||||
},
|
||||
"artifact_bindings" : [ {
|
||||
"artifact_id" : "art-67db3cae2605",
|
||||
"fingerprint" : "8771c3e294d0edbc8dd3aaa6995a6db32166ae6bfb3e063170cc79b298d17e71"
|
||||
}, {
|
||||
"artifact_id" : "art-ee733c010b67",
|
||||
"fingerprint" : "5d81eef8be13043330b42b546e10beed8ba00dfaa42df67477a8778c2b9484d7"
|
||||
}, {
|
||||
"artifact_id" : "art-ab1efa5013f0",
|
||||
"fingerprint" : "3250840c4fe08c134ea966ce7c8fcaccae2c2a45660d7173c5e262a2e5076548"
|
||||
}, {
|
||||
"artifact_id" : "art-453fed2836ce",
|
||||
"fingerprint" : "fe3ab2b7eb3b10a042de14484d53e718e6b505edb1ad19053668565bfe3a8f25"
|
||||
}, {
|
||||
"artifact_id" : "art-52434bb77d67",
|
||||
"fingerprint" : "7fcbf90e1e9a69d6ce3be80ea1901d66c9992f68dc72e4c636dcd2ad52fa2898"
|
||||
}, {
|
||||
"artifact_id" : "art-edc19227b5c8",
|
||||
"fingerprint" : "451fc8d646049944d5aa7c7dc254636eec0151aaff6e2051059304b9d6ebc31e"
|
||||
}, {
|
||||
"artifact_id" : "art-a1131a305e06",
|
||||
"fingerprint" : "aa757df4600823ec6655235a1081061777124ccee74b31481bbe402e53635f53"
|
||||
}, {
|
||||
"artifact_id" : "art-e8a9cff3fb90",
|
||||
"fingerprint" : "38162157f5685cce25b835d3a2caee3465fd2ec3ee5db2a5089d7d5ae5ea3bb7"
|
||||
}, {
|
||||
"artifact_id" : "art-b1513b30e866",
|
||||
"fingerprint" : "8cab5b3d553af9ed371bd19fc624763accc90e5d926d300ba31dd8d9d6bab803"
|
||||
}, {
|
||||
"artifact_id" : "art-ff5ad76c4e5e",
|
||||
"fingerprint" : "d9ff5a6df0f65f5fcfb8f22376beeb266d429e2c6e9c82f51c12bc6ee4c3fcdc"
|
||||
}, {
|
||||
"artifact_id" : "art-704961ee43df",
|
||||
"fingerprint" : "b9ff45c77f7779a6acebcc537c2ddac0e27e91162b656a04dc4772ec5f24fc26"
|
||||
}, {
|
||||
"artifact_id" : "art-895e14841cc2",
|
||||
"fingerprint" : "43f370324fedccda888d49409e2a64f182f08b4143b4bdd5bafaf498feebd21a"
|
||||
}, {
|
||||
"artifact_id" : "art-21bacf6b47b9",
|
||||
"fingerprint" : "cb047e655f4e196f22ca0048149df5dcb959f895faeaeb94920182d4a2d16b38"
|
||||
}, {
|
||||
"artifact_id" : "art-4c80bb3fc4ba",
|
||||
"fingerprint" : "25eba17b67dd5159f5fb9d3edd531d54a44d1c76f997385840dac53517b98886"
|
||||
}, {
|
||||
"artifact_id" : "art-58970a0dffaf",
|
||||
"fingerprint" : "32ca96c14290de8009fd2db623f55514bed2345b7d5881a80dd3fe6235012854"
|
||||
}, {
|
||||
"artifact_id" : "art-8d83870d938b",
|
||||
"fingerprint" : "7a093fe00c805bcf38d4facff14a6d4c2b8baeaa2d553fa8d3db9f2d76b32f3b"
|
||||
}, {
|
||||
"artifact_id" : "art-b94f1f9218ea",
|
||||
"fingerprint" : "77c5277220421226d16f58b1bcfb4eaf157fa4f6c740c5aaebefaf88fb91c22e"
|
||||
} ]
|
||||
} ],
|
||||
"palettes" : [ {
|
||||
"palette_id" : "schema-schema-1-original",
|
||||
"schema_id" : "schema-1",
|
||||
"version" : 5,
|
||||
"colors" : [ "#489848", "#407840", "#286840", "#285830", "#282828", "#887040", "#A89060", "#605030" ]
|
||||
} ]
|
||||
}
|
||||
4
test-projects/main/assets/tileset/studio.asset.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "tileset"
|
||||
}
|
||||
BIN
test-projects/main/assets/tileset/tileset-001.png
Normal file
|
After Width: | Height: | Size: 92 B |
BIN
test-projects/main/assets/tileset/tileset-002.001.png
Normal file
|
After Width: | Height: | Size: 184 B |
BIN
test-projects/main/assets/tileset/tileset-002.002.png
Normal file
|
After Width: | Height: | Size: 172 B |
BIN
test-projects/main/assets/tileset/tileset-002.003.png
Normal file
|
After Width: | Height: | Size: 191 B |
BIN
test-projects/main/assets/tileset/tileset-002.004.png
Normal file
|
After Width: | Height: | Size: 193 B |
BIN
test-projects/main/assets/tileset/tileset-002.005.png
Normal file
|
After Width: | Height: | Size: 171 B |
BIN
test-projects/main/assets/tileset/tileset-002.006.png
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
test-projects/main/assets/tileset/tileset-002.007.png
Normal file
|
After Width: | Height: | Size: 171 B |
BIN
test-projects/main/assets/tileset/tileset-002.008.png
Normal file
|
After Width: | Height: | Size: 170 B |
BIN
test-projects/main/assets/tileset/tileset-002.009.png
Normal file
|
After Width: | Height: | Size: 201 B |
BIN
test-projects/main/assets/tileset/tileset-002.010.png
Normal file
|
After Width: | Height: | Size: 195 B |
BIN
test-projects/main/assets/tileset/tileset-002.011.png
Normal file
|
After Width: | Height: | Size: 188 B |
BIN
test-projects/main/assets/tileset/tileset-002.012.png
Normal file
|
After Width: | Height: | Size: 203 B |
BIN
test-projects/main/assets/tileset/tileset-002.013.png
Normal file
|
After Width: | Height: | Size: 186 B |
BIN
test-projects/main/assets/tileset/tileset-002.014.png
Normal file
|
After Width: | Height: | Size: 184 B |
BIN
test-projects/main/assets/tileset/tileset-002.015.png
Normal file
|
After Width: | Height: | Size: 175 B |
BIN
test-projects/main/assets/tileset/tileset-002.016.png
Normal file
|
After Width: | Height: | Size: 192 B |
106
test-projects/main/assets/tileset/tileset.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<tileset columns="0" name="tileset" tilecount="17" tiledversion="1.12.1" tileheight="8" tilewidth="8" version="1.10">
|
||||
<grid height="1" orientation="orthogonal" width="1"/>
|
||||
<tile id="0">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="0"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-001.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="1">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="1"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.001.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="2">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="2"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.002.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="3">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="3"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.003.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="4">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="4"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.004.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="5">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="5"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.005.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="6">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="6"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.006.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="7">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="7"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.007.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="8">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="8"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.008.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="9">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="9"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.009.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="10">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="10"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.010.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="11">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="11"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.011.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="12">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="12"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.012.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="13">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="13"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.013.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="14">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="14"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.014.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="15">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="15"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.015.png" width="8"/>
|
||||
</tile>
|
||||
<tile id="16">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="16"/>
|
||||
</properties>
|
||||
<image height="8" source="tileset-002.016.png" width="8"/>
|
||||
</tile>
|
||||
</tileset>
|
||||
BIN
test-projects/main/assets/tileset/toco.aseprite
Normal file
@ -25,6 +25,8 @@
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"file" : "right-palette.png",
|
||||
"index" : 0
|
||||
"index" : 0,
|
||||
"artifact_id" : "art-92ac40fe2160",
|
||||
"fingerprint" : "0526996900bdeef23c72fe987fb45800f11895946dcd5f0f7f8390c879069a37"
|
||||
} ]
|
||||
}
|
||||
21
test-projects/main/assets/ui/atlas2/glyph-material.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"schemas" : [ {
|
||||
"schema_id" : "schema-1",
|
||||
"status" : "ok",
|
||||
"version" : 1,
|
||||
"material_indices" : {
|
||||
"1" : "#FBF236",
|
||||
"2" : "#E58198",
|
||||
"3" : "#2BD142",
|
||||
"4" : "#503DD7",
|
||||
"5" : "#23AB67",
|
||||
"6" : "#DA594B"
|
||||
},
|
||||
"artifact_bindings" : [ {
|
||||
"artifact_id" : "art-92ac40fe2160",
|
||||
"fingerprint" : "0526996900bdeef23c72fe987fb45800f11895946dcd5f0f7f8390c879069a37"
|
||||
} ]
|
||||
} ],
|
||||
"palettes" : [ ]
|
||||
}
|
||||
4
test-projects/main/assets/ui/atlas2/studio.asset.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||