dev/studio-tiled-parser-assets-scene-asset-type #6
@ -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-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-15","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[{"id":"AGD-0030","file":"AGD-0030-tiled-parser-and-scene-asset-type.md","status":"open","created_at":"2026-04-15","updated_at":"2026-04-15"}],"decisions":[],"plans":[],"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-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-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-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-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-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-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-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-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-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-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-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-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"}]}
|
{"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-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-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-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"}]}
|
{"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
|
ticket: studio-play-stop-cartridge-flow
|
||||||
title: Studio Play/Stop and Debugger Workspace Integration
|
title: Studio Play/Stop and Debugger Workspace Integration
|
||||||
status: done
|
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,115 +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: open
|
|
||||||
created: 2026-04-15
|
|
||||||
resolved:
|
|
||||||
decision:
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- [ ] Which `Tiled` surface is the primary input contract: TMX XML, JSON export, TSX, or a constrained subset of those?
|
|
||||||
- [ ] Is the new asset type a raw imported `Tiled` document, a normalized Studio-owned intermediate asset, or a published runtime-facing `SCENE` derivative?
|
|
||||||
- [ ] Should `Assets` own only creation/import and metadata management, or also the main editing/reimport flow for this new asset type?
|
|
||||||
- [ ] How should tileset references inside `Tiled` map onto existing glyph-bank assets and asset identity rules in Studio?
|
|
||||||
- [ ] What parts of `Tiled` are intentionally out of scope for the first wave: object layers, properties, flips, collisions, infinite maps, templates, Wang sets, animations?
|
|
||||||
- [ ] Does the parser need to preserve full round-trip fidelity with `Tiled`, or only import the supported subset into Studio-owned structures?
|
|
||||||
- [ ] What is the canonical failure model when a `Tiled` document references unsupported constructs or broken external assets?
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
### Option A - Import `Tiled` into a new Studio-owned scene asset type in `Assets`
|
|
||||||
- **Approach:** Introduce a new asset type in `Assets`; parse supported `Tiled` files into a Studio-owned asset model managed from that workspace.
|
|
||||||
- **Pro:** Aligns with the requested ownership shift, keeps authoring/import centered in `Assets`, and allows Studio to define a strict supported subset.
|
|
||||||
- **Con:** Requires defining a clean normalized asset contract and a mapping layer from `Tiled` concepts to Studio/runtime concepts.
|
|
||||||
- **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.
|
|
||||||
|
|
||||||
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 glyph banks and tilesets map without creating identity ambiguity;
|
|
||||||
4. how much of `Tiled` fidelity is preserved versus normalized away;
|
|
||||||
5. 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.
|
|
||||||
|
|
||||||
That makes Option A the current best fit for convergence, but it still needs the agenda to narrow the supported `Tiled` subset and the resulting asset contract before a new decision can be written.
|
|
||||||
|
|
||||||
## Resolution
|
|
||||||
|
|
||||||
Recommended next step:
|
|
||||||
|
|
||||||
- converge this agenda around a first-wave `Tiled` subset;
|
|
||||||
- define whether the new asset type is raw-imported or normalized;
|
|
||||||
- then write a replacement decision for asset ownership, parser boundary, and wave-1 scope.
|
|
||||||
@ -4,6 +4,7 @@ import java.util.Locale;
|
|||||||
|
|
||||||
public enum AssetFamilyCatalog {
|
public enum AssetFamilyCatalog {
|
||||||
GLYPH_BANK("glyph_bank"),
|
GLYPH_BANK("glyph_bank"),
|
||||||
|
SCENE_BANK("scene_bank"),
|
||||||
SOUND_BANK("sound_bank"),
|
SOUND_BANK("sound_bank"),
|
||||||
UNKNOWN("unknown");
|
UNKNOWN("unknown");
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import java.util.Locale;
|
|||||||
|
|
||||||
public enum OutputFormatCatalog {
|
public enum OutputFormatCatalog {
|
||||||
GLYPH_INDEXED_V1(AssetFamilyCatalog.GLYPH_BANK, "GLYPH/indexed_v1", "GLYPH/indexed_v1"),
|
GLYPH_INDEXED_V1(AssetFamilyCatalog.GLYPH_BANK, "GLYPH/indexed_v1", "GLYPH/indexed_v1"),
|
||||||
|
SCENE_TILED_V1(AssetFamilyCatalog.SCENE_BANK, "SCENE/tilemap_v1", "SCENE/tilemap_v1"),
|
||||||
SOUND_V1(AssetFamilyCatalog.SOUND_BANK, "SOUND/v1", "SOUND/v1"),
|
SOUND_V1(AssetFamilyCatalog.SOUND_BANK, "SOUND/v1", "SOUND/v1"),
|
||||||
UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown");
|
UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown");
|
||||||
|
|
||||||
@ -53,6 +54,9 @@ public enum OutputFormatCatalog {
|
|||||||
return UNKNOWN;
|
return UNKNOWN;
|
||||||
}
|
}
|
||||||
final String normalized = manifestValue.trim().toLowerCase(Locale.ROOT);
|
final String normalized = manifestValue.trim().toLowerCase(Locale.ROOT);
|
||||||
|
if ("scene/tiled_v1".equals(normalized)) {
|
||||||
|
return SCENE_TILED_V1;
|
||||||
|
}
|
||||||
if ("sound/bank_v1".equals(normalized)) {
|
if ("sound/bank_v1".equals(normalized)) {
|
||||||
return SOUND_V1;
|
return SOUND_V1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,15 @@ package p.packer.models;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public record PackerAssetArtifactSelection(
|
public record PackerAssetArtifactSelection(
|
||||||
|
String artifactId,
|
||||||
String file,
|
String file,
|
||||||
int index) {
|
int index,
|
||||||
|
String fingerprint) {
|
||||||
|
|
||||||
public PackerAssetArtifactSelection {
|
public PackerAssetArtifactSelection {
|
||||||
|
artifactId = artifactId == null || artifactId.isBlank() ? null : artifactId.trim();
|
||||||
file = Objects.requireNonNull(file, "file").trim();
|
file = Objects.requireNonNull(file, "file").trim();
|
||||||
|
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint.trim();
|
||||||
if (file.isBlank()) {
|
if (file.isBlank()) {
|
||||||
throw new IllegalArgumentException("file must not be blank");
|
throw new IllegalArgumentException("file must not be blank");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,9 @@ public class PackerAssetWalker {
|
|||||||
diagnostics.addAll(walkResult.diagnostics());
|
diagnostics.addAll(walkResult.diagnostics());
|
||||||
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
||||||
}
|
}
|
||||||
|
case SCENE_BANK -> {
|
||||||
|
return new PackerWalkResult(List.of(), diagnostics);
|
||||||
|
}
|
||||||
case UNKNOWN -> {
|
case UNKNOWN -> {
|
||||||
diagnostics.add(new PackerDiagnostic(
|
diagnostics.add(new PackerDiagnostic(
|
||||||
PackerDiagnosticSeverity.WARNING,
|
PackerDiagnosticSeverity.WARNING,
|
||||||
|
|||||||
@ -8,6 +8,10 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public final class PackerRuntimeAssetMaterializer {
|
public final class PackerRuntimeAssetMaterializer {
|
||||||
|
private static final Set<String> RUNTIME_RESERVED_FILE_NAMES = Set.of(
|
||||||
|
"asset.json",
|
||||||
|
"studio.asset.json");
|
||||||
|
|
||||||
private final PackerAssetWalker assetWalker;
|
private final PackerAssetWalker assetWalker;
|
||||||
|
|
||||||
public PackerRuntimeAssetMaterializer(PackerAssetWalker assetWalker) {
|
public PackerRuntimeAssetMaterializer(PackerAssetWalker assetWalker) {
|
||||||
@ -114,7 +118,7 @@ public final class PackerRuntimeAssetMaterializer {
|
|||||||
private List<String> listAvailableFiles(Path assetRoot) {
|
private List<String> listAvailableFiles(Path assetRoot) {
|
||||||
try (var paths = Files.list(assetRoot)
|
try (var paths = Files.list(assetRoot)
|
||||||
.filter(Files::isRegularFile)
|
.filter(Files::isRegularFile)
|
||||||
.filter(path -> !path.getFileName().toString().equalsIgnoreCase("asset.json"))
|
.filter(path -> !isRuntimeReservedFile(path))
|
||||||
.map(path -> assetRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'))
|
.map(path -> assetRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'))
|
||||||
.sorted(String.CASE_INSENSITIVE_ORDER)) {
|
.sorted(String.CASE_INSENSITIVE_ORDER)) {
|
||||||
return paths.toList();
|
return paths.toList();
|
||||||
@ -123,6 +127,10 @@ public final class PackerRuntimeAssetMaterializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isRuntimeReservedFile(Path path) {
|
||||||
|
return RUNTIME_RESERVED_FILE_NAMES.contains(path.getFileName().toString().toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
private PackerRuntimeWalkFile toRuntimeWalkFile(
|
private PackerRuntimeWalkFile toRuntimeWalkFile(
|
||||||
Path assetRoot,
|
Path assetRoot,
|
||||||
PackerProbeResult probeResult,
|
PackerProbeResult probeResult,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import p.packer.PackerWorkspacePaths;
|
import p.packer.PackerWorkspacePaths;
|
||||||
import p.packer.PackerWorkspaceService;
|
import p.packer.PackerWorkspaceService;
|
||||||
import p.packer.events.PackerEventKind;
|
import p.packer.events.PackerEventKind;
|
||||||
@ -38,6 +39,7 @@ import java.util.stream.Stream;
|
|||||||
|
|
||||||
public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
|
public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
|
||||||
private static final int GLYPH_BANK_COLOR_KEY_RGB565 = 0xF81F;
|
private static final int GLYPH_BANK_COLOR_KEY_RGB565 = 0xF81F;
|
||||||
|
private static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
|
||||||
|
|
||||||
private final ObjectMapper mapper;
|
private final ObjectMapper mapper;
|
||||||
private final PackerWorkspaceFoundation workspaceFoundation;
|
private final PackerWorkspaceFoundation workspaceFoundation;
|
||||||
@ -48,6 +50,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
private final FileSystemPackerCacheRepository cacheRepository;
|
private final FileSystemPackerCacheRepository cacheRepository;
|
||||||
private final PackerProjectWriteCoordinator writeCoordinator;
|
private final PackerProjectWriteCoordinator writeCoordinator;
|
||||||
private final PackerEventSink eventSink;
|
private final PackerEventSink eventSink;
|
||||||
|
private final PackerGlyphMaterialSupport glyphMaterialSupport;
|
||||||
|
|
||||||
public FileSystemPackerWorkspaceService(
|
public FileSystemPackerWorkspaceService(
|
||||||
ObjectMapper mapper,
|
ObjectMapper mapper,
|
||||||
@ -68,6 +71,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
|
this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
|
||||||
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
|
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
|
||||||
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
|
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
|
||||||
|
this.glyphMaterialSupport = new PackerGlyphMaterialSupport(mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -81,7 +85,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
public ListAssetsResult listAssets(ListAssetsRequest request) {
|
public ListAssetsResult listAssets(ListAssetsRequest request) {
|
||||||
final var project = Objects.requireNonNull(request, "request").project();
|
final var project = Objects.requireNonNull(request, "request").project();
|
||||||
final var snapshot = request.deepSync()
|
final var snapshot = request.deepSync()
|
||||||
? runtimeRegistry.refresh(project).snapshot()
|
? writeCoordinator.execute(project, () -> refreshAfterArtifactIdentityBackfill(project))
|
||||||
: runtimeRegistry.getOrLoad(project).snapshot();
|
: runtimeRegistry.getOrLoad(project).snapshot();
|
||||||
final var events = new PackerOperationEventEmitter(project, eventSink);
|
final var events = new PackerOperationEventEmitter(project, eventSink);
|
||||||
final var registry = snapshot.registry();
|
final var registry = snapshot.registry();
|
||||||
@ -145,7 +149,11 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) {
|
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
|
@Override
|
||||||
@ -171,7 +179,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
@Override
|
@Override
|
||||||
public ValidatePackWorkspaceResult validatePackWorkspace(ValidatePackWorkspaceRequest request) {
|
public ValidatePackWorkspaceResult validatePackWorkspace(ValidatePackWorkspaceRequest request) {
|
||||||
final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
|
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()
|
final List<PackerPackValidationAssetDTO> blockedAssets = snapshot.assets().stream()
|
||||||
.filter(this::isEffectivelyIncludedInPackSet)
|
.filter(this::isEffectivelyIncludedInPackSet)
|
||||||
.map(this::toPackValidationAssetDTO)
|
.map(this::toPackValidationAssetDTO)
|
||||||
@ -287,6 +295,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
|
final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
|
||||||
Files.createDirectories(assetRoot);
|
Files.createDirectories(assetRoot);
|
||||||
writeManifest(manifestPath, request, entry.assetUuid());
|
writeManifest(manifestPath, request, entry.assetUuid());
|
||||||
|
writeStudioSupportFiles(assetRoot, request);
|
||||||
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
|
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
|
||||||
workspaceFoundation.saveRegistry(project, updated);
|
workspaceFoundation.saveRegistry(project, updated);
|
||||||
final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
|
final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
|
||||||
@ -320,7 +329,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
final long startedAt = System.currentTimeMillis();
|
final long startedAt = System.currentTimeMillis();
|
||||||
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
|
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
|
||||||
|
|
||||||
final PackerRuntimeSnapshot refreshedSnapshot = runtimeRegistry.refresh(project).snapshot();
|
final PackerRuntimeSnapshot refreshedSnapshot = refreshAfterArtifactIdentityBackfill(project);
|
||||||
final PackerRuntimeSnapshot frozenSnapshot = createFrozenPackingSnapshot(refreshedSnapshot);
|
final PackerRuntimeSnapshot frozenSnapshot = createFrozenPackingSnapshot(refreshedSnapshot);
|
||||||
final List<PackerPackValidationAssetDTO> blockedAssets = frozenSnapshot.assets().stream()
|
final List<PackerPackValidationAssetDTO> blockedAssets = frozenSnapshot.assets().stream()
|
||||||
.filter(this::isEffectivelyIncludedInPackSet)
|
.filter(this::isEffectivelyIncludedInPackSet)
|
||||||
@ -498,6 +507,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 currentEntry = evaluation.resolved().registryEntry().orElseThrow();
|
||||||
final PackerRegistryEntry updatedEntry = new PackerRegistryEntry(
|
final PackerRegistryEntry updatedEntry = new PackerRegistryEntry(
|
||||||
currentEntry.assetId(),
|
currentEntry.assetId(),
|
||||||
@ -667,6 +682,57 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void writeStudioSupportFiles(Path assetRoot, CreateAssetRequest request) throws IOException {
|
||||||
|
if (request.assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Map<String, Object> supportFile = new LinkedHashMap<>();
|
||||||
|
supportFile.put("schema_version", 1);
|
||||||
|
supportFile.put("map_width", 16);
|
||||||
|
supportFile.put("map_height", 16);
|
||||||
|
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) {
|
private String normalizeRelativeAssetRoot(String candidate) {
|
||||||
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
||||||
if (raw.isBlank()) {
|
if (raw.isBlank()) {
|
||||||
@ -781,6 +847,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
parsed.diagnostics(),
|
parsed.diagnostics(),
|
||||||
runtimeAsset.walkDiagnostics(),
|
runtimeAsset.walkDiagnostics(),
|
||||||
collectPackRelevantWalkFileDiagnostics(runtimeAsset),
|
collectPackRelevantWalkFileDiagnostics(runtimeAsset),
|
||||||
|
glyphMaterialSupport.validate(runtimeAsset),
|
||||||
packExecutionSupportDiagnostics(parsed.declaration(), runtimeAsset.manifestPath()),
|
packExecutionSupportDiagnostics(parsed.declaration(), runtimeAsset.manifestPath()),
|
||||||
identityMismatchDiagnostics(registryEntry, parsed, runtimeAsset.manifestPath()))
|
identityMismatchDiagnostics(registryEntry, parsed, runtimeAsset.manifestPath()))
|
||||||
.flatMap(Collection::stream)
|
.flatMap(Collection::stream)
|
||||||
@ -831,6 +898,84 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
cacheRepository.save(project, snapshot.cacheState());
|
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) {
|
private PackerRuntimeSnapshot createFrozenPackingSnapshot(PackerRuntimeSnapshot snapshot) {
|
||||||
final PackerRuntimeAssetMaterializer materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(mapper));
|
final PackerRuntimeAssetMaterializer materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(mapper));
|
||||||
final List<PackerRuntimeAsset> assets = snapshot.assets().stream()
|
final List<PackerRuntimeAsset> assets = snapshot.assets().stream()
|
||||||
@ -1256,7 +1401,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
patchManifestArtifacts(manifest, request);
|
patchManifestArtifacts(manifest, request, evaluation.resolved().runtimeAsset());
|
||||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||||
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
|
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
|
||||||
currentSnapshot,
|
currentSnapshot,
|
||||||
@ -1367,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");
|
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");
|
final var artifactsNode = manifest.putArray("artifacts");
|
||||||
for (int index = 0; index < request.selectedFiles().size(); index += 1) {
|
for (int index = 0; index < request.selectedFiles().size(); index += 1) {
|
||||||
final String file = Objects.requireNonNullElse(request.selectedFiles().get(index), "").trim();
|
final String file = Objects.requireNonNullElse(request.selectedFiles().get(index), "").trim();
|
||||||
if (file.isBlank() || !isTrustedRelativePath(file)) {
|
if (file.isBlank() || !isTrustedRelativePath(file)) {
|
||||||
throw new IllegalArgumentException("Selected artifact file paths must stay inside the asset root.");
|
throw new IllegalArgumentException("Selected artifact file paths must stay inside the asset root.");
|
||||||
}
|
}
|
||||||
|
final ExistingArtifactEntry existing = existingArtifactsByFile.get(file);
|
||||||
final var artifactNode = artifactsNode.addObject();
|
final var artifactNode = artifactsNode.addObject();
|
||||||
|
artifactNode.put("artifact_id", existing != null && existing.artifactId() != null ? existing.artifactId() : nextArtifactId());
|
||||||
artifactNode.put("file", file);
|
artifactNode.put("file", file);
|
||||||
artifactNode.put("index", index);
|
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) {
|
private void patchManifestPipelinePalettes(ObjectNode manifest, ApplyPaletteOverhaulingRequest request) {
|
||||||
final ObjectNode outputNode = mutableObject(manifest, "output");
|
final ObjectNode outputNode = mutableObject(manifest, "output");
|
||||||
final ObjectNode pipelineNode = mutableObject(outputNode, "pipeline");
|
final ObjectNode pipelineNode = mutableObject(outputNode, "pipeline");
|
||||||
|
|||||||
@ -121,7 +121,7 @@ public final class PackerAssetDeclarationParser {
|
|||||||
diagnostics.add(new PackerDiagnostic(
|
diagnostics.add(new PackerDiagnostic(
|
||||||
PackerDiagnosticSeverity.ERROR,
|
PackerDiagnosticSeverity.ERROR,
|
||||||
PackerDiagnosticCategory.STRUCTURAL,
|
PackerDiagnosticCategory.STRUCTURAL,
|
||||||
"Field 'type' must be one of: glyph_bank, palette_bank, sound_bank.",
|
"Field 'type' must be one of: glyph_bank, scene_bank, sound_bank.",
|
||||||
manifestPath,
|
manifestPath,
|
||||||
true));
|
true));
|
||||||
return null;
|
return null;
|
||||||
@ -404,8 +404,10 @@ public final class PackerAssetDeclarationParser {
|
|||||||
true));
|
true));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
final JsonNode artifactIdNode = entry.path("artifact_id");
|
||||||
final JsonNode fileNode = entry.path("file");
|
final JsonNode fileNode = entry.path("file");
|
||||||
final JsonNode indexNode = entry.path("index");
|
final JsonNode indexNode = entry.path("index");
|
||||||
|
final JsonNode fingerprintNode = entry.path("fingerprint");
|
||||||
if (!fileNode.isTextual() || fileNode.asText().isBlank() || !indexNode.isInt() || indexNode.asInt() < 0) {
|
if (!fileNode.isTextual() || fileNode.asText().isBlank() || !indexNode.isInt() || indexNode.asInt() < 0) {
|
||||||
diagnostics.add(new PackerDiagnostic(
|
diagnostics.add(new PackerDiagnostic(
|
||||||
PackerDiagnosticSeverity.ERROR,
|
PackerDiagnosticSeverity.ERROR,
|
||||||
@ -425,7 +427,13 @@ public final class PackerAssetDeclarationParser {
|
|||||||
true));
|
true));
|
||||||
continue;
|
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));
|
result.sort(Comparator.comparingInt(PackerAssetArtifactSelection::index));
|
||||||
return List.copyOf(result);
|
return List.copyOf(result);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package p.packer.services;
|
package p.packer.services;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
@ -16,6 +17,8 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public final class PackerAssetDetailsService {
|
public final class PackerAssetDetailsService {
|
||||||
|
private static final ObjectMapper GLYPH_MATERIAL_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
private final PackerRuntimeRegistry runtimeRegistry;
|
private final PackerRuntimeRegistry runtimeRegistry;
|
||||||
private final PackerAssetReferenceResolver assetReferenceResolver;
|
private final PackerAssetReferenceResolver assetReferenceResolver;
|
||||||
|
|
||||||
@ -48,6 +51,7 @@ public final class PackerAssetDetailsService {
|
|||||||
diagnostics.addAll(parsed.diagnostics());
|
diagnostics.addAll(parsed.diagnostics());
|
||||||
diagnostics.addAll(runtimeAsset.walkDiagnostics());
|
diagnostics.addAll(runtimeAsset.walkDiagnostics());
|
||||||
appendWalkFileDiagnostics(runtimeAsset, diagnostics);
|
appendWalkFileDiagnostics(runtimeAsset, diagnostics);
|
||||||
|
diagnostics.addAll(new PackerGlyphMaterialSupport(GLYPH_MATERIAL_MAPPER).validate(runtimeAsset));
|
||||||
if (!parsed.valid()) {
|
if (!parsed.valid()) {
|
||||||
return failureResult(project, request.assetReference(), resolved, diagnostics);
|
return failureResult(project, request.assetReference(), resolved, diagnostics);
|
||||||
}
|
}
|
||||||
@ -149,8 +153,9 @@ public final class PackerAssetDetailsService {
|
|||||||
.map(this::toBankCompositionFile)
|
.map(this::toBankCompositionFile)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final List<PackerBankCompositionFile> selectedFiles = selectedPathsFor(declaration).stream()
|
final List<PackerBankCompositionFile> selectedFiles = declaration.artifacts().stream()
|
||||||
.map(path -> resolveSelectedBankFile(runtimeAsset.assetRoot(), path, walkFilesByPath, assetCacheEntry))
|
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
|
||||||
|
.map(artifact -> resolveSelectedBankFile(runtimeAsset.assetRoot(), artifact, walkFilesByPath, assetCacheEntry))
|
||||||
.flatMap(Optional::stream)
|
.flatMap(Optional::stream)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -160,13 +165,6 @@ public final class PackerAssetDetailsService {
|
|||||||
walkProjection.measuredBankSizeBytes());
|
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) {
|
private List<Map<String, Object>> resolvePipelinePalettes(PackerAssetDeclaration declaration) {
|
||||||
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
|
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
|
||||||
if (!(palettesNode instanceof ArrayNode palettesArray)) {
|
if (!(palettesNode instanceof ArrayNode palettesArray)) {
|
||||||
@ -219,16 +217,17 @@ public final class PackerAssetDetailsService {
|
|||||||
|
|
||||||
private Optional<PackerBankCompositionFile> resolveSelectedBankFile(
|
private Optional<PackerBankCompositionFile> resolveSelectedBankFile(
|
||||||
Path assetRoot,
|
Path assetRoot,
|
||||||
String relativePath,
|
PackerAssetArtifactSelection artifact,
|
||||||
Map<String, PackerRuntimeWalkFile> walkFilesByPath,
|
Map<String, PackerRuntimeWalkFile> walkFilesByPath,
|
||||||
Optional<PackerAssetCacheEntry> assetCacheEntry) {
|
Optional<PackerAssetCacheEntry> assetCacheEntry) {
|
||||||
|
final String relativePath = artifact.file();
|
||||||
final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath);
|
final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath);
|
||||||
if (runtimeWalkFile != null) {
|
if (runtimeWalkFile != null) {
|
||||||
return Optional.of(toBankCompositionFile(runtimeWalkFile));
|
return Optional.of(toSelectedBankCompositionFile(runtimeWalkFile, artifact));
|
||||||
}
|
}
|
||||||
final Optional<PackerFileCacheEntry> cachedFile = assetCacheEntry.flatMap(entry -> entry.findFile(relativePath));
|
final Optional<PackerFileCacheEntry> cachedFile = assetCacheEntry.flatMap(entry -> entry.findFile(relativePath));
|
||||||
if (cachedFile.isPresent()) {
|
if (cachedFile.isPresent()) {
|
||||||
return Optional.of(toBankCompositionFile(cachedFile.get()));
|
return Optional.of(toSelectedBankCompositionFile(cachedFile.get(), artifact));
|
||||||
}
|
}
|
||||||
|
|
||||||
final Path filePath = assetRoot.resolve(relativePath).toAbsolutePath().normalize();
|
final Path filePath = assetRoot.resolve(relativePath).toAbsolutePath().normalize();
|
||||||
@ -242,8 +241,8 @@ public final class PackerAssetDetailsService {
|
|||||||
filePath.getFileName().toString(),
|
filePath.getFileName().toString(),
|
||||||
Files.size(filePath),
|
Files.size(filePath),
|
||||||
Files.getLastModifiedTime(filePath).toMillis(),
|
Files.getLastModifiedTime(filePath).toMillis(),
|
||||||
null,
|
artifact.fingerprint(),
|
||||||
Map.of()));
|
selectedArtifactMetadata(artifact, Map.of())));
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@ -269,6 +268,43 @@ public final class PackerAssetDetailsService {
|
|||||||
file.metadata());
|
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(
|
private List<PackerCodecConfigurationField> metadataFields(
|
||||||
List<PackerCodecConfigurationField> definitions,
|
List<PackerCodecConfigurationField> definitions,
|
||||||
Map<String, String> outputMetadata) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -76,12 +76,13 @@ final class PackerRuntimeAssetMaterializerTest {
|
|||||||
final Path assetRoot = Files.createDirectories(tempDir.resolve("runtime-default"));
|
final Path assetRoot = Files.createDirectories(tempDir.resolve("runtime-default"));
|
||||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||||
Files.writeString(manifestPath, "{}");
|
Files.writeString(manifestPath, "{}");
|
||||||
|
Files.writeString(assetRoot.resolve("studio.asset.json"), "{}");
|
||||||
writeTile(assetRoot.resolve("selected.png"), 16);
|
writeTile(assetRoot.resolve("selected.png"), 16);
|
||||||
writeTile(assetRoot.resolve("extra.png"), 16);
|
writeTile(assetRoot.resolve("extra.png"), 16);
|
||||||
|
|
||||||
final PackerAssetDeclaration declaration = declaration(
|
final PackerAssetDeclaration declaration = declaration(
|
||||||
Map.of("tile_size", "16x16"),
|
Map.of("tile_size", "16x16"),
|
||||||
List.of(new PackerAssetArtifactSelection("selected.png", 0)),
|
List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
|
||||||
palettePipeline());
|
palettePipeline());
|
||||||
final var materialized = materializer.materialize(
|
final var materialized = materializer.materialize(
|
||||||
assetRoot,
|
assetRoot,
|
||||||
@ -91,6 +92,7 @@ final class PackerRuntimeAssetMaterializerTest {
|
|||||||
Optional.empty());
|
Optional.empty());
|
||||||
|
|
||||||
assertEquals(2, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size());
|
assertEquals(2, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size());
|
||||||
|
assertEquals(List.of("extra.png", "selected.png"), materialized.runtimeAsset().walkProjection().availableFiles());
|
||||||
assertTrue(materialized.runtimeAsset().walkProjection().buildCandidateFiles().stream()
|
assertTrue(materialized.runtimeAsset().walkProjection().buildCandidateFiles().stream()
|
||||||
.allMatch(file -> file.contentBytes().isEmpty()));
|
.allMatch(file -> file.contentBytes().isEmpty()));
|
||||||
}
|
}
|
||||||
@ -106,7 +108,7 @@ final class PackerRuntimeAssetMaterializerTest {
|
|||||||
|
|
||||||
final PackerAssetDeclaration declaration = declaration(
|
final PackerAssetDeclaration declaration = declaration(
|
||||||
Map.of("tile_size", "16x16"),
|
Map.of("tile_size", "16x16"),
|
||||||
List.of(new PackerAssetArtifactSelection("selected.png", 0)),
|
List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
|
||||||
palettePipeline());
|
palettePipeline());
|
||||||
final var materialized = materializer.materialize(
|
final var materialized = materializer.materialize(
|
||||||
assetRoot,
|
assetRoot,
|
||||||
|
|||||||
@ -512,6 +512,35 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
|
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsSceneBankAssetAndWritesStudioSupportFile() throws Exception {
|
||||||
|
final Path projectRoot = tempDir.resolve("created-scene-bank");
|
||||||
|
final FileSystemPackerWorkspaceService service = service();
|
||||||
|
|
||||||
|
final var result = service.createAsset(new CreateAssetRequest(
|
||||||
|
project(projectRoot),
|
||||||
|
"scenes/overworld",
|
||||||
|
"overworld",
|
||||||
|
AssetFamilyCatalog.SCENE_BANK,
|
||||||
|
OutputFormatCatalog.SCENE_TILED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
false));
|
||||||
|
|
||||||
|
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||||
|
final Path assetRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||||
|
assertTrue(Files.isRegularFile(assetRoot.resolve("asset.json")));
|
||||||
|
assertTrue(Files.isRegularFile(assetRoot.resolve("scene-bank.studio.json")));
|
||||||
|
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(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());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception {
|
void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception {
|
||||||
final Path projectRoot = tempDir.resolve("created-details");
|
final Path projectRoot = tempDir.resolve("created-details");
|
||||||
@ -696,6 +725,31 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
assertFalse(registry.path("assets").get(0).path("included_in_build").asBoolean(true));
|
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
|
@Test
|
||||||
void updateAssetContractDropsPersistedBankCompositionWhenContractFingerprintChanges() throws Exception {
|
void updateAssetContractDropsPersistedBankCompositionWhenContractFingerprintChanges() throws Exception {
|
||||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-drops-bank"));
|
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-drops-bank"));
|
||||||
@ -734,8 +788,8 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
void applyBankCompositionWritesArtifactsAndRefreshesSnapshotWithoutWholeProjectReload() throws Exception {
|
void applyBankCompositionWritesArtifactsAndRefreshesSnapshotWithoutWholeProjectReload() throws Exception {
|
||||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-bank-composition"));
|
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-bank-composition"));
|
||||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||||
Files.writeString(assetRoot.resolve("confirm.png"), "fixture");
|
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||||
Files.writeString(assetRoot.resolve("cancel.png"), "fixture");
|
writeTilePng(assetRoot.resolve("cancel.png"), 16);
|
||||||
final CountingLoader loader = countingLoader();
|
final CountingLoader loader = countingLoader();
|
||||||
final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader);
|
final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader);
|
||||||
|
|
||||||
@ -753,10 +807,14 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
|
|
||||||
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||||
assertTrue(manifest.path("inputs").isMissingNode());
|
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("cancel.png", manifest.path("artifacts").get(0).path("file").asText());
|
||||||
assertEquals(0, manifest.path("artifacts").get(0).path("index").asInt());
|
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("confirm.png", manifest.path("artifacts").get(1).path("file").asText());
|
||||||
assertEquals(1, manifest.path("artifacts").get(1).path("index").asInt());
|
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(
|
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
|
||||||
project(projectRoot),
|
project(projectRoot),
|
||||||
@ -767,9 +825,154 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
assertEquals(
|
assertEquals(
|
||||||
List.of("cancel.png", "confirm.png"),
|
List.of("cancel.png", "confirm.png"),
|
||||||
detailsResult.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList());
|
detailsResult.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList());
|
||||||
|
assertNotNull(detailsResult.details().bankComposition().selectedFiles().getFirst().metadata().get("artifactId"));
|
||||||
assertEquals(1, loader.loadCount());
|
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
|
@Test
|
||||||
void applyPaletteOverhaulingWritesPipelinePalettesInOrder() throws Exception {
|
void applyPaletteOverhaulingWritesPipelinePalettesInOrder() throws Exception {
|
||||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-palette-overhauling"));
|
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-palette-overhauling"));
|
||||||
@ -1350,11 +1553,15 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void writeTilePng(Path path, int tileSize) throws Exception {
|
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());
|
Files.createDirectories(path.getParent());
|
||||||
final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
|
final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
|
||||||
for (int y = 0; y < tileSize; y += 1) {
|
for (int y = 0; y < tileSize; y += 1) {
|
||||||
for (int x = 0; x < tileSize; x += 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());
|
ImageIO.write(image, "png", path.toFile());
|
||||||
@ -1371,6 +1578,10 @@ final class FileSystemPackerWorkspaceServiceTest {
|
|||||||
ImageIO.write(image, "png", path.toFile());
|
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) {
|
private static int readLeInt(byte[] bytes, int offset) {
|
||||||
return (bytes[offset] & 0xFF)
|
return (bytes[offset] & 0xFF)
|
||||||
| ((bytes[offset + 1] & 0xFF) << 8)
|
| ((bytes[offset + 1] & 0xFF) << 8)
|
||||||
|
|||||||
@ -67,6 +67,56 @@ final class PackerAssetDeclarationParserTest {
|
|||||||
assertEquals(128, result.declaration().outputPipelineMetadata().get("samples").path("1").path("length").asInt());
|
assertEquals(128, result.declaration().outputPipelineMetadata().get("samples").path("1").path("length").asInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesSceneBankDeclaration() throws Exception {
|
||||||
|
final Path manifest = tempDir.resolve("scene-asset.json");
|
||||||
|
Files.writeString(manifest, """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"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
|
@Test
|
||||||
void rejectsNonObjectPipelineMetadata() throws Exception {
|
void rejectsNonObjectPipelineMetadata() throws Exception {
|
||||||
final Path manifest = tempDir.resolve("asset.json");
|
final Path manifest = tempDir.resolve("asset.json");
|
||||||
|
|||||||
@ -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.collections.ObservableList;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
@ -35,6 +36,8 @@ public abstract class StudioDualListView<T> extends HBox {
|
|||||||
private Consumer<List<T>> onMoveToLeft = ignored -> { };
|
private Consumer<List<T>> onMoveToLeft = ignored -> { };
|
||||||
private Consumer<T> onLeftSelectionChanged = ignored -> { };
|
private Consumer<T> onLeftSelectionChanged = ignored -> { };
|
||||||
private Consumer<T> onRightSelectionChanged = ignored -> { };
|
private Consumer<T> onRightSelectionChanged = ignored -> { };
|
||||||
|
private Consumer<T> onLeftItemActivated = ignored -> { };
|
||||||
|
private Consumer<T> onRightItemActivated = ignored -> { };
|
||||||
private IntConsumer onMoveUp = ignored -> { };
|
private IntConsumer onMoveUp = ignored -> { };
|
||||||
private IntConsumer onMoveDown = ignored -> { };
|
private IntConsumer onMoveDown = ignored -> { };
|
||||||
private boolean interactionEnabled = true;
|
private boolean interactionEnabled = true;
|
||||||
@ -145,6 +148,14 @@ public abstract class StudioDualListView<T> extends HBox {
|
|||||||
this.onRightSelectionChanged = Objects.requireNonNull(onRightSelectionChanged, "onRightSelectionChanged");
|
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) {
|
public void setOnMoveUp(IntConsumer onMoveUp) {
|
||||||
this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
|
this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
|
||||||
}
|
}
|
||||||
@ -188,6 +199,19 @@ public abstract class StudioDualListView<T> extends HBox {
|
|||||||
|
|
||||||
private ListCell<T> createCell(boolean indexed) {
|
private ListCell<T> createCell(boolean indexed) {
|
||||||
return new ListCell<>() {
|
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
|
@Override
|
||||||
protected void updateItem(T item, boolean empty) {
|
protected void updateItem(T item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
|
|||||||
@ -145,6 +145,9 @@ public enum I18n {
|
|||||||
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
||||||
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
|
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
|
||||||
ASSETS_SECTION_BANK_COMPOSITION("assets.section.bankComposition"),
|
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_SECTION_PALETTE_OVERHAULING("assets.section.paletteOverhauling"),
|
||||||
ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"),
|
ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"),
|
||||||
ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"),
|
ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"),
|
||||||
@ -154,10 +157,17 @@ public enum I18n {
|
|||||||
ASSETS_ACTIONS_EMPTY("assets.actions.empty"),
|
ASSETS_ACTIONS_EMPTY("assets.actions.empty"),
|
||||||
ASSETS_ACTION_REGISTER("assets.action.register"),
|
ASSETS_ACTION_REGISTER("assets.action.register"),
|
||||||
ASSETS_ACTION_ANALYSE("assets.action.analyse"),
|
ASSETS_ACTION_ANALYSE("assets.action.analyse"),
|
||||||
|
ASSETS_ACTION_GENERATE_TSX("assets.action.generateTsx"),
|
||||||
|
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_DELETE("assets.action.delete"),
|
||||||
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||||
ASSETS_ACTION_RELOCATE("assets.action.relocate"),
|
ASSETS_ACTION_RELOCATE("assets.action.relocate"),
|
||||||
|
ASSETS_ACTION_REMOVE("assets.action.remove"),
|
||||||
ASSETS_DELETE_DIALOG_TITLE("assets.deleteDialog.title"),
|
ASSETS_DELETE_DIALOG_TITLE("assets.deleteDialog.title"),
|
||||||
ASSETS_DELETE_DIALOG_DESCRIPTION("assets.deleteDialog.description"),
|
ASSETS_DELETE_DIALOG_DESCRIPTION("assets.deleteDialog.description"),
|
||||||
ASSETS_DELETE_DIALOG_PROMPT("assets.deleteDialog.prompt"),
|
ASSETS_DELETE_DIALOG_PROMPT("assets.deleteDialog.prompt"),
|
||||||
@ -190,10 +200,32 @@ public enum I18n {
|
|||||||
ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"),
|
ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"),
|
||||||
ASSETS_LABEL_ASSET_ID("assets.label.assetId"),
|
ASSETS_LABEL_ASSET_ID("assets.label.assetId"),
|
||||||
ASSETS_LABEL_TYPE("assets.label.type"),
|
ASSETS_LABEL_TYPE("assets.label.type"),
|
||||||
|
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_GLYPH_BANK("assets.type.glyphBank"),
|
||||||
|
ASSETS_TYPE_SCENE_BANK("assets.type.sceneBank"),
|
||||||
ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"),
|
ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"),
|
||||||
ASSETS_TYPE_SOUND_BANK("assets.type.soundBank"),
|
ASSETS_TYPE_SOUND_BANK("assets.type.soundBank"),
|
||||||
ASSETS_TYPE_UNKNOWN("assets.type.unknown"),
|
ASSETS_TYPE_UNKNOWN("assets.type.unknown"),
|
||||||
|
ASSETS_SPECIALIZATION_NONE("assets.specialization.none"),
|
||||||
|
ASSETS_SPECIALIZATION_TILESET("assets.specialization.tileset"),
|
||||||
|
ASSETS_SPECIALIZATION_SPRITES("assets.specialization.sprites"),
|
||||||
|
ASSETS_SPECIALIZATION_UI("assets.specialization.ui"),
|
||||||
|
ASSETS_SCENE_STATUS_PENDING_VALIDATION("assets.sceneStatus.pendingValidation"),
|
||||||
|
ASSETS_SCENE_STATUS_VALIDATED_PENDING_ACCEPTANCE("assets.sceneStatus.validatedPendingAcceptance"),
|
||||||
|
ASSETS_SCENE_STATUS_READY("assets.sceneStatus.ready"),
|
||||||
|
ASSETS_SCENE_STATUS_VALIDATION_FAILED("assets.sceneStatus.validationFailed"),
|
||||||
ASSETS_LABEL_LOCATION("assets.label.location"),
|
ASSETS_LABEL_LOCATION("assets.label.location"),
|
||||||
ASSETS_LABEL_BANK("assets.label.bank"),
|
ASSETS_LABEL_BANK("assets.label.bank"),
|
||||||
ASSETS_LABEL_TARGET_LOCATION("assets.label.targetLocation"),
|
ASSETS_LABEL_TARGET_LOCATION("assets.label.targetLocation"),
|
||||||
@ -241,6 +273,22 @@ public enum I18n {
|
|||||||
ASSETS_PALETTE_OVERHAULING_TILE_SELECTOR("assets.paletteOverhauling.tileSelector"),
|
ASSETS_PALETTE_OVERHAULING_TILE_SELECTOR("assets.paletteOverhauling.tileSelector"),
|
||||||
ASSETS_PALETTE_OVERHAULING_APPLIED_PALETTE("assets.paletteOverhauling.appliedPalette"),
|
ASSETS_PALETTE_OVERHAULING_APPLIED_PALETTE("assets.paletteOverhauling.appliedPalette"),
|
||||||
ASSETS_PALETTE_OVERHAULING_PREVIEW_EMPTY("assets.paletteOverhauling.previewEmpty"),
|
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_CODEC_CONFIGURATION_EMPTY("assets.details.codecConfiguration.empty"),
|
||||||
ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"),
|
ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"),
|
||||||
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),
|
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),
|
||||||
@ -254,10 +302,12 @@ public enum I18n {
|
|||||||
ASSETS_ADD_WIZARD_LABEL_NAME("assets.addWizard.label.name"),
|
ASSETS_ADD_WIZARD_LABEL_NAME("assets.addWizard.label.name"),
|
||||||
ASSETS_ADD_WIZARD_LABEL_ROOT("assets.addWizard.label.root"),
|
ASSETS_ADD_WIZARD_LABEL_ROOT("assets.addWizard.label.root"),
|
||||||
ASSETS_ADD_WIZARD_LABEL_TYPE("assets.addWizard.label.type"),
|
ASSETS_ADD_WIZARD_LABEL_TYPE("assets.addWizard.label.type"),
|
||||||
|
ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION("assets.addWizard.label.specialization"),
|
||||||
ASSETS_ADD_WIZARD_LABEL_FORMAT("assets.addWizard.label.format"),
|
ASSETS_ADD_WIZARD_LABEL_FORMAT("assets.addWizard.label.format"),
|
||||||
ASSETS_ADD_WIZARD_LABEL_CODEC("assets.addWizard.label.codec"),
|
ASSETS_ADD_WIZARD_LABEL_CODEC("assets.addWizard.label.codec"),
|
||||||
ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"),
|
ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"),
|
||||||
ASSETS_ADD_WIZARD_PROMPT_TYPE("assets.addWizard.prompt.type"),
|
ASSETS_ADD_WIZARD_PROMPT_TYPE("assets.addWizard.prompt.type"),
|
||||||
|
ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION("assets.addWizard.prompt.specialization"),
|
||||||
ASSETS_ADD_WIZARD_PROMPT_FORMAT("assets.addWizard.prompt.format"),
|
ASSETS_ADD_WIZARD_PROMPT_FORMAT("assets.addWizard.prompt.format"),
|
||||||
ASSETS_ADD_WIZARD_PROMPT_CODEC("assets.addWizard.prompt.codec"),
|
ASSETS_ADD_WIZARD_PROMPT_CODEC("assets.addWizard.prompt.codec"),
|
||||||
ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"),
|
ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"),
|
||||||
@ -270,6 +320,7 @@ public enum I18n {
|
|||||||
ASSETS_ADD_WIZARD_ERROR_ROOT_OUTSIDE_ASSETS("assets.addWizard.error.rootOutsideAssets"),
|
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_ROOT_ALREADY_ASSET("assets.addWizard.error.rootAlreadyAsset"),
|
||||||
ASSETS_ADD_WIZARD_ERROR_TYPE("assets.addWizard.error.type"),
|
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_FORMAT("assets.addWizard.error.format"),
|
||||||
ASSETS_ADD_WIZARD_ERROR_CODEC("assets.addWizard.error.codec"),
|
ASSETS_ADD_WIZARD_ERROR_CODEC("assets.addWizard.error.codec"),
|
||||||
ASSETS_ADD_WIZARD_ERROR_UNSUPPORTED_COMBINATION("assets.addWizard.error.unsupportedCombination"),
|
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.PackerAssetDetailsDTO;
|
||||||
import p.packer.dtos.PackerDiagnosticDTO;
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
import p.packer.messages.*;
|
import p.packer.messages.*;
|
||||||
|
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||||
import p.studio.controls.forms.StudioSection;
|
import p.studio.controls.forms.StudioSection;
|
||||||
@ -22,22 +23,34 @@ import p.studio.workspaces.assets.dialogs.AssetDiagnosticsDialog;
|
|||||||
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
|
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
|
||||||
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
|
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
|
||||||
import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhaulingControl;
|
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.details.summary.AssetDetailsSummaryControl;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioMetadataSnapshot;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||||
|
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
||||||
|
import p.studio.workspaces.assets.scene.SceneBankWorkflowResult;
|
||||||
|
import p.studio.workspaces.assets.scene.SceneBankWorkflowService;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledAssetGenerationResult;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService;
|
||||||
import p.studio.workspaces.assets.wizards.DeleteAssetDialog;
|
import p.studio.workspaces.assets.wizards.DeleteAssetDialog;
|
||||||
import p.studio.workspaces.assets.wizards.MoveAssetWizard;
|
import p.studio.workspaces.assets.wizards.MoveAssetWizard;
|
||||||
import p.studio.workspaces.framework.StudioEventAware;
|
import p.studio.workspaces.framework.StudioEventAware;
|
||||||
import p.studio.workspaces.framework.StudioEventBindings;
|
import p.studio.workspaces.framework.StudioEventBindings;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
@ -52,7 +65,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
private final AssetDetailsSummaryControl summaryControl;
|
private final AssetDetailsSummaryControl summaryControl;
|
||||||
private final AssetDetailsContractControl contractControl;
|
private final AssetDetailsContractControl contractControl;
|
||||||
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
||||||
|
private final AssetDetailsColorSchemaGroupsControl colorSchemaGroupsControl;
|
||||||
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
|
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();
|
||||||
private final VBox actionsContent = new VBox(10);
|
private final VBox actionsContent = new VBox(10);
|
||||||
private final ScrollPane actionsScroll = new ScrollPane();
|
private final ScrollPane actionsScroll = new ScrollPane();
|
||||||
private final VBox actionsSection;
|
private final VBox actionsSection;
|
||||||
@ -72,7 +90,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
|
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
|
||||||
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
|
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
|
||||||
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
|
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
|
||||||
|
this.colorSchemaGroupsControl = new AssetDetailsColorSchemaGroupsControl(projectReference, workspaceBus);
|
||||||
this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(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();
|
this.actionsSection = createActionsSection();
|
||||||
|
|
||||||
getStyleClass().add("assets-workspace-pane");
|
getStyleClass().add("assets-workspace-pane");
|
||||||
@ -256,13 +279,43 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
renderActions();
|
renderActions();
|
||||||
if (!readyMounted) {
|
if (!readyMounted) {
|
||||||
readyMounted = true;
|
readyMounted = true;
|
||||||
detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl, paletteOverhaulingControl);
|
detailsContent.getChildren().setAll(buildDetailsSections());
|
||||||
}
|
}
|
||||||
syncActionsSectionHeight(summaryControl.getHeight());
|
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() {
|
private VBox createActionsSection() {
|
||||||
actionsContent.getStyleClass().add("assets-details-actions-content");
|
actionsContent.getStyleClass().add("assets-details-actions-content");
|
||||||
actionsScroll.setContent(actionsContent);
|
actionsScroll.setContent(actionsContent);
|
||||||
@ -303,9 +356,6 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
|
|
||||||
private java.util.List<Node> buildActionNodes() {
|
private java.util.List<Node> buildActionNodes() {
|
||||||
final java.util.List<Node> nodes = new java.util.ArrayList<>();
|
final java.util.List<Node> nodes = new java.util.ArrayList<>();
|
||||||
if (actionFeedbackMessage != null && !actionFeedbackMessage.isBlank()) {
|
|
||||||
nodes.add(AssetDetailsUiSupport.createSectionMessage(actionFeedbackMessage));
|
|
||||||
}
|
|
||||||
if (viewState.selectedAssetDetails() == null) {
|
if (viewState.selectedAssetDetails() == null) {
|
||||||
nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
|
nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
|
||||||
return nodes;
|
return nodes;
|
||||||
@ -324,7 +374,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
analyseButton.setDisable(actionRunning || viewState.selectedAssetDetails().diagnostics().isEmpty());
|
analyseButton.setDisable(actionRunning || viewState.selectedAssetDetails().diagnostics().isEmpty());
|
||||||
analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
|
analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
|
||||||
nodes.add(analyseButton);
|
nodes.add(analyseButton);
|
||||||
|
if (canGenerateTsx()) {
|
||||||
|
final Button generateTsxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_GENERATE_TSX));
|
||||||
|
generateTsxButton.setDisable(actionRunning);
|
||||||
|
generateTsxButton.setOnAction(ignored -> generateTsx());
|
||||||
|
nodes.add(generateTsxButton);
|
||||||
|
}
|
||||||
final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
||||||
AssetDetailsUiSupport.applyActionTone(
|
AssetDetailsUiSupport.applyActionTone(
|
||||||
buildParticipationButton,
|
buildParticipationButton,
|
||||||
@ -413,7 +468,16 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
|
|
||||||
private boolean canToggleBuildParticipation() {
|
private boolean canToggleBuildParticipation() {
|
||||||
return viewState.selectedAssetDetails() != null
|
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() {
|
private AssetWorkspaceBuildParticipation currentBuildParticipation() {
|
||||||
@ -449,6 +513,98 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canGenerateTsx() {
|
||||||
|
return viewState.selectedAssetDetails() != null
|
||||||
|
&& viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.GLYPH_BANK
|
||||||
|
&& viewState.selectedAssetDetails().summary().glyphSpecialization() == AssetStudioGlyphSpecialization.TILESET;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canGenerateTmx() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canValidateSceneBank() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateTsx() {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final TiledAssetGenerationResult result = tiledGenerationService.generateTilesetTsx(details);
|
||||||
|
Platform.runLater(() -> applyTiledGenerationResult("glyph-bank-tileset", result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateTmx(int layerIndex) {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final TiledAssetGenerationResult result = tiledGenerationService.generateSceneBankTilemap(projectReference, details, layerIndex);
|
||||||
|
Platform.runLater(() -> applyTiledGenerationResult("scene-bank-layer-" + layerIndex, result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyTiledGenerationResult(String source, TiledAssetGenerationResult result) {
|
||||||
|
actionRunning = false;
|
||||||
|
actionFeedbackMessage = result.message();
|
||||||
|
workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
|
||||||
|
renderActions();
|
||||||
|
if (result.success() && viewState.selectedAssetReference() != null) {
|
||||||
|
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||||
|
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSceneBank(int layerIndex) {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final SceneBankWorkflowResult result = sceneBankWorkflowService.validateLayer(projectReference, details, layerIndex);
|
||||||
|
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-validate-layer-" + layerIndex, result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptSceneBank(int layerIndex) {
|
||||||
|
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actionRunning = true;
|
||||||
|
actionFeedbackMessage = null;
|
||||||
|
renderActions();
|
||||||
|
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||||
|
Container.backgroundTasks().submit(() -> {
|
||||||
|
final SceneBankWorkflowResult result = sceneBankWorkflowService.acceptLayer(projectReference, details, layerIndex);
|
||||||
|
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-accept-layer-" + layerIndex, result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySceneBankWorkflowResult(String source, SceneBankWorkflowResult result) {
|
||||||
|
actionRunning = false;
|
||||||
|
actionFeedbackMessage = result.message();
|
||||||
|
workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
|
||||||
|
renderActions();
|
||||||
|
if (viewState.selectedAssetReference() != null) {
|
||||||
|
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||||
|
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void applyBuildParticipationResult(UpdateAssetBuildParticipationResult result) {
|
private void applyBuildParticipationResult(UpdateAssetBuildParticipationResult result) {
|
||||||
actionRunning = false;
|
actionRunning = false;
|
||||||
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
||||||
@ -534,29 +690,71 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
|||||||
PackerAssetDetailsDTO details,
|
PackerAssetDetailsDTO details,
|
||||||
java.util.List<PackerDiagnosticDTO> diagnostics,
|
java.util.List<PackerDiagnosticDTO> diagnostics,
|
||||||
java.util.List<PackerAssetActionAvailabilityDTO> actions) {
|
java.util.List<PackerAssetActionAvailabilityDTO> actions) {
|
||||||
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
|
final var baseSummary = AssetListPackerMappings.mapSummary(details.summary());
|
||||||
for (PackerDiagnosticDTO diagnostic : diagnostics) {
|
final AssetStudioMetadataSnapshot studioMetadata = studioMetadataService.read(
|
||||||
if (!mergedDiagnostics.contains(diagnostic)) {
|
baseSummary.assetRoot(),
|
||||||
mergedDiagnostics.add(diagnostic);
|
baseSummary.assetFamily());
|
||||||
}
|
final java.util.List<AssetWorkspaceAssetAction> mappedActions = actions.stream()
|
||||||
}
|
.map(action -> new AssetWorkspaceAssetAction(
|
||||||
return new AssetWorkspaceAssetDetails(
|
action.action(),
|
||||||
AssetListPackerMappings.mapSummary(details.summary()),
|
action.enabled(),
|
||||||
actions.stream()
|
action.visible(),
|
||||||
.map(action -> new AssetWorkspaceAssetAction(
|
action.reason()))
|
||||||
action.action(),
|
.toList();
|
||||||
action.enabled(),
|
final p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary mappedSummary =
|
||||||
action.visible(),
|
new p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary(
|
||||||
action.reason()))
|
baseSummary.assetReference(),
|
||||||
.toList(),
|
baseSummary.assetName(),
|
||||||
|
baseSummary.state(),
|
||||||
|
baseSummary.buildParticipation(),
|
||||||
|
baseSummary.assetId(),
|
||||||
|
baseSummary.assetFamily(),
|
||||||
|
studioMetadata.glyphSpecialization(),
|
||||||
|
baseSummary.assetRoot(),
|
||||||
|
baseSummary.preload(),
|
||||||
|
baseSummary.hasDiagnostics());
|
||||||
|
final AssetWorkspaceBankCompositionDetails bankComposition = mapBankComposition(details.bankComposition());
|
||||||
|
final AssetWorkspaceAssetDetails draftDetails = new AssetWorkspaceAssetDetails(
|
||||||
|
mappedSummary,
|
||||||
|
mappedActions,
|
||||||
details.outputFormat(),
|
details.outputFormat(),
|
||||||
details.outputCodec(),
|
details.outputCodec(),
|
||||||
details.availableOutputCodecs(),
|
details.availableOutputCodecs(),
|
||||||
details.codecConfigurationFieldsByCodec(),
|
details.codecConfigurationFieldsByCodec(),
|
||||||
details.metadataFields(),
|
details.metadataFields(),
|
||||||
details.outputPipeline(),
|
details.outputPipeline(),
|
||||||
mapBankComposition(details.bankComposition()),
|
bankComposition,
|
||||||
details.pipelinePalettes(),
|
details.pipelinePalettes(),
|
||||||
|
studioMetadata.glyphMaterialMetadata(),
|
||||||
|
studioMetadata.sceneBankMetadata(),
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
final SceneBankWorkflowResult sceneBankWorkflow = sceneBankWorkflowService.inspect(projectReference, draftDetails);
|
||||||
|
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
|
||||||
|
for (PackerDiagnosticDTO diagnostic : diagnostics) {
|
||||||
|
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||||
|
mergedDiagnostics.add(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (PackerDiagnosticDTO diagnostic : sceneBankWorkflow.diagnostics()) {
|
||||||
|
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||||
|
mergedDiagnostics.add(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
mappedSummary,
|
||||||
|
mappedActions,
|
||||||
|
details.outputFormat(),
|
||||||
|
details.outputCodec(),
|
||||||
|
details.availableOutputCodecs(),
|
||||||
|
details.codecConfigurationFieldsByCodec(),
|
||||||
|
details.metadataFields(),
|
||||||
|
details.outputPipeline(),
|
||||||
|
bankComposition,
|
||||||
|
details.pipelinePalettes(),
|
||||||
|
studioMetadata.glyphMaterialMetadata(),
|
||||||
|
studioMetadata.sceneBankMetadata(),
|
||||||
|
sceneBankWorkflow.validation(),
|
||||||
mergedDiagnostics);
|
mergedDiagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import p.studio.controls.forms.StudioFormSection;
|
|||||||
import p.studio.controls.forms.StudioSection;
|
import p.studio.controls.forms.StudioSection;
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankStatus;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||||
|
|
||||||
@ -29,6 +31,10 @@ public final class AssetDetailsUiSupport {
|
|||||||
return StudioSection.createSection(title, content);
|
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) {
|
public static VBox createSection(String title, Node headerActions, Node content) {
|
||||||
return StudioFormSection.createEditableSection(title, headerActions, content, StudioFormMode.READ_ONLY);
|
return StudioFormSection.createEditableSection(title, headerActions, content, StudioFormMode.READ_ONLY);
|
||||||
}
|
}
|
||||||
@ -74,6 +80,14 @@ public final class AssetDetailsUiSupport {
|
|||||||
return chip;
|
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) {
|
public static Button createActionButton(String text) {
|
||||||
final Button button = new Button(text);
|
final Button button = new Button(text);
|
||||||
button.getStyleClass().addAll("studio-button", "studio-button-primary", "assets-details-action-button");
|
button.getStyleClass().addAll("studio-button", "studio-button-primary", "assets-details-action-button");
|
||||||
@ -114,6 +128,7 @@ public final class AssetDetailsUiSupport {
|
|||||||
public static String typeLabel(AssetFamilyCatalog assetFamily) {
|
public static String typeLabel(AssetFamilyCatalog assetFamily) {
|
||||||
return switch (assetFamily) {
|
return switch (assetFamily) {
|
||||||
case GLYPH_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_GLYPH_BANK);
|
case GLYPH_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_GLYPH_BANK);
|
||||||
|
case SCENE_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SCENE_BANK);
|
||||||
case SOUND_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SOUND_BANK);
|
case SOUND_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SOUND_BANK);
|
||||||
case UNKNOWN -> Container.i18n().text(I18n.ASSETS_TYPE_UNKNOWN);
|
case UNKNOWN -> Container.i18n().text(I18n.ASSETS_TYPE_UNKNOWN);
|
||||||
};
|
};
|
||||||
@ -122,11 +137,31 @@ public final class AssetDetailsUiSupport {
|
|||||||
public static String typeChipTone(AssetFamilyCatalog assetFamily) {
|
public static String typeChipTone(AssetFamilyCatalog assetFamily) {
|
||||||
return switch (assetFamily) {
|
return switch (assetFamily) {
|
||||||
case GLYPH_BANK -> "assets-details-chip-image";
|
case GLYPH_BANK -> "assets-details-chip-image";
|
||||||
|
case SCENE_BANK -> "assets-details-chip-scene";
|
||||||
case SOUND_BANK -> "assets-details-chip-audio";
|
case SOUND_BANK -> "assets-details-chip-audio";
|
||||||
case UNKNOWN -> "assets-details-chip-generic";
|
case UNKNOWN -> "assets-details-chip-generic";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String specializationLabel(AssetStudioGlyphSpecialization specialization) {
|
||||||
|
return switch (specialization) {
|
||||||
|
case NONE -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_NONE);
|
||||||
|
case TILESET -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_TILESET);
|
||||||
|
case SPRITES -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_SPRITES);
|
||||||
|
case UI -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_UI);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String sceneBankStatusLabel(AssetWorkspaceSceneBankStatus status) {
|
||||||
|
return switch (status) {
|
||||||
|
case NOT_APPLICABLE -> "";
|
||||||
|
case PENDING_VALIDATION -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_PENDING_VALIDATION);
|
||||||
|
case VALIDATED_PENDING_ACCEPTANCE -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_VALIDATED_PENDING_ACCEPTANCE);
|
||||||
|
case READY -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_READY);
|
||||||
|
case VALIDATION_FAILED -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_VALIDATION_FAILED);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static String actionLabel(AssetAction action) {
|
public static String actionLabel(AssetAction action) {
|
||||||
return switch (action) {
|
return switch (action) {
|
||||||
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);
|
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package p.studio.workspaces.assets.details;
|
package p.studio.workspaces.assets.details;
|
||||||
|
|
||||||
import p.packer.dtos.PackerAssetSummaryDTO;
|
import p.packer.dtos.PackerAssetSummaryDTO;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||||
@ -25,6 +26,7 @@ public final class AssetListPackerMappings {
|
|||||||
buildParticipation,
|
buildParticipation,
|
||||||
summary.identity().assetId(),
|
summary.identity().assetId(),
|
||||||
summary.assetFamily(),
|
summary.assetFamily(),
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
summary.identity().assetRoot(),
|
summary.identity().assetRoot(),
|
||||||
summary.preloadEnabled(),
|
summary.preloadEnabled(),
|
||||||
summary.hasDiagnostics());
|
summary.hasDiagnostics());
|
||||||
|
|||||||
@ -21,6 +21,12 @@ public final class AssetBankCapacityService {
|
|||||||
final Map<String, Object> safePipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
final Map<String, Object> safePipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
||||||
return switch (safeFamily) {
|
return switch (safeFamily) {
|
||||||
case GLYPH_BANK -> evaluateGlyphBank(artifactCount, safeMetadata);
|
case GLYPH_BANK -> evaluateGlyphBank(artifactCount, safeMetadata);
|
||||||
|
case SCENE_BANK -> new AssetDetailsBankCompositionCapacityState(
|
||||||
|
0.0d,
|
||||||
|
StudioAssetCapacitySeverity.GREEN,
|
||||||
|
false,
|
||||||
|
artifactCount + " support files",
|
||||||
|
"");
|
||||||
case SOUND_BANK -> evaluateSoundBank(resolveSoundBankUsedBytes(safePipeline, usedBytes));
|
case SOUND_BANK -> evaluateSoundBank(resolveSoundBankUsedBytes(safePipeline, usedBytes));
|
||||||
case UNKNOWN -> new AssetDetailsBankCompositionCapacityState(
|
case UNKNOWN -> new AssetDetailsBankCompositionCapacityState(
|
||||||
0.0d,
|
0.0d,
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import p.packer.dtos.PackerBankCompositionDetailsDTO;
|
||||||
|
import p.packer.dtos.PackerBankCompositionFileDTO;
|
||||||
import p.packer.messages.ApplyBankCompositionRequest;
|
import p.packer.messages.ApplyBankCompositionRequest;
|
||||||
|
import p.packer.messages.GetAssetDetailsRequest;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.banks.StudioAssetCapacityMeter;
|
import p.studio.controls.banks.StudioAssetCapacityMeter;
|
||||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||||
@ -15,12 +18,14 @@ import p.studio.lsp.events.StudioWorkspaceEventBus;
|
|||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
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.StudioAssetBankCompositionAppliedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyFailedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyFailedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyRequestedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyRequestedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionCapacityChangedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionCapacityChangedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionDraftChangedEvent;
|
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.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||||
|
|
||||||
@ -76,6 +81,19 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
|||||||
}));
|
}));
|
||||||
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
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
|
@Override
|
||||||
@ -181,12 +199,9 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
|||||||
final var response = Container.packer().workspaceService().applyBankComposition(request);
|
final var response = Container.packer().workspaceService().applyBankComposition(request);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (response.success()) {
|
if (response.success()) {
|
||||||
coordinator.apply();
|
|
||||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||||
renderSection();
|
|
||||||
publishStateNotifications();
|
|
||||||
workspaceBus.publish(new StudioAssetBankCompositionAppliedEvent(assetReference));
|
workspaceBus.publish(new StudioAssetBankCompositionAppliedEvent(assetReference));
|
||||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
|
Container.backgroundTasks().submit(() -> publishAppliedBankComposition(assetReference));
|
||||||
} else {
|
} else {
|
||||||
workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent(
|
workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent(
|
||||||
assetReference,
|
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
|
@Override
|
||||||
protected void reset() {
|
protected void reset() {
|
||||||
coordinator.reset();
|
coordinator.reset();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package p.studio.workspaces.assets.details.palette;
|
package p.studio.workspaces.assets.details.palette;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
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.StudioAssetLogEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
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 p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -31,9 +36,12 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
|||||||
private final StudioWorkspaceEventBus workspaceBus;
|
private final StudioWorkspaceEventBus workspaceBus;
|
||||||
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||||
private final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
private final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||||
|
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||||
private final AssetDetailsPaletteOverhaulingDualListView dualListView = new AssetDetailsPaletteOverhaulingDualListView();
|
private final AssetDetailsPaletteOverhaulingDualListView dualListView = new AssetDetailsPaletteOverhaulingDualListView();
|
||||||
private final AssetDetailsPaletteOverhaulingPreviewPane previewPane = new AssetDetailsPaletteOverhaulingPreviewPane();
|
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 final VBox content = new VBox(12, body);
|
||||||
|
|
||||||
private AssetWorkspaceDetailsViewState viewState;
|
private AssetWorkspaceDetailsViewState viewState;
|
||||||
@ -42,9 +50,12 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
|||||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||||
body.getStyleClass().add("assets-details-palette-overhauling-body");
|
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);
|
content.setFillWidth(true);
|
||||||
HBox.setHgrow(dualListView, Priority.ALWAYS);
|
HBox.setHgrow(listWithActions, Priority.ALWAYS);
|
||||||
HBox.setHgrow(previewPane, Priority.ALWAYS);
|
HBox.setHgrow(previewPane, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(dualListView, Priority.ALWAYS);
|
||||||
dualListView.setMaxWidth(Double.MAX_VALUE);
|
dualListView.setMaxWidth(Double.MAX_VALUE);
|
||||||
previewPane.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,
|
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
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
|
@Override
|
||||||
@ -110,6 +128,8 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
|||||||
coordinator.viewModel().renderableFileCount());
|
coordinator.viewModel().renderableFileCount());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
dualListView.setOnLeftItemActivated(item -> openPaletteWizard(item));
|
||||||
|
dualListView.setOnRightItemActivated(item -> openPaletteWizard(item));
|
||||||
if (viewModel.previewPaletteFile() != null) {
|
if (viewModel.previewPaletteFile() != null) {
|
||||||
if (viewModel.selectedFiles().contains(viewModel.previewPaletteFile())) {
|
if (viewModel.selectedFiles().contains(viewModel.previewPaletteFile())) {
|
||||||
dualListView.selectRightItem(viewModel.previewPaletteFile());
|
dualListView.selectRightItem(viewModel.previewPaletteFile());
|
||||||
@ -134,6 +154,17 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
|||||||
viewModel.previewPaletteFile(),
|
viewModel.previewPaletteFile(),
|
||||||
viewModel.renderableFileCount());
|
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);
|
renderFormSection(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +263,67 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
|||||||
: "asset-details:" + viewState.selectedAssetReference();
|
: "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")
|
@SuppressWarnings("unchecked")
|
||||||
private static Map<String, Object> palettePayload(Map<String, Object> metadata) {
|
private static Map<String, Object> palettePayload(Map<String, Object> metadata) {
|
||||||
final Object palette = metadata.get("palette");
|
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.controls.forms.StudioFormSession;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
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.util.*;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public final class AssetDetailsPaletteOverhaulingCoordinator {
|
public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||||
private StudioFormSession<AssetDetailsPaletteOverhaulingDraft> formSession;
|
private StudioFormSession<AssetDetailsPaletteOverhaulingDraft> formSession;
|
||||||
|
private AssetWorkspaceAssetDetails details;
|
||||||
|
private AssetStudioGlyphMaterialMetadata localMaterialMetadata;
|
||||||
|
|
||||||
public void replaceDetails(AssetWorkspaceAssetDetails details) {
|
public void replaceDetails(AssetWorkspaceAssetDetails details) {
|
||||||
|
this.details = details;
|
||||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||||
formSession = null;
|
formSession = null;
|
||||||
|
localMaterialMetadata = null;
|
||||||
return;
|
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> allFiles = paletteFiles(details);
|
||||||
final List<AssetWorkspaceBankCompositionFile> previewTileFiles = details.bankComposition().selectedFiles().stream()
|
final List<AssetWorkspaceBankCompositionFile> previewTileFiles = details.bankComposition().selectedFiles().stream()
|
||||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::supportsPalettePreview)
|
.filter(AssetDetailsPaletteOverhaulingCoordinator::supportsPalettePreview)
|
||||||
@ -37,12 +53,21 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
|||||||
available,
|
available,
|
||||||
initialSelected,
|
initialSelected,
|
||||||
previewTileFiles.isEmpty() ? null : previewTileFiles.getFirst().path(),
|
previewTileFiles.isEmpty() ? null : previewTileFiles.getFirst().path(),
|
||||||
null);
|
initialSelected.isEmpty()
|
||||||
|
? allFiles.isEmpty() ? null : allFiles.getFirst().path()
|
||||||
|
: initialSelected.getFirst().path());
|
||||||
if (formSession == null) {
|
if (formSession == null) {
|
||||||
formSession = new StudioFormSession<>(source);
|
formSession = new StudioFormSession<>(source);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Objects.equals(formSession.source(), source)) {
|
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);
|
formSession.replaceSource(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,6 +76,10 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
|||||||
return formSession != null;
|
return formSession != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AssetWorkspaceAssetDetails details() {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
public void beginEdit() {
|
public void beginEdit() {
|
||||||
if (ready()) {
|
if (ready()) {
|
||||||
formSession.beginEdit();
|
formSession.beginEdit();
|
||||||
@ -77,7 +106,10 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
|||||||
|
|
||||||
public void moveToSelected(List<AssetWorkspaceBankCompositionFile> files) {
|
public void moveToSelected(List<AssetWorkspaceBankCompositionFile> files) {
|
||||||
if (ready() && formSession.mode() == StudioFormMode.EDITING) {
|
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 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(
|
return new AssetDetailsPaletteOverhaulingViewModel(
|
||||||
formSession.mode() == StudioFormMode.EDITING,
|
formSession.mode() == StudioFormMode.EDITING,
|
||||||
formSession.isDirty(),
|
formSession.isDirty(),
|
||||||
draft.previewTileFiles(),
|
schemaPreviewTileFiles,
|
||||||
draft.allFiles(),
|
draft.allFiles(),
|
||||||
draft.availableFiles(),
|
draft.availableFiles(),
|
||||||
draft.selectedFiles(),
|
draft.selectedFiles(),
|
||||||
fileByPath(draft.previewTileFiles(), draft.previewTilePath()),
|
previewTileFile,
|
||||||
fileByPath(draft.allFiles(), draft.previewPalettePath()),
|
previewPaletteFile,
|
||||||
draft.allFiles().size());
|
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) {
|
if (file == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return nestedMap(file.metadata(), "tile") != null && nestedMap(file.metadata(), "palette") != null;
|
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")
|
@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);
|
final Object value = metadata.get(key);
|
||||||
if (value instanceof Map<?, ?> nested) {
|
if (value instanceof Map<?, ?> nested) {
|
||||||
return (Map<String, Object>) nested;
|
return (Map<String, Object>) nested;
|
||||||
@ -133,37 +256,236 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<AssetWorkspaceBankCompositionFile> paletteFiles(AssetWorkspaceAssetDetails details) {
|
private AssetStudioGlyphMaterialMetadata materialMetadata() {
|
||||||
final Map<String, AssetWorkspaceBankCompositionFile> ordered = new LinkedHashMap<>();
|
if (details == null) {
|
||||||
// `bankComposition` files are already projected by the packer read model from the current cache/walk state.
|
return new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null);
|
||||||
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().availableFiles()) {
|
|
||||||
if (supportsPalettePreview(file)) {
|
|
||||||
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().selectedFiles()) {
|
return localMaterialMetadata == null
|
||||||
if (supportsPalettePreview(file)) {
|
? new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)
|
||||||
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
|
: 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());
|
return List.copyOf(ordered.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
private List<PaletteRecord> allPaletteRecords() {
|
||||||
private static AssetWorkspaceBankCompositionFile canonicalPaletteFile(AssetWorkspaceBankCompositionFile file) {
|
final AssetStudioGlyphMaterialMetadata metadata = materialMetadata();
|
||||||
final String hash = paletteHash(file);
|
final List<PaletteRecord> records = new ArrayList<>();
|
||||||
final Map<String, Object> metadata = new LinkedHashMap<>(file.metadata());
|
for (AssetStudioGlyphMaterialSchema schema : metadata.schemas()) {
|
||||||
metadata.put("paletteHash", hash);
|
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(
|
return new AssetWorkspaceBankCompositionFile(
|
||||||
hash,
|
record.paletteId(),
|
||||||
hash,
|
displayName(record),
|
||||||
file.size(),
|
0L,
|
||||||
file.lastModified(),
|
0L,
|
||||||
file.fingerprint(),
|
null,
|
||||||
metadata);
|
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,
|
AssetWorkspaceAssetDetails details,
|
||||||
List<AssetWorkspaceBankCompositionFile> allFiles) {
|
List<AssetWorkspaceBankCompositionFile> allFiles) {
|
||||||
final List<AssetWorkspaceBankCompositionFile> selected = new ArrayList<>();
|
final List<AssetWorkspaceBankCompositionFile> selected = new ArrayList<>();
|
||||||
@ -186,32 +508,21 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
|||||||
&& Objects.equals(left.get("convertedRgb565"), right.get("convertedRgb565"));
|
&& Objects.equals(left.get("convertedRgb565"), right.get("convertedRgb565"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
private static int htmlToArgb8888(String htmlColor) {
|
||||||
private static String paletteHash(AssetWorkspaceBankCompositionFile file) {
|
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||||
final Map<String, Object> palette = nestedMap(file.metadata(), "palette");
|
return 0xFF000000 | rgb;
|
||||||
final Object convertedValue = palette == null ? null : palette.get("convertedRgb565");
|
}
|
||||||
final List<Integer> colors = convertedValue instanceof List<?> converted
|
|
||||||
? (List<Integer>) converted
|
private static int htmlToRgb565(String htmlColor) {
|
||||||
: List.of();
|
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||||
try {
|
final int red = (rgb >> 16) & 0xFF;
|
||||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
final int green = (rgb >> 8) & 0xFF;
|
||||||
for (int index = 0; index < 15; index++) {
|
final int blue = rgb & 0xFF;
|
||||||
final int color = index < colors.size() && colors.get(index) != null ? colors.get(index) : 0;
|
return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
||||||
digest.update(ByteBuffer.allocate(4).putInt(color).array());
|
}
|
||||||
}
|
|
||||||
long value = 0L;
|
private static String normalizeHtmlColor(String color) {
|
||||||
final byte[] bytes = digest.digest();
|
return Objects.requireNonNull(color, "color").trim().toUpperCase();
|
||||||
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 AssetWorkspaceBankCompositionFile fileByPath(List<AssetWorkspaceBankCompositionFile> files, String path) {
|
private static AssetWorkspaceBankCompositionFile fileByPath(List<AssetWorkspaceBankCompositionFile> files, String path) {
|
||||||
@ -223,4 +534,93 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.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.control.Label;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.banks.StudioDualListView;
|
import p.studio.controls.banks.StudioDualListView;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
@ -28,8 +29,15 @@ public final class AssetDetailsPaletteOverhaulingDualListView extends StudioDual
|
|||||||
final Label hashLabel = new Label(item.displayName());
|
final Label hashLabel = new Label(item.displayName());
|
||||||
hashLabel.getStyleClass().add("assets-details-palette-hash");
|
hashLabel.getStyleClass().add("assets-details-palette-hash");
|
||||||
final HBox strip = createPaletteStrip(item);
|
final HBox strip = createPaletteStrip(item);
|
||||||
final HBox root = new HBox(8, hashLabel, strip);
|
final HBox top = new HBox(8, hashLabel, strip);
|
||||||
root.getStyleClass().add("assets-details-palette-item");
|
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;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -146,6 +146,8 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
|||||||
final Integer height = asInteger(tile.get("height"));
|
final Integer height = asInteger(tile.get("height"));
|
||||||
final List<Integer> indices = paletteIndices(tile.get("paletteIndices"));
|
final List<Integer> indices = paletteIndices(tile.get("paletteIndices"));
|
||||||
final List<Integer> colors = paletteDisplayColors(paletteFile.metadata());
|
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()) {
|
if (width == null || height == null || width <= 0 || height <= 0 || indices.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -153,7 +155,7 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
|||||||
final double pixelWidth = PREVIEW_SIZE / width;
|
final double pixelWidth = PREVIEW_SIZE / width;
|
||||||
final double pixelHeight = PREVIEW_SIZE / height;
|
final double pixelHeight = PREVIEW_SIZE / height;
|
||||||
for (int index = 0; index < indices.size(); index++) {
|
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 x = index % width;
|
||||||
final int y = index / width;
|
final int y = index / width;
|
||||||
graphics.setFill(toFxColor(paletteIndexToArgb(colors, paletteIndex)));
|
graphics.setFill(toFxColor(paletteIndexToArgb(colors, paletteIndex)));
|
||||||
@ -206,6 +208,35 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
|||||||
return List.of();
|
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) {
|
private Integer asInteger(Object value) {
|
||||||
if (value instanceof Number number) {
|
if (value instanceof Number number) {
|
||||||
return number.intValue();
|
return number.intValue();
|
||||||
@ -220,6 +251,17 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
|||||||
return colorAt(colors, paletteIndex - 1);
|
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) {
|
private int colorAt(List<Integer> colors, int index) {
|
||||||
if (index < 0 || index >= colors.size()) {
|
if (index < 0 || index >= colors.size()) {
|
||||||
return 0x00000000;
|
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;
|
package p.studio.workspaces.assets.details.summary;
|
||||||
|
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||||
@ -8,6 +9,8 @@ import p.studio.lsp.events.StudioWorkspaceEventBus;
|
|||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
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.messages.AssetWorkspaceAssetSummary;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
||||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||||
@ -56,11 +59,20 @@ public final class AssetDetailsSummaryControl extends VBox implements StudioCont
|
|||||||
content.getChildren().setAll(
|
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_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())),
|
||||||
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot())));
|
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot())));
|
||||||
|
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(
|
||||||
|
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||||
|
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization()),
|
||||||
|
"assets-details-chip-fixed"));
|
||||||
|
}
|
||||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||||
Container.i18n().text(I18n.ASSETS_LABEL_TYPE),
|
Container.i18n().text(I18n.ASSETS_LABEL_TYPE),
|
||||||
AssetDetailsUiSupport.createChip(
|
typeBox));
|
||||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
|
||||||
AssetDetailsUiSupport.typeLabel(summary.assetFamily()))));
|
|
||||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||||
Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION),
|
Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION),
|
||||||
AssetDetailsUiSupport.createChip(
|
AssetDetailsUiSupport.createChip(
|
||||||
|
|||||||
@ -113,6 +113,7 @@ public final class AssetListItemControl extends VBox {
|
|||||||
private String assetRowToneClass(AssetFamilyCatalog assetFamily) {
|
private String assetRowToneClass(AssetFamilyCatalog assetFamily) {
|
||||||
return switch (assetFamily) {
|
return switch (assetFamily) {
|
||||||
case GLYPH_BANK -> "assets-workspace-asset-row-tone-image";
|
case GLYPH_BANK -> "assets-workspace-asset-row-tone-image";
|
||||||
|
case SCENE_BANK -> "assets-workspace-asset-row-tone-scene";
|
||||||
case SOUND_BANK -> "assets-workspace-asset-row-tone-audio";
|
case SOUND_BANK -> "assets-workspace-asset-row-tone-audio";
|
||||||
default -> "assets-workspace-asset-row-tone-generic";
|
default -> "assets-workspace-asset-row-tone-generic";
|
||||||
};
|
};
|
||||||
@ -121,6 +122,7 @@ public final class AssetListItemControl extends VBox {
|
|||||||
private String assetNameToneClass(AssetFamilyCatalog assetFamily) {
|
private String assetNameToneClass(AssetFamilyCatalog assetFamily) {
|
||||||
return switch (assetFamily) {
|
return switch (assetFamily) {
|
||||||
case GLYPH_BANK -> "assets-workspace-asset-name-tone-image";
|
case GLYPH_BANK -> "assets-workspace-asset-name-tone-image";
|
||||||
|
case SCENE_BANK -> "assets-workspace-asset-name-tone-scene";
|
||||||
case SOUND_BANK -> "assets-workspace-asset-name-tone-audio";
|
case SOUND_BANK -> "assets-workspace-asset-name-tone-audio";
|
||||||
default -> "assets-workspace-asset-name-tone-generic";
|
default -> "assets-workspace-asset-name-tone-generic";
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
|||||||
import p.packer.dtos.PackerDiagnosticDTO;
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
import p.packer.messages.assets.OutputCodecCatalog;
|
import p.packer.messages.assets.OutputCodecCatalog;
|
||||||
import p.packer.messages.assets.OutputFormatCatalog;
|
import p.packer.messages.assets.OutputFormatCatalog;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -20,6 +22,9 @@ public record AssetWorkspaceAssetDetails(
|
|||||||
Map<String, Object> outputPipeline,
|
Map<String, Object> outputPipeline,
|
||||||
AssetWorkspaceBankCompositionDetails bankComposition,
|
AssetWorkspaceBankCompositionDetails bankComposition,
|
||||||
List<Map<String, Object>> pipelinePalettes,
|
List<Map<String, Object>> pipelinePalettes,
|
||||||
|
AssetStudioGlyphMaterialMetadata glyphMaterialMetadata,
|
||||||
|
AssetStudioSceneBankMetadata sceneBankMetadata,
|
||||||
|
AssetWorkspaceSceneBankValidation sceneBankValidation,
|
||||||
List<PackerDiagnosticDTO> diagnostics) {
|
List<PackerDiagnosticDTO> diagnostics) {
|
||||||
|
|
||||||
public AssetWorkspaceAssetDetails {
|
public AssetWorkspaceAssetDetails {
|
||||||
@ -33,6 +38,43 @@ public record AssetWorkspaceAssetDetails(
|
|||||||
outputPipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
outputPipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
||||||
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
||||||
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
|
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
|
||||||
|
sceneBankValidation = Objects.requireNonNullElse(sceneBankValidation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package p.studio.workspaces.assets.messages;
|
|||||||
|
|
||||||
import p.packer.messages.AssetReference;
|
import p.packer.messages.AssetReference;
|
||||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -13,6 +14,7 @@ public record AssetWorkspaceAssetSummary(
|
|||||||
AssetWorkspaceBuildParticipation buildParticipation,
|
AssetWorkspaceBuildParticipation buildParticipation,
|
||||||
Integer assetId,
|
Integer assetId,
|
||||||
AssetFamilyCatalog assetFamily,
|
AssetFamilyCatalog assetFamily,
|
||||||
|
AssetStudioGlyphSpecialization glyphSpecialization,
|
||||||
Path assetRoot,
|
Path assetRoot,
|
||||||
boolean preload,
|
boolean preload,
|
||||||
boolean hasDiagnostics) {
|
boolean hasDiagnostics) {
|
||||||
@ -23,6 +25,7 @@ public record AssetWorkspaceAssetSummary(
|
|||||||
Objects.requireNonNull(state, "state");
|
Objects.requireNonNull(state, "state");
|
||||||
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
||||||
assetFamily = Objects.requireNonNullElse(assetFamily, AssetFamilyCatalog.UNKNOWN);
|
assetFamily = Objects.requireNonNullElse(assetFamily, AssetFamilyCatalog.UNKNOWN);
|
||||||
|
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
|
||||||
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||||
if (assetName.isBlank()) {
|
if (assetName.isBlank()) {
|
||||||
throw new IllegalArgumentException("assetName must not be blank");
|
throw new IllegalArgumentException("assetName must not be blank");
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
package p.studio.workspaces.assets.messages;
|
||||||
|
|
||||||
|
public enum AssetWorkspaceSceneBankStatus {
|
||||||
|
NOT_APPLICABLE,
|
||||||
|
PENDING_VALIDATION,
|
||||||
|
VALIDATED_PENDING_ACCEPTANCE,
|
||||||
|
READY,
|
||||||
|
VALIDATION_FAILED
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package p.studio.workspaces.assets.messages;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus status,
|
||||||
|
boolean pendingExternalChanges,
|
||||||
|
boolean canAccept,
|
||||||
|
String currentFingerprint,
|
||||||
|
String validatedFingerprint,
|
||||||
|
String acceptedFingerprint) {
|
||||||
|
|
||||||
|
public static final AssetWorkspaceSceneBankValidation NOT_APPLICABLE = new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.NOT_APPLICABLE,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
|
public AssetWorkspaceSceneBankValidation {
|
||||||
|
status = Objects.requireNonNullElse(status, AssetWorkspaceSceneBankStatus.NOT_APPLICABLE);
|
||||||
|
currentFingerprint = Objects.requireNonNullElse(currentFingerprint, "");
|
||||||
|
validatedFingerprint = Objects.requireNonNullElse(validatedFingerprint, "");
|
||||||
|
acceptedFingerprint = Objects.requireNonNullElse(acceptedFingerprint, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package p.studio.workspaces.assets.metadata;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum AssetStudioGlyphSpecialization {
|
||||||
|
NONE("none"),
|
||||||
|
TILESET("tileset"),
|
||||||
|
SPRITES("sprites"),
|
||||||
|
UI("ui");
|
||||||
|
|
||||||
|
private final String manifestValue;
|
||||||
|
|
||||||
|
AssetStudioGlyphSpecialization(String manifestValue) {
|
||||||
|
this.manifestValue = manifestValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String manifestValue() {
|
||||||
|
return manifestValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AssetStudioGlyphSpecialization fromManifestValue(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
final String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
for (AssetStudioGlyphSpecialization candidate : values()) {
|
||||||
|
if (candidate.manifestValue.equals(normalized)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,290 @@
|
|||||||
|
package p.studio.workspaces.assets.metadata;
|
||||||
|
|
||||||
|
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 java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
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();
|
||||||
|
|
||||||
|
public AssetStudioMetadataSnapshot read(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||||
|
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||||
|
return new AssetStudioMetadataSnapshot(
|
||||||
|
readGlyphSpecialization(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 {
|
||||||
|
final Path metadataPath = Objects.requireNonNull(assetRoot, "assetRoot")
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize()
|
||||||
|
.resolve(STUDIO_ASSET_METADATA_FILE);
|
||||||
|
final AssetStudioGlyphSpecialization normalized = Objects.requireNonNullElse(
|
||||||
|
specialization,
|
||||||
|
AssetStudioGlyphSpecialization.NONE);
|
||||||
|
if (normalized == AssetStudioGlyphSpecialization.NONE) {
|
||||||
|
Files.deleteIfExists(metadataPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var root = mapper.createObjectNode();
|
||||||
|
root.put("schema_version", 1);
|
||||||
|
root.put("glyph_bank_specialization", normalized.manifestValue());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
final Path metadataPath = assetRoot.resolve(STUDIO_ASSET_METADATA_FILE);
|
||||||
|
if (!Files.isRegularFile(metadataPath)) {
|
||||||
|
return AssetStudioGlyphSpecialization.NONE;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final JsonNode root = mapper.readTree(metadataPath.toFile());
|
||||||
|
if (root == null || !root.isObject()) {
|
||||||
|
return AssetStudioGlyphSpecialization.NONE;
|
||||||
|
}
|
||||||
|
return AssetStudioGlyphSpecialization.fromManifestValue(root.path("glyph_bank_specialization").asText(null));
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return AssetStudioGlyphSpecialization.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetStudioSceneBankMetadata readSceneBankMetadata(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||||
|
if (assetFamily != AssetFamilyCatalog.SCENE_BANK) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Path supportFile = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
|
||||||
|
if (!Files.isRegularFile(supportFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final JsonNode root = mapper.readTree(supportFile.toFile());
|
||||||
|
if (root == null || !root.isObject()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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 != 4) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(root.path("layers") instanceof ArrayNode layersNode) || layersNode.size() != layerCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final List<AssetStudioSceneLayerBinding> bindings = new ArrayList<>();
|
||||||
|
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() || width <= 0 || height <= 0 || tilemap.isBlank() || !indexes.add(index)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package p.studio.workspaces.assets.metadata;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetStudioMetadataSnapshot(
|
||||||
|
AssetStudioGlyphSpecialization glyphSpecialization,
|
||||||
|
AssetStudioSceneBankMetadata sceneBankMetadata,
|
||||||
|
AssetStudioGlyphMaterialMetadata glyphMaterialMetadata) {
|
||||||
|
|
||||||
|
public AssetStudioMetadataSnapshot {
|
||||||
|
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package p.studio.workspaces.assets.metadata;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record AssetStudioSceneBankMetadata(
|
||||||
|
int mapWidth,
|
||||||
|
int mapHeight,
|
||||||
|
int layerCount,
|
||||||
|
List<AssetStudioSceneLayerBinding> layerBindings,
|
||||||
|
Path supportFile) {
|
||||||
|
|
||||||
|
public AssetStudioSceneBankMetadata {
|
||||||
|
layerBindings = List.copyOf(Objects.requireNonNull(layerBindings, "layerBindings"));
|
||||||
|
supportFile = Objects.requireNonNull(supportFile, "supportFile").toAbsolutePath().normalize();
|
||||||
|
if (mapWidth <= 0 || mapHeight <= 0) {
|
||||||
|
throw new IllegalArgumentException("map dimensions must be positive");
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
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 {
|
||||||
|
layerName = Objects.requireNonNull(layerName, "layerName").trim();
|
||||||
|
tilemap = Objects.requireNonNull(tilemap, "tilemap").trim();
|
||||||
|
tilesetAssetRoot = Objects.requireNonNull(tilesetAssetRoot, "tilesetAssetRoot").trim();
|
||||||
|
if (index <= 0) {
|
||||||
|
throw new IllegalArgumentException("index must be positive");
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package p.studio.workspaces.assets.scene;
|
||||||
|
|
||||||
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record SceneBankWorkflowResult(
|
||||||
|
boolean success,
|
||||||
|
String message,
|
||||||
|
AssetWorkspaceSceneBankValidation validation,
|
||||||
|
List<PackerDiagnosticDTO> diagnostics) {
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult {
|
||||||
|
message = Objects.requireNonNullElse(message, "");
|
||||||
|
validation = Objects.requireNonNullElse(validation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||||
|
diagnostics = List.copyOf(Objects.requireNonNullElse(diagnostics, List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,391 @@
|
|||||||
|
package p.studio.workspaces.assets.scene;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||||
|
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankStatus;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledMapDocument;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledTilesetDocument;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledUnsupportedFeatureException;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledXmlCodec;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class SceneBankWorkflowService {
|
||||||
|
private static final String VALIDATION_FILE = "scene-bank.validation.json";
|
||||||
|
private static final String ACCEPTANCE_FILE = "scene-bank.acceptance.json";
|
||||||
|
|
||||||
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult inspect(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
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, null);
|
||||||
|
final String currentFingerprint = diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)
|
||||||
|
? ""
|
||||||
|
: computeFingerprint(projectReference, details);
|
||||||
|
final String validatedFingerprint = readFingerprint(details.summary().assetRoot().resolve(VALIDATION_FILE));
|
||||||
|
final String acceptedFingerprint = readFingerprint(details.summary().assetRoot().resolve(ACCEPTANCE_FILE));
|
||||||
|
final AssetWorkspaceSceneBankValidation validation = validationState(
|
||||||
|
diagnostics,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
return new SceneBankWorkflowResult(
|
||||||
|
diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking),
|
||||||
|
messageFor(validation),
|
||||||
|
validation,
|
||||||
|
diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult validate(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
final SceneBankWorkflowResult inspection = inspect(projectReference, details);
|
||||||
|
final Path validationFile = details.summary().assetRoot().resolve(VALIDATION_FILE);
|
||||||
|
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 inspect(projectReference, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneBankWorkflowResult accept(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
final SceneBankWorkflowResult inspection = inspect(projectReference, details);
|
||||||
|
if (!inspection.validation().canAccept()) {
|
||||||
|
return new SceneBankWorkflowResult(
|
||||||
|
false,
|
||||||
|
"Scene Bank must validate successfully before acceptance.",
|
||||||
|
inspection.validation(),
|
||||||
|
inspection.diagnostics());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
writeFingerprint(details.summary().assetRoot().resolve(ACCEPTANCE_FILE), inspection.validation().currentFingerprint());
|
||||||
|
writeFingerprint(details.summary().assetRoot().resolve(VALIDATION_FILE), inspection.validation().currentFingerprint());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
return new SceneBankWorkflowResult(
|
||||||
|
false,
|
||||||
|
"Unable to persist acceptance state: " + exception.getMessage(),
|
||||||
|
inspection.validation(),
|
||||||
|
inspection.diagnostics());
|
||||||
|
}
|
||||||
|
return inspect(projectReference, 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) {
|
||||||
|
diagnostics.add(error("Scene Bank support metadata is missing or invalid.", details.summary().assetRoot().resolve("scene-bank.studio.json")));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
final Path tmxPath = details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize();
|
||||||
|
final Path tsxPath = projectReference.rootPath()
|
||||||
|
.resolve("assets")
|
||||||
|
.resolve(binding.tilesetAssetRoot())
|
||||||
|
.resolve("tileset.tsx")
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
if (!Files.isRegularFile(tmxPath)) {
|
||||||
|
diagnostics.add(error("TMX file is missing for layer " + binding.index() + ": " + binding.tilemap(), tmxPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Files.isRegularFile(tsxPath)) {
|
||||||
|
diagnostics.add(error("Referenced TSX file is missing for layer " + binding.index() + ": " + binding.tilesetAssetRoot(), tsxPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final TiledTilesetDocument tileset = codec.readTileset(tsxPath);
|
||||||
|
final TiledMapDocument map = codec.readMap(tmxPath);
|
||||||
|
final String expectedSource = details.summary().assetRoot()
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize()
|
||||||
|
.relativize(tsxPath)
|
||||||
|
.toString()
|
||||||
|
.replace('\\', '/');
|
||||||
|
if (map.tilesets().size() != 1) {
|
||||||
|
diagnostics.add(error("TMX must reference exactly one TSX in wave 1: " + binding.tilemap(), tmxPath));
|
||||||
|
} 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() != 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()) {
|
||||||
|
diagnostics.add(error("TMX tile size does not match referenced TSX: " + binding.tilemap(), tmxPath));
|
||||||
|
}
|
||||||
|
if (map.tileLayers().size() != 1 || !binding.layerName().equals(map.tileLayers().getFirst().name())) {
|
||||||
|
diagnostics.add(error("TMX layer mapping does not match scene-bank support metadata: " + binding.tilemap(), tmxPath));
|
||||||
|
}
|
||||||
|
} catch (TiledUnsupportedFeatureException exception) {
|
||||||
|
diagnostics.add(error(exception.getMessage(), tmxPath));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
diagnostics.add(error("Unable to validate Scene Bank XML: " + exception.getMessage(), tmxPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String computeFingerprint(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
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());
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.stream()
|
||||||
|
.distinct()
|
||||||
|
.sorted(Comparator.comparing(Path::toString))
|
||||||
|
.forEach(path -> updateDigest(digest, path));
|
||||||
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
digest.update(path.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
digest.update((byte) 0);
|
||||||
|
digest.update(Files.readAllBytes(path));
|
||||||
|
digest.update((byte) 0);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceSceneBankValidation validationState(
|
||||||
|
List<PackerDiagnosticDTO> diagnostics,
|
||||||
|
String currentFingerprint,
|
||||||
|
String validatedFingerprint,
|
||||||
|
String acceptedFingerprint) {
|
||||||
|
if (diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)) {
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.VALIDATION_FAILED,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
}
|
||||||
|
final boolean validated = !currentFingerprint.isBlank() && currentFingerprint.equals(validatedFingerprint);
|
||||||
|
final boolean accepted = validated && currentFingerprint.equals(acceptedFingerprint);
|
||||||
|
if (accepted) {
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.READY,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
}
|
||||||
|
if (validated) {
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.VALIDATED_PENDING_ACCEPTANCE,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
acceptedFingerprint);
|
||||||
|
}
|
||||||
|
return new AssetWorkspaceSceneBankValidation(
|
||||||
|
AssetWorkspaceSceneBankStatus.PENDING_VALIDATION,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
currentFingerprint,
|
||||||
|
validatedFingerprint,
|
||||||
|
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 -> "";
|
||||||
|
case PENDING_VALIDATION -> "Scene Bank has pending external changes and requires validation.";
|
||||||
|
case VALIDATED_PENDING_ACCEPTANCE -> "Scene Bank validation succeeded and is waiting for explicit acceptance.";
|
||||||
|
case READY -> "Scene Bank is ready.";
|
||||||
|
case VALIDATION_FAILED -> "Scene Bank validation failed.";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readFingerprint(Path path) {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final JsonNode root = mapper.readTree(path.toFile());
|
||||||
|
return root.path("fingerprint").asText("").trim();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFingerprint(Path path, String fingerprint) throws IOException {
|
||||||
|
final var root = mapper.createObjectNode();
|
||||||
|
root.put("schema_version", 1);
|
||||||
|
root.put("fingerprint", Objects.requireNonNullElse(fingerprint, ""));
|
||||||
|
root.put("recorded_at", Instant.now().toString());
|
||||||
|
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackerDiagnosticDTO error(String message, Path path) {
|
||||||
|
return new PackerDiagnosticDTO(
|
||||||
|
PackerDiagnosticSeverity.ERROR,
|
||||||
|
PackerDiagnosticCategory.STRUCTURAL,
|
||||||
|
message,
|
||||||
|
path,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledAssetGenerationResult(
|
||||||
|
boolean success,
|
||||||
|
String message,
|
||||||
|
List<Path> writtenFiles) {
|
||||||
|
|
||||||
|
public TiledAssetGenerationResult {
|
||||||
|
message = Objects.requireNonNullElse(message, "");
|
||||||
|
writtenFiles = List.copyOf(Objects.requireNonNullElse(writtenFiles, List.of()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||||
|
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
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.AssetWorkspaceAssetDetails;
|
||||||
|
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class TiledAssetGenerationService {
|
||||||
|
public static final String GENERATED_TSX_FILE = "tileset.tsx";
|
||||||
|
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
public TiledAssetGenerationResult generateTilesetTsx(AssetWorkspaceAssetDetails details) {
|
||||||
|
if (details == null
|
||||||
|
|| details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK
|
||||||
|
|| details.summary().glyphSpecialization() != AssetStudioGlyphSpecialization.TILESET) {
|
||||||
|
return new TiledAssetGenerationResult(false, "TSX generation is available only for Tileset-specialized glyph banks.", List.of());
|
||||||
|
}
|
||||||
|
final List<AssetWorkspaceBankCompositionFile> selectedFiles = details.bankComposition().selectedFiles();
|
||||||
|
if (selectedFiles.isEmpty()) {
|
||||||
|
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<>();
|
||||||
|
for (int index = 0; index < selectedFiles.size(); index += 1) {
|
||||||
|
final AssetWorkspaceBankCompositionFile file = selectedFiles.get(index);
|
||||||
|
tiles.add(new TiledTilesetTile(
|
||||||
|
index,
|
||||||
|
file.path(),
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
List.of(new TiledProperty("glyph_id", "int", Integer.toString(index))),
|
||||||
|
null));
|
||||||
|
}
|
||||||
|
final TiledTilesetDocument document = new TiledTilesetDocument(
|
||||||
|
"1.10",
|
||||||
|
"1.12.1",
|
||||||
|
details.summary().assetName(),
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
tiles.size(),
|
||||||
|
0,
|
||||||
|
List.of(),
|
||||||
|
tiles);
|
||||||
|
final Path outputPath = details.summary().assetRoot().resolve(GENERATED_TSX_FILE);
|
||||||
|
try {
|
||||||
|
codec.writeTileset(outputPath, document);
|
||||||
|
return new TiledAssetGenerationResult(true, "Generated TSX successfully.", List.of(outputPath));
|
||||||
|
} catch (IOException exception) {
|
||||||
|
return new TiledAssetGenerationResult(false, "Unable to generate TSX: " + exception.getMessage(), List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TiledAssetGenerationResult generateSceneBankTilemaps(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||||
|
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 List<Path> writtenFiles = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||||
|
if (!binding.enabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final TiledAssetGenerationResult result = generateSceneBankTilemap(projectReference, details, binding.index());
|
||||||
|
if (!result.success()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseTileSize(List<PackerCodecConfigurationFieldDTO> metadataFields) {
|
||||||
|
final String value = metadataFields.stream()
|
||||||
|
.filter(field -> "tile_size".equals(field.key()))
|
||||||
|
.map(PackerCodecConfigurationFieldDTO::value)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("16x16");
|
||||||
|
final String normalized = value.trim().toLowerCase();
|
||||||
|
if (normalized.startsWith("8x8")) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
if (normalized.startsWith("32x32")) {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledMapDocument(
|
||||||
|
String version,
|
||||||
|
String tiledVersion,
|
||||||
|
String orientation,
|
||||||
|
String renderOrder,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int tileWidth,
|
||||||
|
int tileHeight,
|
||||||
|
int nextLayerId,
|
||||||
|
int nextObjectId,
|
||||||
|
List<TiledProperty> properties,
|
||||||
|
List<TiledTilesetReference> tilesets,
|
||||||
|
List<TiledTileLayer> tileLayers,
|
||||||
|
List<TiledObjectLayer> objectLayers) {
|
||||||
|
|
||||||
|
public TiledMapDocument {
|
||||||
|
version = Objects.requireNonNullElse(version, "1.10");
|
||||||
|
tiledVersion = Objects.requireNonNullElse(tiledVersion, "1.12.1");
|
||||||
|
orientation = Objects.requireNonNullElse(orientation, "orthogonal");
|
||||||
|
renderOrder = Objects.requireNonNullElse(renderOrder, "right-down");
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
tilesets = List.copyOf(Objects.requireNonNull(tilesets, "tilesets"));
|
||||||
|
tileLayers = List.copyOf(Objects.requireNonNull(tileLayers, "tileLayers"));
|
||||||
|
objectLayers = List.copyOf(Objects.requireNonNull(objectLayers, "objectLayers"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledObjectData(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
double width,
|
||||||
|
double height,
|
||||||
|
List<TiledPoint> polygonPoints,
|
||||||
|
List<TiledProperty> properties) {
|
||||||
|
|
||||||
|
public TiledObjectData {
|
||||||
|
name = Objects.requireNonNullElse(name, "").trim();
|
||||||
|
polygonPoints = List.copyOf(Objects.requireNonNull(polygonPoints, "polygonPoints"));
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledObjectLayer(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
List<TiledObjectData> objects,
|
||||||
|
List<TiledProperty> properties) {
|
||||||
|
|
||||||
|
public TiledObjectLayer {
|
||||||
|
name = Objects.requireNonNullElse(name, "").trim();
|
||||||
|
objects = List.copyOf(Objects.requireNonNull(objects, "objects"));
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
public record TiledPoint(double x, double y) {
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledProperty(String name, String type, String value) {
|
||||||
|
public TiledProperty {
|
||||||
|
name = Objects.requireNonNull(name, "name").trim();
|
||||||
|
type = Objects.requireNonNullElse(type, "string").trim();
|
||||||
|
value = Objects.requireNonNullElse(value, "").trim();
|
||||||
|
if (name.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("name must not be blank");
|
||||||
|
}
|
||||||
|
if (type.isBlank()) {
|
||||||
|
type = "string";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTileLayer(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
List<Long> gids,
|
||||||
|
List<TiledProperty> properties) {
|
||||||
|
|
||||||
|
public TiledTileLayer {
|
||||||
|
name = Objects.requireNonNullElse(name, "").trim();
|
||||||
|
gids = List.copyOf(Objects.requireNonNull(gids, "gids"));
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTilesetDocument(
|
||||||
|
String version,
|
||||||
|
String tiledVersion,
|
||||||
|
String name,
|
||||||
|
int tileWidth,
|
||||||
|
int tileHeight,
|
||||||
|
int tileCount,
|
||||||
|
int columns,
|
||||||
|
List<TiledProperty> properties,
|
||||||
|
List<TiledTilesetTile> tiles) {
|
||||||
|
|
||||||
|
public TiledTilesetDocument {
|
||||||
|
version = Objects.requireNonNullElse(version, "1.10");
|
||||||
|
tiledVersion = Objects.requireNonNullElse(tiledVersion, "1.12.1");
|
||||||
|
name = Objects.requireNonNull(name, "name").trim();
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
tiles = List.copyOf(Objects.requireNonNull(tiles, "tiles"));
|
||||||
|
if (name.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("name must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTilesetReference(int firstGid, String source) {
|
||||||
|
public TiledTilesetReference {
|
||||||
|
source = Objects.requireNonNull(source, "source").trim();
|
||||||
|
if (firstGid <= 0) {
|
||||||
|
throw new IllegalArgumentException("firstGid must be positive");
|
||||||
|
}
|
||||||
|
if (source.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("source must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TiledTilesetTile(
|
||||||
|
int id,
|
||||||
|
String imageSource,
|
||||||
|
int imageWidth,
|
||||||
|
int imageHeight,
|
||||||
|
List<TiledProperty> properties,
|
||||||
|
TiledObjectLayer collisionLayer) {
|
||||||
|
|
||||||
|
public TiledTilesetTile {
|
||||||
|
imageSource = Objects.requireNonNull(imageSource, "imageSource").trim();
|
||||||
|
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||||
|
if (imageSource.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("imageSource must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
public final class TiledUnsupportedFeatureException extends RuntimeException {
|
||||||
|
public TiledUnsupportedFeatureException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,460 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.transform.OutputKeys;
|
||||||
|
import javax.xml.transform.TransformerFactory;
|
||||||
|
import javax.xml.transform.dom.DOMSource;
|
||||||
|
import javax.xml.transform.stream.StreamResult;
|
||||||
|
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.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class TiledXmlCodec {
|
||||||
|
public TiledMapDocument readMap(Path path) throws IOException {
|
||||||
|
final Document document = parseDocument(path);
|
||||||
|
final Element root = document.getDocumentElement();
|
||||||
|
if (!"map".equals(root.getTagName())) {
|
||||||
|
throw new IOException("TMX root element must be <map>.");
|
||||||
|
}
|
||||||
|
if (!"0".equals(root.getAttribute("infinite")) && !root.getAttribute("infinite").isBlank()) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Infinite maps are not supported in wave 1.");
|
||||||
|
}
|
||||||
|
rejectMapUnsupportedChildren(root);
|
||||||
|
return new TiledMapDocument(
|
||||||
|
root.getAttribute("version"),
|
||||||
|
root.getAttribute("tiledversion"),
|
||||||
|
root.getAttribute("orientation"),
|
||||||
|
root.getAttribute("renderorder"),
|
||||||
|
intAttribute(root, "width", 0),
|
||||||
|
intAttribute(root, "height", 0),
|
||||||
|
intAttribute(root, "tilewidth", 0),
|
||||||
|
intAttribute(root, "tileheight", 0),
|
||||||
|
intAttribute(root, "nextlayerid", 0),
|
||||||
|
intAttribute(root, "nextobjectid", 0),
|
||||||
|
readProperties(child(root, "properties")),
|
||||||
|
readTilesets(root),
|
||||||
|
readTileLayers(root),
|
||||||
|
readObjectLayers(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeMap(Path path, TiledMapDocument map) throws IOException {
|
||||||
|
final Document document = newDocument();
|
||||||
|
final Element root = document.createElement("map");
|
||||||
|
document.appendChild(root);
|
||||||
|
root.setAttribute("version", map.version());
|
||||||
|
root.setAttribute("tiledversion", map.tiledVersion());
|
||||||
|
root.setAttribute("orientation", map.orientation());
|
||||||
|
root.setAttribute("renderorder", map.renderOrder());
|
||||||
|
root.setAttribute("width", Integer.toString(map.width()));
|
||||||
|
root.setAttribute("height", Integer.toString(map.height()));
|
||||||
|
root.setAttribute("tilewidth", Integer.toString(map.tileWidth()));
|
||||||
|
root.setAttribute("tileheight", Integer.toString(map.tileHeight()));
|
||||||
|
root.setAttribute("infinite", "0");
|
||||||
|
root.setAttribute("nextlayerid", Integer.toString(map.nextLayerId()));
|
||||||
|
root.setAttribute("nextobjectid", Integer.toString(map.nextObjectId()));
|
||||||
|
appendProperties(document, root, map.properties());
|
||||||
|
for (TiledTilesetReference tileset : map.tilesets()) {
|
||||||
|
final Element tilesetElement = document.createElement("tileset");
|
||||||
|
tilesetElement.setAttribute("firstgid", Integer.toString(tileset.firstGid()));
|
||||||
|
tilesetElement.setAttribute("source", tileset.source());
|
||||||
|
root.appendChild(tilesetElement);
|
||||||
|
}
|
||||||
|
for (TiledTileLayer layer : map.tileLayers()) {
|
||||||
|
final Element layerElement = document.createElement("layer");
|
||||||
|
layerElement.setAttribute("id", Integer.toString(layer.id()));
|
||||||
|
layerElement.setAttribute("name", layer.name());
|
||||||
|
layerElement.setAttribute("width", Integer.toString(layer.width()));
|
||||||
|
layerElement.setAttribute("height", Integer.toString(layer.height()));
|
||||||
|
appendProperties(document, layerElement, layer.properties());
|
||||||
|
final Element dataElement = document.createElement("data");
|
||||||
|
dataElement.setAttribute("encoding", "csv");
|
||||||
|
dataElement.setTextContent(csv(layer.gids(), layer.width()));
|
||||||
|
layerElement.appendChild(dataElement);
|
||||||
|
root.appendChild(layerElement);
|
||||||
|
}
|
||||||
|
for (TiledObjectLayer layer : map.objectLayers()) {
|
||||||
|
root.appendChild(writeObjectLayer(document, layer, false));
|
||||||
|
}
|
||||||
|
writeDocument(path, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TiledTilesetDocument readTileset(Path path) throws IOException {
|
||||||
|
final Document document = parseDocument(path);
|
||||||
|
final Element root = document.getDocumentElement();
|
||||||
|
if (!"tileset".equals(root.getTagName())) {
|
||||||
|
throw new IOException("TSX root element must be <tileset>.");
|
||||||
|
}
|
||||||
|
rejectTilesetUnsupportedChildren(root);
|
||||||
|
final List<TiledTilesetTile> tiles = new ArrayList<>();
|
||||||
|
for (Element tileElement : children(root, "tile")) {
|
||||||
|
if (child(tileElement, "animation") != null) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Tiled animations are not supported in wave 1.");
|
||||||
|
}
|
||||||
|
final Element imageElement = child(tileElement, "image");
|
||||||
|
if (imageElement == null) {
|
||||||
|
throw new IOException("TSX tile must contain an <image> element.");
|
||||||
|
}
|
||||||
|
tiles.add(new TiledTilesetTile(
|
||||||
|
intAttribute(tileElement, "id", 0),
|
||||||
|
imageElement.getAttribute("source"),
|
||||||
|
intAttribute(imageElement, "width", 0),
|
||||||
|
intAttribute(imageElement, "height", 0),
|
||||||
|
readProperties(child(tileElement, "properties")),
|
||||||
|
readEmbeddedObjectLayer(tileElement)));
|
||||||
|
}
|
||||||
|
tiles.sort(Comparator.comparingInt(TiledTilesetTile::id));
|
||||||
|
return new TiledTilesetDocument(
|
||||||
|
root.getAttribute("version"),
|
||||||
|
root.getAttribute("tiledversion"),
|
||||||
|
root.getAttribute("name"),
|
||||||
|
intAttribute(root, "tilewidth", 0),
|
||||||
|
intAttribute(root, "tileheight", 0),
|
||||||
|
intAttribute(root, "tilecount", tiles.size()),
|
||||||
|
intAttribute(root, "columns", 0),
|
||||||
|
readProperties(child(root, "properties")),
|
||||||
|
tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTileset(Path path, TiledTilesetDocument tileset) throws IOException {
|
||||||
|
final Document document = newDocument();
|
||||||
|
final Element root = document.createElement("tileset");
|
||||||
|
document.appendChild(root);
|
||||||
|
root.setAttribute("version", tileset.version());
|
||||||
|
root.setAttribute("tiledversion", tileset.tiledVersion());
|
||||||
|
root.setAttribute("name", tileset.name());
|
||||||
|
root.setAttribute("tilewidth", Integer.toString(tileset.tileWidth()));
|
||||||
|
root.setAttribute("tileheight", Integer.toString(tileset.tileHeight()));
|
||||||
|
root.setAttribute("tilecount", Integer.toString(tileset.tileCount()));
|
||||||
|
root.setAttribute("columns", Integer.toString(tileset.columns()));
|
||||||
|
final Element grid = document.createElement("grid");
|
||||||
|
grid.setAttribute("orientation", "orthogonal");
|
||||||
|
grid.setAttribute("width", "1");
|
||||||
|
grid.setAttribute("height", "1");
|
||||||
|
root.appendChild(grid);
|
||||||
|
appendProperties(document, root, tileset.properties());
|
||||||
|
for (TiledTilesetTile tile : tileset.tiles()) {
|
||||||
|
final Element tileElement = document.createElement("tile");
|
||||||
|
tileElement.setAttribute("id", Integer.toString(tile.id()));
|
||||||
|
appendProperties(document, tileElement, tile.properties());
|
||||||
|
final Element imageElement = document.createElement("image");
|
||||||
|
imageElement.setAttribute("source", tile.imageSource());
|
||||||
|
imageElement.setAttribute("width", Integer.toString(tile.imageWidth()));
|
||||||
|
imageElement.setAttribute("height", Integer.toString(tile.imageHeight()));
|
||||||
|
tileElement.appendChild(imageElement);
|
||||||
|
if (tile.collisionLayer() != null) {
|
||||||
|
tileElement.appendChild(writeObjectLayer(document, tile.collisionLayer(), true));
|
||||||
|
}
|
||||||
|
root.appendChild(tileElement);
|
||||||
|
}
|
||||||
|
writeDocument(path, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document parseDocument(Path path) throws IOException {
|
||||||
|
try {
|
||||||
|
final var factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(false);
|
||||||
|
return factory.newDocumentBuilder().parse(path.toFile());
|
||||||
|
} catch (TiledUnsupportedFeatureException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IOException("Unable to parse Tiled XML: " + exception.getMessage(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Document newDocument() throws IOException {
|
||||||
|
try {
|
||||||
|
return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IOException("Unable to create XML document: " + exception.getMessage(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDocument(Path path, Document document) throws IOException {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(Objects.requireNonNull(path, "path").toAbsolutePath().normalize().getParent());
|
||||||
|
final var transformer = TransformerFactory.newInstance().newTransformer();
|
||||||
|
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||||
|
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||||
|
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
|
||||||
|
transformer.transform(new DOMSource(document), new StreamResult(path.toFile()));
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new IOException("Unable to write Tiled XML: " + exception.getMessage(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectMapUnsupportedChildren(Element root) {
|
||||||
|
for (Element child : childElements(root)) {
|
||||||
|
final String tag = child.getTagName();
|
||||||
|
if ("imagelayer".equals(tag) || "group".equals(tag) || "template".equals(tag)) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Unsupported TMX feature in wave 1: " + tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectTilesetUnsupportedChildren(Element root) {
|
||||||
|
for (Element child : childElements(root)) {
|
||||||
|
final String tag = child.getTagName();
|
||||||
|
if ("wangsets".equals(tag) || "tileoffset".equals(tag) || "transformations".equals(tag)) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Unsupported TSX feature in wave 1: " + tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledTilesetReference> readTilesets(Element root) {
|
||||||
|
final List<TiledTilesetReference> tilesets = new ArrayList<>();
|
||||||
|
for (Element tilesetElement : children(root, "tileset")) {
|
||||||
|
if (!tilesetElement.hasAttribute("source")) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Inline tilesets are not supported in wave 1.");
|
||||||
|
}
|
||||||
|
tilesets.add(new TiledTilesetReference(
|
||||||
|
intAttribute(tilesetElement, "firstgid", 1),
|
||||||
|
tilesetElement.getAttribute("source")));
|
||||||
|
}
|
||||||
|
return List.copyOf(tilesets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledTileLayer> readTileLayers(Element root) {
|
||||||
|
final List<TiledTileLayer> layers = new ArrayList<>();
|
||||||
|
for (Element layerElement : children(root, "layer")) {
|
||||||
|
final Element dataElement = child(layerElement, "data");
|
||||||
|
if (dataElement == null || !"csv".equalsIgnoreCase(dataElement.getAttribute("encoding"))) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Wave 1 supports only CSV tile layer encoding.");
|
||||||
|
}
|
||||||
|
layers.add(new TiledTileLayer(
|
||||||
|
intAttribute(layerElement, "id", 0),
|
||||||
|
layerElement.getAttribute("name"),
|
||||||
|
intAttribute(layerElement, "width", 0),
|
||||||
|
intAttribute(layerElement, "height", 0),
|
||||||
|
parseCsvData(dataElement.getTextContent()),
|
||||||
|
readProperties(child(layerElement, "properties"))));
|
||||||
|
}
|
||||||
|
return List.copyOf(layers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledObjectLayer> readObjectLayers(Element root) {
|
||||||
|
final List<TiledObjectLayer> layers = new ArrayList<>();
|
||||||
|
for (Element objectGroup : children(root, "objectgroup")) {
|
||||||
|
layers.add(readObjectLayer(objectGroup));
|
||||||
|
}
|
||||||
|
return List.copyOf(layers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TiledObjectLayer readEmbeddedObjectLayer(Element parent) {
|
||||||
|
final Element objectGroup = child(parent, "objectgroup");
|
||||||
|
return objectGroup == null ? null : readObjectLayer(objectGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TiledObjectLayer readObjectLayer(Element objectGroup) {
|
||||||
|
final List<TiledObjectData> objects = new ArrayList<>();
|
||||||
|
for (Element objectElement : children(objectGroup, "object")) {
|
||||||
|
final Element polygon = child(objectElement, "polygon");
|
||||||
|
if (child(objectElement, "ellipse") != null || child(objectElement, "point") != null || child(objectElement, "polyline") != null) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Unsupported object-layer geometry in wave 1.");
|
||||||
|
}
|
||||||
|
objects.add(new TiledObjectData(
|
||||||
|
intAttribute(objectElement, "id", 0),
|
||||||
|
objectElement.getAttribute("name"),
|
||||||
|
doubleAttribute(objectElement, "x", 0.0d),
|
||||||
|
doubleAttribute(objectElement, "y", 0.0d),
|
||||||
|
doubleAttribute(objectElement, "width", 0.0d),
|
||||||
|
doubleAttribute(objectElement, "height", 0.0d),
|
||||||
|
polygon == null ? List.of() : parsePolygon(polygon.getAttribute("points")),
|
||||||
|
readProperties(child(objectElement, "properties"))));
|
||||||
|
}
|
||||||
|
return new TiledObjectLayer(
|
||||||
|
intAttribute(objectGroup, "id", 0),
|
||||||
|
objectGroup.getAttribute("name"),
|
||||||
|
objects,
|
||||||
|
readProperties(child(objectGroup, "properties")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Element writeObjectLayer(Document document, TiledObjectLayer layer, boolean embedded) {
|
||||||
|
final Element objectGroup = document.createElement("objectgroup");
|
||||||
|
if (layer.id() > 0) {
|
||||||
|
objectGroup.setAttribute("id", Integer.toString(layer.id()));
|
||||||
|
}
|
||||||
|
if (!layer.name().isBlank()) {
|
||||||
|
objectGroup.setAttribute("name", layer.name());
|
||||||
|
}
|
||||||
|
if (embedded) {
|
||||||
|
objectGroup.setAttribute("draworder", "index");
|
||||||
|
}
|
||||||
|
appendProperties(document, objectGroup, layer.properties());
|
||||||
|
for (TiledObjectData object : layer.objects()) {
|
||||||
|
final Element objectElement = document.createElement("object");
|
||||||
|
if (object.id() > 0) {
|
||||||
|
objectElement.setAttribute("id", Integer.toString(object.id()));
|
||||||
|
}
|
||||||
|
if (!object.name().isBlank()) {
|
||||||
|
objectElement.setAttribute("name", object.name());
|
||||||
|
}
|
||||||
|
objectElement.setAttribute("x", formatDecimal(object.x()));
|
||||||
|
objectElement.setAttribute("y", formatDecimal(object.y()));
|
||||||
|
if (object.width() > 0.0d) {
|
||||||
|
objectElement.setAttribute("width", formatDecimal(object.width()));
|
||||||
|
}
|
||||||
|
if (object.height() > 0.0d) {
|
||||||
|
objectElement.setAttribute("height", formatDecimal(object.height()));
|
||||||
|
}
|
||||||
|
appendProperties(document, objectElement, object.properties());
|
||||||
|
if (!object.polygonPoints().isEmpty()) {
|
||||||
|
final Element polygon = document.createElement("polygon");
|
||||||
|
polygon.setAttribute("points", polygonPoints(object.polygonPoints()));
|
||||||
|
objectElement.appendChild(polygon);
|
||||||
|
}
|
||||||
|
objectGroup.appendChild(objectElement);
|
||||||
|
}
|
||||||
|
return objectGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendProperties(Document document, Element parent, List<TiledProperty> properties) {
|
||||||
|
if (properties.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Element propertiesElement = document.createElement("properties");
|
||||||
|
for (TiledProperty property : properties) {
|
||||||
|
final Element propertyElement = document.createElement("property");
|
||||||
|
propertyElement.setAttribute("name", property.name());
|
||||||
|
if (!"string".equals(property.type())) {
|
||||||
|
propertyElement.setAttribute("type", property.type());
|
||||||
|
}
|
||||||
|
propertyElement.setAttribute("value", property.value());
|
||||||
|
propertiesElement.appendChild(propertyElement);
|
||||||
|
}
|
||||||
|
parent.appendChild(propertiesElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledProperty> readProperties(Element propertiesElement) {
|
||||||
|
if (propertiesElement == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final List<TiledProperty> properties = new ArrayList<>();
|
||||||
|
for (Element propertyElement : children(propertiesElement, "property")) {
|
||||||
|
properties.add(new TiledProperty(
|
||||||
|
propertyElement.getAttribute("name"),
|
||||||
|
propertyElement.getAttribute("type"),
|
||||||
|
propertyElement.hasAttribute("value")
|
||||||
|
? propertyElement.getAttribute("value")
|
||||||
|
: propertyElement.getTextContent()));
|
||||||
|
}
|
||||||
|
return List.copyOf(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> parseCsvData(String text) {
|
||||||
|
final List<Long> gids = new ArrayList<>();
|
||||||
|
for (String token : Objects.requireNonNullElse(text, "").split(",")) {
|
||||||
|
final String normalized = token.trim();
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gids.add(Long.parseLong(normalized));
|
||||||
|
}
|
||||||
|
return List.copyOf(gids);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TiledPoint> parsePolygon(String points) {
|
||||||
|
final List<TiledPoint> parsed = new ArrayList<>();
|
||||||
|
for (String segment : Objects.requireNonNullElse(points, "").trim().split(" ")) {
|
||||||
|
final String normalized = segment.trim();
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String[] pair = normalized.split(",");
|
||||||
|
if (pair.length != 2) {
|
||||||
|
throw new TiledUnsupportedFeatureException("Invalid polygon point format in Tiled XML.");
|
||||||
|
}
|
||||||
|
parsed.add(new TiledPoint(Double.parseDouble(pair[0]), Double.parseDouble(pair[1])));
|
||||||
|
}
|
||||||
|
return List.copyOf(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String csv(List<Long> gids, int width) {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
for (int index = 0; index < gids.size(); index += 1) {
|
||||||
|
if (index > 0) {
|
||||||
|
builder.append(index % Math.max(width, 1) == 0 ? ",\n" : ",");
|
||||||
|
}
|
||||||
|
builder.append(Long.toUnsignedString(gids.get(index)));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String polygonPoints(List<TiledPoint> points) {
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
for (int index = 0; index < points.size(); index += 1) {
|
||||||
|
if (index > 0) {
|
||||||
|
builder.append(' ');
|
||||||
|
}
|
||||||
|
builder.append(formatDecimal(points.get(index).x()))
|
||||||
|
.append(',')
|
||||||
|
.append(formatDecimal(points.get(index).y()));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int intAttribute(Element element, String attribute, int fallback) {
|
||||||
|
final String value = element.getAttribute(attribute);
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private double doubleAttribute(Element element, String attribute, double fallback) {
|
||||||
|
final String value = element.getAttribute(attribute);
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Double.parseDouble(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDecimal(double value) {
|
||||||
|
if (Math.rint(value) == value) {
|
||||||
|
return Long.toString(Math.round(value));
|
||||||
|
}
|
||||||
|
return String.format(Locale.ROOT, "%.6f", value)
|
||||||
|
.replaceAll("0+$", "")
|
||||||
|
.replaceAll("\\.$", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Element child(Element parent, String name) {
|
||||||
|
for (Element child : childElements(parent)) {
|
||||||
|
if (name.equals(child.getTagName())) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Element> children(Element parent, String name) {
|
||||||
|
final List<Element> elements = new ArrayList<>();
|
||||||
|
for (Element child : childElements(parent)) {
|
||||||
|
if (name.equals(child.getTagName())) {
|
||||||
|
elements.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Element> childElements(Element parent) {
|
||||||
|
final NodeList children = parent.getChildNodes();
|
||||||
|
final List<Element> elements = new ArrayList<>();
|
||||||
|
for (int index = 0; index < children.getLength(); index += 1) {
|
||||||
|
final Node node = children.item(index);
|
||||||
|
if (node instanceof Element element) {
|
||||||
|
elements.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,6 +24,8 @@ import p.studio.Container;
|
|||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -47,9 +49,11 @@ public final class AddAssetWizard {
|
|||||||
private final TextField assetRootField = new TextField();
|
private final TextField assetRootField = new TextField();
|
||||||
private final TextField assetNameField = new TextField();
|
private final TextField assetNameField = new TextField();
|
||||||
private final ComboBox<AssetFamilyCatalog> assetFamilyCombo = new ComboBox<>();
|
private final ComboBox<AssetFamilyCatalog> assetFamilyCombo = new ComboBox<>();
|
||||||
|
private final ComboBox<AssetStudioGlyphSpecialization> glyphSpecializationCombo = new ComboBox<>();
|
||||||
private final ComboBox<OutputFormatCatalog> outputFormatCombo = new ComboBox<>();
|
private final ComboBox<OutputFormatCatalog> outputFormatCombo = new ComboBox<>();
|
||||||
private final ComboBox<OutputCodecCatalog> outputCodecCombo = new ComboBox<>();
|
private final ComboBox<OutputCodecCatalog> outputCodecCombo = new ComboBox<>();
|
||||||
private final CheckBox preloadCheckBox = new CheckBox();
|
private final CheckBox preloadCheckBox = new CheckBox();
|
||||||
|
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
|
||||||
|
|
||||||
private int stepIndex;
|
private int stepIndex;
|
||||||
private boolean creating;
|
private boolean creating;
|
||||||
@ -65,6 +69,7 @@ public final class AddAssetWizard {
|
|||||||
|
|
||||||
preloadCheckBox.setSelected(false);
|
preloadCheckBox.setSelected(false);
|
||||||
configureAssetFamilyCombo();
|
configureAssetFamilyCombo();
|
||||||
|
configureGlyphSpecializationCombo();
|
||||||
configureOutputFormatCombo();
|
configureOutputFormatCombo();
|
||||||
configureOutputCodecCombo();
|
configureOutputCodecCombo();
|
||||||
renderStep();
|
renderStep();
|
||||||
@ -131,7 +136,39 @@ public final class AddAssetWizard {
|
|||||||
});
|
});
|
||||||
assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||||
if (!Objects.equals(oldValue, newValue)) {
|
if (!Objects.equals(oldValue, newValue)) {
|
||||||
|
if (newValue != AssetFamilyCatalog.GLYPH_BANK) {
|
||||||
|
glyphSpecializationCombo.getSelectionModel().clearSelection();
|
||||||
|
} else if (glyphSpecializationCombo.getValue() == null) {
|
||||||
|
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.SPRITES);
|
||||||
|
}
|
||||||
refreshOutputFormats();
|
refreshOutputFormats();
|
||||||
|
if (stepIndex == 1) {
|
||||||
|
renderStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureGlyphSpecializationCombo() {
|
||||||
|
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.SPRITES);
|
||||||
|
glyphSpecializationCombo.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
glyphSpecializationCombo.setButtonCell(new javafx.scene.control.ListCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -224,17 +261,23 @@ public final class AddAssetWizard {
|
|||||||
|
|
||||||
final Label nameLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME));
|
final Label nameLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME));
|
||||||
final Label typeLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE));
|
final Label typeLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE));
|
||||||
|
final Label specializationLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION));
|
||||||
final Label formatLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT));
|
final Label formatLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT));
|
||||||
final Label codecLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC));
|
final Label codecLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC));
|
||||||
final Label preloadLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_PRELOAD));
|
final Label preloadLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_PRELOAD));
|
||||||
final Label noteLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE));
|
final Label noteLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE));
|
||||||
noteLabel.setWrapText(true);
|
noteLabel.setWrapText(true);
|
||||||
noteLabel.getStyleClass().add("studio-launcher-subtitle");
|
noteLabel.getStyleClass().add("studio-launcher-subtitle");
|
||||||
|
final VBox specializationBox = new VBox(6, specializationLabel, glyphSpecializationCombo);
|
||||||
|
final boolean specializationVisible = selectedFamily() == AssetFamilyCatalog.GLYPH_BANK;
|
||||||
|
specializationBox.setVisible(specializationVisible);
|
||||||
|
specializationBox.setManaged(specializationVisible);
|
||||||
|
|
||||||
preloadCheckBox.setText("");
|
preloadCheckBox.setText("");
|
||||||
stepBody.getChildren().setAll(
|
stepBody.getChildren().setAll(
|
||||||
new VBox(6, nameLabel, assetNameField),
|
new VBox(6, nameLabel, assetNameField),
|
||||||
new VBox(6, typeLabel, assetFamilyCombo),
|
new VBox(6, typeLabel, assetFamilyCombo),
|
||||||
|
specializationBox,
|
||||||
new VBox(6, formatLabel, outputFormatCombo),
|
new VBox(6, formatLabel, outputFormatCombo),
|
||||||
new VBox(6, codecLabel, outputCodecCombo),
|
new VBox(6, codecLabel, outputCodecCombo),
|
||||||
new VBox(6, preloadLabel, preloadCheckBox),
|
new VBox(6, preloadLabel, preloadCheckBox),
|
||||||
@ -293,6 +336,11 @@ public final class AddAssetWizard {
|
|||||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_TYPE));
|
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_TYPE));
|
||||||
return false;
|
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) {
|
if (selectedFormat() == null || selectedFormat() == OutputFormatCatalog.UNKNOWN) {
|
||||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_FORMAT));
|
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_FORMAT));
|
||||||
return false;
|
return false;
|
||||||
@ -360,6 +408,15 @@ public final class AddAssetWizard {
|
|||||||
private void applyCreateResult(CreateAssetResult createResult) {
|
private void applyCreateResult(CreateAssetResult createResult) {
|
||||||
creating = false;
|
creating = false;
|
||||||
if (createResult.status() == PackerOperationStatus.SUCCESS && createResult.assetReference() != null) {
|
if (createResult.status() == PackerOperationStatus.SUCCESS && createResult.assetReference() != null) {
|
||||||
|
try {
|
||||||
|
persistStudioMetadata(createResult.assetRoot());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
feedbackLabel.setText(exception.getMessage() == null || exception.getMessage().isBlank()
|
||||||
|
? "Unable to persist Studio metadata."
|
||||||
|
: exception.getMessage());
|
||||||
|
renderStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
result.set(createResult.assetReference());
|
result.set(createResult.assetReference());
|
||||||
stage.close();
|
stage.close();
|
||||||
return;
|
return;
|
||||||
@ -409,6 +466,17 @@ public final class AddAssetWizard {
|
|||||||
return outputCodecCombo.getValue();
|
return outputCodecCombo.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AssetStudioGlyphSpecialization selectedGlyphSpecialization() {
|
||||||
|
return glyphSpecializationCombo.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistStudioMetadata(Path assetRoot) throws IOException {
|
||||||
|
if (selectedFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
studioMetadataService.writeGlyphSpecialization(assetRoot, selectedGlyphSpecialization());
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizedRelativeRoot(String candidate) {
|
private String normalizedRelativeRoot(String candidate) {
|
||||||
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
||||||
if (raw.isBlank()) {
|
if (raw.isBlank()) {
|
||||||
|
|||||||
@ -135,6 +135,9 @@ assets.badge.diagnostics=Diagnostics
|
|||||||
assets.section.summary=Summary
|
assets.section.summary=Summary
|
||||||
assets.section.runtimeContract=Runtime Contract
|
assets.section.runtimeContract=Runtime Contract
|
||||||
assets.section.bankComposition=Bank Composition
|
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.section.paletteOverhauling=Palette Overhauling
|
||||||
assets.subsection.codecConfiguration=Codec Configuration
|
assets.subsection.codecConfiguration=Codec Configuration
|
||||||
assets.subsection.metadata=Metadata
|
assets.subsection.metadata=Metadata
|
||||||
@ -144,6 +147,12 @@ assets.section.actions=Actions
|
|||||||
assets.actions.empty=No actions available for this asset.
|
assets.actions.empty=No actions available for this asset.
|
||||||
assets.action.register=Register
|
assets.action.register=Register
|
||||||
assets.action.analyse=Analyse
|
assets.action.analyse=Analyse
|
||||||
|
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.action.delete=Delete
|
||||||
assets.deleteDialog.title=Delete Asset
|
assets.deleteDialog.title=Delete Asset
|
||||||
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
|
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
|
||||||
@ -181,10 +190,30 @@ assets.label.registration=Registration
|
|||||||
assets.label.buildParticipation=Build Participation
|
assets.label.buildParticipation=Build Participation
|
||||||
assets.label.assetId=Asset ID
|
assets.label.assetId=Asset ID
|
||||||
assets.label.type=Type
|
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.glyphBank=Glyph Bank
|
||||||
|
assets.type.sceneBank=Scene Bank
|
||||||
assets.type.paletteBank=Palette Bank
|
assets.type.paletteBank=Palette Bank
|
||||||
assets.type.soundBank=Sound Bank
|
assets.type.soundBank=Sound Bank
|
||||||
assets.type.unknown=Unknown
|
assets.type.unknown=Unknown
|
||||||
|
assets.specialization.none=None
|
||||||
|
assets.specialization.tileset=Tileset
|
||||||
|
assets.specialization.sprites=Sprites
|
||||||
|
assets.specialization.ui=UI
|
||||||
|
assets.sceneStatus.pendingValidation=Pending Validation
|
||||||
|
assets.sceneStatus.validatedPendingAcceptance=Validated / Pending Acceptance
|
||||||
|
assets.sceneStatus.ready=Ready
|
||||||
|
assets.sceneStatus.validationFailed=Validation Failed
|
||||||
assets.label.location=Location
|
assets.label.location=Location
|
||||||
assets.label.bank=Bank
|
assets.label.bank=Bank
|
||||||
assets.label.targetLocation=Target Location
|
assets.label.targetLocation=Target Location
|
||||||
@ -197,6 +226,8 @@ assets.value.included=Included
|
|||||||
assets.value.excluded=Excluded
|
assets.value.excluded=Excluded
|
||||||
assets.value.yes=Yes
|
assets.value.yes=Yes
|
||||||
assets.value.no=No
|
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.idle=Assets workspace idle.
|
||||||
assets.progress.refreshing=Refreshing assets...
|
assets.progress.refreshing=Refreshing assets...
|
||||||
assets.progress.loadingDetails=Loading selected asset details...
|
assets.progress.loadingDetails=Loading selected asset details...
|
||||||
@ -232,6 +263,22 @@ assets.paletteOverhauling.selected=Bank Palettes
|
|||||||
assets.paletteOverhauling.tileSelector=Preview Tile
|
assets.paletteOverhauling.tileSelector=Preview Tile
|
||||||
assets.paletteOverhauling.appliedPalette=Applied Palette
|
assets.paletteOverhauling.appliedPalette=Applied Palette
|
||||||
assets.paletteOverhauling.previewEmpty=Select at least one palette to inspect it on a tile.
|
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.codecConfiguration.empty=This codec does not expose configuration fields yet.
|
||||||
assets.details.metadata.empty=This asset does not expose metadata fields yet.
|
assets.details.metadata.empty=This asset does not expose metadata fields yet.
|
||||||
assets.addWizard.title=Add Asset
|
assets.addWizard.title=Add Asset
|
||||||
@ -245,10 +292,12 @@ assets.addWizard.step.summary.description=Confirm the registered asset you are a
|
|||||||
assets.addWizard.label.name=Asset Name
|
assets.addWizard.label.name=Asset Name
|
||||||
assets.addWizard.label.root=Asset Root
|
assets.addWizard.label.root=Asset Root
|
||||||
assets.addWizard.label.type=Asset Type
|
assets.addWizard.label.type=Asset Type
|
||||||
|
assets.addWizard.label.specialization=Studio Specialization
|
||||||
assets.addWizard.label.format=Output Format
|
assets.addWizard.label.format=Output Format
|
||||||
assets.addWizard.label.codec=Output Codec
|
assets.addWizard.label.codec=Output Codec
|
||||||
assets.addWizard.label.preload=Preload on startup
|
assets.addWizard.label.preload=Preload on startup
|
||||||
assets.addWizard.prompt.type=Choose asset type
|
assets.addWizard.prompt.type=Choose asset type
|
||||||
|
assets.addWizard.prompt.specialization=Choose Studio specialization
|
||||||
assets.addWizard.prompt.format=Choose output format
|
assets.addWizard.prompt.format=Choose output format
|
||||||
assets.addWizard.prompt.codec=Choose output codec
|
assets.addWizard.prompt.codec=Choose output codec
|
||||||
assets.addWizard.assetsRootHint=Assets root: {0}
|
assets.addWizard.assetsRootHint=Assets root: {0}
|
||||||
@ -261,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.rootOutsideAssets=The selected directory must stay inside assets/.
|
||||||
assets.addWizard.error.rootAlreadyAsset=The selected root already contains asset.json.
|
assets.addWizard.error.rootAlreadyAsset=The selected root already contains asset.json.
|
||||||
assets.addWizard.error.type=Choose an asset type before continuing.
|
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.format=Choose an output format before continuing.
|
||||||
assets.addWizard.error.codec=Choose an output codec 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.
|
assets.addWizard.error.unsupportedCombination=The selected asset type, output format, and codec combination is not supported.
|
||||||
|
|||||||
@ -114,6 +114,20 @@
|
|||||||
-fx-padding: 8;
|
-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 {
|
.studio-button.studio-button-primary {
|
||||||
-fx-background-color: #27507a;
|
-fx-background-color: #27507a;
|
||||||
-fx-border-color: #5ea0de;
|
-fx-border-color: #5ea0de;
|
||||||
@ -1213,6 +1227,15 @@
|
|||||||
-fx-background-color: #162a20;
|
-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 {
|
.assets-workspace-asset-row-tone-generic {
|
||||||
-fx-background-color: #11151b;
|
-fx-background-color: #11151b;
|
||||||
-fx-border-color: #334050;
|
-fx-border-color: #334050;
|
||||||
@ -1240,6 +1263,10 @@
|
|||||||
-fx-text-fill: #aee8c5;
|
-fx-text-fill: #aee8c5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assets-workspace-asset-name-tone-scene {
|
||||||
|
-fx-text-fill: #f5d47a;
|
||||||
|
}
|
||||||
|
|
||||||
.assets-workspace-asset-name-tone-generic {
|
.assets-workspace-asset-name-tone-generic {
|
||||||
-fx-text-fill: #f6fbff;
|
-fx-text-fill: #f6fbff;
|
||||||
}
|
}
|
||||||
@ -1457,6 +1484,12 @@
|
|||||||
-fx-max-height: 446;
|
-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 {
|
.assets-details-bank-composition-body {
|
||||||
-fx-alignment: top-left;
|
-fx-alignment: top-left;
|
||||||
}
|
}
|
||||||
@ -1465,6 +1498,76 @@
|
|||||||
-fx-alignment: top-left;
|
-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 {
|
.assets-details-bank-file-cell {
|
||||||
-fx-alignment: center-left;
|
-fx-alignment: center-left;
|
||||||
}
|
}
|
||||||
@ -1500,6 +1603,43 @@
|
|||||||
-fx-alignment: center-left;
|
-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 {
|
.assets-details-palette-hash {
|
||||||
-fx-text-fill: #9fc3e7;
|
-fx-text-fill: #9fc3e7;
|
||||||
-fx-font-size: 10px;
|
-fx-font-size: 10px;
|
||||||
@ -1594,6 +1734,124 @@
|
|||||||
-fx-border-width: 1;
|
-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 {
|
.assets-details-chip-image {
|
||||||
-fx-background-color: #11283a;
|
-fx-background-color: #11283a;
|
||||||
-fx-border-color: #5cb6ff;
|
-fx-border-color: #5cb6ff;
|
||||||
@ -1606,6 +1864,12 @@
|
|||||||
-fx-text-fill: #dde6ff;
|
-fx-text-fill: #dde6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assets-details-chip-scene {
|
||||||
|
-fx-background-color: #2a1f12;
|
||||||
|
-fx-border-color: #d4a94a;
|
||||||
|
-fx-text-fill: #ffe5a6;
|
||||||
|
}
|
||||||
|
|
||||||
.assets-details-chip-generic {
|
.assets-details-chip-generic {
|
||||||
-fx-background-color: #1e242c;
|
-fx-background-color: #1e242c;
|
||||||
-fx-border-color: #5f7388;
|
-fx-border-color: #5f7388;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import p.packer.messages.assets.AssetFamilyCatalog;
|
|||||||
import p.packer.messages.assets.OutputCodecCatalog;
|
import p.packer.messages.assets.OutputCodecCatalog;
|
||||||
import p.packer.messages.assets.OutputFormatCatalog;
|
import p.packer.messages.assets.OutputFormatCatalog;
|
||||||
import p.packer.messages.assets.PackerCodecConfigurationFieldType;
|
import p.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||||
|
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||||
import p.studio.workspaces.assets.messages.*;
|
import p.studio.workspaces.assets.messages.*;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -114,6 +115,9 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
|||||||
List.of(),
|
List.of(),
|
||||||
0L),
|
0L),
|
||||||
List.of(),
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
List.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +140,9 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
|||||||
List.of(),
|
List.of(),
|
||||||
0L),
|
0L),
|
||||||
List.of(),
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
List.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +166,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
|||||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
1,
|
1,
|
||||||
family,
|
family,
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
Path.of("/tmp/bank"),
|
Path.of("/tmp/bank"),
|
||||||
false,
|
false,
|
||||||
false);
|
false);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import p.packer.messages.AssetReference;
|
|||||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||||
import p.packer.messages.assets.OutputCodecCatalog;
|
import p.packer.messages.assets.OutputCodecCatalog;
|
||||||
import p.packer.messages.assets.OutputFormatCatalog;
|
import p.packer.messages.assets.OutputFormatCatalog;
|
||||||
|
import p.studio.workspaces.assets.metadata.*;
|
||||||
import p.studio.workspaces.assets.messages.*;
|
import p.studio.workspaces.assets.messages.*;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -15,64 +16,388 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||||
@Test
|
@Test
|
||||||
void defaultsToFirstTilePaletteWhenNoSelectedFilesExist() {
|
void derivesOriginalPaletteFromReadySchemaWhenNoPaletteRecordExists() {
|
||||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
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());
|
assertTrue(coordinator.ready());
|
||||||
assertEquals(0, coordinator.viewModel().selectedFiles().size());
|
assertEquals(1, coordinator.viewModel().allFiles().size());
|
||||||
assertNull(coordinator.viewModel().previewTileFile());
|
assertEquals("schema-1 / original", coordinator.viewModel().allFiles().getFirst().displayName());
|
||||||
assertNull(coordinator.viewModel().previewPaletteFile());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void clickingSelectedPaletteChangesOnlyPreviewPalette() {
|
void initialSelectionStillMatchesBuildFacingPipelinePalettePayload() {
|
||||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
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))));
|
final AssetStudioGlyphMaterialPalette persisted = new AssetStudioGlyphMaterialPalette(
|
||||||
coordinator.beginEdit();
|
"palette-1",
|
||||||
|
"schema-1",
|
||||||
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
|
1,
|
||||||
coordinator.moveToSelected(List.of(first));
|
List.of("#FF0000", "#00FF00"));
|
||||||
coordinator.selectPreviewPalette(first);
|
coordinator.replaceDetails(details(
|
||||||
|
List.of(tile("tile-a.png", "artifact-a")),
|
||||||
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
|
new AssetStudioGlyphMaterialMetadata(
|
||||||
assertEquals(1, coordinator.viewModel().selectedFiles().size());
|
List.of(new AssetStudioGlyphMaterialSchema(
|
||||||
}
|
"schema-1",
|
||||||
|
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||||
@Test
|
1,
|
||||||
void removingPreviewedPaletteFallsBackToFirstRemainingSelectedPalette() {
|
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||||
final List<AssetWorkspaceBankCompositionFile> generated = files("tile", 3);
|
List.of(persisted),
|
||||||
coordinator.replaceDetails(tileDetails(List.of(generated.get(0), generated.get(1)), List.of(generated.get(2))));
|
null),
|
||||||
coordinator.beginEdit();
|
List.of(Map.of(
|
||||||
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
|
"originalArgb8888", List.of(0xFFFF0000, 0xFF00FF00),
|
||||||
coordinator.moveToSelected(List.of(first));
|
"convertedRgb565", List.of(0xF800, 0x07E0)))));
|
||||||
coordinator.selectPreviewPalette(first);
|
|
||||||
|
|
||||||
coordinator.moveToAvailable(List.of(first));
|
|
||||||
|
|
||||||
assertEquals(1, coordinator.viewModel().selectedFiles().size());
|
assertEquals(1, coordinator.viewModel().selectedFiles().size());
|
||||||
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
|
assertEquals("palette-1", coordinator.viewModel().selectedFiles().getFirst().metadata().get("paletteId"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void deduplicatesPalettesByColorHash() {
|
void previewPaletteDefaultsToAvailablePaletteWhenBuildSelectionIsEmpty() {
|
||||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||||
final AssetWorkspaceBankCompositionFile first = file("tile-a.png", List.of(0xFFFF0000, 0xFF00FF00));
|
coordinator.replaceDetails(details(
|
||||||
final AssetWorkspaceBankCompositionFile duplicate = file("tile-b.png", List.of(0xFFFF0000, 0xFF00FF00));
|
List.of(tile("tile-a.png", "artifact-a")),
|
||||||
final AssetWorkspaceBankCompositionFile unique = file("tile-c.png", List.of(0xFF0000FF));
|
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(2, coordinator.viewModel().allFiles().size());
|
||||||
assertEquals(10, coordinator.viewModel().allFiles().getFirst().displayName().length());
|
final AssetWorkspaceBankCompositionFile original = coordinator.viewModel().allFiles().stream()
|
||||||
assertEquals(coordinator.viewModel().allFiles().stream().map(AssetWorkspaceBankCompositionFile::displayName).distinct().count(),
|
.filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsOriginal)
|
||||||
coordinator.viewModel().allFiles().size());
|
.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> selectedFiles,
|
||||||
List<AssetWorkspaceBankCompositionFile> availableFiles) {
|
AssetStudioGlyphMaterialMetadata metadata,
|
||||||
|
List<Map<String, Object>> pipelinePalettes) {
|
||||||
return new AssetWorkspaceAssetDetails(
|
return new AssetWorkspaceAssetDetails(
|
||||||
new AssetWorkspaceAssetSummary(
|
new AssetWorkspaceAssetSummary(
|
||||||
AssetReference.forAssetId(1),
|
AssetReference.forAssetId(1),
|
||||||
@ -81,6 +406,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
|||||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
1,
|
1,
|
||||||
AssetFamilyCatalog.GLYPH_BANK,
|
AssetFamilyCatalog.GLYPH_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.SPRITES,
|
||||||
Path.of("/tmp/bank"),
|
Path.of("/tmp/bank"),
|
||||||
false,
|
false,
|
||||||
false),
|
false),
|
||||||
@ -91,46 +417,29 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
|||||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
List.of(),
|
List.of(),
|
||||||
Map.of(),
|
Map.of(),
|
||||||
new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L),
|
new AssetWorkspaceBankCompositionDetails(List.of(), selectedFiles, 0L),
|
||||||
selectedFiles.stream().map(file -> (Map<String, Object>) file.metadata().get("palette")).toList(),
|
pipelinePalettes,
|
||||||
|
metadata,
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
List.of());
|
List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AssetWorkspaceBankCompositionFile> files(String prefix, int count) {
|
private AssetWorkspaceBankCompositionFile tile(String path, String artifactId) {
|
||||||
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) {
|
|
||||||
return new AssetWorkspaceBankCompositionFile(
|
return new AssetWorkspaceBankCompositionFile(
|
||||||
path,
|
path,
|
||||||
path,
|
path,
|
||||||
256L,
|
256L,
|
||||||
1L,
|
1L,
|
||||||
null,
|
"artifact-a".equals(artifactId) ? "fp-a" : "fp-b",
|
||||||
Map.of(
|
Map.of(
|
||||||
|
"artifactId", artifactId,
|
||||||
"tile", Map.of(
|
"tile", Map.of(
|
||||||
"width", 2,
|
"width", 2,
|
||||||
"height", 2,
|
"height", 2,
|
||||||
"paletteIndices", List.of(0, 1, 2, 0)),
|
"paletteIndices", List.of(0, 1, 2, 0)),
|
||||||
"palette", Map.of(
|
"palette", Map.of(
|
||||||
"originalArgb8888", colors,
|
"originalArgb8888", List.of(0xFFFF0000, 0xFF00FF00),
|
||||||
"convertedRgb565", colors.stream().map(this::toRgb565).toList())));
|
"convertedRgb565", List.of(0xF800, 0x07E0))));
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
package p.studio.workspaces.assets.metadata;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
final class AssetStudioMetadataServiceTest {
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private final AssetStudioMetadataService service = new AssetStudioMetadataService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readsGlyphSpecializationFromStudioMetadataFile() throws Exception {
|
||||||
|
final Path assetRoot = tempDir.resolve("tileset");
|
||||||
|
Files.createDirectories(assetRoot);
|
||||||
|
service.writeGlyphSpecialization(assetRoot, AssetStudioGlyphSpecialization.TILESET);
|
||||||
|
|
||||||
|
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.GLYPH_BANK);
|
||||||
|
|
||||||
|
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");
|
||||||
|
Files.createDirectories(assetRoot);
|
||||||
|
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"map_width": 20,
|
||||||
|
"map_height": 12,
|
||||||
|
"layer_count": 4,
|
||||||
|
"layers": [
|
||||||
|
{ "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": "" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||||
|
|
||||||
|
assertNotNull(snapshot.sceneBankMetadata());
|
||||||
|
assertEquals(20, snapshot.sceneBankMetadata().mapWidth());
|
||||||
|
assertEquals(12, snapshot.sceneBankMetadata().mapHeight());
|
||||||
|
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()
|
||||||
|
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||||
|
.map(AssetStudioSceneLayerBinding::tilemap)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsSceneBankSupportMetadataWhenLayerCountIsNotFixedAtFour() throws Exception {
|
||||||
|
final Path assetRoot = tempDir.resolve("scene-invalid");
|
||||||
|
Files.createDirectories(assetRoot);
|
||||||
|
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"map_width": 16,
|
||||||
|
"map_height": 16,
|
||||||
|
"layer_count": 3,
|
||||||
|
"layers": [
|
||||||
|
{ "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": "" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||||
|
|
||||||
|
assertNull(snapshot.sceneBankMetadata());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,204 @@
|
|||||||
|
package p.studio.workspaces.assets.scene;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||||
|
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.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
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.*;
|
||||||
|
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService;
|
||||||
|
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
final class SceneBankWorkflowServiceTest {
|
||||||
|
private final TiledAssetGenerationService generationService = new TiledAssetGenerationService();
|
||||||
|
private final SceneBankWorkflowService workflowService = new SceneBankWorkflowService();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validateThenAcceptPromotesSceneBankToReadyUntilFilesChange() throws Exception {
|
||||||
|
final ProjectReference project = projectReference();
|
||||||
|
final AssetWorkspaceAssetDetails sceneDetails = createValidScene(project);
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult initial = workflowService.inspect(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.PENDING_VALIDATION, initial.validation().status());
|
||||||
|
assertFalse(initial.validation().canAccept());
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult validated = workflowService.validate(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.VALIDATED_PENDING_ACCEPTANCE, validated.validation().status());
|
||||||
|
assertTrue(validated.validation().canAccept());
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult accepted = workflowService.accept(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.READY, accepted.validation().status());
|
||||||
|
assertFalse(accepted.validation().pendingExternalChanges());
|
||||||
|
|
||||||
|
final Path tmxPath = sceneDetails.summary().assetRoot().resolve("ground.tmx");
|
||||||
|
Files.writeString(tmxPath, Files.readString(tmxPath).replaceFirst("0,0,0", "1,0,0"));
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult changed = workflowService.inspect(project, sceneDetails);
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.PENDING_VALIDATION, changed.validation().status());
|
||||||
|
assertTrue(changed.validation().pendingExternalChanges());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validationFailsWhenReferencedTsxIsMissing() throws Exception {
|
||||||
|
final ProjectReference project = projectReference();
|
||||||
|
final Path sceneRoot = project.rootPath().resolve("assets/scenes/broken");
|
||||||
|
Files.createDirectories(sceneRoot);
|
||||||
|
final AssetWorkspaceAssetDetails brokenScene = new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(2),
|
||||||
|
"broken_scene",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
|
2,
|
||||||
|
AssetFamilyCatalog.SCENE_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
|
sceneRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.SCENE_TILED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
new AssetStudioSceneBankMetadata(
|
||||||
|
8,
|
||||||
|
8,
|
||||||
|
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());
|
||||||
|
|
||||||
|
Files.writeString(sceneRoot.resolve("ground.tmx"), """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down"
|
||||||
|
width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="../../tilesets/missing/tileset.tsx"/>
|
||||||
|
<layer id="1" name="Ground" width="8" height="8">
|
||||||
|
<data encoding="csv">0</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
|
""");
|
||||||
|
|
||||||
|
final SceneBankWorkflowResult result = workflowService.inspect(project, brokenScene);
|
||||||
|
|
||||||
|
assertEquals(AssetWorkspaceSceneBankStatus.VALIDATION_FAILED, result.validation().status());
|
||||||
|
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Referenced TSX file is missing")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectReference projectReference() {
|
||||||
|
return new ProjectReference("main", "1", "pbs", 1, tempDir.resolve("project"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails createValidScene(ProjectReference project) throws Exception {
|
||||||
|
final Path tilesetRoot = project.rootPath().resolve("assets/tilesets/overworld");
|
||||||
|
Files.createDirectories(tilesetRoot);
|
||||||
|
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||||
|
assertTrue(generationService.generateTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||||
|
|
||||||
|
final Path sceneRoot = project.rootPath().resolve("assets/scenes/overworld");
|
||||||
|
Files.createDirectories(sceneRoot);
|
||||||
|
final AssetWorkspaceAssetDetails scene = sceneDetails(sceneRoot);
|
||||||
|
assertTrue(generationService.generateSceneBankTilemaps(project, scene).success());
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails tilesetDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(1),
|
||||||
|
"overworld_tileset",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
|
1,
|
||||||
|
AssetFamilyCatalog.GLYPH_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.TILESET,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.GLYPH_INDEXED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(new PackerCodecConfigurationFieldDTO(
|
||||||
|
"tile_size",
|
||||||
|
"Tile Size",
|
||||||
|
PackerCodecConfigurationFieldType.ENUM,
|
||||||
|
"16x16",
|
||||||
|
true,
|
||||||
|
List.of("8x8", "16x16", "32x32"))),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(
|
||||||
|
List.of(),
|
||||||
|
List.of(new AssetWorkspaceBankCompositionFile("a.png", "a.png", 1L, 1L, null, Map.of())),
|
||||||
|
0L),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(2),
|
||||||
|
"overworld_scene",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
|
2,
|
||||||
|
AssetFamilyCatalog.SCENE_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.SCENE_TILED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
new AssetStudioSceneBankMetadata(
|
||||||
|
16,
|
||||||
|
12,
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||||
|
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.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
final class TiledAssetGenerationServiceTest {
|
||||||
|
private final TiledAssetGenerationService service = new TiledAssetGenerationService();
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generatesTsxForTilesetSpecializedGlyphBank() throws Exception {
|
||||||
|
final Path assetRoot = tempDir.resolve("assets/tilesets/overworld");
|
||||||
|
Files.createDirectories(assetRoot);
|
||||||
|
Files.writeString(assetRoot.resolve("a.png"), "fixture");
|
||||||
|
Files.writeString(assetRoot.resolve("b.png"), "fixture");
|
||||||
|
|
||||||
|
final TiledAssetGenerationResult result = service.generateTilesetTsx(tilesetDetails(assetRoot));
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
final Path tsxPath = assetRoot.resolve(TiledAssetGenerationService.GENERATED_TSX_FILE);
|
||||||
|
assertTrue(Files.isRegularFile(tsxPath));
|
||||||
|
final TiledTilesetDocument tileset = codec.readTileset(tsxPath);
|
||||||
|
assertEquals(2, tileset.tiles().size());
|
||||||
|
assertEquals("a.png", tileset.tiles().get(0).imageSource());
|
||||||
|
assertEquals("glyph_id", tileset.tiles().get(0).properties().getFirst().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generatesTmxFilesForSceneBankUsingReferencedTsx() throws Exception {
|
||||||
|
final Path projectRoot = tempDir.resolve("project");
|
||||||
|
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||||
|
Files.createDirectories(tilesetRoot);
|
||||||
|
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||||
|
assertTrue(service.generateTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||||
|
|
||||||
|
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||||
|
Files.createDirectories(sceneRoot);
|
||||||
|
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||||
|
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||||
|
|
||||||
|
final TiledAssetGenerationResult result = service.generateSceneBankTilemaps(projectReference, details);
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
final Path tmxPath = sceneRoot.resolve("ground.tmx");
|
||||||
|
assertTrue(Files.isRegularFile(tmxPath));
|
||||||
|
final TiledMapDocument map = codec.readMap(tmxPath);
|
||||||
|
assertEquals("../../tilesets/overworld/tileset.tsx", map.tilesets().getFirst().source());
|
||||||
|
assertEquals(16, map.width());
|
||||||
|
assertEquals(12, map.height());
|
||||||
|
assertEquals("Ground", map.tileLayers().getFirst().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails tilesetDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(1),
|
||||||
|
"overworld_tileset",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||||
|
1,
|
||||||
|
AssetFamilyCatalog.GLYPH_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.TILESET,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.GLYPH_INDEXED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(new PackerCodecConfigurationFieldDTO(
|
||||||
|
"tile_size",
|
||||||
|
"Tile Size",
|
||||||
|
PackerCodecConfigurationFieldType.ENUM,
|
||||||
|
"16x16",
|
||||||
|
true,
|
||||||
|
List.of("8x8", "16x16", "32x32"))),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(
|
||||||
|
List.of(),
|
||||||
|
List.of(
|
||||||
|
new AssetWorkspaceBankCompositionFile("a.png", "a.png", 1L, 1L, null, Map.of()),
|
||||||
|
new AssetWorkspaceBankCompositionFile("b.png", "b.png", 1L, 1L, null, Map.of())),
|
||||||
|
0L),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||||
|
List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
|
||||||
|
return new AssetWorkspaceAssetDetails(
|
||||||
|
new AssetWorkspaceAssetSummary(
|
||||||
|
AssetReference.forAssetId(2),
|
||||||
|
"overworld_scene",
|
||||||
|
AssetWorkspaceAssetState.REGISTERED,
|
||||||
|
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||||
|
2,
|
||||||
|
AssetFamilyCatalog.SCENE_BANK,
|
||||||
|
AssetStudioGlyphSpecialization.NONE,
|
||||||
|
assetRoot,
|
||||||
|
false,
|
||||||
|
false),
|
||||||
|
List.of(),
|
||||||
|
OutputFormatCatalog.SCENE_TILED_V1,
|
||||||
|
OutputCodecCatalog.NONE,
|
||||||
|
List.of(OutputCodecCatalog.NONE),
|
||||||
|
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||||
|
List.of(),
|
||||||
|
Map.of(),
|
||||||
|
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
new AssetStudioSceneBankMetadata(
|
||||||
|
16,
|
||||||
|
12,
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package p.studio.workspaces.assets.tiled;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class TiledXmlCodecTest {
|
||||||
|
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readsFixtureTmxAndPreservesWaveOneSurfaces() throws Exception {
|
||||||
|
final TiledMapDocument map = codec.readMap(Path.of("..", "test-projects", "main", "assets", "scenes", "primeiro mapa.tmx").toAbsolutePath().normalize());
|
||||||
|
|
||||||
|
assertEquals(25, map.width());
|
||||||
|
assertEquals(25, map.height());
|
||||||
|
assertEquals(1, map.tilesets().size());
|
||||||
|
assertEquals("../Zelda3/primeiro tileset.tsx", map.tilesets().getFirst().source());
|
||||||
|
assertEquals(1, map.tileLayers().size());
|
||||||
|
assertEquals(625, map.tileLayers().getFirst().gids().size());
|
||||||
|
assertEquals(1, map.objectLayers().size());
|
||||||
|
assertEquals(9, map.objectLayers().getFirst().objects().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readsFixtureTsxWithPropertiesAndCollisionObjects() throws Exception {
|
||||||
|
final TiledTilesetDocument tileset = codec.readTileset(Path.of("..", "test-projects", "main", "assets", "Zelda3", "primeiro tileset.tsx").toAbsolutePath().normalize());
|
||||||
|
|
||||||
|
assertEquals(32, tileset.tileWidth());
|
||||||
|
assertEquals(32, tileset.tileHeight());
|
||||||
|
assertEquals(13, tileset.tileCount());
|
||||||
|
assertEquals(13, tileset.tiles().size());
|
||||||
|
assertEquals("glyph_id", tileset.tiles().getFirst().properties().getFirst().name());
|
||||||
|
assertNotNull(tileset.tiles().getFirst().collisionLayer());
|
||||||
|
assertEquals(2, tileset.tiles().getFirst().collisionLayer().objects().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUnsupportedInfiniteMaps() throws Exception {
|
||||||
|
final Path path = tempDir.resolve("infinite.tmx");
|
||||||
|
Files.writeString(path, """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down"
|
||||||
|
width="10" height="10" tilewidth="16" tileheight="16" infinite="1"/>
|
||||||
|
""");
|
||||||
|
|
||||||
|
assertThrows(TiledUnsupportedFeatureException.class, () -> codec.readMap(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1592,5 +1592,319 @@
|
|||||||
"blocking" : true
|
"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,
|
"schema_version" : 1,
|
||||||
"next_asset_id" : 16,
|
"next_asset_id" : 18,
|
||||||
"assets" : [ {
|
"assets" : [ {
|
||||||
"asset_id" : 3,
|
"asset_id" : 3,
|
||||||
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||||
@ -41,5 +41,15 @@
|
|||||||
"asset_uuid" : "87396aab-337e-479e-b1f4-ec296678389e",
|
"asset_uuid" : "87396aab-337e-479e-b1f4-ec296678389e",
|
||||||
"root" : "Zelda3",
|
"root" : "Zelda3",
|
||||||
"included_in_build" : true
|
"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" : [ {
|
"artifacts" : [ {
|
||||||
"file" : "link00.png",
|
"file" : "link00.png",
|
||||||
"index" : 0
|
"index" : 0,
|
||||||
|
"artifact_id" : "art-b82d5371eb9c",
|
||||||
|
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||||
}, {
|
}, {
|
||||||
"file" : "link01.png",
|
"file" : "link01.png",
|
||||||
"index" : 1
|
"index" : 1,
|
||||||
|
"artifact_id" : "art-acf4398779de",
|
||||||
|
"fingerprint" : "e2130efbb9643b41a4fb62619f2713402f9622c7db040fe18d929609ef70b89a"
|
||||||
}, {
|
}, {
|
||||||
"file" : "link02.png",
|
"file" : "link02.png",
|
||||||
"index" : 2
|
"index" : 2,
|
||||||
|
"artifact_id" : "art-88d5de66ceea",
|
||||||
|
"fingerprint" : "9216dfc6a6226fac398e9f781550d23fbfaa65bd377cdda69187149beca114ec"
|
||||||
}, {
|
}, {
|
||||||
"file" : "link03.png",
|
"file" : "link03.png",
|
||||||
"index" : 3
|
"index" : 3,
|
||||||
|
"artifact_id" : "art-18a3b61a9561",
|
||||||
|
"fingerprint" : "ebc4b7ca7bb1455288681c5d71424f60f658a44cb343d0c3934329cec676c867"
|
||||||
}, {
|
}, {
|
||||||
"file" : "link04.png",
|
"file" : "link04.png",
|
||||||
"index" : 4
|
"index" : 4,
|
||||||
|
"artifact_id" : "art-231ecb85b204",
|
||||||
|
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||||
}, {
|
}, {
|
||||||
"file" : "link05.png",
|
"file" : "link05.png",
|
||||||
"index" : 5
|
"index" : 5,
|
||||||
|
"artifact_id" : "art-f36b8455db22",
|
||||||
|
"fingerprint" : "1ba8ce75c445396334737143e0cfa56c87a1fb64cca8ad0962192594849249eb"
|
||||||
}, {
|
}, {
|
||||||
"file" : "link06.png",
|
"file" : "link06.png",
|
||||||
"index" : 6
|
"index" : 6,
|
||||||
|
"artifact_id" : "art-884d6b34d741",
|
||||||
|
"fingerprint" : "5e0954447699c6d5eac7550f1a10cf520efcddad0b9fbd9a716434da9d9550d4"
|
||||||
}, {
|
}, {
|
||||||
"file" : "link07.png",
|
"file" : "link07.png",
|
||||||
"index" : 7
|
"index" : 7,
|
||||||
|
"artifact_id" : "art-ca5e39fb056c",
|
||||||
|
"fingerprint" : "6104ba7f216a937994d478ed07766ec28f48ad72fdd6620508535a1f877b8ddb"
|
||||||
} ]
|
} ]
|
||||||
}
|
}
|
||||||
47
test-projects/main/assets/Zelda3/glyph-material.json
Normal file
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" : [ ]
|
||||||
|
}
|
||||||
50
test-projects/main/assets/Zelda3/primeiro tileset.tsx
Normal file
50
test-projects/main/assets/Zelda3/primeiro tileset.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset version="1.10" tiledversion="1.12.1" name="primeiro tileset" tilewidth="32" tileheight="32" tilecount="13" columns="0">
|
||||||
|
<grid orientation="orthogonal" width="1" height="1"/>
|
||||||
|
<tile id="1">
|
||||||
|
<image source="flag00.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="2">
|
||||||
|
<image source="flag01.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="3">
|
||||||
|
<image source="flag02.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="4">
|
||||||
|
<image source="flag03.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="5">
|
||||||
|
<image source="link00.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="6">
|
||||||
|
<image source="link01.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="7">
|
||||||
|
<image source="link02.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="8">
|
||||||
|
<image source="link03.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="9">
|
||||||
|
<image source="link04.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="10">
|
||||||
|
<image source="link05.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="11">
|
||||||
|
<image source="link06.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="12">
|
||||||
|
<image source="link07.png" width="32" height="32"/>
|
||||||
|
</tile>
|
||||||
|
<tile id="0">
|
||||||
|
<properties>
|
||||||
|
<property name="glyph_id" type="int" value="0"/>
|
||||||
|
</properties>
|
||||||
|
<image source="../recovered/atlas2/confirm.png" width="16" height="16"/>
|
||||||
|
<objectgroup draworder="index" id="2">
|
||||||
|
<object id="1" x="4" y="4" width="8" height="8"/>
|
||||||
|
<object id="2" x="1.59375" y="0.15625" width="2.75" height="2.90625"/>
|
||||||
|
</objectgroup>
|
||||||
|
</tile>
|
||||||
|
</tileset>
|
||||||
4
test-projects/main/assets/Zelda3/studio.asset.json
Normal file
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
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" : [ {
|
"artifacts" : [ {
|
||||||
"file" : "confirm.png",
|
"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"
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="25" height="25" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="12">
|
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="25" height="25" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="20">
|
||||||
<tileset firstgid="1" source="primeiro tileset.tsx"/>
|
<tileset firstgid="1" source="../Zelda3/primeiro tileset.tsx"/>
|
||||||
<layer id="1" name="Tile Layer 1" width="25" height="25">
|
<layer id="1" name="Tile Layer 1" width="25" height="25">
|
||||||
<data encoding="csv">
|
<data encoding="csv">
|
||||||
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,
|
||||||
@ -32,5 +32,15 @@
|
|||||||
</layer>
|
</layer>
|
||||||
<objectgroup id="3" name="Object Layer 1">
|
<objectgroup id="3" name="Object Layer 1">
|
||||||
<object id="11" x="16" y="16" width="64" height="64"/>
|
<object id="11" x="16" y="16" width="64" height="64"/>
|
||||||
|
<object id="12" x="179.608" y="48.6275">
|
||||||
|
<polygon points="0,0 1.17647,50.1961 46.2745,51.3725 44.7059,19.2157 12.9412,19.6078 12.549,-0.784314"/>
|
||||||
|
</object>
|
||||||
|
<object id="13" x="116.078" y="26.2745" width="34.902" height="39.2157"/>
|
||||||
|
<object id="14" x="96" y="96" width="52.2353" height="45.9608"/>
|
||||||
|
<object id="15" x="160" y="112" width="32" height="32"/>
|
||||||
|
<object id="16" x="16" y="96" width="64" height="32"/>
|
||||||
|
<object id="17" x="96" y="0" width="80" height="64"/>
|
||||||
|
<object id="18" x="153.725" y="92.1569"/>
|
||||||
|
<object id="19" x="98.0392" y="-48.2353" height="23.5294"/>
|
||||||
</objectgroup>
|
</objectgroup>
|
||||||
</map>
|
</map>
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<tileset version="1.10" tiledversion="1.12.1" name="primeiro tileset" tilewidth="16" tileheight="16" tilecount="1" columns="0">
|
|
||||||
<grid orientation="orthogonal" width="1" height="1"/>
|
|
||||||
<tile id="0">
|
|
||||||
<image source="../recovered/atlas2/confirm.png" width="16" height="16"/>
|
|
||||||
</tile>
|
|
||||||
</tileset>
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"automappingRulesFile": "",
|
|
||||||
"commands": [
|
|
||||||
],
|
|
||||||
"compatibilityVersion": 1100,
|
|
||||||
"extensionsPath": "extensions",
|
|
||||||
"folders": [
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"properties": [
|
|
||||||
],
|
|
||||||
"propertyTypes": [
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"activeFile": "primeiro tileset.tsx",
|
|
||||||
"expandedProjectPaths": [
|
|
||||||
"."
|
|
||||||
],
|
|
||||||
"fileStates": {
|
|
||||||
"primeiro mapa.tmx": {
|
|
||||||
"scale": 2.55,
|
|
||||||
"selectedLayer": 1,
|
|
||||||
"viewCenter": {
|
|
||||||
"x": 200,
|
|
||||||
"y": 200.58823529411768
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"primeiro tileset.tsx": {
|
|
||||||
"dynamicWrapping": true,
|
|
||||||
"scaleInDock": 1,
|
|
||||||
"scaleInEditor": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"last.imagePath": "/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/test-projects/main/assets/recovered/atlas2",
|
|
||||||
"map.height": 25,
|
|
||||||
"map.lastUsedFormat": "tmx",
|
|
||||||
"map.tileHeight": 16,
|
|
||||||
"map.tileWidth": 16,
|
|
||||||
"map.width": 25,
|
|
||||||
"openFiles": [
|
|
||||||
"primeiro mapa.tmx",
|
|
||||||
"primeiro tileset.tsx"
|
|
||||||
],
|
|
||||||
"project": "scene1.tiled-project",
|
|
||||||
"recentFiles": [
|
|
||||||
"primeiro mapa.tmx",
|
|
||||||
"primeiro tileset.tsx"
|
|
||||||
],
|
|
||||||
"tileset.lastUsedFormat": "tsx",
|
|
||||||
"tileset.type": 1
|
|
||||||
}
|
|
||||||
14
test-projects/main/assets/tiled/asset.json
Normal file
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
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
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
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"
|
||||||
|
} ]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user