diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md new file mode 100644 index 000000000..1ef1dc138 --- /dev/null +++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md @@ -0,0 +1,8 @@ +# PR_26180_OWNER_022-delete-remaining-root-src-files Branch Validation + +- Expected branch: `PR_26180_OWNER_022-delete-remaining-root-src-files` +- Actual branch: `PR_26180_OWNER_022-delete-remaining-root-src-files` +- Branch validation: PASS +- Worktree at branch start: clean after removal of previous PR022 hard-stop artifacts +- Current action: HARD STOP report artifacts only +- Source files modified: none diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md new file mode 100644 index 000000000..739995467 --- /dev/null +++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md @@ -0,0 +1,13 @@ +# PR_26180_OWNER_022-delete-remaining-root-src-files Manual Validation Notes + +- Reviewed current Project Instructions version `2026.06.28.021`. +- Confirmed canonical report path is `dev/reports/` and ZIP path is `dev/workspace/zips/`. +- Confirmed current branch is `PR_26180_OWNER_022-delete-remaining-root-src-files`. +- Confirmed 65 tracked root `src` files remain before attempted deletion. +- Confirmed active references still point to root `src/shared` from dev tests, validation scripts, and tooling baselines. +- No runtime, UI, API, database, product, `www/`, `api/`, or source files were modified. +- Manual recommendation: create a follow-up migration PR that updates the active tests/scripts/tooling baselines away from root `src/shared` before deleting the remaining files. + +- Final `git diff --check` and `git diff --cached --check`: PASS. +- Final `npm run validate:canonical-structure`: PASS. +- Targeted root `src` scan still reports active references and remains the hard-stop blocker. diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md new file mode 100644 index 000000000..5f94761be --- /dev/null +++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md @@ -0,0 +1,66 @@ +# PR_26180_OWNER_022-delete-remaining-root-src-files Hard Stop Report + +## Outcome + +HARD STOP. No root `src/` files were deleted, archived, or moved. + +## Reason + +The required root `src/` reference audit found active current validation/test/tooling references that still require root `src/shared` files. The task explicitly requires a hard stop if runtime, test, package, or CI references still require root `src`. + +## Branch + +- Branch: `PR_26180_OWNER_022-delete-remaining-root-src-files` +- HEAD: `2e38e9e31cf0b9a2718681ea95a6b8054f4a7a93` +- Worktree before reports: clean +- Base requested: `PR_26180_OWNER_021-archive-legacy-games-samples-teardown` + +## Root Src Count + +- Before: 65 tracked files under `src/` +- After: 65 tracked files under `src/` +- Active reference scan matches: 1284 + +## High-Signal Evidence + +- `dev\tests\shared\InMemoryProjectDataStore.test.mjs:14:} from "../../../src/shared/contracts/projectDataStoreContract.js";` +- `dev\tests\tools\ToolManifestBoundary.test.mjs:11: "www/src/shared/schemas/tools/palette-browser.schema.json",` +- `dev\scripts\validate-json-contracts.mjs:499: return `www/src/shared/schemas/tools/${normalized}.schema.json`;` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3100: "file": "www/src/shared/color/color.js",` +- `dev\tools\toolbox-dev\checkBoundaryHardeningGuard.mjs:16: ['src/shared/', 'shared'],` +- `dev\tests\shared\ContractIndexValidation.test.mjs:59: const actualRootContractFiles = fs.readdirSync(resolvePath("src/shared/contracts"))` +- `dev\tests\validation\samples.shared.boundaries.report.json:2: "$schema": "../../../src/shared/schemas/samples/sample.tool-payload.schema.json",` +- `dev\scripts\validate-json-contracts.mjs:653: const gameManifestSchemaPath = "src/shared/schemas/game.manifest.schema.json";` +- `dev\scripts\validate-json-contracts.mjs:665: errors.push("src/shared/schemas/game.manifest.schema.json not found");` +- `dev\scripts\validate-json-contracts.mjs:706: if (!schemaPath.startsWith("www/src/shared/schemas/tools/") || !schemaPath.endsWith(".schema.json")) {` +- `dev\tests\validation\samples.runtime.validation.report.json:2: "$schema": "../../../src/shared/schemas/samples/sample.tool-payload.schema.json",` +- `dev\tests\validation\samples.curriculum.validation.json:2: "$schema": "../../../src/shared/schemas/samples/sample.tool-payload.schema.json",` +- `dev\tests\fixtures\workspace-v2\uat.manifest.json:2: "$schema": "src/shared/schemas/game.manifest.schema.json",` +- `dev\tests\fixtures\workspace-v2\uat.manifest.json:17: "$schema": "src/shared/schemas/tools/palette-manager-v2.schema.json",` +- `dev\tests\fixtures\workspace-v2\uat.manifest.json:59: "$schema": "src/shared/schemas/tools/asset-manager-v2.schema.json",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3105: "file": "src/shared/contracts/auditEventContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3110: "file": "src/shared/contracts/backupSnapshotContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3115: "file": "src/shared/contracts/collaborationRoleContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3120: "file": "src/shared/contracts/creatorProfileContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3125: "file": "src/shared/contracts/downloadGrantContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3130: "file": "src/shared/contracts/entitlementContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3135: "file": "src/shared/contracts/installReceiptContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3140: "file": "src/shared/contracts/libraryItemContract.js",` +- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3145: "file": "src/shared/contracts/marketplaceListingContract.js",` + +## Targeted Validation Evidence + +- `git diff --check`: PASS before report generation. +- `npm run validate:canonical-structure`: PASS. +- `node ./dev/scripts/run-node-test-files.mjs dev/tests/shared/InMemoryProjectDataStore.test.mjs dev/tests/tools/ToolManifestBoundary.test.mjs dev/tests/shared/ContractIndexValidation.test.mjs`: FAILED. + - `InMemoryProjectDataStore.test.mjs`: PASS. + - `ToolManifestBoundary.test.mjs`: PASS. + - `ContractIndexValidation.test.mjs`: FAILS on missing legacy `src/shared/contracts/replayContracts.js`. + +## Decision + +The PR stopped before deleting or archiving remaining root `src` files. The next scoped PR should first migrate or retire the active validation/test/tooling dependencies that still point at root `src/shared`, then retry root `src` deletion only after `git ls-files -- src` and active reference scans prove it is safe. + +## Files Changed By This Outcome + +Reports only. No runtime, API, UI, database, or product files were modified. diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md new file mode 100644 index 000000000..5aa376d1d --- /dev/null +++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md @@ -0,0 +1,13 @@ +# PR_26180_OWNER_022-delete-remaining-root-src-files Requirement Checklist + +| Requirement | Status | Notes | +|---|---:|---| +| Work only on remaining tracked files under root `src/` | PASS | Audit inspected tracked `src/` files only. | +| Do not touch `dev/workspace/generated/tool-images/**` | PASS | Protected developer workspace path was not touched. | +| Preserve no root `src` file unless active refs prove it required | HARD STOP | Active refs still prove current validation/test dependencies require root `src`. | +| Delete definitely obsolete files instead of archiving | BLOCKED | Deletion would occur while active test/tooling refs remain. | +| Required reference audit before delete/archive | PASS | Scan found 1284 matches. | +| Stop on active runtime/test/package/CI refs requiring root `src` | PASS | Hard stop triggered. | +| Report `git ls-files -- src` count before and after | PASS | Before 65, after 65. | +| Produce reports under `dev/reports/` | PASS | This checklist and companion reports were generated. | +| Produce ZIP under `dev/workspace/zips/` | PASS | Hard-stop ZIP generated for this outcome. | diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md new file mode 100644 index 000000000..8731ff895 --- /dev/null +++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md @@ -0,0 +1,14 @@ +# PR_26180_OWNER_022-delete-remaining-root-src-files Validation Report + +## Commands + +| Command | Result | Notes | +|---|---:|---| +| `git diff --check` and `git diff --cached --check` | PASS | Ran after report generation against unstaged and staged changes. | +| `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed after report generation. | +| Targeted root `src` reference scan | HARD STOP | Found 1284 active matches outside archive/report/workspace paths. | +| `node ./dev/scripts/run-node-test-files.mjs dev/tests/shared/InMemoryProjectDataStore.test.mjs dev/tests/tools/ToolManifestBoundary.test.mjs dev/tests/shared/ContractIndexValidation.test.mjs` | FAIL | First two tests passed; `ContractIndexValidation` failed on missing legacy `replayContracts.js`. | + +## Validation Decision + +No source deletion/archive was performed because the hard-stop condition was met before implementation. diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt index 843a15fdf..769ed9684 100644 --- a/dev/reports/codex_changed_files.txt +++ b/dev/reports/codex_changed_files.txt @@ -1,72 +1,8 @@ -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/README.md -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/how_to_use.html -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/index.html -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/main.js -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/parallaxEditor.css -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/cloud-bands.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/foreground-occluder.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/hero-proxy.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-short.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-tall.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/sky-gradient.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_bushes_foreground.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_clouds.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_hills.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_leaf_overlay.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_learning_level_parallax.json -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_mountains.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_sky.svg -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/parallax_sample.json -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/sample-manifest.json -D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/tilemap_parallax_sample.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/README.md -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/BATCH_GUARDRAIL_CONTRACT.md -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/CONTROL_SERVICE_CONTRACTS.md -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/OBJECT_VECTOR_STUDIO_V2_REQUIREMENTS.md -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/how_to_use.html -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/index.html -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/ToolStarterApp.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/bootstrap.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/AccordionSection.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ActionNavControl.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/InspectorControl.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/PreviewPanelControl.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/SourceInputControl.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/StatusLogControl.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ToolStarterShellControl.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ToolStateSerializer.js -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/playwright.config.mjs -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/possible.future.adds.txt -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/styles/toolStarter.css -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/README.md -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/palettes/vector-native-primary.palette.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.parallax.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.svg -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-arena.tilemap.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-ui.tileset.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-hud.vector.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-large.vector.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-small.vector.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-player.vector.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-title.vector.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/config/template.project.json -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/ROLLBACK_NOTES.md -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/STARTER_GUIDE.md -D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/runtime/bootstrap.runtime.json -M dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md -M dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md -M dev/build/ProjectInstructions/PROJECT_STATE.md -M dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md -M dev/build/ProjectInstructions/repository/canonical_repository_structure.md -A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_branch-validation.md -A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_manual-validation-notes.md -A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_report.md -A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_requirement-checklist.md -A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_validation-report.md -M dev/tools/toolbox-dev/checkSharedExtractionGuard.baseline.json -D www/src/tools/common/GameManifestLoader.js -D www/src/tools/common/WorkspaceDirtyNotifier.js -M dev/reports/codex_review.diff -M dev/reports/codex_changed_files.txt +dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md +dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md +dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md +dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md +dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md +dev/reports/codex_changed_files.txt +dev/reports/codex_review.diff +dev/workspace/zips/PR_26180_OWNER_022-delete-remaining-root-src-files_HARD_STOP.zip diff --git a/dev/reports/codex_review.diff b/dev/reports/codex_review.diff index a7784c727..2c3b8aa0a 100644 --- a/dev/reports/codex_review.diff +++ b/dev/reports/codex_review.diff @@ -1,17944 +1,229 @@ -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/README.md b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/README.md -deleted file mode 100644 -index 69d45af59..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/README.md -+++ /dev/null -@@ -1,22 +0,0 @@ --Toolbox Aid --David Quesenberry --04/21/2026 --README.md -- --# Parallax Scene Studio -- --High-level overview for 'Parallax Scene Studio' under 'tools/Parallax Scene Studio/'. -- --## Purpose --- Provide a focused workspace for this tool's primary authoring/debug workflow. --- Keep output deterministic and compatible with shared Toolbox Aid contracts. --- Integrate cleanly with shared header, workspace lifecycle, and tool navigation. -- --## What To Expect --- Browser-based tool interface with panel/workspace controls. --- Shared header context and status integration. --- Safe workflows for loading, editing, previewing, and exporting relevant data. -- --## Entry Points --- Tool UI: 'tools/Parallax Scene Studio/index.html' --- How-to guide: 'tools/Parallax Scene Studio/how_to_use.html' -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/how_to_use.html b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/how_to_use.html -deleted file mode 100644 -index 1e6f31a85..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/how_to_use.html -+++ /dev/null -@@ -1,91 +0,0 @@ -- -- -- -- -- -- Parallax Scene Studio - How To Use -- -- -- -- -- --
--
--
--

Parallax Scene Studio: How To Use

--

Detailed workflow with practical steps and control guidance.

--
-- --
--

Details

--
--

Parallax composition studio for layered scene depth, sample loading, simulation, and export flows.

--
--
-- --
--

Tool vs Workspace Saves

--
--

Tool save/export
Action: Use tool save/export controls for JSON, PNG, sheets, runtime payloads, and tool-local artifacts.
Why this step matters: Creates the actual outputs consumed by samples/games/pipeline steps.

--

Workspace save
Action: Use Workspace Manager save controls to persist cross-tool session state.
Why this step matters: Restores coordinated context (selected tool state, shared refs) but does not replace explicit artifact exports.

--
--
--
--

Workflow

--
--
--

Tool Mode

--

1. Compose parallax scenes
Action: Launch this tool from tools/<tool>/index.html (not through Workspace Manager).
Why this step matters: Uses this tool as a focused single-tool surface.

--

2. Build/edit in the tool
Action: Arrange parallax layers, tune depth/motion, and preview scene behavior.
Why this step matters: Creates layered scene composition data for runtime playback.

--

3. Save/export deliverables
Action: Save scene data and export assets/config used in target experiences.
Why this step matters: Preserves visual composition and reproducible setup.

--

4. Continue or close
Action: Open another tool directly as needed.
Why this step matters: Tool mode is best for isolated tasks and rapid single-tool iteration.

--
-- --
--

Workspace Mode

--

1. Create a named workspace
Action: Click New Workspace and enter a meaningful name (example: UAT 001 Workspace).
Why this step matters: Starts from a clear baseline and makes saved snapshots easy to identify.

--

2. Open tools from Workspace Manager tiles
Action: Select needed tool tiles and perform each tool's primary task.
Why this step matters: Keeps multi-tool work coordinated under one shared context.

--

3. Save both levels intentionally
Action: Use tool-level save/export for artifacts, then click Save Workspace for session state.
Why this step matters: Preserves both deliverable files and cross-tool orchestration state.

--

4. Checkpoint and resume
Action: Use Save Workspace As for milestones and Open Workspace to resume later.
Why this step matters: Supports versioned progress and reliable continuation.

--
--
--
-- --
--

Buttons

--
--

Add Layer #addLayerButton
Details: Triggers a tool action that updates your current workflow state.

--

Apply Layer Settings #applyLayerSettingsButton
Details: Triggers a tool action that updates your current workflow state.

--

Apply Map Meta #applyMapMetaButton
Details: Triggers a tool action that updates your current workflow state.

--

Apply Source #applyImageSourceButton
Details: Triggers a tool action that updates your current workflow state.

--

Apply Suggested Fix #applyRemediationButton
Details: Triggers a tool action that updates your current workflow state.

--

Choose File (layerImageFileInput) #layerImageFileInput
Details: Triggers a tool action that updates your current workflow state.

--

Choose File (loadAssetRegistryInput) #loadAssetRegistryInput
Details: Loads saved data so you can continue existing work.

--

Choose File (loadProjectInput) #loadProjectInput
Details: Loads saved data so you can continue existing work.

--

Duplicate #duplicateLayerButton
Details: Clones current content to speed iterative changes.

--

Exit Simulation #exitSimulationButton
Details: Triggers a tool action that updates your current workflow state.

--

Export Tilemap Patch #exportParallaxPatchButton
Details: Writes deliverable output for downstream use.

--

Inspect Issues #inspectRemediationButton
Details: Checks state/output so issues are caught before final save/export.

--

Jump to Problem #jumpToProblemButton
Details: Triggers a tool action that updates your current workflow state.

--

Load Assets Registry #loadAssetRegistryButton
Details: Loads saved data so you can continue existing work.

--

Load Project #loadProjectButton
Details: Loads saved data so you can continue existing work.

--

Load Sample #loadSampleButton
Details: Loads saved data so you can continue existing work.

--

Move Down #moveLayerDownButton
Details: Triggers a tool action that updates your current workflow state.

--

Move Up #moveLayerUpButton
Details: Triggers a tool action that updates your current workflow state.

--

New Project #newProjectButton
Details: Starts a fresh working baseline for the next task.

--

Package Project #packageProjectButton
Details: Writes deliverable output for downstream use.

--

Pause #pauseSimulationButton
Details: Triggers a tool action that updates your current workflow state.

--

Play #playSimulationButton
Details: Triggers a tool action that updates your current workflow state.

--

Refresh Debug View #refreshDebugVisualizationButton
Details: Checks state/output so issues are caught before final save/export.

--

Refresh Pipeline View #refreshExperienceButton
Details: Checks state/output so issues are caught before final save/export.

--

Remove Layer #removeLayerButton
Details: Removes current selection or closes the active context.

--

Restart Position #restartSimulationButton
Details: Triggers a tool action that updates your current workflow state.

--

Save Assets Registry #saveAssetRegistryButton
Details: Persists current state to prevent data loss and preserve progress.

--

Save Project #saveProjectButton
Details: Persists current state to prevent data loss and preserve progress.

--

Simulate #simulateButton
Details: Triggers a tool action that updates your current workflow state.

--
--
--
-- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/index.html b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/index.html -deleted file mode 100644 -index deca47a3f..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/index.html -+++ /dev/null -@@ -1,280 +0,0 @@ -- -- -- -- -- -- -- Tools Platform -- -- -- -- -- -- --
-- --
--
--
--
--
--
--
--
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- --
-- -- -- -- -- --
-- --
-- --
-- -- --
--
-- Parallax Preview -- -- --
--
-- --
-- -- --
--
-- -- -- -- -- --
--
--
Mode: EDIT
--
Ready.
--
-- -- --
--
-- --
-- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/main.js b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/main.js -deleted file mode 100644 -index c505107f3..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/main.js -+++ /dev/null -@@ -1,2082 +0,0 @@ --/* --Toolbox Aid --David Quesenberry --03/30/2026 --main.js --*/ --import { -- buildAssetDependencyGraph, -- createAssetId, -- createAssetRegistry, -- createRegistryDownloadPayload, -- findRegistryEntryById, -- mergeAssetRegistries, -- normalizeProjectRelativePath, -- sanitizeAssetRegistry, -- upsertRegistryEntry --} from "../shared/projectAssetRegistry.js"; --import { downloadTextFile, readFileText } from '../../src/engine/persistence/FilePersistenceService.js'; --import { registerAssetPipelineCandidate } from "../shared/assetPipelineFoundation.js"; --import { -- getBlockingAssetValidationMessage, -- hasBlockingAssetValidationFindings, -- summarizeAssetValidation, -- validateProjectAssetState --} from "../shared/projectAssetValidation.js"; --import { -- buildProjectAssetRemediation, -- getPrimaryRemediationAction, -- summarizeProjectAssetRemediation --} from "../shared/projectAssetRemediation.js"; --import { buildProjectPackage, summarizeProjectPackaging } from "../shared/projectPackaging.js"; --import { buildEditorExperienceLayer, summarizeEditorExperienceLayer } from "../shared/editorExperienceLayer.js"; --import { buildDebugVisualizationLayer, summarizeDebugVisualizationLayer } from "../shared/debugVisualizationLayer.js"; --import { registerToolBootContract } from "../shared/toolBootContract.js"; --import { -- getToolLoadQuerySnapshot, -- getToolLoadRequestedDataPaths, -- summarizeToolLoadData, -- logToolLoadRequest, -- logToolLoadFetch, -- logToolLoadLoaded, -- logToolLoadWarning --} from "../shared/toolLoadDiagnostics.js"; --import { createLivePreviewSyncBridge, validateStateBindingPayload } from "../shared/livePreviewSyncChannel.js"; --import { addToolModeMetadata, assertStandaloneToolDocument, offerImportMismatchOptions } from "../shared/documentModeGuards.js"; -- --function normalizeSamplePresetPath(pathValue) { -- if (typeof pathValue !== "string") { -- return ""; -- } -- const trimmed = pathValue.trim().replace(/\\/g, "/"); -- if (!trimmed || trimmed.includes("..")) { -- return ""; -- } -- if (trimmed.startsWith("/samples/")) { -- return trimmed; -- } -- if (trimmed.startsWith("./samples/")) { -- return trimmed; -- } -- if (trimmed.startsWith("samples/")) { -- return `./${trimmed}`; -- } -- return ""; --} -- --function buildPresetLoadedStatus(sampleId, samplePresetPath) { -- const normalizedSampleId = typeof sampleId === "string" ? sampleId.trim() : ""; -- if (normalizedSampleId) { -- return `Loaded preset from sample ${normalizedSampleId}.`; -- } -- const normalizedPath = typeof samplePresetPath === "string" ? samplePresetPath.trim() : ""; -- return normalizedPath ? `Loaded preset from ${normalizedPath}.` : "Loaded preset."; --} -- --function clamp(value, min, max, fallback) { -- const number = Number(value); -- if (!Number.isFinite(number)) { -- return fallback; -- } -- return Math.max(min, Math.min(max, number)); --} -- --function cloneDeep(value) { -- if (typeof structuredClone === "function") { -- return structuredClone(value); -- } -- return JSON.parse(JSON.stringify(value)); --} -- --function normalizeMapMeta(rawMap) { -- const width = clamp(rawMap?.width, 4, 1024, 32); -- const height = clamp(rawMap?.height, 4, 1024, 18); -- const tileSize = clamp(rawMap?.tileSize, 8, 128, 24); -- const name = typeof rawMap?.name === "string" && rawMap.name.trim() ? rawMap.name.trim() : "untitled-map"; -- -- return { -- name, -- width, -- height, -- tileSize, -- pixelWidth: width * tileSize, -- pixelHeight: height * tileSize -- }; --} -- --function createDefaultLayer(index = 0, name = "Parallax Layer") { -- return { -- id: `parallax-layer-${Date.now()}-${index}`, -- name, -- parallaxSourceId: "", -- drawOrder: index, -- imageSource: "", -- scrollFactorX: 0.4, -- scrollFactorY: 0.3, -- offsetX: 0, -- offsetY: 0, -- repeatX: true, -- repeatY: false, -- wrapMode: "wrap", -- opacity: 1, -- visible: true -- }; --} -- --function normalizeLayer(rawLayer, index = 0) { -- const fallback = createDefaultLayer(index, `Parallax Layer ${index + 1}`); -- const repeatX = rawLayer?.repeatX !== false; -- const repeatY = rawLayer?.repeatY === true; -- const wrapMode = rawLayer?.wrapMode === "clamp" ? "clamp" : "wrap"; -- -- return { -- id: typeof rawLayer?.id === "string" && rawLayer.id.trim() ? rawLayer.id.trim() : fallback.id, -- name: typeof rawLayer?.name === "string" && rawLayer.name.trim() ? rawLayer.name.trim() : fallback.name, -- parallaxSourceId: typeof rawLayer?.parallaxSourceId === "string" ? rawLayer.parallaxSourceId.trim() : "", -- drawOrder: Math.trunc(clamp(rawLayer?.drawOrder, -999, 999, index)), -- imageSource: typeof rawLayer?.imageSource === "string" ? rawLayer.imageSource : "", -- scrollFactorX: clamp(rawLayer?.scrollFactorX, -4, 4, fallback.scrollFactorX), -- scrollFactorY: clamp(rawLayer?.scrollFactorY, -4, 4, fallback.scrollFactorY), -- offsetX: Math.trunc(clamp(rawLayer?.offsetX, -4096, 4096, 0)), -- offsetY: Math.trunc(clamp(rawLayer?.offsetY, -4096, 4096, 0)), -- repeatX, -- repeatY, -- wrapMode, -- opacity: clamp(rawLayer?.opacity, 0, 1, 1), -- visible: rawLayer?.visible !== false -- }; --} -- --function sortLayersByOrder(layers) { -- layers.sort((a, b) => { -- if (a.drawOrder !== b.drawOrder) { -- return a.drawOrder - b.drawOrder; -- } -- return a.name.localeCompare(b.name); -- }); --} -- --function normalizeDrawOrderSequence(layers) { -- sortLayersByOrder(layers); -- layers.forEach((layer, index) => { -- layer.drawOrder = index; -- }); --} -- --function sanitizeAssetRefs(rawAssetRefs) { -- if (!rawAssetRefs || typeof rawAssetRefs !== "object") { -- return { -- parallaxSourceIds: [] -- }; -- } -- -- const parallaxSourceIds = Array.isArray(rawAssetRefs.parallaxSourceIds) -- ? rawAssetRefs.parallaxSourceIds.filter((value) => typeof value === "string" && value.trim()) -- : []; -- -- return { -- parallaxSourceIds: Array.from(new Set(parallaxSourceIds)) -- }; --} -- --function createRegistryManagedParallaxSaveDocument(documentModel) { -- const output = cloneDeep(documentModel); -- output.assetRefs = sanitizeAssetRefs(output.assetRefs); -- -- output.layers = (Array.isArray(output.layers) ? output.layers : []).map((layer) => { -- const nextLayer = normalizeLayer(layer); -- if (nextLayer.parallaxSourceId && !normalizeProjectRelativePath(nextLayer.imageSource || "")) { -- nextLayer.imageSource = ""; -- } -- return nextLayer; -- }); -- -- return output; --} -- --function createInitialParallaxDocument(options = {}) { -- const map = normalizeMapMeta(options.map || {}); -- const createdAt = new Date().toISOString(); -- const layers = [ -- { -- ...createDefaultLayer(0, "Sky"), -- scrollFactorX: 0.2, -- scrollFactorY: 0.05, -- opacity: 1, -- repeatY: false -- }, -- { -- ...createDefaultLayer(1, "Far Mountains"), -- scrollFactorX: 0.4, -- scrollFactorY: 0.15, -- opacity: 0.95 -- }, -- { -- ...createDefaultLayer(2, "Near Trees"), -- scrollFactorX: 0.75, -- scrollFactorY: 0.35, -- opacity: 0.9 -- } -- ]; -- -- normalizeDrawOrderSequence(layers); -- -- return { -- schema: "toolbox.parallax/1", -- version: 1, -- companionEditor: "ParallaxEditor", -- map, -- layers, -- assetRefs: { -- parallaxSourceIds: [] -- }, -- metadata: { -- createdAt, -- updatedAt: createdAt, -- source: "parallax-editor-companion" -- } -- }; --} -- --function sanitizeParallaxDocument(rawDocument, fallbackMap = null) { -- const fallback = createInitialParallaxDocument({ map: fallbackMap || undefined }); -- if (!rawDocument || typeof rawDocument !== "object") { -- return fallback; -- } -- -- const map = normalizeMapMeta(rawDocument.map || fallback.map); -- const rawLayers = Array.isArray(rawDocument.layers) ? rawDocument.layers : []; -- const layers = rawLayers.length > 0 -- ? rawLayers.map((layer, index) => normalizeLayer(layer, index)) -- : cloneDeep(fallback.layers); -- normalizeDrawOrderSequence(layers); -- -- return { -- schema: "toolbox.parallax/1", -- version: Number.isFinite(rawDocument.version) ? rawDocument.version : 1, -- companionEditor: "ParallaxEditor", -- map, -- layers, -- assetRefs: sanitizeAssetRefs(rawDocument.assetRefs), -- metadata: { -- createdAt: typeof rawDocument?.metadata?.createdAt === "string" ? rawDocument.metadata.createdAt : fallback.metadata.createdAt, -- updatedAt: new Date().toISOString(), -- source: "parallax-editor-companion" -- } -- }; --} -- --function extractParallaxDocument(rawAnyDocument) { -- if (!rawAnyDocument || typeof rawAnyDocument !== "object") { -- throw new Error("Expected a JSON object."); -- } -- -- if (rawAnyDocument.schema === "toolbox.parallax/1") { -- return sanitizeParallaxDocument(rawAnyDocument); -- } -- -- if (rawAnyDocument.schema === "toolbox.tilemap/1") { -- const map = normalizeMapMeta(rawAnyDocument.map || {}); -- const rawParallax = rawAnyDocument.parallax && typeof rawAnyDocument.parallax === "object" -- ? rawAnyDocument.parallax -- : { schema: "toolbox.parallax/1", layers: [] }; -- -- const merged = { -- schema: "toolbox.parallax/1", -- version: 1, -- companionEditor: "ParallaxEditor", -- map, -- layers: Array.isArray(rawParallax.layers) ? rawParallax.layers : [], -- assetRefs: rawAnyDocument.assetRefs -- }; -- -- return sanitizeParallaxDocument(merged, map); -- } -- -- if (rawAnyDocument.parallax && rawAnyDocument.map) { -- const map = normalizeMapMeta(rawAnyDocument.map); -- const merged = { -- schema: "toolbox.parallax/1", -- version: 1, -- companionEditor: "ParallaxEditor", -- map, -- layers: Array.isArray(rawAnyDocument.parallax.layers) ? rawAnyDocument.parallax.layers : [], -- assetRefs: rawAnyDocument.assetRefs -- }; -- return sanitizeParallaxDocument(merged, map); -- } -- -- throw new Error("Unsupported schema. Expected toolbox.tilemap/1 or toolbox.parallax/1."); --} -- --function extractParallaxDocumentFromSamplePreset(rawPreset) { -- if (!rawPreset || typeof rawPreset !== "object") { -- return rawPreset; -- } -- -- const payload = rawPreset.payload; -- if (payload && typeof payload === "object") { -- if (payload.parallaxDocument && typeof payload.parallaxDocument === "object") { -- return payload.parallaxDocument; -- } -- if (payload.parallax && typeof payload.parallax === "object") { -- return payload.parallax; -- } -- const config = payload.config && typeof payload.config === "object" -- ? payload.config -- : null; -- if (config?.parallaxDocument && typeof config.parallaxDocument === "object") { -- return config.parallaxDocument; -- } -- if (config?.parallax && typeof config.parallax === "object") { -- return config.parallax; -- } -- } -- const rootConfig = rawPreset.config && typeof rawPreset.config === "object" -- ? rawPreset.config -- : null; -- if (rootConfig?.parallaxDocument && typeof rootConfig.parallaxDocument === "object") { -- return rootConfig.parallaxDocument; -- } -- if (rootConfig?.parallax && typeof rootConfig.parallax === "object") { -- return rootConfig.parallax; -- } -- -- return rawPreset; --} -- --function createTilemapParallaxPatch(parallaxDocument) { -- return { -- schema: "toolbox.tilemap-parallax-patch/1", -- map: cloneDeep(parallaxDocument.map), -- assetRefs: sanitizeAssetRefs(parallaxDocument.assetRefs), -- parallax: { -- schema: "toolbox.parallax/1", -- companionEditor: "ParallaxEditor", -- layers: cloneDeep(parallaxDocument.layers) -- }, -- metadata: { -- generatedAt: new Date().toISOString(), -- generatedBy: "tools/Parallax Scene Studio" -- } -- }; --} -- --function summarizeGraphFindings(findings) { -- return Array.isArray(findings) && findings.length > 0 -- ? ` Graph findings: ${findings.length}.` -- : " Graph findings: none."; --} -- --function mod(value, range) { -- if (range === 0) { -- return 0; -- } -- return ((value % range) + range) % range; --} -- --function getLayerVisualColor(layer) { -- const hashSource = `${layer.id}|${layer.name}`; -- let hash = 0; -- for (let i = 0; i < hashSource.length; i += 1) { -- hash = ((hash << 5) - hash) + hashSource.charCodeAt(i); -- hash |= 0; -- } -- const hue = Math.abs(hash) % 360; -- return `hsl(${hue} 56% 42%)`; --} -- --class ParallaxEditorApp { -- constructor(documentModel) { -- this.documentModel = documentModel; -- this.selectedLayerId = documentModel.layers[0]?.id || ""; -- this.cameraX = 0; -- this.cameraY = 0; -- this.refs = {}; -- this.imageCache = new Map(); -- this.imageCacheVersion = 0; -- this.isSimulationMode = false; -- this.simulation = { -- rafId: 0, -- lastTimestamp: 0, -- accumulatorMs: 0, -- playing: false, -- baseCameraX: 0, -- baseCameraY: 0, -- startCameraX: 0, -- startCameraY: 0, -- endCameraX: 0, -- endCameraY: 0, -- traversedDistance: 0, -- traversalDistance: 0, -- traversalDurationMs: 14000 -- }; -- this.assetRegistry = createAssetRegistry({ projectId: documentModel?.map?.name || "parallax-project" }); -- this.assetDependencyGraphSnapshot = null; -- this.validationResult = null; -- this.remediationResult = { remediation: { status: "unavailable", actions: [] } }; -- this.lastPackageResult = null; -- this.lastRuntimeResult = null; -- this.editorExperienceResult = null; -- this.debugVisualizationResult = null; -- this.livePreviewSync = createLivePreviewSyncBridge({ sourceId: "parallax-editor" }); -- this.livePreviewSyncFrame = 0; -- this.pendingLivePreviewReason = "init"; -- this.boundRuntimeState = null; -- this.lastRuntimeBindingStatusAt = 0; -- this.skipExternalProjectStateUntil = 0; -- this.transientLayerImageSourceByName = new Map(); -- } -- -- invalidateImageCache() { -- this.imageCache.clear(); -- this.imageCacheVersion += 1; -- } -- -- clearTransientLayerImageSources() { -- this.transientLayerImageSourceByName.forEach((objectUrl) => { -- if (typeof objectUrl === "string" && objectUrl.startsWith("blob:")) { -- URL.revokeObjectURL(objectUrl); -- } -- }); -- this.transientLayerImageSourceByName.clear(); -- } -- -- setTransientLayerImageSource(imageSource, objectUrl) { -- const key = typeof imageSource === "string" ? imageSource.trim() : ""; -- if (!key || typeof objectUrl !== "string" || !objectUrl.trim()) { -- return; -- } -- const existing = this.transientLayerImageSourceByName.get(key); -- if (typeof existing === "string" && existing.startsWith("blob:")) { -- URL.revokeObjectURL(existing); -- } -- this.transientLayerImageSourceByName.set(key, objectUrl.trim()); -- } -- -- getTransientLayerImageSource(imageSource) { -- const key = typeof imageSource === "string" ? imageSource.trim() : ""; -- if (!key) { -- return ""; -- } -- return this.transientLayerImageSourceByName.get(key) || ""; -- } -- -- init(rootDocument) { -- this.captureRefs(rootDocument); -- this.attachEvents(); -- this.syncFullscreenState(); -- this.syncInputsFromDocument(); -- this.renderAll(); -- this.bindRuntimeStateSync(); -- this.queueLivePreviewSync("init"); -- void this.tryLoadPresetFromQuery(); -- } -- -- captureRefs(rootDocument) { -- this.refs.newProjectButton = rootDocument.getElementById("newProjectButton"); -- this.refs.loadProjectButton = rootDocument.getElementById("loadProjectButton"); -- this.refs.loadProjectInput = rootDocument.getElementById("loadProjectInput"); -- this.refs.saveProjectButton = rootDocument.getElementById("saveProjectButton"); -- this.refs.loadAssetRegistryButton = rootDocument.getElementById("loadAssetRegistryButton"); -- this.refs.loadAssetRegistryInput = rootDocument.getElementById("loadAssetRegistryInput"); -- this.refs.saveAssetRegistryButton = rootDocument.getElementById("saveAssetRegistryButton"); -- this.refs.simulateButton = rootDocument.getElementById("simulateButton"); -- this.refs.playSimulationButton = rootDocument.getElementById("playSimulationButton"); -- this.refs.pauseSimulationButton = rootDocument.getElementById("pauseSimulationButton"); -- this.refs.restartSimulationButton = rootDocument.getElementById("restartSimulationButton"); -- this.refs.exitSimulationButton = rootDocument.getElementById("exitSimulationButton"); -- this.refs.exportParallaxPatchButton = rootDocument.getElementById("exportParallaxPatchButton"); -- this.refs.packageProjectButton = rootDocument.getElementById("packageProjectButton"); -- this.refs.remediationSummaryText = rootDocument.getElementById("remediationSummaryText"); -- this.refs.experienceSummaryText = rootDocument.getElementById("experienceSummaryText"); -- this.refs.experienceDetailsText = rootDocument.getElementById("experienceDetailsText"); -- this.refs.refreshExperienceButton = rootDocument.getElementById("refreshExperienceButton"); -- this.refs.debugSummaryText = rootDocument.getElementById("debugSummaryText"); -- this.refs.debugDetailsText = rootDocument.getElementById("debugDetailsText"); -- this.refs.refreshDebugVisualizationButton = rootDocument.getElementById("refreshDebugVisualizationButton"); -- this.refs.inspectRemediationButton = rootDocument.getElementById("inspectRemediationButton"); -- this.refs.jumpToProblemButton = rootDocument.getElementById("jumpToProblemButton"); -- this.refs.applyRemediationButton = rootDocument.getElementById("applyRemediationButton"); -- -- this.refs.projectNameInput = rootDocument.getElementById("projectNameInput"); -- this.refs.mapWidthInput = rootDocument.getElementById("mapWidthInput"); -- this.refs.mapHeightInput = rootDocument.getElementById("mapHeightInput"); -- this.refs.tileSizeInput = rootDocument.getElementById("tileSizeInput"); -- this.refs.applyMapMetaButton = rootDocument.getElementById("applyMapMetaButton"); -- -- this.refs.layerList = rootDocument.getElementById("layerList"); -- this.refs.newLayerNameInput = rootDocument.getElementById("newLayerNameInput"); -- this.refs.addLayerButton = rootDocument.getElementById("addLayerButton"); -- this.refs.removeLayerButton = rootDocument.getElementById("removeLayerButton"); -- this.refs.duplicateLayerButton = rootDocument.getElementById("duplicateLayerButton"); -- this.refs.moveLayerUpButton = rootDocument.getElementById("moveLayerUpButton"); -- this.refs.moveLayerDownButton = rootDocument.getElementById("moveLayerDownButton"); -- -- this.refs.cameraXInput = rootDocument.getElementById("cameraXInput"); -- this.refs.cameraYInput = rootDocument.getElementById("cameraYInput"); -- this.refs.cameraReadout = rootDocument.getElementById("cameraReadout"); -- -- this.refs.layerNameInput = rootDocument.getElementById("layerNameInput"); -- this.refs.layerDrawOrderInput = rootDocument.getElementById("layerDrawOrderInput"); -- this.refs.layerOpacityInput = rootDocument.getElementById("layerOpacityInput"); -- this.refs.layerVisibleSelect = rootDocument.getElementById("layerVisibleSelect"); -- -- this.refs.layerImageSourceInput = rootDocument.getElementById("layerImageSourceInput"); -- this.refs.applyImageSourceButton = rootDocument.getElementById("applyImageSourceButton"); -- this.refs.layerImageFileInput = rootDocument.getElementById("layerImageFileInput"); -- -- this.refs.scrollFactorXInput = rootDocument.getElementById("scrollFactorXInput"); -- this.refs.scrollFactorYInput = rootDocument.getElementById("scrollFactorYInput"); -- this.refs.offsetXInput = rootDocument.getElementById("offsetXInput"); -- this.refs.offsetYInput = rootDocument.getElementById("offsetYInput"); -- this.refs.repeatXSelect = rootDocument.getElementById("repeatXSelect"); -- this.refs.repeatYSelect = rootDocument.getElementById("repeatYSelect"); -- this.refs.wrapModeSelect = rootDocument.getElementById("wrapModeSelect"); -- this.refs.applyLayerSettingsButton = rootDocument.getElementById("applyLayerSettingsButton"); -- -- this.refs.previewMeta = rootDocument.getElementById("previewMeta"); -- this.refs.simulationContext = rootDocument.getElementById("simulationContext"); -- this.refs.previewDetailsText = rootDocument.getElementById("previewDetailsText"); -- this.refs.statusText = rootDocument.getElementById("statusText"); -- this.refs.appShell = rootDocument.querySelector(".app-shell"); -- this.refs.leftSidebar = rootDocument.getElementById("leftSidebar"); -- this.refs.rightSidebar = rootDocument.getElementById("rightSidebar"); -- this.refs.leftPanelAccordions = Array.from(rootDocument.querySelectorAll("#leftSidebar .panel-accordion")); -- this.refs.rightPanelAccordions = Array.from(rootDocument.querySelectorAll("#rightSidebar .panel-accordion")); -- this.refs.overlayToggleButtons = Array.from(rootDocument.querySelectorAll("[data-overlay-toggle]")); -- this.refs.previewWrap = rootDocument.querySelector(".preview-wrap"); -- this.refs.previewCanvas = rootDocument.getElementById("previewCanvas"); -- this.refs.previewContext = this.refs.previewCanvas.getContext("2d", { alpha: false }); -- } -- -- attachEvents() { -- this.refs.newProjectButton.addEventListener("click", () => this.handleNewProject()); -- this.refs.loadProjectButton.addEventListener("click", () => this.refs.loadProjectInput.click()); -- this.refs.loadProjectInput.addEventListener("change", (event) => this.handleLoadProject(event)); -- this.refs.saveProjectButton.addEventListener("click", () => this.handleSaveProject()); -- this.refs.loadAssetRegistryButton.addEventListener("click", () => this.refs.loadAssetRegistryInput.click()); -- this.refs.loadAssetRegistryInput.addEventListener("change", (event) => this.handleLoadAssetRegistry(event)); -- this.refs.saveAssetRegistryButton.addEventListener("click", () => this.handleSaveAssetRegistry()); -- this.refs.simulateButton.addEventListener("click", () => this.enterSimulationMode()); -- this.refs.playSimulationButton.addEventListener("click", () => this.resumeSimulation()); -- this.refs.pauseSimulationButton.addEventListener("click", () => this.pauseSimulation()); -- this.refs.restartSimulationButton.addEventListener("click", () => this.restartSimulationPosition()); -- this.refs.exitSimulationButton.addEventListener("click", () => this.exitSimulationMode()); -- this.refs.exportParallaxPatchButton.addEventListener("click", () => this.handleExportTilemapPatch()); -- this.refs.packageProjectButton.addEventListener("click", () => this.handlePackageProject()); -- this.refs.refreshExperienceButton.addEventListener("click", () => this.refreshExperienceSnapshot()); -- this.refs.refreshDebugVisualizationButton.addEventListener("click", () => this.refreshDebugVisualizationSnapshot()); -- this.refs.inspectRemediationButton.addEventListener("click", () => this.inspectRemediationActions()); -- this.refs.jumpToProblemButton.addEventListener("click", () => this.jumpToRemediationProblem()); -- this.refs.applyRemediationButton.addEventListener("click", () => this.applyRemediationAction()); -- this.refs.overlayToggleButtons.forEach((button) => { -- button.addEventListener("click", () => { -- const side = button.dataset.overlaySide === "left" ? "left" : "right"; -- const targetId = button.dataset.overlayTarget || ""; -- this.toggleOverlayPanel(side, targetId); -- }); -- }); -- this.refs.leftPanelAccordions.forEach((panel) => { -- panel.addEventListener("toggle", () => this.handleOverlayAccordionToggle("left", panel)); -- }); -- this.refs.rightPanelAccordions.forEach((panel) => { -- panel.addEventListener("toggle", () => this.handleOverlayAccordionToggle("right", panel)); -- }); -- document.addEventListener("fullscreenchange", () => { -- this.syncFullscreenState(); -- this.refs.leftSidebar?.classList.remove("visible-overlay"); -- this.refs.rightSidebar?.classList.remove("visible-overlay"); -- this.syncOverlayToggleButtons(); -- }); -- -- this.refs.applyMapMetaButton.addEventListener("click", () => this.applyMapMetaFromInputs()); -- this.refs.projectNameInput.addEventListener("change", () => this.applyMapMetaFromInputs()); -- -- this.refs.addLayerButton.addEventListener("click", () => this.addLayer()); -- this.refs.removeLayerButton.addEventListener("click", () => this.removeSelectedLayer()); -- this.refs.duplicateLayerButton.addEventListener("click", () => this.duplicateSelectedLayer()); -- this.refs.moveLayerUpButton.addEventListener("click", () => this.moveSelectedLayer(-1)); -- this.refs.moveLayerDownButton.addEventListener("click", () => this.moveSelectedLayer(1)); -- -- this.refs.layerNameInput.addEventListener("change", () => this.applyBasicLayerFields()); -- this.refs.layerDrawOrderInput.addEventListener("change", () => this.applyBasicLayerFields()); -- this.refs.layerOpacityInput.addEventListener("change", () => this.applyBasicLayerFields()); -- this.refs.layerVisibleSelect.addEventListener("change", () => this.applyBasicLayerFields()); -- -- this.refs.applyImageSourceButton.addEventListener("click", () => this.applyImageSourceFromInput()); -- this.refs.layerImageFileInput.addEventListener("change", (event) => this.assignLocalImageFile(event)); -- -- this.refs.applyLayerSettingsButton.addEventListener("click", () => this.applyExtendedLayerSettings()); -- -- this.refs.cameraXInput.addEventListener("input", () => this.handleCameraChange()); -- this.refs.cameraYInput.addEventListener("input", () => this.handleCameraChange()); -- if (this.refs.previewWrap) { -- this.refs.previewWrap.addEventListener("scroll", () => { -- if (this.isSimulationMode) { -- this.updateSimulationContextReadout(); -- } -- }); -- } -- } -- -- syncFullscreenState() { -- document.body.classList.toggle("fullscreen-mode", Boolean(document.fullscreenElement)); -- this.syncOverlayToggleButtons(); -- } -- -- syncOverlayToggleButtons() { -- const fullscreenActive = Boolean(document.fullscreenElement); -- this.refs.overlayToggleButtons.forEach((button) => { -- const side = button.dataset.overlaySide === "left" ? "left" : "right"; -- const targetId = button.dataset.overlayTarget || ""; -- const sidebar = this.getOverlaySidebar(side); -- const target = targetId ? document.getElementById(targetId) : null; -- const active = Boolean( -- fullscreenActive -- && sidebar instanceof HTMLElement -- && sidebar.classList.contains("visible-overlay") -- && target instanceof HTMLElement -- && target.open -- ); -- button.setAttribute("aria-expanded", active ? "true" : "false"); -- const symbol = button.querySelector(".overlay-toggle-symbol"); -- if (symbol) { -- symbol.textContent = active ? "-" : "+"; -- } -- const hideWhileOverlayOpen = Boolean( -- fullscreenActive -- && sidebar instanceof HTMLElement -- && sidebar.classList.contains("visible-overlay") -- ); -- button.classList.toggle("is-hidden-while-overlay-open", hideWhileOverlayOpen); -- }); -- } -- -- getOverlaySidebar(side) { -- return side === "left" ? this.refs.leftSidebar : this.refs.rightSidebar; -- } -- -- getOverlayPanels(side) { -- return side === "left" ? this.refs.leftPanelAccordions : this.refs.rightPanelAccordions; -- } -- -- toggleOverlayPanel(side, targetId) { -- const sidebar = this.getOverlaySidebar(side); -- const panels = this.getOverlayPanels(side); -- if (!(sidebar instanceof HTMLElement) || !Array.isArray(panels) || panels.length === 0) { -- return; -- } -- const target = panels.find((panel) => panel.id === targetId); -- if (!(target instanceof HTMLElement)) { -- return; -- } -- -- const fullscreenActive = Boolean(document.fullscreenElement); -- if (!fullscreenActive) { -- target.open = !target.open; -- this.syncOverlayToggleButtons(); -- return; -- } -- -- const overlayVisible = sidebar.classList.contains("visible-overlay"); -- if (!overlayVisible) { -- sidebar.classList.add("visible-overlay"); -- target.open = true; -- } else { -- target.open = !target.open; -- } -- if (!panels.some((panel) => panel.open)) { -- sidebar.classList.remove("visible-overlay"); -- } -- -- this.syncOverlayToggleButtons(); -- } -- -- handleOverlayAccordionToggle(side, panel) { -- if (!document.fullscreenElement) { -- return; -- } -- const sidebar = this.getOverlaySidebar(side); -- const panels = this.getOverlayPanels(side); -- if (!(sidebar instanceof HTMLElement) || !Array.isArray(panels)) { -- return; -- } -- if (panel.open) { -- sidebar.classList.add("visible-overlay"); -- } else if (!panels.some((entry) => entry.open)) { -- sidebar.classList.remove("visible-overlay"); -- } -- this.syncOverlayToggleButtons(); -- } -- -- getSelectedLayer() { -- return this.documentModel.layers.find((layer) => layer.id === this.selectedLayerId) || this.documentModel.layers[0] || null; -- } -- -- touchDocument() { -- this.documentModel.metadata.updatedAt = new Date().toISOString(); -- this.queueLivePreviewSync("document-update"); -- } -- -- bindRuntimeStateSync() { -- this.livePreviewSync.subscribe((payload, envelope) => { -- if (envelope?.eventType !== "runtime-state-binding") { -- return; -- } -- const validation = validateStateBindingPayload(payload); -- if (!validation.valid || !payload?.runtimeState) { -- return; -- } -- this.boundRuntimeState = payload.runtimeState; -- const now = Date.now(); -- if ((now - this.lastRuntimeBindingStatusAt) < 500) { -- return; -- } -- this.lastRuntimeBindingStatusAt = now; -- const heroX = Number(this.boundRuntimeState.heroX).toFixed(1); -- const heroY = Number(this.boundRuntimeState.heroY).toFixed(1); -- const cameraX = Number(this.boundRuntimeState.cameraX).toFixed(1); -- const cameraY = Number(this.boundRuntimeState.cameraY).toFixed(1); -- this.updateStatus(`Runtime bound: hero(${heroX}, ${heroY}) camera(${cameraX}, ${cameraY}).`); -- }); -- } -- -- publishLivePreviewSync(reason = "update") { -- this.livePreviewSync.publish( -- { -- toolId: "parallax-editor", -- reason, -- parallaxDocument: cloneDeep(this.documentModel) -- }, -- "tool-live-preview-sync" -- ); -- } -- -- queueLivePreviewSync(reason = "update") { -- this.pendingLivePreviewReason = reason; -- if (this.livePreviewSyncFrame) { -- return; -- } -- const schedule = typeof requestAnimationFrame === "function" -- ? requestAnimationFrame -- : (callback) => setTimeout(callback, 16); -- this.livePreviewSyncFrame = schedule(() => { -- this.livePreviewSyncFrame = 0; -- this.publishLivePreviewSync(this.pendingLivePreviewReason); -- }); -- } -- -- updateStatus(message) { -- this.refs.statusText.textContent = message; -- } -- -- syncInputsFromDocument() { -- this.refs.projectNameInput.value = this.documentModel.map.name; -- this.refs.mapWidthInput.value = String(this.documentModel.map.width); -- this.refs.mapHeightInput.value = String(this.documentModel.map.height); -- this.refs.tileSizeInput.value = String(this.documentModel.map.tileSize); -- this.updateCameraInputBounds(); -- this.refs.cameraXInput.value = String(this.cameraX); -- this.refs.cameraYInput.value = String(this.cameraY); -- } -- -- updateCameraInputBounds() { -- const maxHorizontal = Math.max(1024, this.documentModel.map.pixelWidth); -- const maxVertical = Math.max(1024, this.documentModel.map.pixelHeight); -- this.refs.cameraXInput.min = String(-maxHorizontal); -- this.refs.cameraXInput.max = String(maxHorizontal); -- this.refs.cameraYInput.min = String(-maxVertical); -- this.refs.cameraYInput.max = String(maxVertical); -- } -- -- refreshSimulationActionState() { -- const inSimulation = this.isSimulationMode; -- this.refs.simulateButton.disabled = inSimulation; -- this.refs.playSimulationButton.disabled = !inSimulation || this.simulation.playing; -- this.refs.pauseSimulationButton.disabled = !inSimulation || !this.simulation.playing; -- this.refs.restartSimulationButton.disabled = !inSimulation; -- this.refs.exitSimulationButton.disabled = !inSimulation; -- this.refs.cameraXInput.disabled = inSimulation; -- this.refs.cameraYInput.disabled = inSimulation; -- } -- -- configureSimulationTraverse(playing) { -- const viewportWidth = this.refs.previewCanvas.width; -- const worldWidth = Math.max(1, this.documentModel.map.pixelWidth); -- const startCameraX = -Math.floor(viewportWidth * 0.2); -- const endCameraX = Math.max(startCameraX + 1, worldWidth - Math.floor(viewportWidth * 0.8)); -- -- this.simulation.startCameraX = startCameraX; -- this.simulation.endCameraX = endCameraX; -- this.simulation.startCameraY = 0; -- this.simulation.endCameraY = 0; -- this.simulation.traversalDistance = Math.max(1, Math.abs(endCameraX - startCameraX)); -- this.simulation.traversalDurationMs = Math.max(9000, Math.min(24000, 9000 + (this.simulation.traversalDistance * 9))); -- this.simulation.traversedDistance = 0; -- this.simulation.playing = playing; -- this.simulation.lastTimestamp = 0; -- this.simulation.accumulatorMs = 0; -- -- this.cameraX = startCameraX; -- this.cameraY = 0; -- if (this.refs.previewWrap) { -- this.refs.previewWrap.scrollTop = 0; -- } -- this.refs.cameraXInput.value = String(this.cameraX); -- this.refs.cameraYInput.value = String(this.cameraY); -- this.refs.cameraReadout.textContent = `camera: ${this.cameraX}, ${this.cameraY}`; -- this.updateSimulationContextReadout(); -- } -- -- getSimulationProgress() { -- if (!this.isSimulationMode || this.simulation.traversalDistance <= 0) { -- return 0; -- } -- return Math.max(0, Math.min(1, this.simulation.traversedDistance / this.simulation.traversalDistance)); -- } -- -- updateSimulationContextReadout() { -- if (!this.isSimulationMode) { -- this.refs.simulationContext.textContent = ""; -- return; -- } -- -- const sortedLayers = cloneDeep(this.documentModel.layers); -- sortLayersByOrder(sortedLayers); -- const progress = this.getSimulationProgress(); -- const progressPercent = Math.round(progress * 100); -- const repeatCount = sortedLayers.filter((layer) => layer.repeatX || layer.repeatY).length; -- const wrapCount = sortedLayers.filter((layer) => layer.wrapMode === "wrap").length; -- const backLayer = sortedLayers[0]?.name || "none"; -- const frontLayer = sortedLayers[sortedLayers.length - 1]?.name || "none"; -- const mode = this.simulation.playing ? "PLAY" : "PAUSE"; -- this.refs.simulationContext.textContent = `${mode} ${progressPercent}% cam:${this.cameraX},${this.cameraY} depth:${backLayer}->${frontLayer} repeat:${repeatCount}/${sortedLayers.length} wrap:${wrapCount}`; -- } -- -- applySimulationCameraAtProgress(progress) { -- const clampedProgress = Math.max(0, Math.min(1, progress)); -- const nextX = this.simulation.startCameraX -- + ((this.simulation.endCameraX - this.simulation.startCameraX) * clampedProgress); -- -- this.cameraX = Math.round(nextX); -- this.cameraY = 0; -- this.refs.cameraXInput.value = String(this.cameraX); -- this.refs.cameraYInput.value = String(this.cameraY); -- this.refs.cameraReadout.textContent = `camera: ${this.cameraX}, ${this.cameraY}`; -- } -- -- enterSimulationMode() { -- if (this.isSimulationMode) { -- return; -- } -- -- this.isSimulationMode = true; -- this.simulation.baseCameraX = this.cameraX; -- this.simulation.baseCameraY = this.cameraY; -- this.configureSimulationTraverse(true); -- this.refreshSimulationActionState(); -- this.updateStatus("Simulation mode active. Running full-map parallax traversal."); -- this.startSimulationLoop(); -- this.renderPreview(); -- } -- -- pauseSimulation() { -- if (!this.isSimulationMode || !this.simulation.playing) { -- return; -- } -- this.simulation.playing = false; -- this.simulation.lastTimestamp = 0; -- this.simulation.accumulatorMs = 0; -- this.refreshSimulationActionState(); -- this.updateSimulationContextReadout(); -- this.renderPreview(); -- this.updateStatus("Simulation paused."); -- } -- -- resumeSimulation() { -- if (!this.isSimulationMode || this.simulation.playing) { -- return; -- } -- this.simulation.playing = true; -- this.simulation.lastTimestamp = 0; -- this.simulation.accumulatorMs = 0; -- this.refreshSimulationActionState(); -- this.updateSimulationContextReadout(); -- this.renderPreview(); -- this.updateStatus("Simulation resumed."); -- } -- -- restartSimulationPosition() { -- if (!this.isSimulationMode) { -- return; -- } -- this.configureSimulationTraverse(this.simulation.playing); -- this.refreshSimulationActionState(); -- this.renderPreview(); -- this.updateStatus("Simulation restarted from the initial traversal position."); -- } -- -- exitSimulationMode() { -- const wasSimulationMode = this.isSimulationMode; -- if (!wasSimulationMode && !this.simulation.rafId) { -- return; -- } -- -- this.isSimulationMode = false; -- if (this.simulation.rafId) { -- cancelAnimationFrame(this.simulation.rafId); -- this.simulation.rafId = 0; -- } -- -- this.simulation.playing = false; -- this.simulation.lastTimestamp = 0; -- this.simulation.accumulatorMs = 0; -- this.simulation.traversedDistance = 0; -- this.simulation.traversalDistance = 0; -- this.cameraX = this.simulation.baseCameraX; -- this.cameraY = this.simulation.baseCameraY; -- this.refs.cameraXInput.value = String(this.cameraX); -- this.refs.cameraYInput.value = String(this.cameraY); -- this.refs.simulationContext.textContent = ""; -- this.refreshSimulationActionState(); -- this.renderPreview(); -- if (wasSimulationMode) { -- this.updateStatus("Exited simulation mode."); -- } -- } -- -- startSimulationLoop() { -- const tick = (timestamp) => { -- if (!this.isSimulationMode) { -- return; -- } -- -- if (this.simulation.playing) { -- if (this.simulation.lastTimestamp === 0) { -- this.simulation.lastTimestamp = timestamp; -- } -- const deltaMs = timestamp - this.simulation.lastTimestamp; -- this.simulation.lastTimestamp = timestamp; -- this.advanceSimulationCamera(deltaMs); -- } -- -- this.renderPreview(); -- this.simulation.rafId = requestAnimationFrame(tick); -- }; -- -- this.simulation.rafId = requestAnimationFrame(tick); -- } -- -- advanceSimulationCamera(deltaMs) { -- const durationMs = Math.max(1, this.simulation.traversalDurationMs); -- const advanceBy = Math.max(0, deltaMs) * (this.simulation.traversalDistance / durationMs); -- this.simulation.traversedDistance = Math.min( -- this.simulation.traversalDistance, -- this.simulation.traversedDistance + advanceBy -- ); -- const progress = this.getSimulationProgress(); -- this.applySimulationCameraAtProgress(progress); -- this.updateSimulationContextReadout(); -- -- if (progress >= 1) { -- this.simulation.playing = false; -- this.refreshSimulationActionState(); -- this.updateStatus("Simulation reached the end of the full-map traverse."); -- } -- } -- -- async tryLoadPresetFromQuery() { -- const searchParams = new URLSearchParams(window.location.search); -- const samplePresetPath = normalizeSamplePresetPath(searchParams.get("samplePresetPath") || ""); -- const launchQuery = getToolLoadQuerySnapshot(searchParams); -- logToolLoadRequest({ -- toolId: "parallax-editor", -- sampleId: String(searchParams.get("sampleId") || "").trim(), -- samplePresetPath, -- requestedDataPaths: getToolLoadRequestedDataPaths(launchQuery), -- launchQuery -- }); -- if (!samplePresetPath) { -- logToolLoadWarning({ -- toolId: "parallax-editor", -- reason: "samplePresetPath missing", -- launchQuery -- }); -- return; -- } -- -- const sampleId = String(searchParams.get("sampleId") || "").trim(); -- this.exitSimulationMode(); -- try { -- const presetUrl = new URL(samplePresetPath, window.location.href); -- const presetHref = presetUrl.toString(); -- logToolLoadFetch({ -- toolId: "parallax-editor", -- phase: "attempt", -- fetchUrl: presetHref, -- requestedPath: samplePresetPath, -- pathSource: "tool-input:query.samplePresetPath" -- }); -- const response = await fetch(presetHref, { cache: "no-store" }); -- logToolLoadFetch({ -- toolId: "parallax-editor", -- phase: "response", -- fetchUrl: presetHref, -- requestedPath: samplePresetPath, -- pathSource: "tool-input:query.samplePresetPath", -- status: response.status, -- ok: response.ok -- }); -- if (!response.ok) { -- throw new Error(`Preset request failed (${response.status}).`); -- } -- const rawPreset = await response.json(); -- await logToolLoadLoaded({ -- toolId: "parallax-editor", -- toolName: "Parallax Scene Studio", -- sampleId, -- samplePresetPath, -- requestedPath: samplePresetPath, -- fetchUrl: presetHref, -- loadedDocument: rawPreset, -- loaded: summarizeToolLoadData(rawPreset) -- }); -- const toolDocument = extractParallaxDocumentFromSamplePreset(rawPreset); -- this.skipExternalProjectStateUntil = Date.now() + 3000; -- this.applyParallaxDocument(extractParallaxDocument(toolDocument)); -- this.queueLivePreviewSync("sample-preset"); -- this.updateStatus(buildPresetLoadedStatus(sampleId, samplePresetPath)); -- } catch (error) { -- logToolLoadWarning({ -- toolId: "parallax-editor", -- sampleId, -- samplePresetPath, -- error: error instanceof Error ? error.message : "unknown error" -- }); -- this.updateStatus(`Preset load failed: ${error instanceof Error ? error.message : "unknown error"}`); -- } -- } -- -- applyParallaxDocument(nextDocument) { -- this.documentModel = nextDocument; -- this.resolveAssetRefsFromRegistry(); -- normalizeDrawOrderSequence(this.documentModel.layers); -- this.selectedLayerId = this.documentModel.layers[0]?.id || ""; -- this.clearTransientLayerImageSources(); -- this.invalidateImageCache(); -- this.cameraX = 0; -- this.cameraY = 0; -- this.syncInputsFromDocument(); -- this.renderAll(); -- } -- -- handleNewProject() { -- this.exitSimulationMode(); -- this.documentModel = createInitialParallaxDocument({ map: this.documentModel.map }); -- this.selectedLayerId = this.documentModel.layers[0]?.id || ""; -- this.clearTransientLayerImageSources(); -- this.invalidateImageCache(); -- this.cameraX = 0; -- this.cameraY = 0; -- this.syncInputsFromDocument(); -- this.renderAll(); -- this.queueLivePreviewSync("new-project"); -- this.updateStatus("Created new parallax document."); -- } -- -- syncAssetRegistryFromDocument() { -- const projectName = this.documentModel?.map?.name || "parallax-map"; -- this.assetRegistry = sanitizeAssetRegistry(this.assetRegistry); -- this.assetRegistry.projectId = this.assetRegistry.projectId || projectName; -- -- const sourceIds = []; -- this.documentModel.layers.forEach((layer, index) => { -- const layerPath = normalizeProjectRelativePath(layer?.imageSource || ""); -- if (!layerPath) { -- return; -- } -- -- const imageId = createAssetId("image", layerPath, `parallax-image-${index + 1}`); -- const sourceId = layer.parallaxSourceId || createAssetId("parallax", layer.id || layer.name || `source-${index + 1}`, `source-${index + 1}`); -- layer.parallaxSourceId = sourceId; -- -- const imageRegistration = registerAssetPipelineCandidate({ -- registry: this.assetRegistry, -- section: "images", -- ingest: { -- id: imageId, -- name: layer.name || `Parallax Layer ${index + 1}`, -- path: layerPath, -- sourceTool: "parallax-editor" -- } -- }); -- this.assetRegistry = imageRegistration.registry; -- -- const sourceRegistration = registerAssetPipelineCandidate({ -- registry: this.assetRegistry, -- section: "parallaxSources", -- ingest: { -- id: sourceId, -- name: layer.name || `Parallax Layer ${index + 1}`, -- path: layerPath, -- sourceTool: "parallax-editor" -- }, -- entryFields: { -- imageId -- } -- }); -- this.assetRegistry = sourceRegistration.registry; -- -- sourceIds.push(sourceId); -- }); -- -- this.documentModel.assetRefs = { -- parallaxSourceIds: Array.from(new Set(sourceIds)) -- }; -- } -- -- resolveAssetRefsFromRegistry() { -- this.documentModel.assetRefs = sanitizeAssetRefs(this.documentModel.assetRefs); -- const sourceIds = this.documentModel.assetRefs.parallaxSourceIds; -- if (!Array.isArray(sourceIds) || sourceIds.length === 0) { -- return { -- resolvedCount: 0, -- unresolvedCount: 0 -- }; -- } -- -- let fallbackIndex = 0; -- let resolvedCount = 0; -- let unresolvedCount = 0; -- this.documentModel.layers.forEach((layer) => { -- const sourceId = layer.parallaxSourceId || sourceIds[fallbackIndex] || ""; -- if (!sourceId) { -- return; -- } -- -- if (!layer.parallaxSourceId) { -- layer.parallaxSourceId = sourceId; -- } -- -- if (normalizeProjectRelativePath(layer.imageSource || "")) { -- fallbackIndex += 1; -- return; -- } -- -- const sourceEntry = findRegistryEntryById(this.assetRegistry, "parallaxSources", sourceId); -- const sourcePath = normalizeProjectRelativePath(sourceEntry?.path || ""); -- if (sourcePath) { -- layer.imageSource = sourcePath; -- resolvedCount += 1; -- } else { -- unresolvedCount += 1; -- } -- fallbackIndex += 1; -- }); -- -- return { -- resolvedCount, -- unresolvedCount -- }; -- } -- -- handleSaveProject() { -- this.touchDocument(); -- this.syncAssetRegistryFromDocument(); -- const validation = this.validateProjectAssets(); -- if (hasBlockingAssetValidationFindings(validation)) { -- this.updateStatus(`${getBlockingAssetValidationMessage("Save Project", validation)} (${summarizeAssetValidation(validation)}).`); -- return; -- } -- const output = addToolModeMetadata(createRegistryManagedParallaxSaveDocument(this.documentModel), { toolId: "parallax-editor" }); -- const { graph, findings } = buildAssetDependencyGraph(this.assetRegistry); -- this.assetDependencyGraphSnapshot = graph; -- output.project = { -- ...(output.project && typeof output.project === "object" ? output.project : {}), -- assetDependencyGraph: graph -- }; -- const payload = JSON.stringify(output, null, 2); -- const fileName = `${this.documentModel.map.name || "map"}.parallax.json`; -- downloadTextFile(payload, fileName); -- this.updateStatus(`Saved ${fileName} (${output.assetRefs.parallaxSourceIds.length} parallax asset refs, ID-based layer references).${summarizeGraphFindings(findings)} Validation: ${summarizeAssetValidation(validation)}.`); -- } -- -- handleSaveAssetRegistry() { -- this.syncAssetRegistryFromDocument(); -- const validation = this.validateProjectAssets(); -- if (hasBlockingAssetValidationFindings(validation)) { -- this.updateStatus(`${getBlockingAssetValidationMessage("Save Asset Registry", validation)} (${summarizeAssetValidation(validation)}).`); -- return; -- } -- const { findings } = buildAssetDependencyGraph(this.assetRegistry); -- const payload = createRegistryDownloadPayload(this.assetRegistry); -- downloadTextFile(payload, "project.assets.json"); -- this.updateStatus(`Saved project.assets.json (${this.assetRegistry.parallaxSources.length} parallax sources).${summarizeGraphFindings(findings)} Validation: ${summarizeAssetValidation(validation)}.`); -- } -- -- handleExportTilemapPatch() { -- const validation = this.validateProjectAssets(); -- if (hasBlockingAssetValidationFindings(validation)) { -- this.updateStatus(`${getBlockingAssetValidationMessage("Export Parallax Patch", validation)} (${summarizeAssetValidation(validation)}).`); -- return; -- } -- const patch = createTilemapParallaxPatch(createRegistryManagedParallaxSaveDocument(this.documentModel)); -- const payload = JSON.stringify(patch, null, 2); -- const fileName = `${this.documentModel.map.name || "map"}.tilemap-parallax.patch.json`; -- downloadTextFile(payload, fileName); -- this.updateStatus(`Exported ${fileName}. Validation: ${summarizeAssetValidation(validation)}.`); -- } -- -- handlePackageProject() { -- this.touchDocument(); -- this.syncAssetRegistryFromDocument(); -- const validation = this.validateProjectAssets(); -- const persistableDocument = createRegistryManagedParallaxSaveDocument(this.documentModel); -- const packageResult = buildProjectPackage({ -- registry: this.assetRegistry, -- validationResult: validation, -- parallaxDocument: persistableDocument -- }); -- this.lastPackageResult = packageResult; -- this.editorExperienceResult = buildEditorExperienceLayer({ -- assetDependencyGraph: validation.assetDependencyGraph, -- validationResult: validation, -- remediationResult: this.remediationResult, -- packageResult: this.lastPackageResult, -- runtimeResult: this.lastRuntimeResult -- }); -- this.debugVisualizationResult = buildDebugVisualizationLayer({ -- assetDependencyGraph: validation.assetDependencyGraph, -- validationResult: validation, -- remediationResult: this.remediationResult, -- packageResult: this.lastPackageResult, -- runtimeResult: this.lastRuntimeResult -- }); -- this.updateEditorExperienceUI(); -- this.updateDebugVisualizationUI(); -- if (packageResult.packageStatus !== "ready") { -- this.updateStatus(`${summarizeProjectPackaging(packageResult)} ${packageResult.manifest.package.reports[0]?.message || ""}`.trim()); -- return; -- } -- const fileBase = `${this.assetRegistry.projectId || this.documentModel.map.name || "parallax-project"}.package`; -- downloadTextFile(`${JSON.stringify(packageResult.manifest, null, 2)}\n`, `${fileBase}.json`); -- downloadTextFile(`${packageResult.reportText}\n`, `${fileBase}.report.txt`); -- this.updateStatus(`${summarizeProjectPackaging(packageResult)} Manifest and report exported.`); -- } -- -- async handleLoadProject(event) { -- this.exitSimulationMode(); -- const file = event.target.files?.[0]; -- if (!file) { -- return; -- } -- -- try { -- const text = await readFileText(file); -- const raw = JSON.parse(text); -- const guard = assertStandaloneToolDocument(raw, { -- expectedLabel: "Parallax project", -- acceptedSchemas: ["toolbox.parallax/1", "toolbox.tilemap/1"], -- requiredToolId: "parallax-editor" -- }); -- if (!guard.ok) { -- const handled = offerImportMismatchOptions(guard, { -- viewerToolId: "state-inspector", -- viewerPayload: raw, -- sourceToolId: "parallax-editor" -- }); -- if (handled) { -- return; -- } -- throw new Error(guard.reason); -- } -- this.documentModel = extractParallaxDocument(raw); -- this.assetDependencyGraphSnapshot = raw?.project?.assetDependencyGraph || null; -- const resolution = this.resolveAssetRefsFromRegistry(); -- normalizeDrawOrderSequence(this.documentModel.layers); -- this.selectedLayerId = this.documentModel.layers[0]?.id || ""; -- this.clearTransientLayerImageSources(); -- this.invalidateImageCache(); -- this.cameraX = 0; -- this.cameraY = 0; -- this.syncInputsFromDocument(); -- this.renderAll(); -- this.queueLivePreviewSync("load-project"); -- const validation = this.validateProjectAssets(); -- if (resolution.resolvedCount > 0) { -- this.updateStatus(`Loaded ${file.name} (${resolution.resolvedCount} layer image refs restored from asset registry, validation: ${summarizeAssetValidation(validation)}).`); -- } else if (resolution.unresolvedCount > 0) { -- this.updateStatus(`Loaded ${file.name} (${resolution.unresolvedCount} registry image refs unresolved; layer image paths are missing, validation: ${summarizeAssetValidation(validation)}).`); -- } else { -- this.updateStatus(`Loaded ${file.name} (validation: ${summarizeAssetValidation(validation)}).`); -- } -- } catch (error) { -- this.updateStatus(`Load failed: ${error instanceof Error ? error.message : "invalid JSON"}`); -- } -- this.refs.loadProjectInput.value = ""; -- } -- -- async handleLoadAssetRegistry(event) { -- const file = event.target.files?.[0]; -- if (!file) { -- return; -- } -- -- try { -- const text = await readFileText(file); -- const parsed = JSON.parse(text); -- this.assetRegistry = mergeAssetRegistries(this.assetRegistry, parsed); -- const resolution = this.resolveAssetRefsFromRegistry(); -- this.invalidateImageCache(); -- this.renderAll(); -- const validation = this.validateProjectAssets(); -- if (resolution.resolvedCount > 0) { -- this.updateStatus(`Loaded ${file.name} (${this.assetRegistry.parallaxSources.length} parallax sources; ${resolution.resolvedCount} layer refs restored; validation: ${summarizeAssetValidation(validation)}).`); -- } else if (resolution.unresolvedCount > 0) { -- this.updateStatus(`Loaded ${file.name} (${this.assetRegistry.parallaxSources.length} parallax sources; ${resolution.unresolvedCount} refs still unresolved; validation: ${summarizeAssetValidation(validation)}).`); -- } else { -- this.updateStatus(`Loaded ${file.name} (${this.assetRegistry.parallaxSources.length} parallax sources, validation: ${summarizeAssetValidation(validation)}).`); -- } -- } catch (error) { -- this.updateStatus(`Asset registry load failed: ${error instanceof Error ? error.message : "invalid JSON"}`); -- } -- this.refs.loadAssetRegistryInput.value = ""; -- } -- -- applyMapMetaFromInputs() { -- const map = normalizeMapMeta({ -- name: this.refs.projectNameInput.value, -- width: this.refs.mapWidthInput.value, -- height: this.refs.mapHeightInput.value, -- tileSize: this.refs.tileSizeInput.value -- }); -- -- this.documentModel.map = map; -- this.updateCameraInputBounds(); -- this.touchDocument(); -- this.renderPreview(); -- this.refs.previewMeta.textContent = `${map.width}x${map.height} @ ${map.tileSize}px (${map.pixelWidth}x${map.pixelHeight})`; -- this.updateStatus("Updated map metadata for parallax preview."); -- } -- -- addLayer() { -- const name = this.refs.newLayerNameInput.value.trim() || `Parallax Layer ${this.documentModel.layers.length + 1}`; -- const newLayer = createDefaultLayer(this.documentModel.layers.length, name); -- newLayer.drawOrder = this.documentModel.layers.length; -- -- this.documentModel.layers.push(newLayer); -- normalizeDrawOrderSequence(this.documentModel.layers); -- this.selectedLayerId = newLayer.id; -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(`Added layer ${name}.`); -- } -- -- removeSelectedLayer() { -- if (this.documentModel.layers.length <= 1) { -- this.updateStatus("Cannot remove the last parallax layer."); -- return; -- } -- -- const index = this.documentModel.layers.findIndex((layer) => layer.id === this.selectedLayerId); -- if (index < 0) { -- return; -- } -- -- const removed = this.documentModel.layers[index]; -- this.documentModel.layers.splice(index, 1); -- normalizeDrawOrderSequence(this.documentModel.layers); -- this.selectedLayerId = this.documentModel.layers[Math.max(0, index - 1)]?.id || this.documentModel.layers[0].id; -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(`Removed layer ${removed.name}.`); -- } -- -- duplicateSelectedLayer() { -- const selected = this.getSelectedLayer(); -- if (!selected) { -- return; -- } -- -- const duplicate = normalizeLayer(cloneDeep(selected), this.documentModel.layers.length); -- duplicate.id = `parallax-layer-${Date.now()}-${Math.floor(Math.random() * 10000)}`; -- duplicate.name = `${selected.name} Copy`; -- duplicate.drawOrder = this.documentModel.layers.length; -- -- this.documentModel.layers.push(duplicate); -- normalizeDrawOrderSequence(this.documentModel.layers); -- this.selectedLayerId = duplicate.id; -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(`Duplicated layer ${selected.name}.`); -- } -- -- moveSelectedLayer(direction) { -- const currentIndex = this.documentModel.layers.findIndex((layer) => layer.id === this.selectedLayerId); -- if (currentIndex < 0) { -- return; -- } -- -- const nextIndex = currentIndex + direction; -- if (nextIndex < 0 || nextIndex >= this.documentModel.layers.length) { -- return; -- } -- -- const [layer] = this.documentModel.layers.splice(currentIndex, 1); -- this.documentModel.layers.splice(nextIndex, 0, layer); -- // Preserve the user-driven array move, then reindex drawOrder without re-sorting. -- this.documentModel.layers.forEach((entry, index) => { -- entry.drawOrder = index; -- }); -- this.selectedLayerId = layer.id; -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(`Moved layer ${layer.name} ${direction < 0 ? "up" : "down"}.`); -- } -- -- applyBasicLayerFields() { -- const layer = this.getSelectedLayer(); -- if (!layer) { -- return; -- } -- -- layer.name = this.refs.layerNameInput.value.trim() || layer.name; -- layer.drawOrder = Math.trunc(clamp(this.refs.layerDrawOrderInput.value, -999, 999, layer.drawOrder)); -- layer.opacity = clamp(this.refs.layerOpacityInput.value, 0, 1, layer.opacity); -- layer.visible = this.refs.layerVisibleSelect.value === "true"; -- -- normalizeDrawOrderSequence(this.documentModel.layers); -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(`Updated basic layer settings for ${layer.name}.`); -- } -- -- applyExtendedLayerSettings() { -- const layer = this.getSelectedLayer(); -- if (!layer) { -- return; -- } -- -- layer.scrollFactorX = clamp(this.refs.scrollFactorXInput.value, -4, 4, layer.scrollFactorX); -- layer.scrollFactorY = clamp(this.refs.scrollFactorYInput.value, -4, 4, layer.scrollFactorY); -- layer.offsetX = Math.trunc(clamp(this.refs.offsetXInput.value, -4096, 4096, layer.offsetX)); -- layer.offsetY = Math.trunc(clamp(this.refs.offsetYInput.value, -4096, 4096, layer.offsetY)); -- layer.repeatX = this.refs.repeatXSelect.value === "true"; -- layer.repeatY = this.refs.repeatYSelect.value === "true"; -- layer.wrapMode = this.refs.wrapModeSelect.value === "clamp" ? "clamp" : "wrap"; -- -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(`Applied scroll/repeat settings for ${layer.name}.`); -- } -- -- applyImageSourceFromInput() { -- const layer = this.getSelectedLayer(); -- if (!layer) { -- return; -- } -- -- const source = this.refs.layerImageSourceInput.value.trim(); -- if (layer.imageSource && layer.imageSource !== source) { -- const previousTransient = this.getTransientLayerImageSource(layer.imageSource); -- if (previousTransient) { -- URL.revokeObjectURL(previousTransient); -- this.transientLayerImageSourceByName.delete(layer.imageSource.trim()); -- } -- } -- layer.imageSource = source; -- -- this.invalidateImageCache(); -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(source ? `Assigned image source to ${layer.name}.` : `Cleared image source for ${layer.name}.`); -- } -- -- assignLocalImageFile(event) { -- const layer = this.getSelectedLayer(); -- if (!layer) { -- return; -- } -- -- const file = event.target.files?.[0]; -- if (!file) { -- return; -- } -- -- const previousSource = layer.imageSource; -- const objectUrl = URL.createObjectURL(file); -- layer.imageSource = file.name; -- this.setTransientLayerImageSource(layer.imageSource, objectUrl); -- if (previousSource && previousSource !== layer.imageSource) { -- const previousTransient = this.getTransientLayerImageSource(previousSource); -- if (previousTransient) { -- URL.revokeObjectURL(previousTransient); -- this.transientLayerImageSourceByName.delete(previousSource.trim()); -- } -- } -- this.invalidateImageCache(); -- this.touchDocument(); -- this.renderAll(); -- this.updateStatus(`Assigned local image file ${file.name} to ${layer.name}.`); -- this.refs.layerImageFileInput.value = ""; -- } -- -- handleCameraChange() { -- if (this.isSimulationMode) { -- return; -- } -- const maxHorizontal = Math.max(1024, this.documentModel.map.pixelWidth); -- const maxVertical = Math.max(1024, this.documentModel.map.pixelHeight); -- this.cameraX = Math.trunc(clamp(this.refs.cameraXInput.value, -maxHorizontal, maxHorizontal, 0)); -- this.cameraY = Math.trunc(clamp(this.refs.cameraYInput.value, -maxVertical, maxVertical, 0)); -- this.refs.cameraReadout.textContent = `camera: ${this.cameraX}, ${this.cameraY}`; -- this.renderPreview(); -- } -- -- renderAll() { -- normalizeDrawOrderSequence(this.documentModel.layers); -- this.renderLayerList(); -- this.renderSelectedLayerFields(); -- this.renderPreview(); -- this.refs.previewMeta.textContent = `${this.documentModel.map.width}x${this.documentModel.map.height} @ ${this.documentModel.map.tileSize}px (${this.documentModel.map.pixelWidth}x${this.documentModel.map.pixelHeight})`; -- this.refs.cameraReadout.textContent = `camera: ${this.cameraX}, ${this.cameraY}`; -- this.refreshSimulationActionState(); -- this.updateSimulationContextReadout(); -- this.updateRemediationUI(); -- this.updateEditorExperienceUI(); -- this.updateDebugVisualizationUI(); -- } -- -- renderLayerList() { -- const list = this.refs.layerList; -- list.innerHTML = ""; -- -- const fragment = document.createDocumentFragment(); -- this.documentModel.layers.forEach((layer) => { -- const item = document.createElement("li"); -- item.className = `layer-item${layer.id === this.selectedLayerId ? " selected" : ""}`; -- -- const row = document.createElement("div"); -- row.className = "layer-row"; -- -- const selectButton = document.createElement("button"); -- selectButton.type = "button"; -- selectButton.className = "layer-select-btn"; -- selectButton.textContent = layer.name; -- selectButton.addEventListener("click", () => { -- this.selectedLayerId = layer.id; -- this.renderAll(); -- this.updateStatus(`Selected layer ${layer.name}.`); -- }); -- -- const orderChip = document.createElement("span"); -- orderChip.className = "layer-order-chip"; -- orderChip.textContent = `order ${layer.drawOrder}`; -- -- row.appendChild(selectButton); -- row.appendChild(orderChip); -- item.appendChild(row); -- fragment.appendChild(item); -- }); -- -- list.appendChild(fragment); -- } -- -- renderSelectedLayerFields() { -- const layer = this.getSelectedLayer(); -- if (!layer) { -- return; -- } -- -- this.refs.layerNameInput.value = layer.name; -- this.refs.layerDrawOrderInput.value = String(layer.drawOrder); -- this.refs.layerOpacityInput.value = String(layer.opacity); -- this.refs.layerVisibleSelect.value = layer.visible ? "true" : "false"; -- -- this.refs.layerImageSourceInput.value = layer.imageSource || ""; -- this.refs.scrollFactorXInput.value = String(layer.scrollFactorX); -- this.refs.scrollFactorYInput.value = String(layer.scrollFactorY); -- this.refs.offsetXInput.value = String(layer.offsetX); -- this.refs.offsetYInput.value = String(layer.offsetY); -- this.refs.repeatXSelect.value = layer.repeatX ? "true" : "false"; -- this.refs.repeatYSelect.value = layer.repeatY ? "true" : "false"; -- this.refs.wrapModeSelect.value = layer.wrapMode; -- } -- -- resolveLayerImageUrl(source) { -- if (!source || source.startsWith("blob:")) { -- return source; -- } -- -- try { -- const url = new URL(source, window.location.href); -- url.searchParams.set("_cb", String(this.imageCacheVersion)); -- return url.toString(); -- } catch (error) { -- const separator = source.includes("?") ? "&" : "?"; -- return `${source}${separator}_cb=${this.imageCacheVersion}`; -- } -- } -- -- getLayerImageRecord(layer) { -- const source = this.getTransientLayerImageSource(layer.imageSource) || layer.imageSource; -- if (!source) { -- return null; -- } -- -- const cacheKey = `${source}::v${this.imageCacheVersion}`; -- if (this.imageCache.has(cacheKey)) { -- return this.imageCache.get(cacheKey); -- } -- -- const record = { -- status: "loading", -- image: new Image(), -- error: null -- }; -- -- record.image.onload = () => { -- record.status = "ready"; -- this.renderPreview(); -- }; -- record.image.onerror = () => { -- record.status = "error"; -- record.error = "Image failed to load."; -- this.renderPreview(); -- }; -- -- if (!source.startsWith("blob:")) { -- record.image.crossOrigin = "anonymous"; -- } -- record.image.src = this.resolveLayerImageUrl(source); -- -- this.imageCache.set(cacheKey, record); -- return record; -- } -- -- drawFallbackLayer(context, layer, viewportWidth, viewportHeight, screenX, screenY) { -- const color = getLayerVisualColor(layer); -- context.fillStyle = color; -- -- if (layer.repeatX || layer.repeatY) { -- context.globalAlpha = layer.opacity * 0.25; -- context.fillRect(0, 0, viewportWidth, viewportHeight); -- context.globalAlpha = layer.opacity * 0.6; -- const stripeHeight = Math.max(24, Math.floor(viewportHeight / 10)); -- for (let y = 0; y < viewportHeight; y += stripeHeight * 2) { -- context.fillRect(0, y, viewportWidth, stripeHeight); -- } -- return; -- } -- -- const width = Math.max(240, Math.floor(viewportWidth * 0.4)); -- const height = Math.max(140, Math.floor(viewportHeight * 0.25)); -- context.globalAlpha = layer.opacity * 0.65; -- context.fillRect(screenX, screenY, width, height); -- } -- -- drawSingleLayer(context, layer, viewportWidth, viewportHeight, mapOriginX, mapOriginY) { -- if (!layer.visible || layer.opacity <= 0) { -- return; -- } -- -- const screenX = Math.round(mapOriginX + (-this.cameraX * layer.scrollFactorX) + layer.offsetX); -- const screenY = Math.round(mapOriginY + (-this.cameraY * layer.scrollFactorY) + layer.offsetY); -- -- context.save(); -- context.globalAlpha = layer.opacity; -- -- const imageRecord = this.getLayerImageRecord(layer); -- if (!imageRecord || imageRecord.status === "error" || imageRecord.status === "loading") { -- this.drawFallbackLayer(context, layer, viewportWidth, viewportHeight, screenX, screenY); -- context.restore(); -- return; -- } -- -- const image = imageRecord.image; -- const tileWidth = Math.max(1, image.naturalWidth || image.width || viewportWidth); -- const tileHeight = Math.max(1, image.naturalHeight || image.height || viewportHeight); -- -- const xPositions = []; -- const yPositions = []; -- -- if (layer.repeatX) { -- if (layer.wrapMode === "wrap") { -- let startX = mod(screenX, tileWidth) - tileWidth; -- for (let x = startX; x <= viewportWidth + tileWidth; x += tileWidth) { -- xPositions.push(x); -- } -- } else { -- for (let x = screenX; x <= viewportWidth + tileWidth; x += tileWidth) { -- xPositions.push(x); -- } -- for (let x = screenX - tileWidth; x >= -tileWidth; x -= tileWidth) { -- xPositions.push(x); -- } -- } -- } else { -- xPositions.push(screenX); -- } -- -- if (layer.repeatY) { -- if (layer.wrapMode === "wrap") { -- let startY = mod(screenY, tileHeight) - tileHeight; -- for (let y = startY; y <= viewportHeight + tileHeight; y += tileHeight) { -- yPositions.push(y); -- } -- } else { -- for (let y = screenY; y <= viewportHeight + tileHeight; y += tileHeight) { -- yPositions.push(y); -- } -- for (let y = screenY - tileHeight; y >= -tileHeight; y -= tileHeight) { -- yPositions.push(y); -- } -- } -- } else { -- yPositions.push(screenY); -- } -- -- for (let yi = 0; yi < yPositions.length; yi += 1) { -- for (let xi = 0; xi < xPositions.length; xi += 1) { -- context.drawImage(image, Math.round(xPositions[xi]), Math.round(yPositions[yi])); -- } -- } -- -- context.restore(); -- } -- -- ensureSimulationViewportFocus(focusX) { -- if (!this.isSimulationMode || !this.refs.previewWrap) { -- return; -- } -- const wrap = this.refs.previewWrap; -- const maxScrollX = Math.max(0, wrap.scrollWidth - wrap.clientWidth); -- const targetLeft = Math.max(0, Math.min(maxScrollX, Math.round(focusX - (wrap.clientWidth * 0.45)))); -- if (wrap.scrollLeft !== targetLeft) { -- wrap.scrollLeft = targetLeft; -- } -- if (wrap.scrollTop !== 0) { -- wrap.scrollTop = 0; -- } -- } -- -- renderPreview() { -- const context = this.refs.previewContext; -- const canvas = this.refs.previewCanvas; -- const desiredWidth = Math.max(960, Math.min(4096, this.documentModel.map.pixelWidth + 360)); -- const desiredHeight = Math.max(540, Math.min(2160, this.documentModel.map.pixelHeight + 260)); -- if (canvas.width !== desiredWidth) { -- canvas.width = desiredWidth; -- } -- if (canvas.height !== desiredHeight) { -- canvas.height = desiredHeight; -- } -- const viewportWidth = canvas.width; -- const viewportHeight = canvas.height; -- const worldWidth = this.documentModel.map.pixelWidth; -- const worldHeight = this.documentModel.map.pixelHeight; -- const mapOriginX = Math.max(20, Math.round((viewportWidth - worldWidth) / 2)); -- const mapOriginY = Math.max(20, Math.round((viewportHeight - worldHeight) / 2)); -- -- context.fillStyle = "#0b1830"; -- context.fillRect(0, 0, viewportWidth, viewportHeight); -- -- context.fillStyle = "#0f2340"; -- for (let y = 0; y < viewportHeight; y += 48) { -- context.fillRect(0, y, viewportWidth, 24); -- } -- -- const sortedLayers = cloneDeep(this.documentModel.layers); -- sortLayersByOrder(sortedLayers); -- -- const progress = this.getSimulationProgress(); -- const proxyX = mapOriginX + Math.round(progress * Math.max(1, worldWidth - 1)); -- const proxyY = mapOriginY + Math.round(worldHeight * 0.62); -- const visibleHeroLayer = sortedLayers.find((layer) => { -- if (!layer.visible) { -- return false; -- } -- const id = typeof layer.id === "string" ? layer.id.toLowerCase() : ""; -- const name = typeof layer.name === "string" ? layer.name.toLowerCase() : ""; -- return id.includes("hero") || name.includes("hero"); -- }); -- const heroDrawOrder = visibleHeroLayer ? visibleHeroLayer.drawOrder : null; -- let markerDrawn = false; -- const drawTraversalMarker = (includeTrackLine) => { -- if (includeTrackLine) { -- context.strokeStyle = "rgba(244, 244, 245, 0.35)"; -- context.lineWidth = 1; -- context.beginPath(); -- context.moveTo(proxyX, mapOriginY); -- context.lineTo(proxyX, mapOriginY + worldHeight); -- context.stroke(); -- } -- context.fillStyle = this.isSimulationMode ? "#f59e0b" : "#e2e8f0"; -- context.beginPath(); -- context.arc(proxyX, proxyY, 8, 0, Math.PI * 2); -- context.fill(); -- }; -- -- sortedLayers.forEach((layer) => { -- this.drawSingleLayer(context, layer, viewportWidth, viewportHeight, mapOriginX, mapOriginY); -- if (!markerDrawn && heroDrawOrder !== null && layer.drawOrder >= heroDrawOrder) { -- drawTraversalMarker(false); -- markerDrawn = true; -- } -- }); -- if (!markerDrawn) { -- drawTraversalMarker(true); -- } -- -- context.strokeStyle = "rgba(255, 255, 255, 0.45)"; -- context.lineWidth = 2; -- context.strokeRect(mapOriginX, mapOriginY, worldWidth, worldHeight); -- this.ensureSimulationViewportFocus(proxyX); -- -- const modeText = this.isSimulationMode -- ? `SIMULATION ${this.simulation.playing ? "PLAY" : "PAUSE"}` -- : "EDIT"; -- const repeatCount = sortedLayers.filter((layer) => layer.repeatX || layer.repeatY).length; -- const wrapCount = sortedLayers.filter((layer) => layer.wrapMode === "wrap").length; -- -- if (this.refs.previewDetailsText instanceof HTMLElement) { -- const lines = [ -- `Mode: ${modeText}`, -- `Map: ${this.documentModel.map.name}`, -- `Camera: ${this.cameraX}, ${this.cameraY}`, -- `Traverse: ${Math.round(progress * 100)}% (${Math.round(this.simulation.traversedDistance)}/${Math.round(this.simulation.traversalDistance || 0)} px)`, -- `Layers: ${sortedLayers.length} repeat=${repeatCount} wrap=${wrapCount}` -- ]; -- sortedLayers.slice(0, 3).forEach((layer) => { -- lines.push(`${layer.drawOrder}:${layer.name} sf(${layer.scrollFactorX.toFixed(2)},${layer.scrollFactorY.toFixed(2)}) ${layer.repeatX ? "RX" : "--"}/${layer.repeatY ? "RY" : "--"} ${layer.wrapMode}`); -- }); -- this.refs.previewDetailsText.textContent = lines.join("\n"); -- } -- this.updateSimulationContextReadout(); -- } -- -- validateProjectAssets() { -- this.validationResult = validateProjectAssetState({ -- registry: this.assetRegistry, -- assetDependencyGraph: this.assetDependencyGraphSnapshot, -- parallaxDocument: this.documentModel -- }); -- this.remediationResult = buildProjectAssetRemediation({ -- validationResult: this.validationResult, -- registry: this.assetRegistry -- }); -- this.editorExperienceResult = buildEditorExperienceLayer({ -- assetDependencyGraph: this.validationResult.assetDependencyGraph, -- validationResult: this.validationResult, -- remediationResult: this.remediationResult, -- packageResult: this.lastPackageResult, -- runtimeResult: this.lastRuntimeResult -- }); -- this.debugVisualizationResult = buildDebugVisualizationLayer({ -- assetDependencyGraph: this.validationResult.assetDependencyGraph, -- validationResult: this.validationResult, -- remediationResult: this.remediationResult, -- packageResult: this.lastPackageResult, -- runtimeResult: this.lastRuntimeResult -- }); -- return this.validationResult; -- } -- -- updateRemediationUI() { -- if (!this.refs.remediationSummaryText) { -- return; -- } -- this.refs.remediationSummaryText.textContent = summarizeProjectAssetRemediation(this.remediationResult); -- const navigateAction = getPrimaryRemediationAction(this.remediationResult, "navigate"); -- const fixAction = getPrimaryRemediationAction(this.remediationResult, "confirmable-fix"); -- this.refs.inspectRemediationButton.disabled = this.remediationResult?.remediation?.status !== "available"; -- this.refs.jumpToProblemButton.disabled = !navigateAction; -- this.refs.applyRemediationButton.disabled = !fixAction; -- } -- -- updateEditorExperienceUI() { -- if (!this.refs.experienceSummaryText || !this.refs.experienceDetailsText) { -- return; -- } -- this.refs.experienceSummaryText.textContent = summarizeEditorExperienceLayer(this.editorExperienceResult); -- this.refs.experienceDetailsText.textContent = this.editorExperienceResult?.experience?.reportText || "No experience snapshot available."; -- } -- -- updateDebugVisualizationUI() { -- if (!this.refs.debugSummaryText || !this.refs.debugDetailsText) { -- return; -- } -- this.refs.debugSummaryText.textContent = summarizeDebugVisualizationLayer(this.debugVisualizationResult); -- this.refs.debugDetailsText.textContent = this.debugVisualizationResult?.debugVisualization?.reportText || "No debug visualization available."; -- } -- -- refreshExperienceSnapshot() { -- this.validateProjectAssets(); -- this.updateEditorExperienceUI(); -- this.updateStatus(summarizeEditorExperienceLayer(this.editorExperienceResult)); -- } -- -- refreshDebugVisualizationSnapshot() { -- this.validateProjectAssets(); -- this.updateDebugVisualizationUI(); -- this.updateStatus(summarizeDebugVisualizationLayer(this.debugVisualizationResult)); -- } -- -- inspectRemediationActions() { -- this.validateProjectAssets(); -- const actions = this.remediationResult?.remediation?.actions || []; -- if (actions.length === 0) { -- this.updateStatus("No remediation actions are currently available."); -- this.updateRemediationUI(); -- return false; -- } -- this.updateStatus(`Remediation actions: ${actions.slice(0, 3).map((action) => `${action.label}: ${action.message}`).join(" | ")}`); -- this.updateRemediationUI(); -- return true; -- } -- -- jumpToRemediationProblem() { -- this.validateProjectAssets(); -- const action = getPrimaryRemediationAction(this.remediationResult, "navigate"); -- if (!action) { -- this.updateStatus("No navigation remediation action is available."); -- this.updateRemediationUI(); -- return false; -- } -- -- if (action.findingCode === "UNRESOLVED_IMAGE_LINK") { -- const targetLayer = this.documentModel.layers.find((layer) => layer.id === action.sourceId) || this.documentModel.layers[0] || null; -- if (targetLayer) { -- this.selectedLayerId = targetLayer.id; -- } -- this.renderAll(); -- this.refs.layerImageSourceInput.focus(); -- this.updateStatus(`Jumped to image assignment for ${action.sourceId || "the current layer"}: ${action.message}`); -- } else if (action.findingCode === "MISSING_ASSET_ID" || action.findingCode === "DUPLICATE_REGISTRY_ID") { -- this.refs.saveAssetRegistryButton.focus(); -- this.updateStatus(`Jumped to project I/O: ${action.message}`); -- } else { -- this.updateStatus(`Inspected ${action.sourceId || "the current issue"}: ${action.message}`); -- } -- this.updateRemediationUI(); -- return true; -- } -- -- applyRemediationAction() { -- this.validateProjectAssets(); -- const action = getPrimaryRemediationAction(this.remediationResult, "confirmable-fix"); -- if (!action) { -- this.updateStatus("No confirmable remediation fix is available."); -- this.updateRemediationUI(); -- return false; -- } -- -- const shouldApply = typeof globalThis.confirm === "function" -- ? globalThis.confirm(`${action.label}? ${action.message}`) -- : true; -- if (!shouldApply) { -- this.updateStatus("Remediation fix canceled."); -- this.updateRemediationUI(); -- return false; -- } -- -- if (action.payload?.fixKind === "refresh-owned-registry-entry") { -- this.syncAssetRegistryFromDocument(); -- } else if (action.payload?.fixKind === "relink-reference" && action.payload.referenceField === "parallaxSourceId") { -- const targetLayer = this.documentModel.layers.find((layer) => layer.id === action.sourceId) || this.getSelectedLayer(); -- if (targetLayer) { -- targetLayer.parallaxSourceId = action.payload.candidateId || ""; -- } -- this.resolveAssetRefsFromRegistry(); -- } else if (action.payload?.fixKind === "refresh-graph-snapshot") { -- const validation = validateProjectAssetState({ -- registry: this.assetRegistry, -- parallaxDocument: this.documentModel -- }); -- this.assetDependencyGraphSnapshot = validation.assetDependencyGraph; -- } -- -- const validation = this.validateProjectAssets(); -- this.renderAll(); -- this.updateStatus(`Applied remediation: ${action.label}. Validation: ${summarizeAssetValidation(validation)}.`); -- return true; -- } --} -- --let parallaxSceneStudioApp = null; -- --function isStrictWorkspaceParallaxSnapshot(snapshot) { -- if (!snapshot || typeof snapshot !== "object") { -- return false; -- } -- const documentModel = snapshot.documentModel; -- const assetRegistry = snapshot.assetRegistry; -- if (!documentModel || typeof documentModel !== "object") { -- return false; -- } -- if (documentModel.schema !== "toolbox.parallax/1") { -- return false; -- } -- if (!documentModel.map || typeof documentModel.map !== "object") { -- return false; -- } -- if (!Array.isArray(documentModel.layers)) { -- return false; -- } -- if (!assetRegistry || typeof assetRegistry !== "object") { -- return false; -- } -- return true; --} -- --function bootParallaxSceneStudio() { -- if (parallaxSceneStudioApp) { -- window.parallaxSceneStudioApp = parallaxSceneStudioApp; -- return parallaxSceneStudioApp; -- } -- -- const initialDocument = createInitialParallaxDocument(); -- const app = new ParallaxEditorApp(initialDocument); -- app.init(document); -- app.applyProjectSystemState = function applyProjectSystemState(snapshot) { -- if (Date.now() <= Number(this.skipExternalProjectStateUntil || 0)) { -- this.skipExternalProjectStateUntil = 0; -- return true; -- } -- if (!isStrictWorkspaceParallaxSnapshot(snapshot)) { -- this.updateStatus("Project state rejected: workspace snapshot is missing required parallax source data."); -- return false; -- } -- const nextDocument = sanitizeParallaxDocument(snapshot?.documentModel); -- this.documentModel = nextDocument; -- this.assetRegistry = sanitizeAssetRegistry(snapshot.assetRegistry); -- this.selectedLayerId = typeof snapshot?.selectedLayerId === "string" && nextDocument.layers.some((layer) => layer.id === snapshot.selectedLayerId) -- ? snapshot.selectedLayerId -- : nextDocument.layers[0]?.id || ""; -- this.cameraX = Number.isFinite(Number(snapshot?.cameraX)) ? Number(snapshot.cameraX) : 0; -- this.cameraY = Number.isFinite(Number(snapshot?.cameraY)) ? Number(snapshot.cameraY) : 0; -- this.resolveAssetRefsFromRegistry(); -- this.clearTransientLayerImageSources(); -- this.invalidateImageCache(); -- this.syncInputsFromDocument(); -- this.renderAll(); -- this.updateStatus(`Project state loaded for ${this.documentModel.map.name}.`); -- return true; -- }; -- parallaxSceneStudioApp = app; -- window.parallaxSceneStudioApp = parallaxSceneStudioApp; -- return parallaxSceneStudioApp; --} -- --registerToolBootContract("parallax-editor", { -- init: bootParallaxSceneStudio, -- destroy() { -- return true; -- }, -- getApi() { -- return window.parallaxSceneStudioApp || null; -- } --}); -- --bootParallaxSceneStudio(); -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/parallaxEditor.css b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/parallaxEditor.css -deleted file mode 100644 -index 7aabd9a85..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/parallaxEditor.css -+++ /dev/null -@@ -1,419 +0,0 @@ --/* --Toolbox Aid --David Quesenberry --03/30/2026 --parallaxEditor.css --*/ --.parallax-editor-page { -- --parallax-border: var(--line, rgba(221, 214, 254, 0.26)); -- --parallax-text: var(--text, #f7f4ff); -- --parallax-muted: var(--muted, #e9ddff); -- --parallax-accent: var(--accent, #ddd6fe); -- margin: 0; -- min-height: 100vh; -- color: var(--parallax-text); --} -- --.app-shell { -- min-height: 100vh; -- display: flex; -- flex-direction: column; -- gap: 12px; -- padding: 12px; -- box-sizing: border-box; --} -- --.toolbar { -- display: flex; -- flex-wrap: wrap; -- gap: 12px; -- border: 1px solid var(--parallax-border); -- border-radius: 10px; -- background: var(--surface-inline, rgba(43, 16, 91, 0.9)); -- padding: 10px; --} -- --.toolbar-group { -- display: flex; -- flex-wrap: wrap; -- align-items: flex-end; -- gap: 8px; --} -- --label { -- display: flex; -- flex-direction: column; -- gap: 4px; -- font-size: 12px; -- color: var(--parallax-muted); --} -- --input, --select, --button { -- font: inherit; --} -- --input, --select { -- color: var(--parallax-text); -- background: var(--surface-inline, rgba(43, 16, 91, 0.9)); -- border: 1px solid var(--parallax-border); -- border-radius: 6px; -- padding: 5px 8px; --} -- --button, --.file-input-label { -- color: var(--parallax-text); -- background: linear-gradient(180deg, var(--panel, rgba(76, 29, 149, 0.88)) 0%, var(--panel2, rgba(49, 17, 102, 0.94)) 100%); -- border: 1px solid var(--parallax-border); -- border-radius: 6px; -- padding: 6px 10px; -- cursor: pointer; --} -- --button:hover, --.file-input-label:hover { -- border-color: var(--card-hover-border, #c4b5fd); -- background: color-mix(in srgb, var(--panel, rgba(76, 29, 149, 0.88)) 72%, var(--accent, #ddd6fe)); --} -- --.inline-label { -- display: inline-flex; -- align-items: center; -- justify-content: center; --} -- --.hidden-file-input { -- position: absolute; -- width: 1px; -- height: 1px; -- opacity: 0; -- pointer-events: none; --} -- --.workspace { -- display: grid; -- grid-template-columns: minmax(250px, 320px) minmax(420px, 1fr) minmax(260px, 360px); -- gap: 12px; -- flex: 1; -- min-height: 0; --} -- --.sidebar, --.preview-panel { -- border: 1px solid var(--parallax-border); -- border-radius: 10px; -- background: var(--surface-inline, rgba(43, 16, 91, 0.9)); -- min-height: 0; --} -- --.sidebar { -- padding: 10px; -- overflow: auto; --} -- --.panel { -- border: 1px solid var(--parallax-border); -- border-radius: 8px; -- padding: 8px; -- margin-bottom: 10px; -- background: linear-gradient(180deg, var(--panel, rgba(76, 29, 149, 0.88)) 0%, var(--panel2, rgba(49, 17, 102, 0.94)) 100%); --} -- --.panel h3 { -- margin: 0 0 8px; -- font-size: 14px; --} -- --.panel-accordion { -- margin: 0 0 10px; -- border: 1px solid var(--parallax-border); -- border-radius: 8px; -- background: linear-gradient(180deg, var(--panel, rgba(76, 29, 149, 0.88)) 0%, var(--panel2, rgba(49, 17, 102, 0.94)) 100%); -- overflow: hidden; --} -- --.panel-accordion__summary { -- list-style: none; -- cursor: pointer; -- padding: 8px; -- border-bottom: 1px solid transparent; --} -- --.panel-accordion__summary::-webkit-details-marker { -- display: none; --} -- --.panel-accordion__summary h3 { -- margin: 0; -- font-size: 14px; --} -- --.panel-accordion[open] .panel-accordion__summary { -- border-bottom-color: var(--parallax-border); --} -- --.panel-accordion__body { -- padding: 8px; --} -- --.preview-panel { -- display: flex; -- flex-direction: column; -- padding: 10px; --} -- --.preview-toolbar { -- display: flex; -- justify-content: space-between; -- align-items: center; -- flex-wrap: wrap; -- gap: 8px; -- margin-bottom: 8px; --} -- --.preview-wrap { -- position: relative; -- flex: 1; -- min-height: 260px; -- border: 1px solid var(--parallax-border); -- border-radius: 8px; -- overflow: auto; -- background: var(--surface-code, rgba(34, 12, 74, 0.92)); --} -- --#previewCanvas { -- image-rendering: auto; -- display: block; --} -- --.status-text, --.muted { -- color: var(--parallax-muted); -- font-size: 12px; --} -- --.status-text { -- margin-top: 8px; --} -- --.preview-details-text { -- margin: 8px 0 0; -- min-height: 84px; -- max-height: 180px; -- overflow: auto; -- border: 1px solid var(--parallax-border); -- border-radius: 8px; -- padding: 10px; -- background: rgba(15, 23, 42, 0.72); -- color: #dbeafe; -- font: 12px/1.35 Consolas, Monaco, monospace; -- white-space: pre-wrap; --} -- --.layer-list { -- list-style: none; -- margin: 0; -- padding: 0; -- display: flex; -- flex-direction: column; -- gap: 6px; --} -- --.layer-item { -- border: 1px solid var(--parallax-border); -- border-radius: 6px; -- background: var(--surface-inline, rgba(43, 16, 91, 0.9)); -- padding: 6px; --} -- --.layer-item.selected { -- border-color: var(--parallax-accent); -- box-shadow: 0 0 0 1px color-mix(in srgb, var(--parallax-accent) 30%, transparent) inset; --} -- --.layer-row { -- display: flex; -- gap: 6px; -- align-items: center; --} -- --.layer-select-btn { -- flex: 1; --} -- --.layer-order-chip { -- font-size: 10px; -- color: var(--parallax-muted); -- border: 1px solid var(--parallax-border); -- border-radius: 999px; -- padding: 2px 6px; -- line-height: 1.2; --} -- --.row { -- display: flex; -- gap: 6px; -- align-items: center; -- margin-top: 6px; --} -- --.row.buttons { -- flex-wrap: wrap; --} -- --.usage-list { -- margin: 0; -- padding-left: 18px; -- color: var(--parallax-muted); -- font-size: 12px; --} -- --.parallax-editor-page .fullscreen-overlay-button { -- position: relative; -- z-index: 80; -- display: none; -- min-height: 32px; -- padding: 0 10px; --} -- --.parallax-editor-page .fullscreen-overlay-toggle-group { -- display: none; -- position: fixed; -- top: 96px; -- z-index: 80; -- gap: 8px; -- flex-direction: column; --} -- --.parallax-editor-page .fullscreen-overlay-toggle-group--left { -- left: 10px; -- align-items: flex-start; --} -- --.parallax-editor-page .fullscreen-overlay-toggle-group--right { -- right: 10px; -- align-items: flex-end; --} -- --.parallax-editor-page .overlay-toggle-symbol { -- display: inline-flex; -- align-items: center; -- justify-content: center; -- width: 14px; -- font-weight: 900; -- line-height: 1; --} -- --body.parallax-editor-page.fullscreen-mode .app-shell { -- min-height: 100vh; -- min-height: 100dvh; -- overflow: hidden; -- padding: 8px; -- gap: 8px; --} -- --body.parallax-editor-page.fullscreen-mode .toolbar { -- padding: 8px; -- gap: 8px; --} -- --body.parallax-editor-page.fullscreen-mode .toolbar-group.tools-platform-control-cluster--workflow, --body.parallax-editor-page.fullscreen-mode .toolbar-group.tools-platform-control-cluster--preview { -- display: none; --} -- --body.parallax-editor-page.fullscreen-mode .workspace { -- grid-template-columns: minmax(0, 1fr); -- height: calc(100dvh - 90px); -- gap: 8px; --} -- --body.parallax-editor-page.fullscreen-mode .sidebar { -- display: none; --} -- --body.parallax-editor-page.fullscreen-mode .sidebar.visible-overlay { -- display: block; -- position: fixed; -- top: 70px; -- bottom: 8px; -- width: min(360px, calc(100vw - 24px)); -- z-index: 50; -- overflow: auto; -- padding-top: 10px; --} -- --body.parallax-editor-page.fullscreen-mode .left-sidebar.visible-overlay { -- left: 12px; --} -- --body.parallax-editor-page.fullscreen-mode .right-sidebar.visible-overlay { -- right: 12px; --} -- --body.parallax-editor-page.fullscreen-mode .preview-panel { -- width: 100%; -- min-width: 0; -- height: 100%; -- padding: 8px; --} -- --body.parallax-editor-page.fullscreen-mode .preview-wrap { -- overflow: hidden; -- min-height: 0; -- height: 100%; --} -- --body.parallax-editor-page.fullscreen-mode #previewCanvas { -- width: 100%; -- height: 100%; --} -- --body.parallax-editor-page.fullscreen-mode .fullscreen-overlay-button { -- display: inline-flex; -- align-items: center; -- justify-content: center; -- gap: 6px; --} -- --body.parallax-editor-page.fullscreen-mode .fullscreen-overlay-toggle-group { -- display: flex; --} -- --body.parallax-editor-page.fullscreen-mode .fullscreen-overlay-button.is-hidden-while-overlay-open { -- visibility: hidden; -- pointer-events: none; --} -- --body.parallax-editor-page.fullscreen-mode .sidebar.visible-overlay .panel-accordion__summary { -- display: flex; -- align-items: center; -- gap: 8px; --} -- --body.parallax-editor-page.fullscreen-mode .sidebar.visible-overlay .panel-accordion__summary::before { -- content: "+"; -- display: inline-flex; -- align-items: center; -- justify-content: center; -- width: 14px; -- font-weight: 900; -- line-height: 1; --} -- --body.parallax-editor-page.fullscreen-mode .sidebar.visible-overlay .panel-accordion[open] > .panel-accordion__summary::before { -- content: "-"; --} -- --body.parallax-editor-page.fullscreen-mode .preview-details-text { -- max-height: 150px; --} -- --@media (max-width: 1260px) { -- .workspace { -- grid-template-columns: 1fr; -- } --} -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/cloud-bands.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/cloud-bands.svg -deleted file mode 100644 -index 5b50167a8..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/cloud-bands.svg -+++ /dev/null -@@ -1,23 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/foreground-occluder.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/foreground-occluder.svg -deleted file mode 100644 -index d46c9f740..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/foreground-occluder.svg -+++ /dev/null -@@ -1,19 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/hero-proxy.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/hero-proxy.svg -deleted file mode 100644 -index 5ab3498d9..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/hero-proxy.svg -+++ /dev/null -@@ -1,9 +0,0 @@ -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-short.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-short.svg -deleted file mode 100644 -index 092831880..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-short.svg -+++ /dev/null -@@ -1,4 +0,0 @@ -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-tall.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-tall.svg -deleted file mode 100644 -index 41f40628e..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-tall.svg -+++ /dev/null -@@ -1,4 +0,0 @@ -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge.svg -deleted file mode 100644 -index 92c68880c..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge.svg -+++ /dev/null -@@ -1,5 +0,0 @@ -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/sky-gradient.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/sky-gradient.svg -deleted file mode 100644 -index 9ed22bed6..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/sky-gradient.svg -+++ /dev/null -@@ -1,10 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_bushes_foreground.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_bushes_foreground.svg -deleted file mode 100644 -index 8197d1925..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_bushes_foreground.svg -+++ /dev/null -@@ -1,32 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_clouds.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_clouds.svg -deleted file mode 100644 -index b53882fa4..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_clouds.svg -+++ /dev/null -@@ -1,29 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_hills.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_hills.svg -deleted file mode 100644 -index f6f01325e..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_hills.svg -+++ /dev/null -@@ -1,13 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_leaf_overlay.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_leaf_overlay.svg -deleted file mode 100644 -index df5696462..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_leaf_overlay.svg -+++ /dev/null -@@ -1,9 +0,0 @@ -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_learning_level_parallax.json b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_learning_level_parallax.json -deleted file mode 100644 -index 20255362f..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_learning_level_parallax.json -+++ /dev/null -@@ -1,109 +0,0 @@ --{ -- "schema": "toolbox.parallax/1", -- "version": 1, -- "companionEditor": "ParallaxEditor", -- "map": { -- "name": "mario-style-learning-level", -- "width": 64, -- "height": 16, -- "tileSize": 24 -- }, -- "layers": [ -- { -- "id": "mario-sky", -- "name": "Mario Sky", -- "drawOrder": 0, -- "imageSource": "./samples/mario_style_sky.svg", -- "scrollFactorX": 0.08, -- "scrollFactorY": 0.03, -- "offsetX": 0, -- "offsetY": 0, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 1, -- "visible": true -- }, -- { -- "id": "mario-clouds", -- "name": "Mario Clouds", -- "drawOrder": 1, -- "imageSource": "./samples/mario_style_clouds.svg", -- "scrollFactorX": 0.18, -- "scrollFactorY": 0.06, -- "offsetX": 0, -- "offsetY": 36, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.95, -- "visible": true -- }, -- { -- "id": "mario-mountains-tall", -- "name": "Mario Mountains Tall", -- "drawOrder": 2, -- "imageSource": "./samples/mario_style_mountains.svg", -- "scrollFactorX": 0.34, -- "scrollFactorY": 0.18, -- "offsetX": 0, -- "offsetY": 178, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.96, -- "visible": true -- }, -- { -- "id": "mario-mountains-short", -- "name": "Mario Mountains Short", -- "drawOrder": 3, -- "imageSource": "./samples/mario_style_hills.svg", -- "scrollFactorX": 0.58, -- "scrollFactorY": 0.22, -- "offsetX": 0, -- "offsetY": 214, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.96, -- "visible": true -- }, -- { -- "id": "mario-hero-proxy", -- "name": "Mario Hero Proxy", -- "drawOrder": 4, -- "imageSource": "./samples/images/hero-proxy.svg", -- "scrollFactorX": 1, -- "scrollFactorY": 1, -- "offsetX": 720, -- "offsetY": 258, -- "repeatX": false, -- "repeatY": false, -- "wrapMode": "clamp", -- "opacity": 1, -- "visible": true -- }, -- { -- "id": "mario-foreground-trees", -- "name": "Mario Foreground Trees", -- "drawOrder": 5, -- "imageSource": "./samples/mario_style_bushes_foreground.svg", -- "scrollFactorX": 1.08, -- "scrollFactorY": 0.36, -- "offsetX": 84, -- "offsetY": 182, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.96, -- "visible": true -- } -- ], -- "metadata": { -- "createdAt": "2026-03-30T00:00:00.000Z", -- "updatedAt": "2026-03-30T00:00:00.000Z", -- "source": "parallax-editor-samples", -- "notes": "Mario-style learning level parallax sample aligned with tilemap concept." -- } --} -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_mountains.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_mountains.svg -deleted file mode 100644 -index 5f3db0956..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_mountains.svg -+++ /dev/null -@@ -1,13 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_sky.svg b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_sky.svg -deleted file mode 100644 -index 80ef1847f..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_sky.svg -+++ /dev/null -@@ -1,11 +0,0 @@ -- -- -- -- -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/parallax_sample.json b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/parallax_sample.json -deleted file mode 100644 -index 50c277ca5..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/parallax_sample.json -+++ /dev/null -@@ -1,108 +0,0 @@ --{ -- "schema": "toolbox.parallax/1", -- "version": 1, -- "companionEditor": "ParallaxEditor", -- "map": { -- "name": "sample-parallax-only", -- "width": 64, -- "height": 24, -- "tileSize": 24 -- }, -- "layers": [ -- { -- "id": "sample-sky", -- "name": "Sample Sky", -- "drawOrder": 0, -- "imageSource": "./samples/images/sky-gradient.svg", -- "scrollFactorX": 0.08, -- "scrollFactorY": 0.03, -- "offsetX": 0, -- "offsetY": 0, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 1, -- "visible": true -- }, -- { -- "id": "sample-clouds", -- "name": "Sample Clouds", -- "drawOrder": 1, -- "imageSource": "./samples/images/cloud-bands.svg", -- "scrollFactorX": 0.2, -- "scrollFactorY": 0.08, -- "offsetX": 0, -- "offsetY": 48, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.92, -- "visible": true -- }, -- { -- "id": "sample-mountains-tall", -- "name": "Sample Mountains Tall", -- "drawOrder": 2, -- "imageSource": "./samples/images/mountain-ridge-tall.svg", -- "scrollFactorX": 0.38, -- "scrollFactorY": 0.2, -- "offsetX": 0, -- "offsetY": 208, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.95, -- "visible": true -- }, -- { -- "id": "sample-mountains-short", -- "name": "Sample Mountains Short", -- "drawOrder": 3, -- "imageSource": "./samples/images/mountain-ridge-short.svg", -- "scrollFactorX": 0.64, -- "scrollFactorY": 0.24, -- "offsetX": 0, -- "offsetY": 242, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.95, -- "visible": true -- }, -- { -- "id": "sample-hero", -- "name": "Sample Hero Proxy", -- "drawOrder": 4, -- "imageSource": "./samples/images/hero-proxy.svg", -- "scrollFactorX": 1, -- "scrollFactorY": 1, -- "offsetX": 780, -- "offsetY": 350, -- "repeatX": false, -- "repeatY": false, -- "wrapMode": "clamp", -- "opacity": 1, -- "visible": true -- }, -- { -- "id": "sample-foreground-trees", -- "name": "Sample Foreground Trees", -- "drawOrder": 5, -- "imageSource": "./samples/images/foreground-occluder.svg", -- "scrollFactorX": 1.08, -- "scrollFactorY": 0.4, -- "offsetX": 84, -- "offsetY": 188, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.96, -- "visible": true -- } -- ], -- "metadata": { -- "createdAt": "2026-03-30T00:00:00.000Z", -- "updatedAt": "2026-03-30T00:00:00.000Z", -- "source": "parallax-editor-samples" -- } --} -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/sample-manifest.json b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/sample-manifest.json -deleted file mode 100644 -index 00775cea6..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/sample-manifest.json -+++ /dev/null -@@ -1,21 +0,0 @@ --{ -- "schema": "toolbox.parallax-sample-manifest/1", -- "version": 1, -- "samples": [ -- { -- "id": "parallax-basic", -- "label": "Parallax Sample (toolbox.parallax/1)", -- "path": "./samples/parallax_sample.json" -- }, -- { -- "id": "tilemap-embedded", -- "label": "Tilemap + Parallax Sample (toolbox.tilemap/1)", -- "path": "./samples/tilemap_parallax_sample.json" -- }, -- { -- "id": "mario-style-learning", -- "label": "Mario Style Learning Level (toolbox.parallax/1)", -- "path": "./samples/mario_style_learning_level_parallax.json" -- } -- ] --} -diff --git a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/tilemap_parallax_sample.json b/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/tilemap_parallax_sample.json -deleted file mode 100644 -index 6ce556238..000000000 ---- a/dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/tilemap_parallax_sample.json -+++ /dev/null -@@ -1,75 +0,0 @@ --{ -- "schema": "toolbox.tilemap/1", -- "version": 1, -- "map": { -- "name": "sample-tilemap-with-parallax", -- "width": 80, -- "height": 30, -- "tileSize": 24 -- }, -- "tileset": [ -- { -- "id": 0, -- "name": "Empty", -- "color": "transparent" -- } -- ], -- "layers": [], -- "markers": [], -- "parallax": { -- "schema": "toolbox.parallax/1", -- "companionEditor": "ParallaxEditor", -- "layers": [ -- { -- "id": "tm-sky", -- "name": "Tilemap Sky", -- "drawOrder": 0, -- "imageSource": "./samples/images/sky-gradient.svg", -- "scrollFactorX": 0.1, -- "scrollFactorY": 0.05, -- "offsetX": 0, -- "offsetY": 0, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 1, -- "visible": true -- }, -- { -- "id": "tm-ridge-tall", -- "name": "Tilemap Ridge Tall", -- "drawOrder": 1, -- "imageSource": "./samples/images/mountain-ridge-tall.svg", -- "scrollFactorX": 0.36, -- "scrollFactorY": 0.18, -- "offsetX": 0, -- "offsetY": 250, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.96, -- "visible": true -- }, -- { -- "id": "tm-ridge-short", -- "name": "Tilemap Ridge Short", -- "drawOrder": 2, -- "imageSource": "./samples/images/mountain-ridge-short.svg", -- "scrollFactorX": 0.88, -- "scrollFactorY": 0.24, -- "offsetX": 0, -- "offsetY": 268, -- "repeatX": true, -- "repeatY": false, -- "wrapMode": "wrap", -- "opacity": 0.96, -- "visible": true -- } -- ] -- }, -- "metadata": { -- "createdAt": "2026-03-30T00:00:00.000Z", -- "updatedAt": "2026-03-30T00:00:00.000Z", -- "notes": "Sample for Parallax Scene Studio direct loading." -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/README.md b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/README.md -deleted file mode 100644 -index 873ee3c42..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/README.md -+++ /dev/null -@@ -1,50 +0,0 @@ --# Object Vector Studio V2 -- --Object Vector Studio V2 is the vector authoring surface for reusable gameplay entities: ships, enemies, pickups, actors, weapons, UI vector assets, and other object-level game pieces. -- --Tagline: "Build the thing." -- --The tool folder was copied from `tools/templates-v2/` for `PR_26132_001-add-world-object-vector-studios`. It keeps the copied template CSS and modular control structure intact while giving the new studio its own first-class tool id: `object-vector-studio-v2`. -- --## Architectural Split -- --Object Vector Studio V2 owns the thing being placed, animated, reused, equipped, collected, or rendered as an object. World Vector Studio V2 owns the place where those things live. -- --Use Object Vector Studio V2 for: -- --- Reusable gameplay entities. --- Ships. --- Enemies. --- Pickups. --- Actors. --- Weapons. --- UI vector assets. --- SVG/vector authoring. --- Primitives, shapes, and path editing. --- Animation-ready structures. -- --Use World Vector Studio V2 instead for terrain, maps, tile/world geometry, layered scenes, parallax, environment composition, collision/world regions, level layout, spawn placement, and camera zones. -- --## Migration Direction -- --- Object Vector Studio V2 is the current vector asset authoring surface. --- World Vector Studio V2 is the current map, layer, parallax, and world layout surface. --- Current object geometry lives in Object Vector Studio V2 payloads as `objects[]`, `objects[].tags`, and `objects[].shapes`. -- --## Examples -- --- Asteroids ship/UFO -> Object Vector Studio V2. --- Weapon icons, pickups, HUD markers, and actor silhouettes: Object Vector Studio V2. --- Platformer level/parallax scene -> World Vector Studio V2. -- --## Runtime Notes -- --- The current implementation is the copied Tool Template V2 shell with Object Vector Studio V2 naming and documentation. --- JavaScript and CSS remain external. --- Paint and Stroke modes use selectable palette swatches, click/drag application, Alt-click sampling, and fill/stroke swap/default shortcuts. This model is intended to scale into shaders, gradients, patterns, neon effects, SVG export, and runtime rendering without storing palette data inside object JSON. --- The copied `docs/CONTROL_SERVICE_CONTRACTS.md` and `docs/BATCH_GUARDRAIL_CONTRACT.md` remain the local implementation contracts. --- `docs/OBJECT_VECTOR_STUDIO_V2_REQUIREMENTS.md` captures the future Object Vector Studio V2 requirements and design constraints. -- --## Validation -- --This documentation PR has no Playwright impact. Runtime implementation remains future work. -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/BATCH_GUARDRAIL_CONTRACT.md b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/BATCH_GUARDRAIL_CONTRACT.md -deleted file mode 100644 -index ae3718606..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/BATCH_GUARDRAIL_CONTRACT.md -+++ /dev/null -@@ -1,51 +0,0 @@ --# Tool Template V2 Batch Guardrail Contract -- --Batch operations process repeated work across real discovered inputs. A First-Class Tool V2 batch flow must be explicit, observable, and safe to resume or diagnose. -- --## Discovery Rules -- --- Discover real files and directories. --- Never assume numeric folder sequences. --- Missing discovered candidates are logged as `SKIP`, not `FAIL`, unless the missing item is the selected single input. --- Logs must identify the resolved input path or identifier for every item. -- --## Per-Item Logging -- --Every item must log exactly one terminal outcome through the tool logger: -- --- `OK`: item completed and wrote or updated the expected output. --- `WARN`: item completed with a recoverable warning. --- `FAIL`: item failed with an actionable error. --- `SKIP`: item was intentionally skipped. -- --Per-item logs must include the item identifier and the reason for `WARN`, `FAIL`, or `SKIP`. -- --## Failure Isolation -- --- One item failure must not stop the batch unless the failure is global. --- Global failures include missing required configuration, unavailable repo access, invalid destination root, or a cancelled run. --- Batch processors must keep processing remaining discovered items after item-level `FAIL` or `SKIP` outcomes. -- --## Summary Contract -- --Every batch run must finish with a summary that includes: -- --- `written` --- `failed` --- `skipped` --- `warnings` -- --The summary must match the per-item log outcomes. -- --## Stop/Cancel Contract -- --Long-running batches must support a stop or cancel pattern when applicable. -- --- Stop/cancel requests must prevent new items from starting. --- Already-running item work should finish or fail safely. --- Cancelled remaining items should be logged as `SKIP` or another clearly documented non-success outcome. --- The final summary must make cancellation visible. -- --## No Silent Fallback -- --Batch processors must not invent default inputs, substitute fallback targets, or claim success when a fallback or partial result occurred. -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/CONTROL_SERVICE_CONTRACTS.md b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/CONTROL_SERVICE_CONTRACTS.md -deleted file mode 100644 -index 917b12484..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/CONTROL_SERVICE_CONTRACTS.md -+++ /dev/null -@@ -1,174 +0,0 @@ --# Tool Template V2 Control And Service Contracts -- --These contracts define the required structure for First-Class Tool V2 code created from Tool Template V2. -- --## Deprecated Shared Tool Surface -- --`tools/shared/` is deprecated for First-Class Tool V2 surfaces. -- --First-Class Tool V2 surfaces must have: -- --- no imports from `tools/shared/` --- no script references to `tools/shared/` --- no CSS references to `tools/shared/` --- no runtime dependency on `tools/shared/` -- --## Control Contract -- --Controls own one UI control or one UI section. -- --Required rules: -- --- One control or section per class. --- One class per file. --- A control owns its DOM references. --- A control owns its event binding. --- A control must not reach into sibling controls directly. --- A control must not perform unrelated business logic. --- A control exposes clear methods that match its responsibility. -- --Common methods: -- --- `init()` or `mount()` prepares the control. --- `bindEvents()` wires DOM events when event wiring is separate from initialization. --- `getValue()` returns the current control value when applicable. --- `setValue(value)` updates the control value when applicable. --- `validate()` returns validation status and messages when applicable. --- `setEnabled(isEnabled)` updates enabled state when applicable. --- `clear()` clears visible state when applicable. -- --Controls communicate through injected callbacks, injected services, or the app/root coordinator. A control must not directly edit another control's DOM or internal state. -- --## Panel Role Contract -- --First-Class Tool V2 surfaces use a consistent three-column layout contract: -- --- Left panel owns user input, setup, and intent controls only. --- Center panel owns the primary work surface, editor, canvas, or preview. --- Right panel owns generated output, summaries, diagnostics, logs, and status. --- Status and log controls belong at the bottom of the right panel unless a PR explicitly justifies a different placement. -- --Controls should be placed according to these panel roles before adding new sections. A control that mixes input and output responsibilities should be split into separate controls. -- --## Service Contract -- --Services contain non-DOM logic. -- --Required rules: -- --- Services must not query or manipulate UI directly. --- Services must not write to status text, textareas, headers, buttons, or panels. --- Services return data, status objects, or errors for controls, the app/root coordinator, or logger to display. --- Services must avoid hidden defaults and silent fallback data. --- Services should be deterministic for the same input unless they explicitly own I/O. -- --Recommended result shape: -- --```js --{ -- ok: true, -- value: {}, -- warnings: [] --} --``` -- --Recommended error shape: -- --```js --{ -- ok: false, -- code: "VALIDATION_FAILED", -- message: "Explain the actionable failure.", -- details: {} --} --``` -- --## App/Root Contract -- --The app/root class is a coordinator only. -- --Required rules: -- --- The app/root class wires controls and services. --- The app/root class coordinates flows between controls and services. --- The app/root class does not own DOM references except for constructing controls when unavoidable. --- The app/root class does not own business logic. --- The app/root class does not write directly to log/status UI. --- The app/root class does not reach into control internals. -- --Allowed responsibilities: -- --- instantiate controls and services --- inject dependencies --- connect callbacks --- coordinate high-level flows such as load, validate, render, export --- route service results to the owning control or logger -- --## Logger Contract -- --Each tool has one status/log writer. -- --Required rules: -- --- Logger is the single writer for status/log output. --- Controls, services, and app/root code must route log output through the logger. --- Logger owns status/log DOM writes. --- Logger supports these levels: -- - `OK` -- - `WARN` -- - `FAIL` -- - `SKIP` -- - `INFO` --- Logger entries must be clear and actionable. --- Batch operations must include the item identifier in per-item log lines. -- --Recommended logger methods: -- --- `ok(message)` --- `warn(message)` --- `fail(message)` --- `skip(message)` --- `info(message)` --- `clear()` -- --## Batch Processor Contract -- --Batch processors handle repeated work across discovered inputs. -- --See `BATCH_GUARDRAIL_CONTRACT.md` for the full batch guardrail contract. -- --Required rules: -- --- Process real discovered inputs only. --- Never assume numeric sequences. --- Log every item through the logger with `OK`, `WARN`, `FAIL`, or `SKIP`. --- One item failure must not stop the batch unless the failure is global. --- Missing inputs are `SKIP` during batch processing. --- Batch failures identify the exact item and underlying error. --- Batch summaries include written, failed, skipped, and warnings. --- Long-running batches support stop or cancel when applicable. -- --Recommended summary shape: -- --```js --{ -- written: 0, -- failed: 0, -- skipped: 0, -- warnings: 0, -- items: [] --} --``` -- --## Review Checklist -- --Before a First-Class Tool V2 PR is ready: -- --- Every control or section has its own class. --- Every service is DOM-free. --- The app/root class coordinates only. --- All logging goes through the logger. --- Batch work logs per item and summarizes counts. --- No inline event handlers exist in HTML. --- No inline ` -- -- -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/index.html b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/index.html -deleted file mode 100644 -index ce7748b53..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/index.html -+++ /dev/null -@@ -1,392 +0,0 @@ -- -- -- -- -- -- Object Vector Studio V2 -- -- -- -- -- -- -- --
-- Hide Header and Details --
--
--
--
--
--
--
--

Object Vector Studio V2

--

First-Class Tools Surface V2

--
--
--
Build vector assets for ships, enemies, pickups, actors, UI elements, and reusable game objects.
--
--
--
--

Author reusable vector gameplay objects for actors, collectibles, hazards, and entity-driven scenes.

--
--
--
--
--
--
-- -- -- -- -- --
-- -- --
--
-- --
-- --
--
--
-- -- --
-- -- -- -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/ToolStarterApp.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/ToolStarterApp.js -deleted file mode 100644 -index cc0d32253..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/ToolStarterApp.js -+++ /dev/null -@@ -1,9178 +0,0 @@ --import { CANONICAL_WORLD_TO_SCREEN_SCALE } from '../../../src/engine/rendering/WorldScreenTransform.js'; --import { combineObjectVectorBounds, inverseTransformObjectVectorShapePoint, normalizeObjectVectorOrigin, normalizeRotationDegrees as normalizeSharedRotationDegrees, objectVectorBoundsCornerPoints, objectVectorSvgTransformAttribute, rotatePointAround as rotateSharedPointAround, transformedObjectVectorShapeBounds, transformObjectVectorShapePoint } from '../../../src/engine/rendering/OrientationTransform.js'; --import ObjectVectorRuntimeAssetService from '../../../src/engine/rendering/ObjectVectorRuntimeAssetService.js'; --import { downloadTextFile, readFileText } from '../../../src/engine/persistence/FilePersistenceService.js'; --import { isPlainObject } from "../../../src/shared/object/objects.js"; -- --const WORKSPACE_TOOL_SESSION_KEY = "workspace.tools.object-vector-studio-v2"; --const WORKSPACE_PALETTE_SESSION_KEY = "workspace.tools.palette-manager-v2"; --const RUNTIME_PALETTE_SESSION_KEY = "object-vector-studio-v2.runtimePalette"; --const TOOL_DISPLAY_MODE_KEY = "object-vector-studio-v2.toolDisplayMode"; --const SNAP_MODE_SESSION_KEY = "object-vector-studio-v2.snapMode"; --const ANGLE_SNAP_SESSION_KEY = "object-vector-studio-v2.angleSnap"; --const ANGLE_SNAP_STEP_SESSION_KEY = "object-vector-studio-v2.angleSnapStep"; --const GRID_RENDER_SESSION_KEY = "object-vector-studio-v2.gridRender"; --const CENTER_ORIGIN_SESSION_KEY = "object-vector-studio-v2.centerOrigin"; --const SVG_NS = "http://www.w3.org/2000/svg"; --const DEFAULT_VIEWPORT = Object.freeze({ height: 2200, width: 3200, x: 0, y: 0, zoom: 1.0 }); --const GRID_STEP = 10; --const OBJECT_PREVIEW_DRAWING_SCALE = GRID_STEP; --const MAX_ZOOM = 10.0; --const MIN_ZOOM = 0.1; --const ZOOM_STEP = 0.1; --const TRANSPARENT_STYLE_COLOR = "#00000000"; --const PREVIEW_HISTORY_LIMIT = 50; --const PREVIEW_DRAG_START_THRESHOLD_PX = 5; --const SNAP_MODES = Object.freeze(["grid", "point", "none"]); --const ANGLE_SNAP_STEPS = Object.freeze([15, 30, 45, 90]); --const POINT_SNAP_RADIUS = 1.5; --const SNAP_MODE_DETAILS = Object.freeze({ -- grid: { -- iconKey: "snapGrid", -- label: "Snap Grid", -- title: "Snap drawing and point dragging to grid intersections" -- }, -- none: { -- iconKey: "snapNone", -- label: "Snap None", -- title: "Disable drawing and point-drag snapping" -- }, -- point: { -- iconKey: "snapPoint", -- label: "Snap Point", -- title: "Snap drawing and point dragging to visible shape points" -- } --}); -- --const objectVectorStudioIcon = (name, glyph) => Object.freeze({ glyph, name }); -- --const OBJECT_VECTOR_STUDIO_ICON_GLYPHS = Object.freeze({ -- add: objectVectorStudioIcon("nf-fa-plus", "\uf067"), -- angle: objectVectorStudioIcon("nf-md-angle_acute", "\u{f0937}"), -- annulusRing: objectVectorStudioIcon("nf-md-circle_double", "\u{f0e95}"), -- arc: objectVectorStudioIcon("nf-md-vector_radius", "\u{f074a}"), -- bri: objectVectorStudioIcon("nf-fa-sun_o", "\uf185"), -- bringForward: objectVectorStudioIcon("nf-fa-arrow_up", "\uf062"), -- bringFront: objectVectorStudioIcon("nf-fa-angle_double_up", "\uf102"), -- capsuleStadium: objectVectorStudioIcon("nf-md-rounded_corner", "\u{f0607}"), -- center: objectVectorStudioIcon("nf-fa-dot_circle_o", "\uf192"), -- circle: objectVectorStudioIcon("nf-md-vector_circle_variant", "\u{f0557}"), -- copy: objectVectorStudioIcon("nf-fa-copy", "\uf0c5"), -- delete: objectVectorStudioIcon("nf-md-trash_can_outline", "\u{f0a7a}"), -- duplicate: objectVectorStudioIcon("nf-fa-copy", "\uf0c5"), -- edit: objectVectorStudioIcon("nf-fa-pencil_square_o", "\uf044"), -- ellipse: objectVectorStudioIcon("nf-md-vector_ellipse", "\u{f0893}"), -- eye: objectVectorStudioIcon("nf-fa-eye", "\uf06e"), -- eyeOff: objectVectorStudioIcon("nf-fa-eye_slash", "\uf070"), -- grid: objectVectorStudioIcon("nf-md-grid", "\u{f02c1}"), -- gridOff: objectVectorStudioIcon("nf-md-grid_off", "\u{f02c2}"), -- polyline: objectVectorStudioIcon("nf-md-vector_polyline", "\u{f0561}"), -- kite: objectVectorStudioIcon("nf-md-vector_polygon", "\u{f0560}"), -- lune: objectVectorStudioIcon("nf-md-moon_waning_crescent", "\u{f0f64}"), -- parallelogram: objectVectorStudioIcon("nf-md-vector_polygon", "\u{f0560}"), -- snapGrid: objectVectorStudioIcon("nf-md-grid_large", "\u{f0758}"), -- snapNone: objectVectorStudioIcon("nf-fa-not_equal", "\u{efcb}"), -- snapPoint: objectVectorStudioIcon("nf-md-vector_point", "\u{f055f}"), -- group: objectVectorStudioIcon("nf-fa-object_group", "\uf247"), -- hue: objectVectorStudioIcon("nf-fa-eyedropper", "\uf1fb"), -- line: objectVectorStudioIcon("nf-md-vector_line", "\u{f055e}"), -- lock: objectVectorStudioIcon("nf-fa-lock", "\uf023"), -- move: objectVectorStudioIcon("nf-fa-arrows", "\uf047"), -- name: objectVectorStudioIcon("nf-fa-font", "\uf031"), -- paint: objectVectorStudioIcon("nf-fa-paint_brush", "\uf1fc"), -- panDown: objectVectorStudioIcon("nf-fa-arrow_down", "\uf063"), -- panLeft: objectVectorStudioIcon("nf-fa-arrow_left", "\uf060"), -- panRight: objectVectorStudioIcon("nf-fa-arrow_right", "\uf061"), -- panUp: objectVectorStudioIcon("nf-fa-arrow_up", "\uf062"), -- paste: objectVectorStudioIcon("nf-oct-paste", "\uf0ea"), -- picker: objectVectorStudioIcon("nf-fa-eye_dropper", "\uf1fb"), -- polygon: objectVectorStudioIcon("nf-md-vector_polygon", "\u{f0560}"), -- rectangle: objectVectorStudioIcon("nf-md-vector_rectangle", "\u{f05c6}"), -- redo: objectVectorStudioIcon("nf-md-redo", "\u{f044e}"), -- reset: objectVectorStudioIcon("nf-fa-undo", "\uf0e2"), -- resize: objectVectorStudioIcon("nf-md-resize", "\u{f0a68}"), -- rhombus: objectVectorStudioIcon("nf-md-rhombus_outline", "\u{f070b}"), -- rotate: objectVectorStudioIcon("nf-fa-repeat", "\uf01e"), -- sat: objectVectorStudioIcon("nf-fa-tint", "\uf043"), -- scale: objectVectorStudioIcon("nf-fa-scale_unbalanced", "\u{eddf}"), -- sector: objectVectorStudioIcon("nf-md-chart_pie", "\u{f012b}"), -- select: objectVectorStudioIcon("nf-md-select", "\u{f0485}"), -- segment: objectVectorStudioIcon("nf-md-circle_slice_4", "\u{f0a9e}"), -- sendBack: objectVectorStudioIcon("nf-fa-angle_double_down", "\uf103"), -- sendBackward: objectVectorStudioIcon("nf-fa-arrow_down", "\uf063"), -- stroke: objectVectorStudioIcon("nf-fa-pencil", "\uf040"), -- square: objectVectorStudioIcon("nf-fa-vector_square", "\u{ee92}"), -- squircle: objectVectorStudioIcon("nf-md-rounded_corner", "\u{f0607}"), -- starPolygon: objectVectorStudioIcon("nf-fa-star_o", "\uf006"), -- tag: objectVectorStudioIcon("nf-fa-tag", "\uf02b"), -- text: objectVectorStudioIcon("nf-fa-font", "\uf031"), -- trapezoidTrapezium: objectVectorStudioIcon("nf-md-vector_polygon", "\u{f0560}"), -- triangle: objectVectorStudioIcon("nf-md-vector_triangle", "\u{f0563}"), -- ungroup: objectVectorStudioIcon("nf-fa-object_ungroup", "\uf248"), -- undo: objectVectorStudioIcon("nf-md-undo", "\u{f054c}"), -- unlock: objectVectorStudioIcon("nf-fa-unlock", "\uf09c"), -- zoomIn: objectVectorStudioIcon("nf-oct-zoom_in", "\u{f531}"), -- zoomOut: objectVectorStudioIcon("nf-oct-zoom_out", "\u{f532}") --}); -- --const OBJECT_VECTOR_STUDIO_STATIC_ICON_TARGETS = Object.freeze([ -- ["#objectVectorStudioV2AddTagButton", "add"], -- ["#objectVectorStudioV2AddObjectButton", "add"], -- ["#objectVectorStudioV2RenameObjectButton", "edit"], -- ["#objectVectorStudioV2DuplicateObjectButton", "duplicate"], -- ["#objectVectorStudioV2PaintModeButton", "paint"], -- ["#objectVectorStudioV2StrokeModeButton", "stroke"], -- [".object-vector-studio-v2__palette-sort [data-palette-sort='hue']", "hue"], -- [".object-vector-studio-v2__palette-sort [data-palette-sort='sat']", "sat"], -- [".object-vector-studio-v2__palette-sort [data-palette-sort='bri']", "bri"], -- [".object-vector-studio-v2__palette-sort [data-palette-sort='name']", "name"], -- [".object-vector-studio-v2__palette-sort [data-palette-sort='tag']", "tag"], -- ["#objectVectorStudioV2ZoomOutButton", "zoomOut"], -- ["#objectVectorStudioV2ZoomInButton", "zoomIn"], -- ["#objectVectorStudioV2PanUpButton", "panUp"], -- ["#objectVectorStudioV2PanDownButton", "panDown"], -- ["#objectVectorStudioV2PanLeftButton", "panLeft"], -- ["#objectVectorStudioV2PanRightButton", "panRight"], -- ["#objectVectorStudioV2ResetViewButton", "reset"], -- ["#objectVectorStudioV2CenterDotButton", "center"], -- ["#objectVectorStudioV2FrameLeftButton", "panLeft"], -- ["#objectVectorStudioV2FrameEarlierButton", "panLeft"], -- ["#objectVectorStudioV2DuplicateFrameButton", "duplicate"], -- ["#objectVectorStudioV2FrameLaterButton", "panRight"], -- ["#objectVectorStudioV2FrameRightButton", "panRight"], -- ["#objectVectorStudioV2DeleteFrameButton", "delete"], -- ["#objectVectorStudioV2PreviewUndoButton", "undo"], -- ["#objectVectorStudioV2PreviewRedoButton", "redo"], -- ["#objectVectorStudioV2PreviewCopyButton", "copy"], -- ["#objectVectorStudioV2PreviewPasteButton", "paste"], -- ["#objectVectorStudioV2AngleSnapButton", "angle"], -- ["#objectVectorStudioV2GridRenderButton", "grid"], -- [".object-vector-studio-v2__shape-icon--select", "select"], -- [".object-vector-studio-v2__shape-icon--annulus-ring", "annulusRing"], -- [".object-vector-studio-v2__shape-icon--triangle", "triangle"], -- [".object-vector-studio-v2__shape-icon--rectangle", "rectangle"], -- [".object-vector-studio-v2__shape-icon--square", "square"], -- [".object-vector-studio-v2__shape-icon--circle", "circle"], -- [".object-vector-studio-v2__shape-icon--capsule-stadium", "capsuleStadium"], -- [".object-vector-studio-v2__shape-icon--ellipse", "ellipse"], -- [".object-vector-studio-v2__shape-icon--kite", "kite"], -- [".object-vector-studio-v2__shape-icon--line", "line"], -- [".object-vector-studio-v2__shape-icon--lune", "lune"], -- [".object-vector-studio-v2__shape-icon--parallelogram", "parallelogram"], -- [".object-vector-studio-v2__shape-icon--picker", "picker"], -- [".object-vector-studio-v2__shape-icon--polygon", "polygon"], -- [".object-vector-studio-v2__shape-icon--polyline", "polyline"], -- [".object-vector-studio-v2__shape-icon--rhombus", "rhombus"], -- [".object-vector-studio-v2__shape-icon--arc", "arc"], -- [".object-vector-studio-v2__shape-icon--sector", "sector"], -- [".object-vector-studio-v2__shape-icon--segment", "segment"], -- [".object-vector-studio-v2__shape-icon--squircle", "squircle"], -- [".object-vector-studio-v2__shape-icon--star-polygon", "starPolygon"], -- [".object-vector-studio-v2__shape-icon--text", "text"], -- [".object-vector-studio-v2__shape-icon--trapezoid-trapezium", "trapezoidTrapezium"], -- [".object-vector-studio-v2__z-icon--bring-forward", "bringForward"], -- [".object-vector-studio-v2__z-icon--send-backward", "sendBackward"], -- [".object-vector-studio-v2__z-icon--bring-front", "bringFront"], -- [".object-vector-studio-v2__z-icon--send-back", "sendBack"], -- [".object-vector-studio-v2__z-icon--group", "group"], -- [".object-vector-studio-v2__z-icon--ungroup", "ungroup"] --]); -- --const OBJECT_STATE_IDS = Object.freeze(["idle", "move", "active", "inactive", "damaged", "destroyed"]); -- --const OBJECT_STATE_LABELS = Object.freeze({ -- active: "Active", -- damaged: "Damaged", -- destroyed: "Destroyed", -- idle: "Idle", -- inactive: "Inactive", -- move: "Move" --}); -- --const OBJECT_STATE_HELP = Object.freeze({ -- active: ["Object is enabled and participating in gameplay.", "Typically the default active runtime state."], -- damaged: ["Object is visually damaged but still active."], -- destroyed: ["Object destruction/death state.", "Usually final or transitional before removal."], -- idle: ["Default stationary state.", "No movement or action animation active."], -- inactive: ["Object is disabled, hidden, sleeping, or not participating in gameplay."], -- move: ["Movement/action state.", "Used for thrusting, walking, flying, or active movement visuals."] --}); -- --const GROUP_COLOR_SWATCHES = Object.freeze(["#22d3ee", "#a78bfa", "#fb7185", "#34d399", "#fbbf24", "#60a5fa", "#f472b6", "#4ade80"]); -- --const SHAPE_TYPE_DETAILS = Object.freeze({ -- arc: "Arc primitive metadata with center, radius, and angle span.", -- circle: "Circle primitive metadata with center point and radius.", -- ellipse: "Ellipse primitive metadata with center point and radius pair.", -- line: "Line primitive metadata with start and end points.", -- polygon: "Polygon primitive metadata with ordered point ownership.", -- polyline: "Polyline primitive metadata with ordered open point ownership.", -- rectangle: "Rectangle primitive metadata with position and dimensions.", -- square: "Square tool metadata backed by rectangle geometry with equal width and height.", -- text: "Text primitive metadata with position, font size, and content." --}); -- --const PRIMITIVE_TOOLS = Object.freeze(["triangle", "rectangle", "square", "circle", "ellipse", "line", "polygon", "polyline", "arc", "text"]); --const POINT_STYLE_VALUES = Object.freeze(["round", "square"]); --const OPEN_POINT_STYLE_TOOLS = Object.freeze(new Set(["line", "polyline", "arc"])); --const CLOSED_POINT_STYLE_TOOLS = Object.freeze(new Set(["polygon", "rectangle"])); --const OBJECT_ID_SIZE_WORDS = Object.freeze(new Set(["large", "medium", "small"])); --const OBJECT_ID_ORDERED_NOUNS = Object.freeze(new Set(["asteroid", "ufo"])); -- --function shapeTool(shape) { -- return String(shape?.tool || "").trim().toLowerCase(); --} -- --function shapeGeometryTool(shape) { -- const tool = shapeTool(shape); -- if (tool === "triangle") { -- return "polygon"; -- } -- if (tool === "square") { -- return "rectangle"; -- } -- return tool; --} -- --function shapeTypeLabel(shapeOrTool) { -- const tool = typeof shapeOrTool === "string" ? shapeOrTool : shapeTool(shapeOrTool); -- return tool.replace(/(^|-)([a-z])/g, (match) => match.toUpperCase()).replaceAll("-", " "); --} -- --function shapeDisplayLabel(shape) { -- return shapeTypeLabel(shape); --} -- --function shapeCountLabel(count) { -- return `${count} ${count === 1 ? "shape" : "shapes"}`; --} -- --function normalizeShapeIndex(value) { -- const index = Number(value); -- return Number.isInteger(index) && index >= 0 ? index : -1; --} -- --function linePoint(geometry, pointKey) { -- return geometry?.[pointKey]; --} -- --function tagList(value) { -- if (Array.isArray(value)) { -- return Array.from(new Set(value.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean))).sort(); -- } -- return Array.from(new Set(String(value || "") -- .split(",") -- .map((tag) => tag.trim().toLowerCase()) -- .filter(Boolean))).sort(); --} -- --function slugifyObjectName(name) { -- const slug = name -- .trim() -- .toLowerCase() -- .replace(/[^a-z0-9]+/g, "-") -- .replace(/^-+|-+$/g, ""); -- return slug || "object"; --} -- --function canonicalObjectSlug(name, gameKey = "") { -- const gameWords = slugifyObjectName(gameKey).split("-").filter(Boolean); -- let words = slugifyObjectName(name).split("-").filter(Boolean); -- if (gameWords.length && gameWords.every((word, index) => words[index] === word)) { -- words = words.slice(gameWords.length); -- } -- words = words.filter((word, index) => index === 0 || word !== words[index - 1]); -- if (words.length === 2 && OBJECT_ID_ORDERED_NOUNS.has(words[0]) && OBJECT_ID_SIZE_WORDS.has(words[1])) { -- words = [words[1], words[0]]; -- } -- if (words.length > 2 && words.at(-1) === "1" && OBJECT_ID_ORDERED_NOUNS.has(words.at(-2))) { -- words = words.slice(0, -1); -- } -- return words.join("-") || "object"; --} -- --function objectGameSegment(objectId) { -- const match = /^object\.([a-z0-9-]+)\./.exec(String(objectId || "")); -- return match?.[1] || ""; --} -- --function payloadGameSlugFromName(name) { -- const slug = slugifyObjectName(name || "game") -- .replace(/-(object-vector-assets|object-vector-asset|object-assets|object-set|payload|assets)$/u, ""); -- return slug || "game"; --} -- --function sortedShapes(object) { -- return [...(object?.shapes || [])].sort((left, right) => left.order - right.order); --} -- --function sortedFrames(state) { -- return [...(state?.frames || [])].sort((left, right) => left.order - right.order); --} -- --function swatchColor(swatch) { -- return swatch?.value || swatch?.hex || swatch?.color || ""; --} -- --function colorMetrics(color) { -- const hex = String(color || "").trim().replace(/^#/, ""); -- if (!/^[0-9a-f]{6}$/i.test(hex)) { -- return { bri: 0, hue: 0, sat: 0 }; -- } -- const red = parseInt(hex.slice(0, 2), 16) / 255; -- const green = parseInt(hex.slice(2, 4), 16) / 255; -- const blue = parseInt(hex.slice(4, 6), 16) / 255; -- const max = Math.max(red, green, blue); -- const min = Math.min(red, green, blue); -- const delta = max - min; -- let hue = 0; -- if (delta !== 0) { -- if (max === red) { -- hue = ((green - blue) / delta) % 6; -- } else if (max === green) { -- hue = (blue - red) / delta + 2; -- } else { -- hue = (red - green) / delta + 4; -- } -- hue = Math.round(hue * 60); -- if (hue < 0) { -- hue += 360; -- } -- } -- return { -- bri: max, -- hue, -- sat: max === 0 ? 0 : delta / max -- }; --} -- --function shapeBounds(shape) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "rectangle") { -- return { -- height: shape.geometry.height, -- width: shape.geometry.width, -- x: shape.geometry.x, -- y: shape.geometry.y -- }; -- } -- if (geometryTool === "circle") { -- return { -- height: shape.geometry.r * 2, -- width: shape.geometry.r * 2, -- x: shape.geometry.cx - shape.geometry.r, -- y: shape.geometry.cy - shape.geometry.r -- }; -- } -- if (geometryTool === "ellipse") { -- return { -- height: shape.geometry.ry * 2, -- width: shape.geometry.rx * 2, -- x: shape.geometry.cx - shape.geometry.rx, -- y: shape.geometry.cy - shape.geometry.ry -- }; -- } -- if (geometryTool === "line") { -- const point1 = linePoint(shape.geometry, "point1"); -- const point2 = linePoint(shape.geometry, "point2"); -- const x = Math.min(point1.x, point2.x); -- const y = Math.min(point1.y, point2.y); -- return { -- height: Math.max(1, Math.abs(point2.y - point1.y)), -- width: Math.max(1, Math.abs(point2.x - point1.x)), -- x, -- y -- }; -- } -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- const xValues = shape.geometry.points.map((point) => point.x); -- const yValues = shape.geometry.points.map((point) => point.y); -- const minX = Math.min(...xValues); -- const minY = Math.min(...yValues); -- return { -- height: Math.max(1, Math.max(...yValues) - minY), -- width: Math.max(1, Math.max(...xValues) - minX), -- x: minX, -- y: minY -- }; -- } -- if (geometryTool === "arc") { -- return { -- height: shape.geometry.r * 2, -- width: shape.geometry.r * 2, -- x: shape.geometry.cx - shape.geometry.r, -- y: shape.geometry.cy - shape.geometry.r -- }; -- } -- if (geometryTool === "text") { -- return { -- height: shape.geometry.fontSize, -- width: Math.max(24, shape.geometry.text.length * shape.geometry.fontSize * 0.6), -- x: shape.geometry.x, -- y: shape.geometry.y - shape.geometry.fontSize -- }; -- } -- throw new Error(`unsupported shape bounds for ${shapeTool(shape)}`); --} -- --function ellipseSamplePoints(cx, cy, rx, ry, sampleCount = 64) { -- return Array.from({ length: sampleCount }, (_, index) => { -- const radians = (index / sampleCount) * Math.PI * 2; -- return { -- x: cx + Math.cos(radians) * rx, -- y: cy + Math.sin(radians) * ry -- }; -- }); --} -- --function arcSamplePoints(geometry, sampleCount = 32) { -- const startAngle = geometry.startAngle; -- const endAngle = geometry.endAngle; -- const span = endAngle - startAngle; -- const steps = Math.max(1, Math.ceil(Math.abs(span) / (360 / sampleCount))); -- -- return Array.from({ length: steps + 1 }, (_, index) => { -- const angle = startAngle + (span * index) / steps; -- const radians = ((angle - 90) * Math.PI) / 180; -- return { -- x: geometry.cx + Math.cos(radians) * geometry.r, -- y: geometry.cy + Math.sin(radians) * geometry.r -- }; -- }); --} -- --function shapeBoundsPoints(shape) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "line") { -- return [linePoint(shape.geometry, "point1"), linePoint(shape.geometry, "point2")].filter(Boolean); -- } -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- return Array.isArray(shape.geometry.points) ? shape.geometry.points : []; -- } -- if (geometryTool === "circle") { -- return ellipseSamplePoints(shape.geometry.cx, shape.geometry.cy, shape.geometry.r, shape.geometry.r); -- } -- if (geometryTool === "ellipse") { -- return ellipseSamplePoints(shape.geometry.cx, shape.geometry.cy, shape.geometry.rx, shape.geometry.ry); -- } -- if (geometryTool === "arc") { -- return arcSamplePoints(shape.geometry); -- } -- return objectVectorBoundsCornerPoints(shapeBounds(shape)); --} -- --function defaultShapeTransform() { -- return { -- rotation: 0, -- scaleX: 1, -- scaleY: 1, -- x: 0, -- y: 0 -- }; --} -- --export class ToolStarterApp { -- constructor({ accordions, actionNav, elements, runtimeAssetService = null, schemaService, shell, statusLog, windowRef = window }) { -- this.accordions = accordions; -- this.actionNav = actionNav; -- this.elements = elements; -- this.schemaService = schemaService; -- this.shell = shell; -- this.statusLog = statusLog; -- this.runtimeAssetService = runtimeAssetService || new ObjectVectorRuntimeAssetService({ logger: statusLog }); -- this.window = windowRef; -- this.currentPayload = null; -- this.runtimePalette = null; -- this.runtimePaletteSource = ""; -- this.selectedObjectId = ""; -- this.selectedShapeIndex = -1; -- this.selectedShapeIndexes = new Set(); -- this.directSelectedShapeIndexes = new Set(); -- this.selectedStateId = ""; -- this.selectedFrameId = ""; -- this.playbackTimerId = 0; -- this.isAnimationPlaying = false; -- this.activeTool = "select"; -- this.paletteTarget = "paint"; -- this.paletteSortMode = "name"; -- this.paletteSortDirection = "asc"; -- this.selectedFillColor = "#ffffff"; -- this.selectedStrokeColor = "#000000"; -- this.selectedFillLabel = "default fill"; -- this.selectedStrokeLabel = "default stroke"; -- this.selectedFillOpacity = 1; -- this.selectedStrokeOpacity = 1; -- this.isPaintDragging = false; -- this.activeDrawing = null; -- this.drawingPreviewPoint = null; -- this.drawingHintClientPoint = null; -- this.previewClipboardShape = null; -- this.previewUndoStack = []; -- this.previewRedoStack = []; -- this.previewPointerEdit = null; -- this.transformInputValues = new Map(); -- this.stateControlStateId = ""; -- this.pendingAddObjectClick = false; -- this.hiddenObjectIds = new Set(); -- this.lockedObjectIds = new Set(); -- this.snapMode = this.readSnapMode(); -- this.angleSnapEnabled = false; -- this.angleSnapStep = 15; -- this.gridRenderEnabled = true; -- this.centerOriginVisible = this.window.sessionStorage?.getItem(CENTER_ORIGIN_SESSION_KEY) !== "0"; -- this.schemaReady = false; -- this.viewportPan = null; -- this.viewport = { ...DEFAULT_VIEWPORT }; -- } -- -- async start() { -- this.window.__objectVectorStudioV2App = this; -- this.shell.mount(); -- this.accordions.forEach((accordion) => accordion.mount()); -- this.actionNav.mount({ -- onCopyJson: () => { -- void this.copyJson(); -- }, -- onExportJson: () => this.exportJson(), -- onImportJson: (file) => { -- void this.importJson(file); -- }, -- onReturnToWorkspace: (url) => { -- this.window.location.href = url; -- } -- }); -- this.statusLog.mount(); -- this.applyNerdFontIcons(); -- this.bindObjectActions(); -- this.bindToolToggles(); -- this.bindSnapControls(); -- this.bindPaletteControls(); -- this.bindViewportControls(); -- this.bindPreviewEditActions(); -- this.bindObjectFilters(); -- this.bindAnimationControls(); -- this.bindJsonDetailsActions(); -- this.bindKeyboardShortcuts(); -- this.applyToolDisplayMode(this.window.sessionStorage?.getItem(TOOL_DISPLAY_MODE_KEY) || "words", false); -- this.applySnapState(); -- this.updateViewport(); -- this.actionNav.setImportEnabled(false); -- this.renderEmptyState("Object Vector Studio V2 schema contract is loading."); -- this.statusLog.write("OK Object Vector Studio V2 layout shell ready."); -- this.statusLog.write("INFO Schema-only loading is idle. Import JSON or launch with workspace toolState data. Runtime palette is required before rendering."); -- this.statusLog.write("INFO Tools primitive buttons enter drawing mode; use the canvas to commit schema-valid geometry."); -- this.statusLog.write("INFO Disabled controls stay inactive until a schema-valid payload, runtime palette, selected object, or active frame is available."); -- this.statusLog.write("INFO Object identity uses object.game.name ids."); -- this.statusLog.write(`INFO Object Vector Studio V2 editor zoom is viewport-only; runtime/world scale remains ${CANONICAL_WORLD_TO_SCREEN_SCALE}:1 through the shared world-to-screen transform.`); -- this.statusLog.write("INFO Paint and stroke selection is structured to scale later into shaders, gradients, patterns, neon, SVG export, and runtime rendering."); -- await this.schemaService.loadSchema(); -- this.schemaReady = true; -- this.statusLog.write(`OK Object Vector Studio V2 schema contract loaded from ${this.schemaService.schemaPath}.`); -- await this.loadRuntimePaletteFromSources(); -- this.renderEmptyState("Schema-only loading is idle. Import JSON or launch with workspace toolState data. Runtime palette is required before rendering."); -- if (this.actionNav.isWorkspaceLaunch()) { -- this.loadWorkspaceToolState(); -- } -- } -- -- async loadRuntimePaletteFromSources() { -- const params = new URLSearchParams(this.window.location.search); -- const paletteSourcePath = params.get("paletteSourcePath") || ""; -- if (paletteSourcePath) { -- await this.loadPaletteFromUrl(paletteSourcePath); -- if (this.runtimePalette) { -- return; -- } -- } -- -- if (this.actionNav.isWorkspaceLaunch() && this.loadPaletteFromWorkspaceSession()) { -- return; -- } -- -- if (this.loadPaletteFromSessionKey(RUNTIME_PALETTE_SESSION_KEY)) { -- return; -- } -- this.loadPaletteFromWorkspaceSession(); -- } -- -- async loadPaletteFromUrl(paletteSourcePath) { -- let paletteUrl; -- try { -- paletteUrl = new URL(paletteSourcePath, this.window.location.href); -- const response = await this.window.fetch(paletteUrl, { cache: "no-store" }); -- if (!response.ok) { -- this.statusLog.write(`FAIL Runtime palette load failed from ${paletteSourcePath}: ${response.status}.`); -- return; -- } -- const palette = await response.json(); -- this.acceptRuntimePalette(palette, `paletteSourcePath:${paletteSourcePath}`, true); -- } catch (error) { -- this.statusLog.write(`FAIL Runtime palette load failed from ${paletteSourcePath}: ${error.message}`); -- } -- } -- -- loadPaletteFromSessionKey(sessionKey) { -- const storedPalette = this.window.sessionStorage?.getItem(sessionKey) || ""; -- if (!storedPalette) { -- return false; -- } -- try { -- const palette = JSON.parse(storedPalette); -- return this.acceptRuntimePalette(palette, sessionKey, sessionKey !== RUNTIME_PALETTE_SESSION_KEY); -- } catch (error) { -- this.statusLog.write(`FAIL Runtime palette load failed from ${sessionKey}: ${error.message}`); -- return false; -- } -- } -- -- loadPaletteFromWorkspaceSession() { -- const storedSession = this.window.sessionStorage?.getItem(WORKSPACE_PALETTE_SESSION_KEY) || ""; -- if (!storedSession) { -- return false; -- } -- try { -- const session = JSON.parse(storedSession); -- const palette = session?.data && typeof session.data === "object" ? session.data : session; -- return this.acceptRuntimePalette(palette, `${WORKSPACE_PALETTE_SESSION_KEY}.data`, true); -- } catch (error) { -- this.statusLog.write(`FAIL Runtime palette load failed from ${WORKSPACE_PALETTE_SESSION_KEY}: ${error.message}`); -- return false; -- } -- } -- -- acceptRuntimePalette(palette, sourceLabel, persistToRuntimeSession) { -- const validation = this.schemaService.validateRuntimePalette(palette, sourceLabel); -- if (!validation.ok) { -- this.runtimePalette = null; -- this.runtimePaletteSource = ""; -- this.statusLog.write(`FAIL Runtime palette validation failed from ${sourceLabel}: ${validation.errors.join(" ")}`); -- return false; -- } -- this.runtimePalette = validation.palette; -- this.runtimePaletteSource = sourceLabel; -- const firstColor = validation.palette.swatches.map((swatch) => swatchColor(swatch)).find(Boolean); -- if (firstColor) { -- this.selectedFillColor = firstColor; -- this.selectedStrokeColor = firstColor; -- const firstSwatch = validation.palette.swatches.find((swatch) => swatchColor(swatch) === firstColor); -- const firstLabel = firstSwatch?.name || firstSwatch?.id || firstSwatch?.symbol || "first swatch"; -- this.selectedFillLabel = firstLabel; -- this.selectedStrokeLabel = firstLabel; -- } -- if (persistToRuntimeSession) { -- this.window.sessionStorage?.setItem(RUNTIME_PALETTE_SESSION_KEY, JSON.stringify(validation.palette)); -- } -- this.statusLog.write(`OK Runtime palette loaded from ${sourceLabel}: ${validation.palette.swatches.length} swatches.`); -- this.statusLog.write("INFO Runtime palette is read-only session/workspace data; Object Vector JSON remains palette-free."); -- return true; -- } -- -- bindObjectActions() { -- this.elements.addObjectButton.addEventListener("pointerdown", () => { -- this.pendingAddObjectClick = true; -- }); -- this.elements.addObjectButton.addEventListener("click", () => this.addObject()); -- this.elements.addTagButton.addEventListener("click", () => this.addTagToSelectedObject()); -- this.elements.objectNameInput.addEventListener("input", () => this.updateObjectPreviewFooterFromNameInput()); -- this.elements.renameObjectButton.addEventListener("click", () => this.renameSelectedObject()); -- this.elements.duplicateObjectButton.addEventListener("click", () => this.duplicateSelectedObject()); -- this.elements.deleteObjectButton?.addEventListener("click", () => this.deleteSelectedObject()); -- this.elements.exportSvgButton.addEventListener("click", () => this.exportSelectedObjectSvg()); -- } -- -- applyNerdFontIcons() { -- OBJECT_VECTOR_STUDIO_STATIC_ICON_TARGETS.forEach(([selector, iconKey]) => { -- this.window.document.querySelectorAll(selector).forEach((element) => this.applyIconGlyph(element, iconKey)); -- }); -- } -- -- applyIconGlyph(element, iconKey) { -- const icon = OBJECT_VECTOR_STUDIO_ICON_GLYPHS[iconKey]; -- if (!element || !icon?.glyph) { -- return; -- } -- element.dataset.ovsIcon = icon.glyph; -- element.dataset.ovsIconKey = iconKey; -- element.dataset.ovsIconName = icon.name; -- element.classList.add("object-vector-studio-v2__nerd-icon"); -- } -- -- bindAnimationControls() { -- this.elements.deleteFrameButton.addEventListener("click", () => this.deleteSelectedFrame()); -- this.elements.duplicateFrameButton.addEventListener("click", () => this.duplicateSelectedFrame()); -- this.elements.frameLeftButton.addEventListener("click", () => this.moveSelectedFrame("earlier", "left")); -- this.elements.frameEarlierButton.addEventListener("click", () => this.moveSelectedFrame("earlier", "earlier")); -- this.elements.frameLaterButton.addEventListener("click", () => this.moveSelectedFrame("later", "later")); -- this.elements.frameRightButton.addEventListener("click", () => this.moveSelectedFrame("later", "right")); -- this.elements.playButton.addEventListener("click", () => this.playAnimation()); -- this.elements.pauseButton.addEventListener("click", () => this.pauseAnimation()); -- this.elements.stopButton.addEventListener("click", () => this.stopAnimation()); -- this.elements.runtimePreviewButton.addEventListener("click", () => { -- void this.previewRuntimeAsset(); -- }); -- this.elements.onionSkinToggle.addEventListener("change", () => { -- this.renderWorkSurface(); -- this.statusLog.write(`OK Onion-skin preview ${this.elements.onionSkinToggle.checked ? "enabled" : "disabled"}.`); -- }); -- } -- -- bindToolToggles() { -- this.elements.toolLabelModeButton.addEventListener("click", () => { -- const nextMode = this.elements.toolToggleGrid.classList.contains("is-icon-only") ? "words" : "icons"; -- this.applyToolDisplayMode(nextMode, true); -- }); -- -- this.elements.toolToggles.forEach((button) => { -- button.addEventListener("click", () => { -- const tool = button.dataset.shapeTool || "unknown"; -- this.setActiveToolButton(button); -- this.setPaletteTarget("stroke", false); -- if (PRIMITIVE_TOOLS.includes(tool)) { -- this.startDrawingMode(tool); -- return; -- } -- this.cancelActiveDrawing("tool switch", { log: false }); -- this.activeTool = tool; -- if (tool === "select" && this.selectedShapeIndex >= 0) { -- const clearedShape = this.selectedShapeIndex; -- this.selectedShapeIndex = -1; -- this.selectedShapeIndexes.clear(); -- this.directSelectedShapeIndexes.clear(); -- this.renderObjectTiles(); -- this.renderSelectedObject(); -- this.renderWorkSurface(); -- this.updateObjectActionState(); -- this.statusLog.write(`OK Select tool cleared selected shape row ${clearedShape}.`); -- return; -- } -- this.statusLog.write(`OK Tools mode selected: ${tool}.`); -- }); -- }); -- } -- -- bindSnapControls() { -- this.elements.snapModeButton.addEventListener("click", () => this.cycleSnapMode()); -- this.elements.angleSnapButton.addEventListener("click", () => { -- this.angleSnapEnabled = !this.angleSnapEnabled; -- this.window.sessionStorage?.setItem(ANGLE_SNAP_SESSION_KEY, this.angleSnapEnabled ? "1" : "0"); -- this.applySnapState(); -- this.statusLog.write(`OK Snap angle ${this.angleSnapEnabled ? "enabled" : "disabled"}: Rotate action ${this.angleSnapEnabled ? `uses dropdown values in ${this.angleSnapStep} degree increments` : "uses raw numeric textbox values"}.`); -- }); -- this.elements.gridRenderButton.addEventListener("click", () => { -- this.gridRenderEnabled = !this.gridRenderEnabled; -- this.window.sessionStorage?.setItem(GRID_RENDER_SESSION_KEY, this.gridRenderEnabled ? "1" : "0"); -- this.applySnapState(); -- this.renderWorkSurface(); -- this.statusLog.write(`OK Grid rendering ${this.gridRenderEnabled ? "enabled" : "disabled"}.`); -- }); -- this.elements.centerDotButton.addEventListener("click", () => this.toggleCenterOriginMarker()); -- } -- -- bindPaletteControls() { -- this.elements.paintModeButton.addEventListener("click", () => this.setPaletteTarget("paint")); -- this.elements.strokeModeButton.addEventListener("click", () => this.setPaletteTarget("stroke")); -- this.elements.strokeWidth.addEventListener("change", () => { -- const width = Number(this.elements.strokeWidth.value); -- if (!Number.isFinite(width) || width <= 0) { -- this.elements.strokeWidth.value = "2"; -- this.statusLog.write("WARN Stroke width reset to 2: value must be greater than 0."); -- return; -- } -- this.statusLog.write(`OK Palette stroke width set to ${width}.`); -- }); -- [ -- this.elements.fillOpacity, -- this.elements.strokeOpacity -- ].forEach((input) => input.addEventListener("input", () => this.clearInputValidity(input))); -- this.elements.fillOpacity.addEventListener("change", () => this.changePaletteOpacity("fill")); -- this.elements.strokeOpacity.addEventListener("change", () => this.changePaletteOpacity("stroke")); -- this.elements.paletteSortButtons.forEach((button) => { -- button.addEventListener("click", () => { -- const nextSortMode = button.dataset.paletteSort || "name"; -- if (this.paletteSortMode === nextSortMode) { -- this.paletteSortDirection = this.paletteSortDirection === "asc" ? "desc" : "asc"; -- } else { -- this.paletteSortMode = nextSortMode; -- this.paletteSortDirection = "asc"; -- } -- this.updatePaletteSortButtons(); -- if (this.runtimePalette) { -- this.renderPalette(); -- } -- this.statusLog.write(`OK Palette sort set to ${this.paletteSortMode} ${this.paletteSortDirection}.`); -- }); -- }); -- this.setPaletteTarget("paint", false); -- this.updatePaletteSortButtons(); -- this.updatePaletteModeSwatches(); -- } -- -- bindJsonDetailsActions() { -- this.elements.copyJsonDetailsButton.addEventListener("click", (event) => { -- event.stopPropagation(); -- void this.copyJson(); -- }); -- } -- -- setPaletteTarget(target, shouldLog = true) { -- this.paletteTarget = target === "stroke" ? "stroke" : "paint"; -- this.elements.paintModeButton.setAttribute("aria-pressed", String(this.paletteTarget === "paint")); -- this.elements.strokeModeButton.setAttribute("aria-pressed", String(this.paletteTarget === "stroke")); -- this.elements.paintModeButton.classList.toggle("is-active", this.paletteTarget === "paint"); -- this.elements.strokeModeButton.classList.toggle("is-active", this.paletteTarget === "stroke"); -- this.updatePaletteModeSwatches(); -- if (this.runtimePalette) { -- this.renderPalette(); -- } -- if (shouldLog) { -- this.cancelActiveDrawing("palette mode change", { log: false }); -- this.activeTool = this.paletteTarget === "stroke" ? "stroke" : "paint"; -- this.setActiveToolButton(null); -- this.statusLog.write(`OK Palette target set to ${this.paletteTarget === "stroke" ? "Stroke" : "Paint"}.`); -- } -- } -- -- updatePaletteSortButtons() { -- this.elements.paletteSortButtons.forEach((button) => { -- const isActive = button.dataset.paletteSort === this.paletteSortMode; -- const baseLabel = button.dataset.paletteSortLabel || button.textContent.replace(/[▲▼]\s*$/u, "").trim() || shapeTypeLabel(button.dataset.paletteSort || "name"); -- button.dataset.paletteSortLabel = baseLabel; -- button.dataset.sortDirection = isActive ? this.paletteSortDirection : ""; -- button.textContent = ""; -- const label = document.createElement("span"); -- label.textContent = baseLabel; -- button.append(label); -- const caret = document.createElement("span"); -- caret.className = "object-vector-studio-v2__palette-sort-caret"; -- caret.setAttribute("aria-hidden", "true"); -- caret.textContent = isActive ? (this.paletteSortDirection === "asc" ? "▲" : "▼") : "◇"; -- button.append(caret); -- button.setAttribute("aria-pressed", String(isActive)); -- button.setAttribute("aria-label", `${baseLabel} sort ${isActive ? this.paletteSortDirection : "inactive"}`); -- button.title = isActive ? `Sort ${baseLabel} ${this.paletteSortDirection === "asc" ? "ascending" : "descending"}` : `Sort ${baseLabel} ascending`; -- button.classList.toggle("is-active", isActive); -- }); -- } -- -- updatePaletteModeSwatches() { -- this.elements.paintModeButton.style.setProperty("--object-vector-studio-v2-mode-swatch", this.selectedFillColor); -- this.elements.paintModeButton.style.setProperty("--object-vector-studio-v2-mode-opacity", String(this.selectedFillOpacity)); -- this.elements.strokeModeButton.style.setProperty("--object-vector-studio-v2-mode-swatch", this.selectedStrokeColor); -- this.elements.strokeModeButton.style.setProperty("--object-vector-studio-v2-mode-opacity", String(this.selectedStrokeOpacity)); -- this.elements.paintModeButton.dataset.paletteModeColor = this.selectedFillColor; -- this.elements.paintModeButton.dataset.paletteModeOpacity = String(this.selectedFillOpacity); -- this.elements.strokeModeButton.dataset.paletteModeColor = this.selectedStrokeColor; -- this.elements.strokeModeButton.dataset.paletteModeOpacity = String(this.selectedStrokeOpacity); -- } -- -- pointStyleValue(value) { -- return POINT_STYLE_VALUES.includes(value) ? value : "square"; -- } -- -- strokeLinejoinValue(value = "round") { -- return this.pointStyleValue(value) === "square" ? "miter" : "round"; -- } -- -- supportsOpenPointStyle(shape) { -- return OPEN_POINT_STYLE_TOOLS.has(shapeGeometryTool(shape)); -- } -- -- supportsClosedPointStyle(shape) { -- return CLOSED_POINT_STYLE_TOOLS.has(shapeGeometryTool(shape)); -- } -- -- shapeSupportsPointRoundingControls(shape) { -- const geometryTool = shapeGeometryTool(shape); -- return !["circle", "ellipse", "text"].includes(geometryTool) && this.shapeGeometryPointCount(shape) > 0; -- } -- -- shapeSupportsRoundingRadiusControl(shape) { -- return ["polygon", "polyline", "rectangle"].includes(shapeGeometryTool(shape)) && this.shapeGeometryPointCount(shape) > 0; -- } -- -- shapeRoundingRadius(shape) { -- const value = Number(shape?.style?.roundingRadius); -- if (Number.isFinite(value) && value >= 0) { -- return value; -- } -- return 4; -- } -- -- shapeUnifiedPointStyle(shape) { -- const pointStyles = this.shapePointStyleValues(shape); -- if (pointStyles.length) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "polygon" || geometryTool === "polyline" || geometryTool === "rectangle") { -- return "square"; -- } -- return this.pointStyleValue(shape?.style?.pointStyle ?? shape?.style?.strokeLinecap); -- } -- return this.pointStyleValue(shape?.style?.pointStyle ?? shape?.style?.strokeLinecap); -- } -- -- shapeStartPointStyle(shape) { -- const pointStyles = this.shapePointStyleValues(shape); -- if (pointStyles.length) { -- return pointStyles[0]; -- } -- return this.pointStyleValue(shape?.style?.startPointStyle ?? shape?.style?.strokeLinecap ?? shape?.style?.pointStyle); -- } -- -- shapeEndPointStyle(shape) { -- const pointStyles = this.shapePointStyleValues(shape); -- if (pointStyles.length) { -- return pointStyles.at(-1); -- } -- return this.pointStyleValue(shape?.style?.endPointStyle ?? shape?.style?.strokeLinecap ?? shape?.style?.pointStyle); -- } -- -- openShapeUsesSplitPointStyles(shape) { -- return this.supportsOpenPointStyle(shape) && this.shapeStartPointStyle(shape) !== this.shapeEndPointStyle(shape); -- } -- -- shapeGeometryPointCount(shape) { -- return this.shapeGeometryPoints(shape).length; -- } -- -- shapeGeometryPoints(shape) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "line") { -- return [linePoint(shape.geometry, "point1"), linePoint(shape.geometry, "point2")]; -- } -- if (geometryTool === "arc") { -- return [ -- this.polarPoint(shape.geometry.cx, shape.geometry.cy, shape.geometry.r, shape.geometry.startAngle), -- this.polarPoint(shape.geometry.cx, shape.geometry.cy, shape.geometry.r, shape.geometry.endAngle) -- ]; -- } -- if (geometryTool === "rectangle") { -- const x = Number(shape.geometry.x) || 0; -- const y = Number(shape.geometry.y) || 0; -- const width = Number(shape.geometry.width) || 0; -- const height = Number(shape.geometry.height) || 0; -- return [ -- { x, y }, -- { x: x + width, y }, -- { x: x + width, y: y + height }, -- { x, y: y + height } -- ]; -- } -- if ((geometryTool === "polygon" || geometryTool === "polyline") && Array.isArray(shape?.geometry?.points)) { -- return shape.geometry.points; -- } -- return []; -- } -- -- shapePointRoundingValues(shape) { -- const pointCount = this.shapeGeometryPointCount(shape); -- if (!pointCount) { -- return []; -- } -- const explicit = Array.isArray(shape?.style?.pointRounding) ? shape.style.pointRounding : null; -- if (explicit) { -- return Array.from({ length: pointCount }, (_, index) => explicit[index] === true); -- } -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "polygon" || geometryTool === "rectangle") { -- return Array.from({ length: pointCount }, () => false); -- } -- return Array.from({ length: pointCount }, (_, index) => { -- if (geometryTool === "line" || geometryTool === "polyline") { -- if (index === 0) { -- return this.pointStyleValue(shape?.style?.startPointStyle ?? shape?.style?.strokeLinecap ?? shape?.style?.pointStyle) === "round"; -- } -- if (index === pointCount - 1) { -- return this.pointStyleValue(shape?.style?.endPointStyle ?? shape?.style?.strokeLinecap ?? shape?.style?.pointStyle) === "round"; -- } -- if (geometryTool === "polyline") { -- return false; -- } -- } -- return this.pointStyleValue(shape?.style?.pointStyle ?? shape?.style?.strokeLinecap) === "round"; -- }); -- } -- -- shapePointStyleValues(shape) { -- return this.shapePointRoundingValues(shape).map((isRounded) => isRounded ? "round" : "square"); -- } -- -- shapeJoinPointIndexes(shape, pointCount = this.shapeGeometryPointCount(shape)) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "polyline") { -- return Array.from({ length: Math.max(0, pointCount - 2) }, (_, index) => index + 1); -- } -- if (geometryTool === "polygon") { -- return Array.from({ length: pointCount }, (_, index) => index); -- } -- return []; -- } -- -- bindViewportControls() { -- this.elements.zoomInButton.addEventListener("click", () => this.zoomViewportByStep(ZOOM_STEP)); -- this.elements.zoomOutButton.addEventListener("click", () => this.zoomViewportByStep(-ZOOM_STEP)); -- this.elements.panUpButton.addEventListener("click", () => this.panViewport(0, -20)); -- this.elements.panDownButton.addEventListener("click", () => this.panViewport(0, 20)); -- this.elements.panLeftButton.addEventListener("click", () => this.panViewport(-20, 0)); -- this.elements.panRightButton.addEventListener("click", () => this.panViewport(20, 0)); -- this.elements.resetViewButton.addEventListener("click", () => this.resetViewport()); -- this.elements.renderSurface.addEventListener("mousemove", (event) => this.updateCoordinateDisplay(event)); -- this.elements.renderSurface.addEventListener("pointerdown", (event) => this.handleRenderSurfacePointerDown(event)); -- this.elements.renderSurface.addEventListener("contextmenu", (event) => event.preventDefault()); -- this.elements.renderSurface.addEventListener("dblclick", (event) => this.finishMultiPointDrawing("double-click", event)); -- this.window.addEventListener("pointermove", (event) => this.updatePreviewPointerEdit(event)); -- this.window.addEventListener("pointermove", (event) => this.updateDrawingPreview(event)); -- this.window.addEventListener("pointermove", (event) => this.updateViewportPan(event)); -- this.elements.renderSurface.addEventListener("wheel", (event) => { -- event.preventDefault(); -- this.zoomViewportByStepAtPointer(event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP, event); -- }, { passive: false }); -- this.window.addEventListener("pointerup", (event) => { -- this.isPaintDragging = false; -- this.finishViewportPan(event); -- this.finishPreviewPointerEdit(event); -- }); -- } -- -- bindPreviewEditActions() { -- this.elements.previewUndoButton.addEventListener("click", () => this.undoPreviewEdit()); -- this.elements.previewRedoButton.addEventListener("click", () => this.redoPreviewEdit()); -- this.elements.previewCopyButton.addEventListener("click", () => this.copyPreviewSelection()); -- this.elements.previewPasteButton.addEventListener("click", () => this.pastePreviewClipboard()); -- } -- -- bindObjectFilters() { -- this.elements.tagFilter.addEventListener("change", () => { -- this.renderObjectTiles(); -- this.statusLog.write(`OK Object tag filter set to ${this.elements.tagFilter.value}.`); -- }); -- this.elements.searchFilter.addEventListener("input", () => { -- this.renderObjectTiles(); -- }); -- } -- -- bindKeyboardShortcuts() { -- const handleShortcut = (event) => { -- if (event.defaultPrevented) { -- return; -- } -- if (event.key === "Enter" && this.activeDrawing) { -- event.preventDefault(); -- this.finishMultiPointDrawing("Enter", event); -- return; -- } -- const tagName = event.target?.tagName || ""; -- if (["INPUT", "SELECT", "TEXTAREA"].includes(tagName)) { -- return; -- } -- if (event.key === "Delete" || event.key === "Backspace") { -- event.preventDefault(); -- this.deleteSelectedShape("keyboard"); -- return; -- } -- const key = event.key.toLowerCase(); -- if (key === "v") { -- event.preventDefault(); -- this.activateToolMode("select", "keyboard"); -- } else if (key === "f") { -- event.preventDefault(); -- this.setPaletteTarget("paint"); -- } else if (key === "s") { -- event.preventDefault(); -- this.setPaletteTarget("stroke"); -- } else if (key === "i") { -- event.preventDefault(); -- this.activateToolMode("picker", "keyboard"); -- } else if (key === "x") { -- event.preventDefault(); -- this.swapFillStrokeColors(); -- } else if (key === "d") { -- event.preventDefault(); -- this.restoreDefaultColors(); -- } -- }; -- this.elements.renderSurface.addEventListener("keydown", handleShortcut); -- this.window.document.addEventListener("keydown", handleShortcut); -- } -- -- applySnapState() { -- this.snapMode = this.readSnapMode(); -- this.angleSnapEnabled = this.window.sessionStorage?.getItem(ANGLE_SNAP_SESSION_KEY) === "1"; -- this.angleSnapStep = this.readAngleSnapStep(); -- this.gridRenderEnabled = this.window.sessionStorage?.getItem(GRID_RENDER_SESSION_KEY) !== "0"; -- this.centerOriginVisible = this.window.sessionStorage?.getItem(CENTER_ORIGIN_SESSION_KEY) !== "0"; -- const snapDetails = SNAP_MODE_DETAILS[this.snapMode] || SNAP_MODE_DETAILS.grid; -- this.elements.snapModeButton.textContent = snapDetails.label; -- this.elements.snapModeButton.dataset.snapMode = this.snapMode; -- this.elements.snapModeButton.setAttribute("aria-label", snapDetails.label); -- this.elements.snapModeButton.setAttribute("aria-pressed", String(this.snapMode !== "none")); -- this.elements.snapModeButton.title = snapDetails.title; -- this.applyIconGlyph(this.elements.snapModeButton, snapDetails.iconKey); -- this.elements.renderSurface.classList.toggle("is-snap-point-mode", this.snapMode === "point"); -- this.elements.angleSnapButton.setAttribute("aria-pressed", String(this.angleSnapEnabled)); -- this.elements.angleSnapButton.textContent = "Snap Angle"; -- this.elements.angleSnapButton.setAttribute("aria-label", "Snap Angle for Shape/Object Rotate"); -- this.elements.angleSnapButton.title = "Snap Angle switches Rotate to a constrained dropdown using the selected 15, 30, 45, or 90 degree step."; -- this.elements.gridRenderButton.setAttribute("aria-pressed", String(this.gridRenderEnabled)); -- this.applyIconGlyph(this.elements.gridRenderButton, this.gridRenderEnabled ? "grid" : "gridOff"); -- this.elements.centerDotButton.setAttribute("aria-pressed", String(this.centerOriginVisible)); -- this.elements.renderSurface.classList.toggle("is-grid-visible", this.gridRenderEnabled); -- this.updateRotateSnapControls(); -- } -- -- updateRotateSnapControls(scope = this.window.document) { -- const root = scope || this.window.document; -- [ -- { -- inputId: "objectVectorStudioV2RotateInput", -- selectId: "objectVectorStudioV2RotateSnapSelect", -- stepId: "objectVectorStudioV2SnapAngleStepSelect" -- }, -- { -- inputId: "objectVectorStudioV2ObjectRotateInput", -- selectId: "objectVectorStudioV2ObjectRotateSnapSelect", -- stepId: "objectVectorStudioV2ObjectSnapAngleStepSelect" -- } -- ].forEach(({ inputId, selectId, stepId }) => { -- const numericInput = root.querySelector?.(`#${inputId}`) || this.window.document.getElementById(inputId); -- const snapSelect = root.querySelector?.(`#${selectId}`) || this.window.document.getElementById(selectId); -- const stepSelect = root.querySelector?.(`#${stepId}`) || this.window.document.getElementById(stepId); -- if (!numericInput && !snapSelect && !stepSelect) { -- return; -- } -- const row = (numericInput || snapSelect || stepSelect)?.closest?.(".object-vector-studio-v2__transform-control-row--rotate") || null; -- const panelDisabled = row?.closest?.(".object-vector-studio-v2__shape-panel")?.classList.contains("is-disabled") === true; -- row?.classList.toggle("is-angle-snap-enabled", this.angleSnapEnabled); -- if (stepSelect) { -- stepSelect.value = String(this.angleSnapStep); -- stepSelect.disabled = panelDisabled || !this.angleSnapEnabled; -- stepSelect.hidden = !this.angleSnapEnabled; -- const stepField = stepSelect.closest(".object-vector-studio-v2__snap-angle-step-field"); -- if (stepField) { -- stepField.hidden = !this.angleSnapEnabled; -- } -- } -- if (snapSelect) { -- const preferred = snapSelect.value || numericInput?.value || this.transformInputValue(snapSelect.id, "15"); -- this.populateRotateSnapSelect(snapSelect, this.angleSnapStep, preferred); -- snapSelect.disabled = panelDisabled || !this.angleSnapEnabled; -- snapSelect.hidden = !this.angleSnapEnabled; -- } -- if (numericInput) { -- numericInput.disabled = panelDisabled || this.angleSnapEnabled; -- numericInput.hidden = this.angleSnapEnabled; -- } -- }); -- } -- -- populateRotateSnapSelect(select, step, preferredValue = "15") { -- if (!select) { -- return; -- } -- const normalizedStep = ANGLE_SNAP_STEPS.includes(Number(step)) ? Number(step) : 15; -- const previous = Number(preferredValue); -- const normalizedPreferred = Number.isFinite(previous) -- ? this.normalizeRotationDegrees(Math.round(previous / normalizedStep) * normalizedStep) -- : normalizedStep; -- select.replaceChildren(); -- for (let value = 0; value < 360; value += normalizedStep) { -- const option = document.createElement("option"); -- option.value = String(value); -- option.textContent = String(value); -- select.append(option); -- } -- select.value = Array.from(select.options).some((option) => option.value === String(normalizedPreferred)) -- ? String(normalizedPreferred) -- : "0"; -- this.transformInputValues.set(select.id, select.value); -- } -- -- readSnapMode() { -- const storedMode = this.window.sessionStorage?.getItem(SNAP_MODE_SESSION_KEY) || "grid"; -- return SNAP_MODES.includes(storedMode) ? storedMode : "grid"; -- } -- -- readAngleSnapStep() { -- const storedStep = Number(this.window.sessionStorage?.getItem(ANGLE_SNAP_STEP_SESSION_KEY) || 15); -- return ANGLE_SNAP_STEPS.includes(storedStep) ? storedStep : 15; -- } -- -- setSnapMode(mode, label = "") { -- if (!SNAP_MODES.includes(mode)) { -- this.statusLog.write(`WARN Snap mode skipped: ${mode || "unknown"} is not supported.`); -- return; -- } -- this.snapMode = mode; -- this.window.sessionStorage?.setItem(SNAP_MODE_SESSION_KEY, mode); -- this.applySnapState(); -- this.renderWorkSurface(); -- this.statusLog.write(`OK ${label || shapeTypeLabel(mode)} mode selected for drawing and point dragging.`); -- } -- -- cycleSnapMode() { -- const currentIndex = SNAP_MODES.indexOf(this.snapMode); -- const nextMode = SNAP_MODES[(currentIndex + 1) % SNAP_MODES.length] || "grid"; -- this.setSnapMode(nextMode, SNAP_MODE_DETAILS[nextMode]?.label); -- } -- -- applyToolDisplayMode(mode, shouldLog) { -- const isCompact = mode === "icons"; -- this.elements.toolToggleGrid.classList.toggle("is-icon-only", isCompact); -- this.elements.toolLabelModeButton.setAttribute("aria-pressed", String(isCompact)); -- this.elements.toolLabelModeButton.textContent = isCompact ? "Words" : "Icons"; -- this.window.sessionStorage?.setItem(TOOL_DISPLAY_MODE_KEY, isCompact ? "icons" : "words"); -- if (shouldLog) { -- this.statusLog.write(`OK Tools display mode set to ${isCompact ? "compact icons" : "words and icons"}.`); -- } -- } -- -- setActiveToolButton(button) { -- this.elements.toolToggles.forEach((candidate) => { -- candidate.classList.toggle("is-active", candidate === button); -- candidate.setAttribute("aria-pressed", String(candidate === button)); -- }); -- } -- -- activateToolMode(tool, sourceLabel) { -- if (!PRIMITIVE_TOOLS.includes(tool)) { -- this.cancelActiveDrawing("tool mode change", { log: false }); -- } -- this.activeTool = tool; -- const button = this.elements.toolToggles.find((candidate) => candidate.dataset.shapeTool === tool) || null; -- this.setActiveToolButton(button); -- if (tool === "paint") { -- this.setPaletteTarget("paint", false); -- } else if (tool === "stroke") { -- this.setPaletteTarget("stroke", false); -- } else { -- this.setPaletteTarget("stroke", false); -- } -- this.statusLog.write(`OK Tools mode selected from ${sourceLabel}: ${tool}.`); -- } -- -- renderEmptyState(message) { -- this.currentPayload = null; -- this.selectedObjectId = ""; -- this.selectedShapeIndex = -1; -- this.selectedShapeIndexes.clear(); -- this.directSelectedShapeIndexes.clear(); -- this.activeDrawing = null; -- this.drawingPreviewPoint = null; -- this.activeDrawing = null; -- this.drawingPreviewPoint = null; -- this.clearPreviewEditState({ clipboard: true }); -- this.selectedStateId = ""; -- this.stateControlStateId = ""; -- this.selectedFrameId = ""; -- this.stopPlaybackTimer(); -- this.updateObjectsHeader(0, 0); -- this.updatePaletteHeader(this.runtimePalette?.swatches?.length || 0); -- this.elements.objectNameInput.value = ""; -- this.elements.objectTagInput.value = ""; -- this.renderObjectTagList(null); -- this.elements.paletteSummary.textContent = this.runtimePalette ? "" : "Palette required before render."; -- this.elements.shapeGeometryDetails.textContent = "No object selected."; -- this.updateShapeGeometryHeader(null); -- this.elements.objectTransform.textContent = "No object selected."; -- this.elements.shapeTransform.textContent = "No shape selected."; -- this.elements.objectPreviewFooter.textContent = "Object ID: none"; -- this.elements.jsonDetails.textContent = "{}"; -- this.renderFrameTimeline(); -- this.updateViewport(); -- this.elements.renderSurface.replaceChildren(); -- this.renderSvgGrid(); -- this.renderCenterOriginMarker(); -- this.elements.objectTiles.replaceChildren(this.createEmptyObjectTile()); -- this.renderDependencyGraph(); -- this.actionNav.setJsonPayloadActionsEnabled(false); -- this.elements.exportSvgButton.disabled = true; -- this.actionNav.setImportEnabled(this.schemaReady && !this.actionNav.isWorkspaceLaunch()); -- this.updateObjectActionState(); -- } -- -- updateObjectsHeader(objectCount, shapeCount) { -- const stateCount = this.objectStates(this.selectedObject()).length; -- this.elements.objectsCount.textContent = `(${objectCount} obj, states ${stateCount}, ${shapeCount} ${shapeCount === 1 ? "shape" : "shapes"})`; -- } -- -- updateShapeGeometryHeader(shape) { -- this.elements.shapeGeometrySummary.textContent = ""; -- if (this.elements.shapeGeometryName) { -- this.elements.shapeGeometryName.textContent = ""; -- } -- } -- -- updatePaletteHeader(swatchCount) { -- this.elements.paletteSwatchCount.textContent = `(${swatchCount} ${swatchCount === 1 ? "swatch" : "swatches"})`; -- } -- -- createEmptyObjectTile() { -- const tile = document.createElement("article"); -- tile.className = "object-vector-studio-v2__object-tile object-vector-studio-v2__object-tile--empty"; -- tile.textContent = "No objects loaded. Import schema-valid Object Vector Studio V2 JSON and provide a runtime palette to show object tiles."; -- return tile; -- } -- -- async importJson(file) { -- if (!file) { -- this.statusLog.write("FAIL Import JSON blocked: choose an Object Vector Studio V2 JSON file."); -- return; -- } -- -- let payload; -- try { -- payload = JSON.parse(await readFileText(file)); -- } catch (error) { -- this.statusLog.write(`FAIL Import JSON blocked: ${file.name || "selected file"} is invalid JSON: ${error.message}`); -- return; -- } -- -- this.loadPayload(payload, `import:${file.name || "selected file"}`); -- } -- -- loadWorkspaceToolState() { -- const storedPayload = this.window.sessionStorage?.getItem(WORKSPACE_TOOL_SESSION_KEY) || ""; -- if (!storedPayload) { -- const message = `Workspace toolState loading blocked: ${WORKSPACE_TOOL_SESSION_KEY} is missing. A valid Object Vector Studio V2 payload is required.`; -- this.renderEmptyState(message); -- this.statusLog.write(`FAIL ${message}`); -- return; -- } -- -- let payload; -- try { -- const session = JSON.parse(storedPayload); -- payload = session?.data && typeof session.data === "object" ? session.data : session; -- } catch (error) { -- const message = `Workspace toolState loading blocked: ${WORKSPACE_TOOL_SESSION_KEY} is invalid JSON: ${error.message}`; -- this.renderEmptyState(message); -- this.statusLog.write(`FAIL ${message}`); -- return; -- } -- -- this.loadPayload(payload, WORKSPACE_TOOL_SESSION_KEY); -- } -- -- loadPayload(payload, sourceLabel) { -- const validation = this.schemaService.validatePayload(payload); -- if (!validation.ok) { -- const message = `Object Vector Studio V2 schema validation failed from ${sourceLabel}: ${validation.errors.join(" ")}`; -- this.renderEmptyState(message); -- this.statusLog.write(`FAIL ${message}`); -- return; -- } -- -- this.currentPayload = validation.payload; -- this.applyLoadedRuntimeState(sourceLabel); -- this.actionNav.setJsonPayloadActionsEnabled(true); -- this.actionNav.setImportEnabled(!this.actionNav.isWorkspaceLaunch()); -- this.renderPayload(); -- this.statusLog.write(`OK Loaded Object Vector Studio V2 schema payload from ${sourceLabel}: ${this.currentPayload.objects.length} objects.`); -- } -- -- applyLoadedRuntimeState(sourceLabel) { -- const selectedObject = this.currentPayload.objects[0] || null; -- this.selectedObjectId = selectedObject?.id || ""; -- const selectedShape = sortedShapes(selectedObject)[0] || null; -- this.selectedShapeIndex = selectedShape ? 0 : -1; -- this.selectedShapeIndexes = new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : []); -- this.directSelectedShapeIndexes = new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : []); -- const selectedState = this.objectStates(selectedObject)[0] || null; -- this.selectedStateId = selectedState?.id || ""; -- this.stateControlStateId = this.selectedStateId || OBJECT_STATE_IDS[0]; -- this.selectedFrameId = selectedState ? sortedFrames(selectedState)[0]?.id || "" : ""; -- this.hiddenObjectIds.clear(); -- this.lockedObjectIds.clear(); -- this.clearPreviewEditState({ clipboard: true }); -- this.activeDrawing = null; -- this.drawingPreviewPoint = null; -- this.viewport = { ...DEFAULT_VIEWPORT }; -- this.updateViewport(); -- if (sourceLabel) { -- this.statusLog.write(`OK Runtime selection initialized from ${sourceLabel}: ${this.selectedObjectId || "no object selected"}.`); -- } -- } -- -- renderPayload(options = {}) { -- if (!this.currentPayload) { -- return; -- } -- -- const validation = this.schemaService.validatePayload(this.currentPayload); -- if (!validation.ok) { -- const message = `Render blocked by Object Vector Studio V2 schema validation: ${validation.errors.join(" ")}`; -- this.renderEmptyState(message); -- this.statusLog.write(`FAIL ${message}`); -- return; -- } -- -- this.currentPayload = validation.payload; -- if (!this.runtimePalette) { -- this.loadPaletteFromSessionKey(RUNTIME_PALETTE_SESSION_KEY) || this.loadPaletteFromWorkspaceSession(); -- } -- const paletteValidation = this.schemaService.validateRuntimePalette(this.runtimePalette, "runtime palette"); -- if (!paletteValidation.ok) { -- const message = `Render blocked: runtime palette is required separately from Object Vector Studio V2 JSON. ${paletteValidation.errors.join(" ")}`; -- this.renderPaletteRequiredState(message); -- this.statusLog.write(`FAIL ${message}`); -- return; -- } -- this.runtimePalette = paletteValidation.palette; -- const preserveEmptyShapeSelection = options.preserveEmptyShapeSelection === true -- && this.selectedObject() -- && this.selectedShapeIndex < 0 -- && !this.selectedShapeIndexes.size -- && !this.directSelectedShapeIndexes.size; -- if (!preserveEmptyShapeSelection) { -- this.ensureSelectedShape(); -- } -- this.ensureSelectedFrame(); -- if (options.syncPaletteSelection !== false) { -- this.syncPaletteSelectionFromCurrentShape({ render: false }); -- } -- if (this.runtimePalette) { -- this.renderPalette(); -- } -- this.renderTagFilter(); -- this.renderObjectTiles(); -- this.renderDependencyGraph(); -- this.renderSelectedObject(); -- this.renderWorkSurface(); -- this.updateObjectActionState(); -- } -- -- renderPaletteRequiredState(message) { -- this.selectedObjectId = ""; -- this.selectedShapeIndex = -1; -- this.selectedShapeIndexes.clear(); -- this.directSelectedShapeIndexes.clear(); -- this.selectedStateId = ""; -- this.stateControlStateId = ""; -- this.selectedFrameId = ""; -- this.stopPlaybackTimer(); -- this.updateObjectsHeader(0, 0); -- this.updatePaletteHeader(0); -- this.elements.paletteSummary.textContent = message; -- this.elements.objectNameInput.value = ""; -- this.elements.objectTagInput.value = ""; -- this.renderObjectTagList(null); -- this.elements.shapeGeometryDetails.textContent = "Runtime palette required before object render."; -- this.updateShapeGeometryHeader(null); -- this.elements.objectTransform.textContent = "Runtime palette required before object transform."; -- this.elements.shapeTransform.textContent = "Runtime palette required before shape transform."; -- this.elements.objectPreviewFooter.textContent = "Object ID: none"; -- this.elements.jsonDetails.textContent = "{}"; -- this.renderFrameTimeline(); -- this.elements.renderSurface.replaceChildren(); -- this.renderSvgGrid(); -- this.renderCenterOriginMarker(); -- const tile = this.createEmptyObjectTile(); -- tile.textContent = "Runtime palette required before rendering object tiles."; -- this.elements.objectTiles.replaceChildren(tile); -- this.renderDependencyGraph(); -- this.actionNav.setJsonPayloadActionsEnabled(Boolean(this.currentPayload)); -- this.elements.exportSvgButton.disabled = true; -- this.actionNav.setImportEnabled(this.schemaReady && !this.actionNav.isWorkspaceLaunch()); -- this.updateObjectActionState(); -- } -- -- renderPalette() { -- const swatchCount = this.runtimePalette.swatches.length; -- this.updatePaletteModeSwatches(); -- this.updatePaletteHeader(swatchCount); -- this.elements.paletteSummary.replaceChildren(); -- this.sortedPaletteSwatches().forEach((swatch, index) => { -- const label = swatch?.name || swatch?.id || swatch?.symbol || `swatch-${index + 1}`; -- const color = swatchColor(swatch); -- const item = document.createElement("button"); -- item.className = "object-vector-studio-v2__palette-swatch"; -- item.type = "button"; -- item.dataset.paletteColor = color; -- item.dataset.paletteLabel = label; -- item.dataset.paletteTags = tagList(swatch?.tags).join(", "); -- item.dataset.paletteDetails = `${label}\n${color || "value unavailable"}`; -- item.title = item.dataset.paletteDetails; -- item.setAttribute("aria-label", `Palette swatch ${label} ${color || "value unavailable"}`); -- item.classList.toggle("is-selected", color && color === this.currentTargetColor()); -- if (color) { -- item.style.setProperty("--object-vector-studio-v2-swatch", color); -- } -- item.addEventListener("click", () => this.selectPaletteColor(color, label)); -- this.elements.paletteSummary.append(item); -- }); -- } -- -- sortedPaletteSwatches() { -- const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); -- return [...this.runtimePalette.swatches].sort((left, right) => { -- const leftLabel = left?.name || left?.id || left?.symbol || ""; -- const rightLabel = right?.name || right?.id || right?.symbol || ""; -- let result = 0; -- if (this.paletteSortMode === "name") { -- result = collator.compare(leftLabel, rightLabel); -- } else if (this.paletteSortMode === "tag") { -- const leftTags = tagList(left?.tags).join(", "); -- const rightTags = tagList(right?.tags).join(", "); -- if (leftTags && !rightTags) { -- return -1; -- } -- if (!leftTags && rightTags) { -- return 1; -- } -- result = collator.compare(leftTags, rightTags) || collator.compare(leftLabel, rightLabel); -- } else { -- const leftMetrics = colorMetrics(swatchColor(left)); -- const rightMetrics = colorMetrics(swatchColor(right)); -- result = (leftMetrics[this.paletteSortMode] - rightMetrics[this.paletteSortMode]) || collator.compare(leftLabel, rightLabel); -- } -- return this.paletteSortDirection === "desc" ? -result : result; -- }); -- } -- -- paletteDisplayName() { -- return this.runtimePalette?.id || this.runtimePalette?.name || this.runtimePaletteSource || "runtime palette"; -- } -- -- renderObjectTiles() { -- const previousScrollTop = this.elements.objectsContent.scrollTop; -- this.updateObjectsHeader(this.currentPayload.objects.length, this.selectedObject()?.shapes.length || 0); -- this.elements.objectTiles.replaceChildren(); -- if (!this.currentPayload.objects.length) { -- this.elements.objectTiles.append(this.createEmptyObjectTile()); -- this.restoreObjectsScroll(previousScrollTop); -- return; -- } -- -- const filteredObjects = this.filteredObjects(); -- if (!filteredObjects.length) { -- const tile = this.createEmptyObjectTile(); -- tile.textContent = "No objects match the current tag or search filter."; -- this.elements.objectTiles.append(tile); -- this.restoreObjectsScroll(previousScrollTop); -- return; -- } -- -- filteredObjects.forEach((object) => { -- const tile = document.createElement("article"); -- tile.className = "object-vector-studio-v2__object-tile"; -- tile.dataset.objectId = object.id; -- tile.classList.toggle("is-selected", object.id === this.selectedObjectId); -- tile.classList.toggle("is-hidden", this.isObjectHidden(object.id)); -- tile.classList.toggle("is-locked", this.isObjectLocked(object.id)); -- tile.setAttribute("aria-pressed", String(object.id === this.selectedObjectId)); -- -- const selectButton = document.createElement("button"); -- selectButton.className = "object-vector-studio-v2__object-select"; -- selectButton.type = "button"; -- selectButton.title = `Select object ${object.name}`; -- selectButton.setAttribute("aria-pressed", String(object.id === this.selectedObjectId)); -- -- const thumbnail = this.createObjectThumbnail(object); -- const name = document.createElement("span"); -- name.className = "object-vector-studio-v2__object-tile-name"; -- name.textContent = object.name; -- -- const meta = document.createElement("span"); -- meta.className = "object-vector-studio-v2__object-tile-meta"; -- const tags = tagList(object.tags); -- const inheritedText = object.baseObjectId ? ` - inherits ${object.baseObjectId}` : ""; -- const gameSegment = objectGameSegment(object.id) || this.payloadGameKey(); -- meta.textContent = `object > ${gameSegment} > ${object.name} | ${shapeCountLabel(object.shapes.length)}${tags.length ? ` | ${tags.join(", ")}` : ""}${inheritedText}`; -- -- const copy = document.createElement("span"); -- copy.className = "object-vector-studio-v2__object-tile-copy"; -- copy.append(name, meta); -- -- selectButton.append(thumbnail, copy); -- selectButton.addEventListener("click", () => { -- this.selectObject(object.id, "tile"); -- }); -- -- const controls = document.createElement("div"); -- controls.className = "object-vector-studio-v2__object-tile-controls"; -- controls.append( -- this.createObjectTileControl(object, "visibility"), -- this.createObjectTileControl(object, "lock"), -- this.createObjectTileControl(object, "delete") -- ); -- tile.append(selectButton, controls); -- tile.addEventListener("click", () => { -- this.selectObject(object.id, "tile"); -- }); -- if (object.id === this.selectedObjectId) { -- tile.append(this.createObjectStatePanel(object), this.createObjectTileShapeList(object)); -- } -- this.elements.objectTiles.append(tile); -- }); -- this.restoreObjectsScroll(previousScrollTop); -- } -- -- renderShapeTiles(object = this.selectedObject()) { -- void object; -- } -- -- restoreObjectsScroll(scrollTop) { -- this.restoreElementScrollTop(this.elements.objectsContent, scrollTop); -- } -- -- captureLeftPanelScrollState() { -- return { -- leftPanelScrollTop: this.elements.leftPanel.scrollTop, -- objectsScrollTop: this.elements.objectsContent.scrollTop -- }; -- } -- -- restoreLeftPanelScrollState(scrollState) { -- this.restoreElementScrollTop(this.elements.leftPanel, scrollState.leftPanelScrollTop); -- this.restoreElementScrollTop(this.elements.objectsContent, scrollState.objectsScrollTop); -- } -- -- restoreElementScrollTop(element, scrollTop) { -- const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight); -- element.scrollTop = Math.min(scrollTop, maxScrollTop); -- } -- -- createObjectTileControl(object, kind) { -- const button = document.createElement("button"); -- button.className = "object-vector-studio-v2__object-tile-control"; -- button.type = "button"; -- const isVisibility = kind === "visibility"; -- const isDelete = kind === "delete"; -- const isLocked = this.isObjectLocked(object.id); -- const isActive = isVisibility ? !this.isObjectHidden(object.id) : !isDelete && isLocked; -- button.dataset.objectControl = kind; -- button.dataset.objectControlId = object.id; -- if (isDelete) { -- button.classList.add("object-vector-studio-v2__object-tile-control--delete"); -- button.setAttribute("aria-label", `Delete object ${object.name}`); -- button.append(this.createIconSpan("delete", true)); -- button.title = isLocked ? "Unlock object before deleting" : "Delete this object"; -- button.disabled = isLocked; -- } else { -- button.setAttribute("aria-pressed", String(isActive)); -- button.setAttribute("aria-label", isVisibility ? `${isActive ? "Hide" : "Show"} object ${object.name}` : `${isActive ? "Unlock" : "Lock"} object ${object.name}`); -- button.append(this.createIconSpan(isVisibility ? "eye" : "lock", isActive)); -- button.title = isVisibility ? "Toggle object visibility" : "Toggle runtime object lock"; -- } -- button.addEventListener("click", (event) => { -- event.stopPropagation(); -- if (isVisibility) { -- this.toggleObjectVisibility(object.id); -- } else if (isDelete) { -- this.deleteObjectById(object.id, "object tile delete"); -- } else { -- this.toggleObjectLock(object.id); -- } -- }); -- return button; -- } -- -- createIconSpan(kind, isActive = true) { -- const icon = document.createElement("span"); -- icon.className = `object-vector-studio-v2__tile-icon object-vector-studio-v2__tile-icon--${kind}`; -- icon.classList.toggle("is-off", !isActive); -- icon.setAttribute("aria-hidden", "true"); -- const iconKey = kind === "eye" && !isActive ? "eyeOff" : kind === "lock" && !isActive ? "unlock" : kind; -- this.applyIconGlyph(icon, iconKey); -- return icon; -- } -- -- createObjectStatePanel(object) { -- const panel = document.createElement("div"); -- panel.className = "object-vector-studio-v2__object-state-panel"; -- panel.addEventListener("click", (event) => event.stopPropagation()); -- -- const controls = document.createElement("div"); -- controls.className = "object-vector-studio-v2__object-state-controls"; -- controls.setAttribute("aria-label", "Object state controls"); -- -- const selectedStateId = this.stateControlSelectionId(object); -- const stateExists = this.objectStates(object).some((state) => state.id === selectedStateId); -- const isLocked = this.isObjectLocked(object.id); -- -- const addButton = document.createElement("button"); -- addButton.type = "button"; -- addButton.dataset.objectStateAction = "add"; -- addButton.textContent = "Add"; -- addButton.title = stateExists ? "Selected state already exists." : `Add ${selectedStateId} state`; -- addButton.disabled = isLocked || stateExists || !OBJECT_STATE_IDS.includes(selectedStateId); -- addButton.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.createSelectedState(selectedStateId); -- }); -- -- const select = document.createElement("select"); -- select.dataset.objectStateSelect = object.id; -- select.setAttribute("aria-label", "State"); -- OBJECT_STATE_IDS.forEach((stateId) => { -- const option = document.createElement("option"); -- option.value = stateId; -- option.textContent = stateId; -- select.append(option); -- }); -- select.value = selectedStateId; -- select.addEventListener("change", (event) => { -- event.stopPropagation(); -- this.stateControlStateId = select.value; -- const nextState = this.objectStates(this.selectedObject()).find((state) => state.id === select.value); -- if (nextState) { -- this.selectState(nextState.id, "state dropdown"); -- return; -- } -- this.renderPayload(); -- this.statusLog.write(`INFO State ${select.value} is ready to add for ${object.name}.`); -- }); -- -- const deleteButton = document.createElement("button"); -- deleteButton.type = "button"; -- deleteButton.dataset.objectStateAction = "delete"; -- deleteButton.textContent = "Delete"; -- deleteButton.title = stateExists ? `Delete ${selectedStateId} state` : "Selected state has not been added."; -- deleteButton.disabled = isLocked || !stateExists || this.objectStates(object).length <= 1; -- deleteButton.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.deleteSelectedState(selectedStateId); -- }); -- -- const helpButton = document.createElement("button"); -- helpButton.className = "object-vector-studio-v2__object-state-help"; -- helpButton.type = "button"; -- helpButton.textContent = "?"; -- const allHelpText = this.allStateHelpText(); -- helpButton.title = allHelpText; -- helpButton.dataset.objectStateHelp = "all"; -- helpButton.setAttribute("aria-label", `State help for all states: ${allHelpText.replace(/\s+/gu, " ")}`); -- -- controls.append(select, addButton, deleteButton, helpButton); -- panel.append(controls, this.createObjectStateTileList(object)); -- return panel; -- } -- -- createObjectStateTileList(object) { -- const states = this.objectStates(object); -- const list = document.createElement("div"); -- list.className = "object-vector-studio-v2__object-state-tiles"; -- list.setAttribute("aria-label", "Object states"); -- if (!states.length) { -- const empty = document.createElement("span"); -- empty.className = "object-vector-studio-v2__shape-list-empty"; -- empty.textContent = "No states."; -- list.append(empty); -- return list; -- } -- -- states.forEach((state) => { -- const button = document.createElement("button"); -- button.type = "button"; -- button.dataset.objectStateTile = state.id; -- button.textContent = state.id; -- button.title = `Select ${state.id} state`; -- button.setAttribute("aria-pressed", String(state.id === this.selectedStateId)); -- button.classList.toggle("is-selected", state.id === this.selectedStateId); -- button.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.selectState(state.id, "state tile"); -- }); -- list.append(button); -- }); -- return list; -- } -- -- stateControlSelectionId(object) { -- if (OBJECT_STATE_IDS.includes(this.stateControlStateId)) { -- return this.stateControlStateId; -- } -- if (OBJECT_STATE_IDS.includes(this.selectedStateId)) { -- return this.selectedStateId; -- } -- const firstState = this.objectStates(object).find((state) => OBJECT_STATE_IDS.includes(state.id)); -- return firstState?.id || OBJECT_STATE_IDS[0]; -- } -- -- createObjectTileShapeList(object) { -- const list = document.createElement("div"); -- list.className = "object-vector-studio-v2__object-tile-shapes"; -- const displayedShapes = sortedShapes(object) -- .map((shape, shapeIndex) => ({ shape, shapeIndex })) -- .reverse(); -- displayedShapes.forEach(({ shape, shapeIndex }) => { -- const displayShape = object.id === this.selectedObjectId ? this.effectiveShape(shape, shapeIndex) : shape; -- const row = document.createElement("div"); -- row.className = "object-vector-studio-v2__object-tile-shape-row"; -- row.classList.toggle("is-selected", this.selectedShapeIndexes.has(shapeIndex)); -- const selectButton = document.createElement("button"); -- selectButton.type = "button"; -- selectButton.className = "object-vector-studio-v2__shape-select"; -- selectButton.dataset.objectTileShapeIndex = String(shapeIndex); -- selectButton.setAttribute("aria-pressed", String(this.selectedShapeIndexes.has(shapeIndex))); -- const label = document.createElement("span"); -- label.className = "object-vector-studio-v2__shape-select-label"; -- label.textContent = `${shapeIndex}. ${shapeDisplayLabel(shape)}`; -- selectButton.append(label); -- selectButton.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.selectShape(shapeIndex, "object tile shape list", { additive: event.shiftKey || event.ctrlKey || event.metaKey }); -- }); -- const actions = document.createElement("div"); -- actions.className = "object-vector-studio-v2__shape-inline-actions"; -- const groupButton = this.createShapeGroupButton(shape, shapeIndex); -- const visibilityButton = document.createElement("button"); -- visibilityButton.type = "button"; -- visibilityButton.className = "object-vector-studio-v2__shape-inline-button object-vector-studio-v2__shape-eye-inline"; -- visibilityButton.dataset.shapeVisibilityIndex = String(shapeIndex); -- visibilityButton.setAttribute("aria-label", `${displayShape.visible ? "Hide" : "Show"} shape ${shapeDisplayLabel(shape)} in the selected state/frame`); -- visibilityButton.title = "Toggle state/frame shape visibility"; -- visibilityButton.append(this.createIconSpan("eye", displayShape.visible)); -- visibilityButton.addEventListener("pointerdown", (event) => { -- event.stopPropagation(); -- }); -- visibilityButton.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.toggleShapeVisibilityByIndex(shapeIndex, object.id); -- }); -- const deleteButton = document.createElement("button"); -- deleteButton.type = "button"; -- deleteButton.className = "object-vector-studio-v2__shape-inline-button object-vector-studio-v2__shape-delete-inline"; -- deleteButton.dataset.shapeDeleteIndex = String(shapeIndex); -- deleteButton.dataset.shapeDeleteObjectId = object.id; -- deleteButton.setAttribute("aria-label", `Delete shape ${shapeDisplayLabel(shape)}`); -- deleteButton.title = "Delete this base shape from every state"; -- deleteButton.append(this.createIconSpan("delete", true)); -- deleteButton.addEventListener("pointerdown", (event) => { -- event.stopPropagation(); -- }); -- deleteButton.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.deleteShapeByIndex(shapeIndex, "object tile shape delete", object.id); -- }); -- if (groupButton) { -- actions.append(groupButton); -- } -- actions.append(visibilityButton, deleteButton); -- row.append(selectButton, actions); -- list.append(row); -- }); -- if (!list.children.length) { -- const empty = document.createElement("div"); -- empty.className = "object-vector-studio-v2__shape-list-empty"; -- empty.textContent = "No shapes."; -- list.append(empty); -- } -- list.append(this.createSelectedShapeActions(object)); -- return list; -- } -- -- createShapeGroupButton(shape, shapeIndex) { -- const groupId = String(shape.groupId || "").trim(); -- if (!groupId) { -- return null; -- } -- const button = document.createElement("button"); -- button.type = "button"; -- button.className = "object-vector-studio-v2__shape-inline-button object-vector-studio-v2__shape-group-button"; -- button.dataset.shapeGroupId = groupId; -- button.setAttribute("aria-label", `Shape group ${groupId}`); -- button.title = `Shape group ${groupId}`; -- button.addEventListener("pointerdown", (event) => { -- event.stopPropagation(); -- }); -- button.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.selectShapeGroup(groupId, shapeIndex, "shape group icon"); -- }); -- button.append(this.createGroupIndicator(groupId)); -- return button; -- } -- -- createGroupIndicator(groupId) { -- const icon = document.createElement("span"); -- icon.className = "object-vector-studio-v2__shape-group-indicator object-vector-studio-v2__tile-icon object-vector-studio-v2__tile-icon--group"; -- icon.style.setProperty("--object-vector-studio-v2-shape-group-color", this.groupColor(groupId)); -- this.applyIconGlyph(icon, "group"); -- return icon; -- } -- -- createSelectedShapeActions(object) { -- const actions = document.createElement("div"); -- actions.className = "object-vector-studio-v2__shape-list-actions object-vector-studio-v2__z-order-actions is-icon-only"; -- actions.setAttribute("aria-label", "Selected shape move and group controls"); -- const shape = this.selectedShape(); -- const isLocked = this.isObjectLocked(object.id); -- const noShapeReason = "Disabled until a schema-valid shape is selected."; -- [ -- { action: "back", iconClass: "send-back", iconKey: "sendBack", label: "Send To Back", title: "Send selected shape to the back." }, -- { action: "backward", iconClass: "send-backward", iconKey: "sendBackward", label: "Move Backward", title: "Move selected shape backward one row." }, -- { action: "forward", iconClass: "bring-forward", iconKey: "bringForward", label: "Move Forward", title: "Move selected shape forward one row." }, -- { action: "front", iconClass: "bring-front", iconKey: "bringFront", label: "Bring To Front", title: "Bring selected shape to the front." } -- ].forEach((definition) => { -- const button = this.createShapeActionButton(definition.label, definition.iconClass, definition.iconKey, definition.title, () => this.changeSelectedShapeOrder(definition.action)); -- this.setControlDisabled(button, !shape || isLocked, isLocked ? `Disabled because ${object.name} is locked for this runtime session.` : noShapeReason, definition.title); -- actions.append(button); -- }); -- -- const groupButton = this.createShapeActionButton("Group", "group", "group", "Group selected shapes. Shift-click shapes to select more than one.", () => this.groupSelectedShapes()); -- this.setControlDisabled( -- groupButton, -- this.selectedShapeIndexes.size < 2 || isLocked, -- isLocked ? `Disabled because ${object.name} is locked for this runtime session.` : "Disabled until two or more shapes are selected. Shift-click shapes in the preview or object shape list to build a group selection.", -- "Group selected shapes. Shift-click shapes to select more than one." -- ); -- actions.append(groupButton); -- -- const ungroupButton = this.createShapeActionButton("Ungroup", "ungroup", "ungroup", "Ungroup selected shapes.", () => this.ungroupSelectedShapes()); -- const hasValidGroup = this.shapeBelongsToValidGroup(object, this.selectedShapeIndex); -- this.setControlDisabled( -- ungroupButton, -- !shape || isLocked || !hasValidGroup, -- isLocked ? `Disabled because ${object.name} is locked for this runtime session.` : "Disabled until the selected shape belongs to a group with at least two shapes.", -- "Ungroup selected shapes." -- ); -- actions.append(ungroupButton); -- return actions; -- } -- -- createShapeActionButton(labelText, iconClass, iconKey, title, onClick) { -- const button = document.createElement("button"); -- button.className = "object-vector-studio-v2__z-action"; -- button.type = "button"; -- button.dataset.shapeListAction = labelText.toLowerCase().replace(/\s+/gu, "-"); -- button.setAttribute("aria-label", labelText); -- button.title = title; -- const icon = document.createElement("span"); -- icon.className = `object-vector-studio-v2__z-icon object-vector-studio-v2__z-icon--${iconClass}`; -- icon.setAttribute("aria-hidden", "true"); -- this.applyIconGlyph(icon, iconKey); -- const label = document.createElement("span"); -- label.className = "object-vector-studio-v2__z-label"; -- label.textContent = labelText; -- button.append(icon, label); -- button.addEventListener("click", (event) => { -- event.stopPropagation(); -- onClick(); -- }); -- return button; -- } -- -- isObjectHidden(objectId) { -- return this.hiddenObjectIds.has(objectId); -- } -- -- isObjectLocked(objectId) { -- return this.lockedObjectIds.has(objectId); -- } -- -- toggleObjectVisibility(objectId) { -- const object = this.currentPayload?.objects.find((candidate) => candidate.id === objectId); -- if (!object) { -- this.statusLog.write(`WARN Object visibility skipped: object id ${objectId || "unknown"} is not available.`); -- return; -- } -- if (this.hiddenObjectIds.has(objectId)) { -- this.hiddenObjectIds.delete(objectId); -- } else { -- this.hiddenObjectIds.add(objectId); -- } -- this.renderPayload(); -- this.statusLog.write(`OK Object ${object.name} visibility set to ${this.isObjectHidden(objectId) ? "hidden" : "visible"} for this runtime session.`); -- } -- -- toggleObjectLock(objectId) { -- const object = this.currentPayload?.objects.find((candidate) => candidate.id === objectId); -- if (!object) { -- this.statusLog.write(`WARN Object lock skipped: object id ${objectId || "unknown"} is not available.`); -- return; -- } -- if (this.lockedObjectIds.has(objectId)) { -- this.lockedObjectIds.delete(objectId); -- } else { -- this.lockedObjectIds.add(objectId); -- } -- this.renderPayload(); -- this.statusLog.write(`OK Object ${object.name} lock set to ${this.isObjectLocked(objectId) ? "locked" : "unlocked"} for this runtime session.`); -- } -- -- filteredObjects() { -- const tag = this.elements.tagFilter.value || "all"; -- const query = this.elements.searchFilter.value.trim().toLowerCase(); -- return this.currentPayload.objects.filter((object) => { -- const matchesTag = tag === "all" || tagList(object.tags).includes(tag); -- const haystack = `${object.id} ${object.name} ${objectGameSegment(object.id)} ${(object.tags || []).join(" ")} ${object.baseObjectId || ""}`.toLowerCase(); -- return matchesTag && (!query || haystack.includes(query)); -- }); -- } -- -- renderTagFilter() { -- const selectedValue = this.elements.tagFilter.value || "all"; -- this.elements.tagFilter.replaceChildren(); -- const allOption = document.createElement("option"); -- allOption.value = "all"; -- allOption.textContent = "All"; -- this.elements.tagFilter.append(allOption); -- this.availableObjectTags().forEach((tag) => { -- const option = document.createElement("option"); -- option.value = tag; -- option.textContent = tag; -- this.elements.tagFilter.append(option); -- }); -- this.elements.tagFilter.value = Array.from(this.elements.tagFilter.options).some((option) => option.value === selectedValue) ? selectedValue : "all"; -- } -- -- availableObjectTags() { -- return Array.from(new Set((this.currentPayload?.objects || []).flatMap((object) => tagList(object.tags)))).sort(); -- } -- -- renderDependencyGraph() { -- if (!this.currentPayload) { -- this.elements.dependencyGraph.textContent = "No dependency graph loaded."; -- return; -- } -- const usageLines = this.currentPayload.objects.map((object) => `${object.id}: ${object.name}${tagList(object.tags).length ? ` [${tagList(object.tags).join(", ")}]` : ""}`); -- const graphLines = this.currentPayload.objects -- .map((object) => `${object.id}${object.baseObjectId ? ` inherits ${object.baseObjectId}` : " has no base object"}`); -- this.elements.dependencyGraph.textContent = [ -- "Dependency details:", -- graphLines.join("\n") || "No objects loaded.", -- "", -- "Object tags:", -- usageLines.join("\n") || "No object tags." -- ].join("\n"); -- } -- -- createObjectThumbnail(object) { -- const svg = document.createElementNS(SVG_NS, "svg"); -- svg.classList.add("object-vector-studio-v2__object-thumbnail"); -- svg.dataset.objectThumbnail = object.id; -- svg.setAttribute("aria-label", `${object.name} thumbnail`); -- svg.setAttribute("role", "img"); -- -- const bounds = this.objectBounds(object, { includeInvisible: false }); -- const transformOrigin = this.objectTransformOrigin(object); -- const padding = 12; -- svg.setAttribute("viewBox", `${bounds.x - padding} ${bounds.y - padding} ${bounds.width + padding * 2} ${bounds.height + padding * 2}`); -- sortedShapes(object).filter((shape) => shape.visible).forEach((shape, shapeIndex) => { -- try { -- const element = this.createSvgShape(shape, { transformOrigin }); -- element.classList.add("object-vector-studio-v2__object-thumbnail-shape"); -- svg.append(element); -- this.appendShapePointStyleCaps(svg, shape, { transformOrigin }); -- } catch (error) { -- this.statusLog.write(`FAIL Thumbnail render failed for ${object.id}/shape-${shapeIndex} (${shapeTool(shape)}): ${error.message}`); -- } -- }); -- if (!svg.children.length) { -- const empty = document.createElementNS(SVG_NS, "rect"); -- empty.setAttribute("x", bounds.x); -- empty.setAttribute("y", bounds.y); -- empty.setAttribute("width", bounds.width); -- empty.setAttribute("height", bounds.height); -- empty.classList.add("object-vector-studio-v2__object-thumbnail-empty"); -- svg.append(empty); -- } -- return svg; -- } -- -- renderSelectedObject() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.elements.objectNameInput.value = ""; -- this.elements.objectTagInput.value = ""; -- this.renderObjectTagList(null); -- this.updateObjectsHeader(this.currentPayload?.objects.length || 0, 0); -- this.elements.shapeGeometryDetails.textContent = "No object selected."; -- this.elements.objectTransform.textContent = "No object selected."; -- this.elements.shapeTransform.textContent = "No shape selected."; -- this.updateShapeGeometryHeader(null); -- this.elements.objectPreviewFooter.textContent = "Object ID: none"; -- this.elements.jsonDetails.textContent = "{}"; -- this.renderFrameTimeline(); -- return; -- } -- -- const shape = this.selectedShape(); -- this.elements.objectNameInput.value = selected.name; -- this.renderObjectTagList(selected); -- this.updateObjectsHeader(this.currentPayload.objects.length, selected.shapes.length); -- this.elements.shapeGeometryDetails.replaceChildren(this.createShapeGeometryDetails(selected, shape)); -- this.elements.objectTransform.replaceChildren(this.createObjectTransformDetails(selected)); -- this.elements.shapeTransform.replaceChildren(this.createShapeTransformDetails(shape)); -- this.updateShapeGeometryHeader(shape); -- this.elements.objectPreviewFooter.textContent = `Object ID: ${selected.id}`; -- this.updateSelectedObjectJsonDetails(); -- this.renderFrameTimeline(); -- } -- -- updateSelectedObjectJsonDetails() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.elements.jsonDetails.textContent = "{}"; -- return; -- } -- const shape = this.selectedShape(); -- this.elements.jsonDetails.textContent = JSON.stringify({ -- object: selected, -- selectedFrame: this.activeFrame(), -- selectedShape: shape, -- selectedShapeIndexes: Array.from(this.selectedShapeIndexes), -- selectedState: this.selectedState() -- }, null, 2); -- } -- -- updateTransformSummaryText() { -- const objectSummary = this.elements.objectTransform.querySelector(".object-vector-studio-v2__transform-summary"); -- const object = this.selectedObject(); -- if (objectSummary && object) { -- objectSummary.textContent = this.formatObjectTransformSummary(object); -- } -- const summary = this.elements.shapeTransform.querySelector(".object-vector-studio-v2__transform-summary"); -- const shape = this.selectedShape(); -- if (!summary || !shape) { -- return; -- } -- summary.textContent = this.formatTransformSummary(this.shapeTransform(this.effectiveShape(shape))); -- } -- -- updateObjectPreviewFooterFromNameInput() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.elements.objectPreviewFooter.textContent = "Object ID: none"; -- return; -- } -- -- const name = this.elements.objectNameInput.value.trim(); -- if (!name) { -- this.elements.objectPreviewFooter.textContent = `Object ID: ${selected.id}`; -- return; -- } -- -- const siblingObjects = this.currentPayload.objects.filter((object) => object.id !== selected.id); -- const previewId = this.uniqueObjectId(name, siblingObjects, objectGameSegment(selected.id) || this.payloadGameKey()); -- this.elements.objectPreviewFooter.textContent = `Object ID: ${previewId}`; -- } -- -- renderObjectTagList(object) { -- this.elements.objectTagList.replaceChildren(); -- if (!object) { -- const empty = document.createElement("span"); -- empty.className = "object-vector-studio-v2__shape-list-empty"; -- empty.textContent = "No selected object tags."; -- this.elements.objectTagList.append(empty); -- return; -- } -- const tags = tagList(object.tags); -- if (!tags.length) { -- const empty = document.createElement("span"); -- empty.className = "object-vector-studio-v2__shape-list-empty"; -- empty.textContent = "No tags."; -- this.elements.objectTagList.append(empty); -- return; -- } -- tags.forEach((tag) => { -- const button = document.createElement("button"); -- button.type = "button"; -- button.dataset.objectTag = tag; -- button.textContent = tag; -- button.title = `Remove tag ${tag}`; -- button.addEventListener("click", () => this.removeTagFromSelectedObject(tag)); -- this.elements.objectTagList.append(button); -- }); -- } -- -- renderFrameTimeline() { -- this.elements.frameTimeline.replaceChildren(); -- const object = this.selectedObject(); -- const state = this.selectedState(); -- if (!object || !state) { -- const empty = document.createElement("p"); -- empty.className = "tool-starter__hint"; -- empty.textContent = "No state timeline selected."; -- this.elements.frameTimeline.append(empty); -- this.updateAnimationActionState(); -- return; -- } -- -- sortedFrames(state).forEach((frame) => { -- const button = document.createElement("div"); -- button.className = "object-vector-studio-v2__frame-tile"; -- button.dataset.stateId = state.id; -- button.dataset.frameId = frame.id; -- button.title = `Select frame ${frame.id} in state ${state.id}`; -- button.setAttribute("aria-pressed", String(frame.id === this.selectedFrameId)); -- button.setAttribute("role", "button"); -- button.tabIndex = 0; -- button.classList.toggle("is-selected", frame.id === this.selectedFrameId); -- button.append(this.createFrameThumbnail(object, frame)); -- const label = document.createElement("span"); -- label.textContent = `${frame.order}. ${frame.id}`; -- button.append(label); -- button.addEventListener("click", () => this.selectFrame(frame.id, "timeline")); -- button.addEventListener("keydown", (event) => { -- if (event.key !== "Enter" && event.key !== " ") { -- return; -- } -- event.preventDefault(); -- this.selectFrame(frame.id, "timeline"); -- }); -- this.elements.frameTimeline.append(button); -- }); -- this.updateAnimationActionState(); -- } -- -- stateHelpText(stateId) { -- const helpLines = OBJECT_STATE_HELP[stateId] || [`No contextual help is available for state ${stateId || "unknown"}.`]; -- return helpLines.join("\n"); -- } -- -- allStateHelpText() { -- return OBJECT_STATE_IDS -- .map((stateId) => `${stateId}\n${this.stateHelpText(stateId)}`) -- .join("\n\n"); -- } -- -- createFrameThumbnail(object, frame) { -- const svg = document.createElementNS(SVG_NS, "svg"); -- svg.classList.add("object-vector-studio-v2__frame-thumbnail"); -- svg.dataset.frameThumbnail = frame.id; -- const bounds = this.objectBoundsForFrame(object, frame); -- const transformOrigin = this.objectTransformOrigin(object); -- const padding = 12; -- svg.setAttribute("viewBox", `${bounds.x - padding} ${bounds.y - padding} ${bounds.width + padding * 2} ${bounds.height + padding * 2}`); -- sortedShapes(object).forEach((shape, shapeIndex) => { -- const renderShape = this.effectiveShapeForFrame(shape, frame, shapeIndex); -- if (!renderShape.visible) { -- return; -- } -- try { -- svg.append(this.createSvgShape(renderShape, { transformOrigin })); -- this.appendShapePointStyleCaps(svg, renderShape, { transformOrigin }); -- } catch (error) { -- this.statusLog.write(`FAIL Frame thumbnail render failed for ${object.id}/${frame.id}/shape-${shapeIndex}: ${error.message}`); -- } -- }); -- return svg; -- } -- -- createShapeGeometryDetails(object, shape) { -- const wrapper = document.createElement("div"); -- wrapper.className = "object-vector-studio-v2__object-detail-stack"; -- if (!shape) { -- const shapePanel = document.createElement("section"); -- shapePanel.className = "object-vector-studio-v2__shape-panel"; -- const empty = document.createElement("p"); -- empty.textContent = "No shape selected. Create a primitive from Tools."; -- shapePanel.append(empty); -- wrapper.append(shapePanel); -- return wrapper; -- } -- -- wrapper.append(this.createShapeGeometryControls(shape), this.createSelectedShapeSummary(shape)); -- if (!this.canUseShapeGeometry()) { -- this.setGeometryPanelDisabled(wrapper, "Shape Geometry is disabled until exactly one shape is selected."); -- } -- return wrapper; -- } -- -- createSelectedShapeSummary(shape) { -- const shapePanel = document.createElement("section"); -- shapePanel.className = "object-vector-studio-v2__shape-panel"; -- const groupRow = document.createElement("div"); -- groupRow.className = "object-vector-studio-v2__shape-group-summary"; -- const label = document.createElement("span"); -- label.className = "object-vector-studio-v2__detail-label"; -- label.textContent = "Group"; -- const value = document.createElement("span"); -- value.className = "object-vector-studio-v2__detail-value object-vector-studio-v2__shape-group-summary-value"; -- if (shape.groupId) { -- value.append(this.createGroupIndicator(shape.groupId), document.createTextNode(shape.groupId)); -- } else { -- value.textContent = "None"; -- } -- groupRow.append(label, value); -- shapePanel.append(groupRow); -- return shapePanel; -- } -- -- shapeSummaryTypeLabel(shape) { -- return shapeTool(shape); -- } -- -- isTriangleShape(shape) { -- return this.shapeSummaryTypeLabel(shape) === "triangle"; -- } -- -- isEditablePolygonShape(shape) { -- const geometryTool = shapeGeometryTool(shape); -- return (geometryTool === "polygon" && !this.isTriangleShape(shape)) || geometryTool === "polyline"; -- } -- -- shapeGeometryHeadingLabel(shape) { -- return shapeTypeLabel(shape); -- } -- -- createObjectTransformDetails(object) { -- const wrapper = document.createElement("div"); -- wrapper.className = "object-vector-studio-v2__object-detail-stack"; -- if (!object) { -- const empty = document.createElement("p"); -- empty.textContent = "No object selected."; -- wrapper.append(empty); -- return wrapper; -- } -- -- const transformPanel = document.createElement("section"); -- transformPanel.className = "object-vector-studio-v2__shape-panel"; -- const summary = document.createElement("p"); -- summary.className = "object-vector-studio-v2__transform-summary"; -- summary.dataset.transformSummary = "object"; -- summary.textContent = this.formatObjectTransformSummary(object); -- transformPanel.append(this.createObjectTransformControls(object), summary); -- wrapper.append(transformPanel); -- return wrapper; -- } -- -- createShapeTransformDetails(shape) { -- const wrapper = document.createElement("div"); -- wrapper.className = "object-vector-studio-v2__object-detail-stack"; -- if (!shape) { -- const empty = document.createElement("p"); -- empty.textContent = "No shape selected. Create a primitive from Tools."; -- wrapper.append(empty); -- return wrapper; -- } -- -- const transformPanel = document.createElement("section"); -- transformPanel.className = "object-vector-studio-v2__shape-panel"; -- const transform = this.shapeTransform(this.effectiveShape(shape)); -- const summary = document.createElement("p"); -- summary.className = "object-vector-studio-v2__transform-summary"; -- summary.dataset.transformSummary = "shape"; -- summary.textContent = this.formatTransformSummary(transform); -- transformPanel.append(this.createShapeTransformControls(shape), summary); -- if (!this.canUseShapeTransform()) { -- this.setTransformPanelDisabled(transformPanel, "Shape Transform is disabled until exactly one shape is selected."); -- } -- wrapper.append(transformPanel); -- return wrapper; -- } -- -- canUseShapeTransform() { -- return this.selectedShapeIndex >= 0 && this.selectedShapeIndexes.size === 1; -- } -- -- canUseShapeGeometry() { -- return this.selectedShapeIndex >= 0 && this.selectedShapeIndexes.size === 1; -- } -- -- setGeometryPanelDisabled(panel, reason) { -- panel.classList.add("is-disabled"); -- panel.querySelectorAll("input, select, button").forEach((control) => { -- control.disabled = true; -- control.dataset.disabledReason = reason; -- control.title = reason; -- }); -- } -- -- setTransformPanelDisabled(panel, reason) { -- panel.classList.add("is-disabled"); -- panel.querySelectorAll("input, select, button").forEach((control) => { -- control.disabled = true; -- control.dataset.disabledReason = reason; -- control.title = reason; -- }); -- } -- -- formatTransformSummary(transform) { -- const rotation = this.normalizeRotationDegrees(transform.rotation); -- const scaleText = transform.scaleX === transform.scaleY -- ? String(transform.scaleX) -- : `${transform.scaleX} x ${transform.scaleY}`; -- return `x ${transform.x}, y ${transform.y}, rot ${rotation}, scale ${scaleText}`; -- } -- -- formatObjectTransformSummary(object) { -- const shapes = sortedShapes(object); -- const origin = this.objectTransformOrigin(object); -- const objectScale = object?.id === this.selectedObjectId ? this.objectScalePreviewValue() : 1; -- if (!shapes.length) { -- return `origin ${origin.x}, ${origin.y}, rot 0, scale ${this.formatScaleInputValue(objectScale)}`; -- } -- const frame = object?.id === this.selectedObjectId ? this.activeFrame() : null; -- const transforms = shapes.map((shape, shapeIndex) => this.shapeTransform(this.effectiveShapeForFrame(shape, frame, shapeIndex))); -- const sameNumber = (key) => transforms.every((transform) => Math.abs(transform[key] - transforms[0][key]) < 0.001); -- const sameScale = transforms.every((transform) => Math.abs(transform.scaleX - transforms[0].scaleX) < 0.001 && Math.abs(transform.scaleY - transforms[0].scaleY) < 0.001); -- let scaleText = "mixed"; -- if (Math.abs(objectScale - 1) >= 0.001) { -- scaleText = this.formatScaleInputValue(objectScale); -- } else if (sameScale) { -- scaleText = Math.abs(transforms[0].scaleX - transforms[0].scaleY) < 0.001 -- ? String(this.formatViewportNumber(transforms[0].scaleX)) -- : `${this.formatViewportNumber(transforms[0].scaleX)} x ${this.formatViewportNumber(transforms[0].scaleY)}`; -- } -- const rotationText = sameNumber("rotation") ? String(this.normalizeRotationDegrees(transforms[0].rotation)) : "mixed"; -- return `origin ${origin.x}, ${origin.y}, rot ${rotationText}, scale ${scaleText}`; -- } -- -- createDetailGrid(entries) { -- const grid = document.createElement("div"); -- grid.className = "object-vector-studio-v2__object-detail-grid"; -- entries.forEach(([label, value]) => { -- const labelElement = document.createElement("span"); -- labelElement.className = "object-vector-studio-v2__detail-label"; -- labelElement.textContent = label; -- const valueElement = document.createElement("span"); -- valueElement.className = "object-vector-studio-v2__detail-value"; -- valueElement.textContent = value; -- grid.append(labelElement, valueElement); -- }); -- return grid; -- } -- -- createShapeGeometryControls(shape) { -- const section = document.createElement("section"); -- const geometryTool = shapeGeometryTool(shape); -- section.className = `object-vector-studio-v2__edit-panel object-vector-studio-v2__edit-panel--geometry object-vector-studio-v2__edit-panel--${geometryTool}`; -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- section.classList.add("object-vector-studio-v2__edit-panel--polygon"); -- } -- const heading = document.createElement("h4"); -- heading.textContent = `${this.shapeGeometryHeadingLabel(shape)} Geometry`; -- const grid = document.createElement("div"); -- grid.className = geometryTool === "polygon" || geometryTool === "polyline" -- ? "object-vector-studio-v2__polygon-point-list" -- : `object-vector-studio-v2__edit-grid object-vector-studio-v2__edit-grid--${geometryTool}${geometryTool === "ellipse" ? " object-vector-studio-v2__edit-grid--ellipse" : ""}`; -- const editablePolygon = this.isEditablePolygonShape(shape); -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- const pointRounding = this.shapePointRoundingValues(shape); -- grid.append(this.createPointRowHeader({ addable: editablePolygon, deletable: editablePolygon })); -- shape.geometry.points.forEach((point, index) => { -- grid.append(this.createPolygonPointRow(point, index, { -- addable: editablePolygon, -- deletable: editablePolygon, -- rounded: pointRounding[index] === true, -- selectable: false -- })); -- }); -- } else { -- this.shapeGeometryFields(shape).forEach((field) => { -- const label = document.createElement("label"); -- label.className = "object-vector-studio-v2__edit-field"; -- if (field.wide) { -- label.classList.add("object-vector-studio-v2__edit-field--wide"); -- } -- const caption = document.createElement("span"); -- caption.textContent = field.label; -- const input = document.createElement("input"); -- input.dataset.shapeGeometryField = field.key; -- input.type = field.kind; -- input.value = field.kind === "number" -- ? (field.coordinate ? this.formatCoordinateInputValue(field.value) : String(this.formatViewportNumber(field.value))) -- : String(field.value); -- this.bindGeometryAutoApplyInput(input); -- label.append(caption, input); -- grid.append(label); -- }); -- } -- section.append(heading); -- const roundingRadiusControl = this.createRoundingRadiusControl(shape); -- if (roundingRadiusControl) { -- section.append(roundingRadiusControl); -- } -- section.append(grid); -- if (geometryTool !== "polygon" && geometryTool !== "polyline" && this.shapeSupportsPointRoundingControls(shape)) { -- section.append(this.createShapePointRoundingControls(shape)); -- } -- return section; -- } -- -- createRoundingRadiusControl(shape) { -- if (!this.shapeSupportsRoundingRadiusControl(shape)) { -- return null; -- } -- const label = document.createElement("label"); -- label.className = "object-vector-studio-v2__rounding-radius-field"; -- const caption = document.createElement("span"); -- caption.textContent = "Rounding Radius"; -- const input = document.createElement("input"); -- input.type = "number"; -- input.min = "0"; -- input.step = "0.1"; -- input.value = String(this.formatViewportNumber(this.shapeRoundingRadius(shape))); -- input.dataset.shapeRoundingRadius = "true"; -- input.setAttribute("aria-label", "Rounding radius"); -- input.addEventListener("input", () => this.clearInputValidity(input)); -- input.addEventListener("change", () => { -- const shapeIndex = this.selectedShapeIndex; -- this.window.setTimeout(() => { -- if (this.selectedShapeIndex !== shapeIndex) { -- return; -- } -- this.updateSelectedShapeRoundingRadius(input); -- }, 0); -- }); -- label.append(caption, input); -- return label; -- } -- -- updateSelectedShapeRoundingRadius(input) { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Rounding radius update skipped: no shape is selected."); -- return; -- } -- if (!this.canUseShapeGeometry()) { -- this.statusLog.write("WARN Rounding radius update skipped: select exactly one shape."); -- return; -- } -- if (!this.shapeSupportsPointRoundingControls(selected)) { -- this.statusLog.write(`WARN Rounding radius update skipped: ${shapeTypeLabel(selected)} does not use rounded points.`); -- return; -- } -- const rawValue = String(input?.value ?? "").trim(); -- const value = Number(rawValue); -- if (rawValue === "" || !Number.isFinite(value) || value < 0) { -- const error = "Rounding Radius must be a finite number greater than or equal to 0."; -- this.markInputInvalid(input, error); -- this.statusLog.write(`FAIL Invalid rounding radius rejected for shape row ${this.selectedShapeIndex}: ${error}`); -- return; -- } -- if (this.guardSelectedObjectMutation("Rounding radius update")) { -- return; -- } -- this.clearInputValidity(input); -- const roundedValue = Number(value.toFixed(3)); -- const nextPayload = this.cloneCurrentPayload(); -- const nextShape = this.findShapeInPayload(nextPayload, this.selectedShapeIndex); -- if (!nextShape) { -- this.statusLog.write(`WARN Rounding radius update skipped: selected shape row ${this.selectedShapeIndex} was not found.`); -- return; -- } -- nextShape.style = { -- ...nextShape.style, -- roundingRadius: roundedValue -- }; -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, `OK Updated rounding radius to ${roundedValue} for shape row ${this.selectedShapeIndex}.`, "Rounding radius update failed schema validation"); -- } -- -- updateSelectedShapePointRounding(pointIndex, isRounded) { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Point rounding update skipped: no shape is selected."); -- return; -- } -- if (!this.canUseShapeGeometry()) { -- this.statusLog.write("WARN Point rounding update skipped: select exactly one shape."); -- return; -- } -- if (!this.shapeSupportsPointRoundingControls(selected)) { -- this.statusLog.write(`WARN Point rounding update skipped: ${shapeTypeLabel(selected)} does not use editable point rows.`); -- return; -- } -- if (this.guardSelectedObjectMutation("Point rounding update")) { -- return; -- } -- const objectIndex = this.currentPayload.objects.findIndex((object) => object.id === this.selectedObjectId); -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects[objectIndex]; -- const nextShape = sortedShapes(nextObject)[this.selectedShapeIndex]; -- if (!nextShape) { -- this.statusLog.write(`WARN Point rounding update skipped: selected shape row ${this.selectedShapeIndex} was not found.`); -- return; -- } -- const pointCount = this.shapeGeometryPointCount(nextShape); -- if (!Number.isInteger(pointIndex) || pointIndex < 0 || pointIndex >= pointCount) { -- this.statusLog.write(`WARN Point rounding update skipped: point ${pointIndex + 1} is not available.`); -- return; -- } -- const pointRounding = this.normalizedPointRounding(nextShape, pointCount); -- pointRounding[pointIndex] = isRounded === true; -- this.applyPointRoundingToShape(nextShape, pointRounding); -- const pointStyle = isRounded ? "round" : "square"; -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, `OK Updated point ${pointIndex + 1} rounding to ${pointStyle} for shape row ${this.selectedShapeIndex}.`, "Point rounding update failed schema validation"); -- } -- -- bindGeometryAutoApplyInput(input) { -- input.addEventListener("input", () => this.clearInputValidity(input)); -- input.addEventListener("change", () => { -- const shapeIndex = this.selectedShapeIndex; -- this.window.setTimeout(() => { -- if (this.selectedShapeIndex !== shapeIndex) { -- return; -- } -- this.applyShapeGeometryEdits({ -- okMessage: `OK Auto-applied geometry edits to shape row ${this.selectedShapeIndex}.` -- }); -- }, 0); -- }); -- } -- -- createPointRowHeader({ addable = false, deletable = false } = {}) { -- const header = document.createElement("div"); -- header.className = "object-vector-studio-v2__polygon-point-header"; -- header.setAttribute("aria-hidden", "true"); -- ["", "", "", "Round", "", ""].forEach((label) => { -- const cell = document.createElement("span"); -- cell.textContent = label; -- header.append(cell); -- }); -- return header; -- } -- -- createPolygonPointRow(point, index, { addable = true, deletable = true, rounded = false, selectable = true } = {}) { -- const row = document.createElement("div"); -- row.className = "object-vector-studio-v2__polygon-point-field"; -- row.dataset.polygonPointIndex = String(index); -- if (selectable) { -- row.dataset.polygonPointSelectable = "true"; -- row.setAttribute("role", "button"); -- row.setAttribute("tabindex", "0"); -- row.setAttribute("aria-pressed", "false"); -- row.title = "Select this point row for Add Point."; -- row.addEventListener("click", (event) => this.handlePolygonPointRowSelection(event, row)); -- row.addEventListener("keydown", (event) => { -- if (event.key === "Enter" || event.key === " ") { -- event.preventDefault(); -- this.togglePolygonPointRowSelection(row, event); -- } -- }); -- } -- const caption = document.createElement("span"); -- caption.className = "object-vector-studio-v2__polygon-point-label"; -- caption.textContent = `Point ${index + 1}`; -- row.append(caption); -- [ -- ["x", "X", point.x], -- ["y", "Y", point.y] -- ].forEach(([axis, labelText, value]) => { -- const label = document.createElement("label"); -- label.className = "object-vector-studio-v2__polygon-point-axis"; -- const axisLabel = document.createElement("span"); -- axisLabel.textContent = labelText; -- const input = document.createElement("input"); -- input.dataset.shapeGeometryField = "points"; -- input.dataset.polygonPointAxis = axis; -- input.dataset.polygonPointIndex = String(index); -- input.inputMode = "decimal"; -- input.type = "text"; -- input.value = this.formatCoordinateInputValue(value); -- this.bindGeometryAutoApplyInput(input); -- label.append(axisLabel, input); -- row.append(label); -- }); -- const roundLabel = document.createElement("label"); -- roundLabel.className = "object-vector-studio-v2__polygon-point-rounding"; -- const roundCaption = document.createElement("span"); -- roundCaption.textContent = "Round"; -- const roundCheckbox = document.createElement("input"); -- roundCheckbox.type = "checkbox"; -- roundCheckbox.checked = rounded === true; -- roundCheckbox.dataset.polygonPointRound = "true"; -- roundCheckbox.dataset.polygonPointIndex = String(index); -- roundCheckbox.setAttribute("aria-label", `Round point ${index + 1}`); -- roundCheckbox.addEventListener("click", (event) => event.stopPropagation()); -- roundCheckbox.addEventListener("change", () => this.updateSelectedShapePointRounding(index, roundCheckbox.checked)); -- roundLabel.append(roundCaption, roundCheckbox); -- row.append(roundLabel); -- if (addable) { -- const addButton = document.createElement("button"); -- addButton.type = "button"; -- addButton.className = "object-vector-studio-v2__polygon-point-add"; -- addButton.dataset.polygonPointAdd = "true"; -- addButton.dataset.polygonPointIndex = String(index); -- addButton.setAttribute("aria-label", `Add point after point ${index + 1}`); -- addButton.title = `Add point after point ${index + 1}`; -- this.applyIconGlyph(addButton, "add"); -- addButton.addEventListener("pointerdown", (event) => event.stopPropagation()); -- addButton.addEventListener("click", (event) => { -- event.preventDefault(); -- event.stopPropagation(); -- this.addPolygonPointAfterRow(index, addButton); -- }); -- row.append(addButton); -- } -- if (deletable) { -- const deleteButton = document.createElement("button"); -- deleteButton.type = "button"; -- deleteButton.className = "object-vector-studio-v2__polygon-point-delete"; -- deleteButton.dataset.polygonPointDelete = "true"; -- deleteButton.dataset.polygonPointIndex = String(index); -- deleteButton.setAttribute("aria-label", `Delete point ${index + 1}`); -- deleteButton.title = `Delete point ${index + 1}`; -- this.applyIconGlyph(deleteButton, "delete"); -- deleteButton.addEventListener("pointerdown", (event) => event.stopPropagation()); -- deleteButton.addEventListener("click", (event) => { -- event.preventDefault(); -- event.stopPropagation(); -- this.deletePolygonPointRow(index, deleteButton); -- }); -- row.append(deleteButton); -- } -- return row; -- } -- -- createShapePointRoundingControls(shape) { -- const panel = document.createElement("div"); -- panel.className = "object-vector-studio-v2__point-rounding-panel"; -- const heading = document.createElement("h4"); -- heading.textContent = "Point Rounding"; -- const list = document.createElement("div"); -- list.className = "object-vector-studio-v2__polygon-point-list object-vector-studio-v2__point-rounding-list"; -- const points = this.shapeGeometryPoints(shape); -- const pointRounding = this.shapePointRoundingValues(shape); -- list.append(this.createPointRowHeader()); -- points.forEach((point, index) => { -- list.append(this.createReadonlyPointRoundingRow(shape, point, index, pointRounding[index] === true)); -- }); -- panel.append(heading, list); -- return panel; -- } -- -- createReadonlyPointRoundingRow(shape, point, index, rounded = false) { -- const row = document.createElement("div"); -- row.className = "object-vector-studio-v2__polygon-point-field object-vector-studio-v2__polygon-point-field--rounding-only"; -- row.dataset.polygonPointIndex = String(index); -- const label = document.createElement("span"); -- label.className = "object-vector-studio-v2__polygon-point-label"; -- const geometryTool = shapeGeometryTool(shape); -- label.textContent = OPEN_POINT_STYLE_TOOLS.has(geometryTool) -- ? (index === 0 ? "Start" : "End") -- : `Point ${index + 1}`; -- row.append(label); -- [ -- ["X", point.x], -- ["Y", point.y] -- ].forEach(([axis, value]) => { -- const field = document.createElement("span"); -- field.className = "object-vector-studio-v2__polygon-point-axis object-vector-studio-v2__polygon-point-axis--readonly"; -- const axisLabel = document.createElement("span"); -- axisLabel.textContent = axis; -- const valueElement = document.createElement("span"); -- valueElement.className = "object-vector-studio-v2__polygon-point-value"; -- valueElement.textContent = this.formatCoordinateInputValue(value); -- field.append(axisLabel, valueElement); -- row.append(field); -- }); -- const roundLabel = document.createElement("label"); -- roundLabel.className = "object-vector-studio-v2__polygon-point-rounding"; -- const roundCaption = document.createElement("span"); -- roundCaption.textContent = "Round"; -- const roundCheckbox = document.createElement("input"); -- roundCheckbox.type = "checkbox"; -- roundCheckbox.checked = rounded === true; -- roundCheckbox.dataset.polygonPointRound = "true"; -- roundCheckbox.dataset.polygonPointIndex = String(index); -- roundCheckbox.setAttribute("aria-label", `Round ${label.textContent.toLowerCase()} point`); -- roundCheckbox.addEventListener("change", () => this.updateSelectedShapePointRounding(index, roundCheckbox.checked)); -- roundLabel.append(roundCaption, roundCheckbox); -- row.append(roundLabel); -- return row; -- } -- -- handlePolygonPointRowSelection(event, row) { -- if (event.target.closest("input, button, select, textarea, label")) { -- return; -- } -- this.togglePolygonPointRowSelection(row, event); -- } -- -- togglePolygonPointRowSelection(row, event = null) { -- if (!row?.dataset || row.dataset.polygonPointSelectable !== "true") { -- return; -- } -- const shouldExtend = Boolean(event?.ctrlKey || event?.metaKey || event?.shiftKey); -- if (!shouldExtend) { -- this.elements.shapeGeometryDetails.querySelectorAll("[data-polygon-point-action-selected='true']").forEach((candidate) => { -- if (candidate !== row) { -- delete candidate.dataset.polygonPointActionSelected; -- candidate.classList.remove("is-action-selected"); -- candidate.setAttribute("aria-pressed", "false"); -- } -- }); -- } -- const isSelected = row.dataset.polygonPointActionSelected === "true"; -- if (isSelected) { -- delete row.dataset.polygonPointActionSelected; -- row.classList.remove("is-action-selected"); -- row.setAttribute("aria-pressed", "false"); -- return; -- } -- row.dataset.polygonPointActionSelected = "true"; -- row.classList.add("is-action-selected"); -- row.setAttribute("aria-pressed", "true"); -- } -- -- createShapeTransformControls(shape) { -- const section = document.createElement("section"); -- section.className = "object-vector-studio-v2__edit-panel object-vector-studio-v2__edit-panel--transform"; -- const transform = this.shapeTransform(this.effectiveShape(shape)); -- section.append( -- this.createMoveControlRow(), -- this.createRotateControlRow(), -- this.createScaleControlRow(transform) -- ); -- return section; -- } -- -- createObjectTransformControls(object) { -- const section = document.createElement("section"); -- section.className = "object-vector-studio-v2__edit-panel object-vector-studio-v2__edit-panel--transform object-vector-studio-v2__edit-panel--object-transform"; -- const origin = this.objectTransformOrigin(object); -- const objectScale = this.objectScaleControlValue(object); -- section.append( -- this.createObjectOriginControlRow(origin), -- this.createObjectRotateControlRow(), -- this.createScaleControlRow({ scaleX: objectScale, scaleY: objectScale }, { -- downLargeId: "objectVectorStudioV2ObjectScaleDownLargeButton", -- downSmallId: "objectVectorStudioV2ObjectScaleDownSmallButton", -- inputId: "objectVectorStudioV2ObjectScaleInput", -- inputLabel: "Object Scale", -- onInput: (input) => this.applyObjectScaleInputLive(input), -- onReset: () => this.resetSelectedObjectScale(), -- onResize: () => this.resizeSelectedObject(), -- onStep: (delta) => this.adjustSelectedObjectScale(delta), -- resetButtonId: "objectVectorStudioV2ObjectResetScaleButton", -- resizeButtonId: "objectVectorStudioV2ObjectResizeButton", -- upLargeId: "objectVectorStudioV2ObjectScaleUpLargeButton", -- upSmallId: "objectVectorStudioV2ObjectScaleUpSmallButton" -- }) -- ); -- return section; -- } -- -- objectScaleControlValue(object) { -- return this.objectScalePreviewValueForObject(object); -- } -- -- objectScalePreviewValue() { -- const value = Number(this.transformInputValue("objectVectorStudioV2ObjectScaleInput", "1")); -- return Number.isFinite(value) && value > 0 ? this.formatViewportNumber(value) : 1; -- } -- -- objectScalePreviewValueForObject(object) { -- return object?.id === this.selectedObjectId ? this.objectScalePreviewValue() : 1; -- } -- -- shapeWithObjectScalePreview(shape, scale = this.objectScalePreviewValue()) { -- if (!shape || Math.abs(scale - 1) < 0.001) { -- return shape; -- } -- const transform = this.shapeTransform(shape); -- return { -- ...shape, -- transform: { -- ...transform, -- scaleX: this.formatViewportNumber(transform.scaleX * scale), -- scaleY: this.formatViewportNumber(transform.scaleY * scale), -- x: this.formatViewportNumber(transform.x * scale), -- y: this.formatViewportNumber(transform.y * scale) -- } -- }; -- } -- -- createMoveControlRow() { -- return this.createTransformAxisControlRow({ -- action: () => this.moveSelectedShape(), -- buttonId: "objectVectorStudioV2MoveShapeButton", -- buttonLabel: "Move", -- iconKey: "move", -- label: "Move", -- rowType: "move", -- xInput: { id: "objectVectorStudioV2MoveXInput", label: "Move X", value: "10" }, -- yInput: { id: "objectVectorStudioV2MoveYInput", label: "Move Y", value: "0" } -- }); -- } -- -- createObjectOriginControlRow(origin) { -- return this.createTransformAxisControlRow({ -- action: () => this.applySelectedObjectOrigin(), -- buttonId: "objectVectorStudioV2ObjectApplyOriginButton", -- buttonLabel: "Apply", -- buttonTitle: "Apply Object Origin", -- label: "Origin", -- rowType: "origin", -- extraButtons: [ -- this.createTransformActionButton({ -- handler: () => this.autoOriginSelectedObjectPivot(), -- id: "objectVectorStudioV2ObjectAutoOriginButton", -- label: "Auto", -- title: "Auto Origin" -- }) -- ], -- xInput: { id: "objectVectorStudioV2ObjectOriginXInput", label: "Object Origin X", value: String(this.formatViewportNumber(origin.x)) }, -- yInput: { id: "objectVectorStudioV2ObjectOriginYInput", label: "Object Origin Y", value: String(this.formatViewportNumber(origin.y)) } -- }); -- } -- -- createRotateControlRow() { -- const row = document.createElement("div"); -- row.className = "object-vector-studio-v2__transform-control-row object-vector-studio-v2__transform-control-row--rotate"; -- row.dataset.transformControlRow = "rotate"; -- const label = document.createElement("span"); -- label.className = "object-vector-studio-v2__transform-control-label"; -- label.textContent = "Rotate"; -- const input = this.createTransformNumberInput("objectVectorStudioV2RotateInput", "Rotate", "15", { -- max: "359", -- min: "-359" -- }); -- const snapSelect = this.createRotateSnapSelect(); -- const stepField = this.createAngleSnapStepField(); -- const button = this.createTransformActionButton({ -- handler: () => this.rotateSelectedShape(), -- iconKey: "rotate", -- id: "objectVectorStudioV2RotateShapeButton", -- label: "Rotate" -- }); -- row.append(label, input, snapSelect, stepField, button); -- this.updateRotateSnapControls(row); -- return row; -- } -- -- createObjectRotateControlRow() { -- const row = document.createElement("div"); -- row.className = "object-vector-studio-v2__transform-control-row object-vector-studio-v2__transform-control-row--rotate object-vector-studio-v2__transform-control-row--object-rotate"; -- row.dataset.transformControlRow = "rotate"; -- const label = document.createElement("span"); -- label.className = "object-vector-studio-v2__transform-control-label"; -- label.textContent = "Rotate"; -- const input = this.createTransformNumberInput("objectVectorStudioV2ObjectRotateInput", "Object Rotate", "15", { -- max: "359", -- min: "-359" -- }); -- const snapSelect = this.createRotateSnapSelect({ -- id: "objectVectorStudioV2ObjectRotateSnapSelect", -- label: "Object Rotate Snap Angle" -- }); -- const stepField = this.createAngleSnapStepField({ -- id: "objectVectorStudioV2ObjectSnapAngleStepSelect", -- label: "Object Snap Angle Step" -- }); -- const button = this.createTransformActionButton({ -- handler: () => this.rotateSelectedObject(), -- iconKey: "rotate", -- id: "objectVectorStudioV2ObjectRotateButton", -- label: "Rotate" -- }); -- row.append(label, input, snapSelect, stepField, button); -- this.updateRotateSnapControls(row); -- return row; -- } -- -- createRotateSnapSelect(options = {}) { -- const { -- id = "objectVectorStudioV2RotateSnapSelect", -- label = "Rotate Snap Angle" -- } = options; -- const select = document.createElement("select"); -- select.id = id; -- select.className = "object-vector-studio-v2__rotate-snap-select"; -- select.setAttribute("aria-label", label); -- select.title = "Valid Rotate values generated by the selected Snap Angle step."; -- this.populateRotateSnapSelect(select, this.angleSnapStep, this.transformInputValue(select.id, "15")); -- select.addEventListener("change", () => { -- this.transformInputValues.set(select.id, select.value); -- }); -- return select; -- } -- -- createAngleSnapStepField(options = {}) { -- const { -- id = "objectVectorStudioV2SnapAngleStepSelect", -- label: labelText = "Snap Angle Step" -- } = options; -- const labelElement = document.createElement("label"); -- labelElement.className = "object-vector-studio-v2__snap-angle-step-field"; -- const text = document.createElement("span"); -- text.textContent = "Step"; -- const select = document.createElement("select"); -- select.id = id; -- select.setAttribute("aria-label", labelText); -- select.title = "Choose the angle increment used to generate Rotate dropdown values."; -- ANGLE_SNAP_STEPS.forEach((step) => { -- const option = document.createElement("option"); -- option.value = String(step); -- option.textContent = String(step); -- select.append(option); -- }); -- select.value = String(this.angleSnapStep); -- select.addEventListener("change", () => { -- const nextStep = Number(select.value); -- this.angleSnapStep = ANGLE_SNAP_STEPS.includes(nextStep) ? nextStep : 15; -- this.window.sessionStorage?.setItem(ANGLE_SNAP_STEP_SESSION_KEY, String(this.angleSnapStep)); -- this.updateRotateSnapControls(); -- this.statusLog.write(`OK Snap angle step set to ${this.angleSnapStep} degrees.`); -- }); -- labelElement.append(text, select); -- return labelElement; -- } -- -- createTransformAxisControlRow({ action, buttonId, buttonLabel, buttonTitle, extraButtons = [], iconKey, label, rowType, xInput, yInput }) { -- const row = document.createElement("div"); -- row.className = `object-vector-studio-v2__transform-control-row object-vector-studio-v2__transform-control-row--${rowType}`; -- row.dataset.transformControlRow = rowType; -- const rowLabel = document.createElement("span"); -- rowLabel.className = "object-vector-studio-v2__transform-control-label"; -- rowLabel.textContent = label; -- const actionButton = this.createTransformActionButton({ -- handler: action, -- iconKey, -- id: buttonId, -- label: buttonLabel, -- title: buttonTitle -- }); -- row.append( -- rowLabel, -- this.createTransformAxisField("X", xInput), -- this.createTransformAxisField("Y", yInput), -- actionButton, -- ...extraButtons -- ); -- return row; -- } -- -- createTransformAxisField(axisLabel, inputOptions) { -- const label = document.createElement("label"); -- label.className = "object-vector-studio-v2__transform-axis-field"; -- const axis = document.createElement("span"); -- axis.textContent = axisLabel; -- label.append(axis, this.createTransformNumberInput(inputOptions.id, inputOptions.label, inputOptions.value)); -- return label; -- } -- -- createTransformNumberInput(id, label, value, options = {}) { -- const input = document.createElement("input"); -- input.id = id; -- input.type = "number"; -- input.step = "0.1"; -- const initialValue = this.transformInputValue(id, value); -- const numericInitialValue = Number(initialValue); -- input.value = String(initialValue).trim() !== "" && Number.isFinite(numericInitialValue) -- ? String(this.formatViewportNumber(numericInitialValue)) -- : initialValue; -- input.setAttribute("aria-label", label); -- if (options.min !== undefined) { -- input.min = options.min; -- } -- if (options.max !== undefined) { -- input.max = options.max; -- } -- input.addEventListener("input", () => { -- this.transformInputValues.set(id, input.value); -- this.clearInputValidity(input); -- }); -- return input; -- } -- -- createTransformActionButton({ handler, iconKey, id, label, title }) { -- const button = document.createElement("button"); -- button.id = id; -- button.type = "button"; -- button.textContent = label; -- if (title) { -- button.title = title; -- button.setAttribute("aria-label", title); -- } -- this.applyIconGlyph(button, iconKey); -- button.addEventListener("click", handler); -- return button; -- } -- -- createScaleControlRow(transform, options = {}) { -- const { -- downLargeId = "objectVectorStudioV2ScaleDownLargeButton", -- downSmallId = "objectVectorStudioV2ScaleDownSmallButton", -- inputId = "objectVectorStudioV2ScaleInput", -- inputLabel = "Scale", -- onInput = (input) => this.applyScaleInputLive(input), -- onReset = () => this.resetSelectedShapeScale(), -- onResize = () => this.resizeSelectedShape(), -- onStep = (delta) => this.adjustSelectedShapeScale(delta), -- resetButtonId = "objectVectorStudioV2ResetShapeScaleButton", -- resizeButtonId = "objectVectorStudioV2ResizeShapeButton", -- upLargeId = "objectVectorStudioV2ScaleUpLargeButton", -- upSmallId = "objectVectorStudioV2ScaleUpSmallButton" -- } = options; -- const row = document.createElement("div"); -- row.className = "object-vector-studio-v2__scale-control-row"; -- const label = document.createElement("span"); -- label.className = "object-vector-studio-v2__scale-control-label"; -- label.textContent = "Scale"; -- row.append(label); -- -- [ -- [downLargeId, "--", -0.1], -- [downSmallId, "-", -0.01] -- ].forEach(([id, text, delta]) => { -- row.append(this.createScaleStepButton(id, text, delta, onStep)); -- }); -- -- const input = document.createElement("input"); -- input.id = inputId; -- input.className = "object-vector-studio-v2__scale-input"; -- input.type = "number"; -- input.min = "0.01"; -- input.step = "0.01"; -- input.value = this.formatScaleInputValue(transform.scaleX); -- input.setAttribute("aria-label", inputLabel); -- input.addEventListener("input", () => { -- this.transformInputValues.set(input.id, input.value); -- onInput(input); -- }); -- row.append(input); -- -- [ -- [upSmallId, "+", 0.01], -- [upLargeId, "++", 0.1] -- ].forEach(([id, text, delta]) => { -- row.append(this.createScaleStepButton(id, text, delta, onStep)); -- }); -- -- const resizeButton = document.createElement("button"); -- resizeButton.id = resizeButtonId; -- resizeButton.className = "object-vector-studio-v2__scale-resize-button"; -- resizeButton.type = "button"; -- resizeButton.textContent = "Resize"; -- resizeButton.title = "Resize Geometry"; -- resizeButton.setAttribute("aria-label", "Resize Geometry"); -- this.applyIconGlyph(resizeButton, "resize"); -- resizeButton.addEventListener("click", onResize); -- row.append(resizeButton); -- -- const resetButton = document.createElement("button"); -- resetButton.id = resetButtonId; -- resetButton.className = "object-vector-studio-v2__scale-reset-button"; -- resetButton.type = "button"; -- resetButton.textContent = "X"; -- resetButton.title = "Reset Scale to 1.0"; -- resetButton.setAttribute("aria-label", `${inputLabel} Reset Scale to 1.0`); -- resetButton.addEventListener("click", onReset); -- row.append(resetButton); -- return row; -- } -- -- createScaleStepButton(id, text, delta, onStep = (scaleDelta) => this.adjustSelectedShapeScale(scaleDelta)) { -- const button = document.createElement("button"); -- button.id = id; -- button.type = "button"; -- button.dataset.scaleStep = String(delta); -- button.textContent = text; -- button.title = `Scale ${delta > 0 ? "up" : "down"} ${Math.abs(delta).toFixed(2)}`; -- button.addEventListener("click", () => onStep(delta)); -- return button; -- } -- -- transformInputValue(id, defaultValue) { -- return this.transformInputValues.has(id) ? this.transformInputValues.get(id) : defaultValue; -- } -- -- shapeGeometryFields(shape) { -- const geometryTool = shapeGeometryTool(shape); -- if (shapeTool(shape) === "square") { -- return [ -- { key: "x", kind: "number", label: "x", value: shape.geometry.x }, -- { key: "y", kind: "number", label: "y", value: shape.geometry.y }, -- { key: "size", kind: "number", label: "Size", value: shape.geometry.width, wide: true } -- ]; -- } -- if (geometryTool === "rectangle") { -- return ["x", "y", "width", "height"].map((key) => ({ key, kind: "number", label: key, value: shape.geometry[key] })); -- } -- if (geometryTool === "circle") { -- return ["cx", "cy", "r"].map((key) => ({ key, kind: "number", label: key, value: shape.geometry[key], wide: key === "r" })); -- } -- if (geometryTool === "ellipse") { -- return [ -- { key: "cx", kind: "number", label: "Cx", value: shape.geometry.cx }, -- { key: "cy", kind: "number", label: "Cy", value: shape.geometry.cy }, -- { key: "rx", kind: "number", label: "Rx", value: shape.geometry.rx }, -- { key: "ry", kind: "number", label: "Ry", value: shape.geometry.ry } -- ]; -- } -- if (geometryTool === "line") { -- const point1 = linePoint(shape.geometry, "point1"); -- const point2 = linePoint(shape.geometry, "point2"); -- return [ -- { coordinate: true, key: "point1.x", kind: "number", label: "Point 1 X", value: point1.x }, -- { coordinate: true, key: "point1.y", kind: "number", label: "Point 1 Y", value: point1.y }, -- { coordinate: true, key: "point2.x", kind: "number", label: "Point 2 X", value: point2.x }, -- { coordinate: true, key: "point2.y", kind: "number", label: "Point 2 Y", value: point2.y } -- ]; -- } -- if (geometryTool === "arc") { -- return ["cx", "cy", "r", "startAngle", "endAngle"].map((key) => ({ -- key, -- kind: "number", -- label: key, -- value: shape.geometry[key], -- wide: key === "r" -- })); -- } -- if (geometryTool === "text") { -- return [ -- { key: "x", kind: "number", label: "x", value: shape.geometry.x }, -- { key: "y", kind: "number", label: "y", value: shape.geometry.y }, -- { key: "fontSize", kind: "number", label: "fontSize", value: shape.geometry.fontSize, wide: true }, -- { key: "text", kind: "text", label: "text", value: shape.geometry.text, wide: true } -- ]; -- } -- return [ -- { -- key: "points", -- kind: "text", -- label: "points", -- value: shape.geometry.points.map((point) => `${point.x},${point.y}`).join(" ") -- } -- ]; -- } -- -- renderWorkSurface() { -- const object = this.selectedObject(); -- this.elements.renderSurface.classList.toggle("is-drawing-mode", Boolean(this.activeDrawing)); -- this.updateRenderSurfaceCursorState(); -- this.removeDrawingHint(); -- this.elements.renderSurface.replaceChildren(); -- this.renderSvgGrid(); -- if (!object) { -- this.renderCenterOriginMarker(); -- return; -- } -- if (this.isObjectHidden(object.id)) { -- this.renderCenterOriginMarker(); -- this.statusLog.write(`OK Render mode svg-work-surface: object ${object.name} is hidden for this runtime session; capture mode none.`); -- return; -- } -- -- if (this.elements.onionSkinToggle.checked) { -- this.renderOnionSkin(object); -- } -- const transformOrigin = this.objectTransformOrigin(object); -- const objectScale = this.objectScalePreviewValueForObject(object); -- let renderedCount = 0; -- const hitLayer = document.createElementNS(SVG_NS, "g"); -- hitLayer.classList.add("object-vector-studio-v2__shape-hit-layer"); -- this.elements.renderSurface.append(hitLayer); -- sortedShapes(object).forEach((shape, shapeIndex) => { -- const renderShape = this.shapeWithObjectScalePreview(this.effectiveShape(shape, shapeIndex), objectScale); -- if (!renderShape.visible) { -- return; -- } -- try { -- const element = this.createSvgShape(renderShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE, transformOrigin }); -- element.dataset.shapeIndex = String(shapeIndex); -- element.dataset.renderShapeTool = shapeTool(shape); -- element.classList.add("object-vector-studio-v2__shape"); -- element.classList.toggle("is-selected", this.selectedShapeIndexes.has(shapeIndex)); -- element.setAttribute("tabindex", "0"); -- this.bindRenderedShapePointerEvents(element, shape, shapeIndex); -- this.elements.renderSurface.append(element); -- this.appendShapePointStyleCaps(this.elements.renderSurface, renderShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE, transformOrigin }); -- hitLayer.append(this.createShapeHitArea(renderShape, shape, shapeIndex, transformOrigin)); -- renderedCount += 1; -- } catch (error) { -- this.statusLog.write(`FAIL Render mode svg-work-surface failed for shape-${shapeIndex} (${shapeTool(shape)}): ${error.message}`); -- } -- }); -- if (!hitLayer.childNodes.length) { -- hitLayer.remove(); -- } -- this.renderObjectBounds(object); -- this.renderObjectOriginMarker(object); -- this.renderSnapPointTargets(object); -- this.renderDrawingPreview(); -- this.renderDrawingHint(); -- this.renderSelectionOverlay(object); -- this.renderCenterOriginMarker(); -- -- const frame = this.activeFrame(); -- const message = `Render mode svg-work-surface: rendered ${object.name} with ${renderedCount} visible shapes${frame ? ` for ${this.selectedStateId}/${frame.id}` : ""}; capture mode none.`; -- this.statusLog.write(`OK ${message}`); -- } -- -- bindRenderedShapePointerEvents(element, shape, shapeIndex) { -- element.addEventListener("pointerdown", (event) => { -- event.stopPropagation(); -- if (event.button === 2) { -- event.preventDefault(); -- return; -- } -- if (this.activeDrawing) { -- this.handleDrawingPointerDown(event); -- return; -- } -- if (this.activeTool === "paint" || this.activeTool === "stroke") { -- this.isPaintDragging = true; -- return; -- } -- this.startPreviewShapeMove(event, shapeIndex); -- }); -- element.addEventListener("click", (event) => { -- event.stopPropagation(); -- if (this.activeDrawing) { -- return; -- } -- this.handleShapePointer(event, shape, shapeIndex, { source: "render surface click" }); -- }); -- element.addEventListener("contextmenu", (event) => this.handleShapeContextMenu(event, shape, shapeIndex)); -- element.addEventListener("pointerenter", (event) => { -- if (this.isPaintDragging) { -- event.stopPropagation(); -- this.handleShapePointer(event, shape, shapeIndex, { source: "render surface drag" }); -- } -- }); -- } -- -- createShapeHitArea(renderShape, sourceShape, shapeIndex, transformOrigin = { x: 0, y: 0 }) { -- const hitArea = this.createSvgShape(renderShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE, transformOrigin }); -- const geometryTool = shapeGeometryTool(sourceShape); -- hitArea.classList.add("object-vector-studio-v2__shape-hit-area"); -- hitArea.dataset.shapeHitIndex = String(shapeIndex); -- hitArea.dataset.shapeHitTool = shapeTool(sourceShape); -- hitArea.removeAttribute("tabindex"); -- hitArea.setAttribute("fill", ["arc", "line", "polyline"].includes(geometryTool) ? "none" : "transparent"); -- hitArea.setAttribute("stroke", "transparent"); -- hitArea.setAttribute("stroke-width", String(Math.max(12, Number(renderShape.style?.strokeWidth) || 1))); -- hitArea.setAttribute("pointer-events", "all"); -- hitArea.setAttribute("aria-hidden", "true"); -- this.bindRenderedShapePointerEvents(hitArea, sourceShape, shapeIndex); -- return hitArea; -- } -- -- renderSvgGrid() { -- if (!this.gridRenderEnabled) { -- return; -- } -- const width = DEFAULT_VIEWPORT.width / this.viewport.zoom; -- const height = DEFAULT_VIEWPORT.height / this.viewport.zoom; -- const minX = this.viewport.x - width / 2; -- const maxX = this.viewport.x + width / 2; -- const minY = this.viewport.y - height / 2; -- const maxY = this.viewport.y + height / 2; -- const group = document.createElementNS(SVG_NS, "g"); -- group.classList.add("object-vector-studio-v2__svg-grid"); -- group.dataset.gridRendered = "true"; -- for (let x = Math.floor(minX / GRID_STEP) * GRID_STEP; x <= maxX; x += GRID_STEP) { -- const line = document.createElementNS(SVG_NS, "line"); -- line.setAttribute("x1", this.formatViewportNumber(x)); -- line.setAttribute("x2", this.formatViewportNumber(x)); -- line.setAttribute("y1", this.formatViewportNumber(minY)); -- line.setAttribute("y2", this.formatViewportNumber(maxY)); -- group.append(line); -- } -- for (let y = Math.floor(minY / GRID_STEP) * GRID_STEP; y <= maxY; y += GRID_STEP) { -- const line = document.createElementNS(SVG_NS, "line"); -- line.setAttribute("x1", this.formatViewportNumber(minX)); -- line.setAttribute("x2", this.formatViewportNumber(maxX)); -- line.setAttribute("y1", this.formatViewportNumber(y)); -- line.setAttribute("y2", this.formatViewportNumber(y)); -- group.append(line); -- } -- this.elements.renderSurface.append(group); -- } -- -- handleShapePointer(event, shape, shapeIndex, options = {}) { -- if (event.altKey || this.activeTool === "picker") { -- this.sampleShapeStyle(shapeIndex); -- return; -- } -- if (event.shiftKey) { -- this.selectShape(shapeIndex, "render surface additive", { additive: true }); -- return; -- } -- const isStrokeMode = this.activeTool === "stroke"; -- const isPaintMode = this.activeTool === "paint"; -- if (isStrokeMode || isPaintMode) { -- if (options.dragStart) { -- this.isPaintDragging = true; -- } -- this.applySelectedPaletteColorToShape(shapeIndex, isStrokeMode ? "stroke" : "fill", options.source || (options.dragStart ? "render surface click" : "render surface drag")); -- return; -- } -- if (this.preserveSelectedShapeClick(shapeIndex, options.source || "render surface")) { -- return; -- } -- this.selectShape(shapeIndex, "render surface"); -- } -- -- preserveSelectedShapeClick(shapeIndex, sourceLabel) { -- const object = this.selectedObject(); -- const normalizedIndex = normalizeShapeIndex(shapeIndex); -- const shapes = sortedShapes(object); -- if (normalizedIndex < 0 || normalizedIndex >= shapes.length || !this.selectedShapeIndexes.has(normalizedIndex)) { -- return false; -- } -- -- const scrollState = this.captureLeftPanelScrollState(); -- this.selectedShapeIndex = normalizedIndex; -- this.syncPaletteSelectionFromCurrentShape({ logMissing: true }); -- this.setPaletteTarget("stroke", false); -- this.renderPayload(); -- this.restoreLeftPanelScrollState(scrollState); -- const shape = this.selectedShape(); -- this.statusLog.write(`OK Preserved selected shape from ${sourceLabel}: row ${this.selectedShapeIndex} (${shapeTool(shape)}). Multi-select count: ${this.selectedShapeIndexes.size}.`); -- return true; -- } -- -- handleShapeContextMenu(event, shape, shapeIndex) { -- event.preventDefault(); -- if (this.activeDrawing || this.previewPointerEdit) { -- return; -- } -- this.applyTransparentStyleToShape(shapeIndex, "fill", "right-click"); -- } -- -- renderOnionSkin(object) { -- const previousFrame = this.previousFrame(); -- if (!previousFrame) { -- return; -- } -- const group = document.createElementNS(SVG_NS, "g"); -- group.classList.add("object-vector-studio-v2__onion-skin"); -- group.dataset.onionSkinFrame = previousFrame.id; -- const transformOrigin = this.objectTransformOrigin(object); -- const objectScale = this.objectScalePreviewValueForObject(object); -- sortedShapes(object).forEach((shape, shapeIndex) => { -- const renderShape = this.shapeWithObjectScalePreview(this.effectiveShapeForFrame(shape, previousFrame, shapeIndex), objectScale); -- if (!renderShape.visible) { -- return; -- } -- try { -- group.append(this.createSvgShape(renderShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE, transformOrigin })); -- this.appendShapePointStyleCaps(group, renderShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE, transformOrigin }); -- } catch (error) { -- this.statusLog.write(`FAIL Onion-skin render failed for ${object.name}/shape-${shapeIndex}: ${error.message}`); -- } -- }); -- this.elements.renderSurface.append(group); -- } -- -- renderObjectBounds(object) { -- const bounds = this.objectBounds(object, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE, includeInvisible: false }); -- const box = document.createElementNS(SVG_NS, "rect"); -- box.classList.add("object-vector-studio-v2__object-bounds"); -- box.dataset.objectBounds = object.id; -- box.setAttribute("x", bounds.x - 6); -- box.setAttribute("y", bounds.y - 6); -- box.setAttribute("width", bounds.width + 12); -- box.setAttribute("height", bounds.height + 12); -- this.elements.renderSurface.append(box); -- } -- -- renderObjectOriginMarker(object) { -- if (!object) { -- return; -- } -- const origin = this.objectTransformOrigin(object); -- const x = this.scaleDrawingValue(origin.x, OBJECT_PREVIEW_DRAWING_SCALE); -- const y = this.scaleDrawingValue(origin.y, OBJECT_PREVIEW_DRAWING_SCALE); -- const marker = document.createElementNS(SVG_NS, "g"); -- marker.classList.add("object-vector-studio-v2__object-origin"); -- marker.dataset.objectOrigin = object.id; -- marker.setAttribute("role", "img"); -- marker.setAttribute("aria-label", "Object origin/pivot marker for object rotation and scale"); -- const markerTitle = document.createElementNS(SVG_NS, "title"); -- markerTitle.textContent = "Object Origin/Pivot: rotate and scale pivot for the selected object."; -- const horizontal = document.createElementNS(SVG_NS, "line"); -- horizontal.setAttribute("x1", x - 8); -- horizontal.setAttribute("x2", x + 8); -- horizontal.setAttribute("y1", y); -- horizontal.setAttribute("y2", y); -- const vertical = document.createElementNS(SVG_NS, "line"); -- vertical.setAttribute("x1", x); -- vertical.setAttribute("x2", x); -- vertical.setAttribute("y1", y - 8); -- vertical.setAttribute("y2", y + 8); -- marker.append(markerTitle, horizontal, vertical); -- this.elements.renderSurface.append(marker); -- } -- -- renderCenterOriginMarker() { -- if (!this.centerOriginVisible) { -- return; -- } -- const marker = document.createElementNS(SVG_NS, "circle"); -- marker.classList.add("object-vector-studio-v2__center-origin-dot"); -- marker.dataset.centerOrigin = "0,0"; -- marker.setAttribute("cx", "0"); -- marker.setAttribute("cy", "0"); -- marker.setAttribute("r", "9"); -- marker.setAttribute("aria-label", "Canvas origin 0,0"); -- this.elements.renderSurface.append(marker); -- } -- -- renderSnapPointTargets(object) { -- if (this.snapMode !== "point") { -- return; -- } -- const points = this.visibleSnapPoints(object); -- if (!points.length) { -- return; -- } -- const group = document.createElementNS(SVG_NS, "g"); -- group.classList.add("object-vector-studio-v2__snap-targets"); -- points.forEach((point, index) => { -- const target = document.createElementNS(SVG_NS, "circle"); -- target.classList.add("object-vector-studio-v2__snap-target"); -- target.dataset.snapTargetIndex = String(index); -- target.setAttribute("cx", this.scaleDrawingValue(point.x, OBJECT_PREVIEW_DRAWING_SCALE)); -- target.setAttribute("cy", this.scaleDrawingValue(point.y, OBJECT_PREVIEW_DRAWING_SCALE)); -- target.setAttribute("r", 4); -- group.append(target); -- }); -- this.elements.renderSurface.append(group); -- } -- -- renderDrawingPreview() { -- if (!this.activeDrawing) { -- return; -- } -- const previewShape = this.previewShapeFromDrawing(); -- if (!previewShape) { -- return; -- } -- try { -- this.renderDrawingSnapLine(previewShape); -- const element = this.createSvgShape(previewShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE }); -- element.classList.add("object-vector-studio-v2__drawing-preview"); -- element.dataset.drawingPreviewTool = this.activeDrawing.tool; -- this.applyDrawingPreviewPresentation(element, previewShape); -- this.elements.renderSurface.append(element); -- this.appendShapePointStyleCaps(this.elements.renderSurface, previewShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE }); -- } catch (error) { -- this.statusLog.write(`FAIL Drawing preview failed for ${this.activeDrawing.tool}: ${error.message}`); -- } -- } -- -- renderDrawingSnapLine(previewShape) { -- if (this.snapMode === "none") { -- return; -- } -- const element = this.createSvgShape(previewShape, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE }); -- element.classList.add("object-vector-studio-v2__snap-line"); -- element.dataset.snapLineMode = this.snapMode; -- element.dataset.snapLineTool = this.activeDrawing?.tool || shapeTool(previewShape); -- element.setAttribute("aria-hidden", "true"); -- this.applyDrawingPreviewPresentation(element, previewShape); -- element.style.strokeDasharray = "none"; -- this.elements.renderSurface.append(element); -- } -- -- renderDrawingHint() { -- const drawing = this.activeDrawing; -- if (!drawing || !["polygon", "polyline"].includes(drawing.tool) || !this.drawingPreviewPoint) { -- this.removeDrawingHint(); -- return; -- } -- const workArea = this.elements.renderSurface.closest(".object-vector-studio-v2__work-area"); -- if (!workArea) { -- return; -- } -- const clientPoint = this.drawingHintClientPoint || this.clientPointFromDrawingPoint(this.drawingPreviewPoint); -- if (!clientPoint) { -- this.removeDrawingHint(); -- return; -- } -- const workAreaBounds = workArea.getBoundingClientRect(); -- const hint = document.createElement("div"); -- hint.className = "object-vector-studio-v2__drawing-hint"; -- hint.dataset.drawingHintTool = drawing.tool; -- hint.textContent = "Double-click / Enter to complete"; -- hint.style.left = `${Math.round(clientPoint.x - workAreaBounds.left + 12)}px`; -- hint.style.top = `${Math.round(clientPoint.y - workAreaBounds.top + 16)}px`; -- workArea.append(hint); -- } -- -- removeDrawingHint() { -- this.elements.renderSurface.closest(".object-vector-studio-v2__work-area") -- ?.querySelector(".object-vector-studio-v2__drawing-hint") -- ?.remove(); -- } -- -- clientPointFromDrawingPoint(point) { -- const bounds = this.elements.renderSurface.getBoundingClientRect(); -- if (!bounds.width || !bounds.height) { -- return null; -- } -- const viewWidth = DEFAULT_VIEWPORT.width / this.viewport.zoom; -- const viewHeight = DEFAULT_VIEWPORT.height / this.viewport.zoom; -- const drawingX = point.x * OBJECT_PREVIEW_DRAWING_SCALE; -- const drawingY = point.y * OBJECT_PREVIEW_DRAWING_SCALE; -- return { -- x: bounds.left + ((drawingX - this.viewport.x + viewWidth / 2) / viewWidth) * bounds.width, -- y: bounds.top + ((drawingY - this.viewport.y + viewHeight / 2) / viewHeight) * bounds.height -- }; -- } -- -- applyDrawingPreviewPresentation(element, previewShape) { -- if (shapeGeometryTool(previewShape) === "text") { -- element.style.strokeDasharray = "none"; -- return; -- } -- element.style.strokeDasharray = this.drawingPreviewDashArray(previewShape.style.strokeWidth); -- const previewPointStyle = this.supportsOpenPointStyle(previewShape) -- ? (this.openShapeUsesSplitPointStyles(previewShape) ? "butt" : this.shapeStartPointStyle(previewShape)) -- : this.shapeUnifiedPointStyle(previewShape); -- element.style.strokeLinecap = previewPointStyle; -- element.style.strokeLinejoin = this.strokeLinejoinValue(this.shapeUnifiedPointStyle(previewShape)); -- } -- -- drawingPreviewDashArray(strokeWidth) { -- const normalizedWidth = Number(strokeWidth); -- const width = Number.isFinite(normalizedWidth) && normalizedWidth > 0 ? normalizedWidth : 2; -- const unitsPerPixel = this.svgUnitsPerCssPixel(); -- const dashPixels = Math.max(12, Math.min(24, width * 1.1)); -- const gapPixels = Math.max(8, Math.min(18, width * 0.75)); -- return `${this.formatViewportNumber(dashPixels * unitsPerPixel)} ${this.formatViewportNumber(gapPixels * unitsPerPixel)}`; -- } -- -- svgUnitsPerCssPixel() { -- const bounds = this.elements.renderSurface.getBoundingClientRect(); -- const viewBox = this.elements.renderSurface.viewBox?.baseVal; -- if (!bounds.width || !viewBox?.width) { -- return 1; -- } -- return viewBox.width / bounds.width; -- } -- -- visibleSnapPoints(object = this.selectedObject(), options = {}) { -- if (!object) { -- return []; -- } -- const points = []; -- const transformOrigin = this.objectTransformOrigin(object); -- const objectScale = this.objectScalePreviewValueForObject(object); -- sortedShapes(object).forEach((shape, shapeIndex) => { -- if (normalizeShapeIndex(options.excludeShapeIndex) === shapeIndex) { -- return; -- } -- const effective = this.shapeWithObjectScalePreview(this.effectiveShape(shape, shapeIndex), objectScale); -- if (!effective.visible) { -- return; -- } -- const transform = this.shapeTransform(effective); -- const definitions = this.geometryPointHandleDefinitions(effective); -- const shapePoints = definitions.length -- ? definitions.map((definition) => definition.point) -- : boundsCornerPoints(shapeBounds(effective)); -- shapePoints.forEach((point) => points.push(this.transformedPoint(point, transform, transformOrigin))); -- }); -- return points; -- } -- -- nearestVisibleSnapPoint(point, options = {}) { -- const candidates = this.visibleSnapPoints(this.selectedObject(), options); -- let nearest = null; -- candidates.forEach((candidate) => { -- const distance = Math.hypot(candidate.x - point.x, candidate.y - point.y); -- if (distance <= POINT_SNAP_RADIUS && (!nearest || distance < nearest.distance)) { -- nearest = { distance, point: candidate }; -- } -- }); -- return nearest?.point || null; -- } -- -- normalizeDrawingPoint(point) { -- return { -- x: this.formatViewportNumber(point?.x || 0), -- y: this.formatViewportNumber(point?.y || 0) -- }; -- } -- -- snapCanvasPoint(point, options = {}) { -- if (this.snapMode === "grid") { -- return { -- x: Math.round(point.x), -- y: Math.round(point.y) -- }; -- } -- if (this.snapMode === "point") { -- const target = this.nearestVisibleSnapPoint(point, options); -- if (target) { -- return { -- x: this.formatViewportNumber(target.x), -- y: this.formatViewportNumber(target.y) -- }; -- } -- } -- return this.normalizeDrawingPoint(point); -- } -- -- supportsDrawingAngleSnap(tool) { -- return ["line", "polygon", "polyline"].includes(tool); -- } -- -- drawingAngleSnapAnchor(drawing) { -- if (!drawing || !this.supportsDrawingAngleSnap(drawing.tool)) { -- return null; -- } -- if (drawing.flow === "two-click") { -- return drawing.start || null; -- } -- if (drawing.flow === "points") { -- return drawing.points.at(-1) || null; -- } -- return null; -- } -- -- snapPointToDrawingAngle(anchor, point) { -- const normalizedAnchor = this.normalizeDrawingPoint(anchor); -- const normalizedPoint = this.normalizeDrawingPoint(point); -- const dx = normalizedPoint.x - normalizedAnchor.x; -- const dy = normalizedPoint.y - normalizedAnchor.y; -- const distance = Math.hypot(dx, dy); -- if (!distance) { -- return normalizedPoint; -- } -- const degrees = Math.atan2(dy, dx) * 180 / Math.PI; -- const snappedDegrees = this.snapAngle(degrees); -- const radians = snappedDegrees * Math.PI / 180; -- return { -- x: this.formatViewportNumber(normalizedAnchor.x + Math.cos(radians) * distance), -- y: this.formatViewportNumber(normalizedAnchor.y + Math.sin(radians) * distance) -- }; -- } -- -- snapDrawingPoint(point, drawing, options = {}) { -- const snappedPoint = this.snapCanvasPoint(point, options); -- if (!this.angleSnapEnabled) { -- return this.normalizeDrawingPoint(snappedPoint); -- } -- const anchor = this.drawingAngleSnapAnchor(drawing); -- if (!anchor) { -- return this.normalizeDrawingPoint(snappedPoint); -- } -- return this.snapPointToDrawingAngle(anchor, snappedPoint); -- } -- -- createSvgShape(shape, { drawingScale = 1, transformOrigin = { x: 0, y: 0 } } = {}) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "rectangle") { -- const roundedPath = this.roundedPointPath(shape, { closed: true, drawingScale }); -- const element = document.createElementNS(SVG_NS, roundedPath ? "path" : "rect"); -- if (roundedPath) { -- element.setAttribute("d", roundedPath); -- element.dataset.roundedPointRender = "path"; -- element.setAttribute("points", this.svgPointList(this.shapeGeometryPoints(shape), drawingScale)); -- } -- element.setAttribute("x", this.scaleDrawingValue(shape.geometry.x, drawingScale)); -- element.setAttribute("y", this.scaleDrawingValue(shape.geometry.y, drawingScale)); -- element.setAttribute("width", this.scaleDrawingValue(shape.geometry.width, drawingScale)); -- element.setAttribute("height", this.scaleDrawingValue(shape.geometry.height, drawingScale)); -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- if (geometryTool === "circle") { -- const element = document.createElementNS(SVG_NS, "circle"); -- element.setAttribute("cx", this.scaleDrawingValue(shape.geometry.cx, drawingScale)); -- element.setAttribute("cy", this.scaleDrawingValue(shape.geometry.cy, drawingScale)); -- element.setAttribute("r", this.scaleDrawingValue(shape.geometry.r, drawingScale)); -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- if (geometryTool === "ellipse") { -- const element = document.createElementNS(SVG_NS, "ellipse"); -- element.setAttribute("cx", this.scaleDrawingValue(shape.geometry.cx, drawingScale)); -- element.setAttribute("cy", this.scaleDrawingValue(shape.geometry.cy, drawingScale)); -- element.setAttribute("rx", this.scaleDrawingValue(shape.geometry.rx, drawingScale)); -- element.setAttribute("ry", this.scaleDrawingValue(shape.geometry.ry, drawingScale)); -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- if (geometryTool === "line") { -- const element = document.createElementNS(SVG_NS, "line"); -- element.setAttribute("x1", this.scaleDrawingValue(shape.geometry.point1.x, drawingScale)); -- element.setAttribute("y1", this.scaleDrawingValue(shape.geometry.point1.y, drawingScale)); -- element.setAttribute("x2", this.scaleDrawingValue(shape.geometry.point2.x, drawingScale)); -- element.setAttribute("y2", this.scaleDrawingValue(shape.geometry.point2.y, drawingScale)); -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- if (geometryTool === "polygon") { -- const roundedPath = this.roundedPointPath(shape, { closed: true, drawingScale }); -- const element = document.createElementNS(SVG_NS, roundedPath ? "path" : "polygon"); -- const points = this.svgPointList(shape.geometry.points, drawingScale); -- if (roundedPath) { -- element.setAttribute("d", roundedPath); -- element.dataset.roundedPointRender = "path"; -- } -- element.setAttribute("points", points); -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- if (geometryTool === "polyline") { -- const roundedPath = this.roundedPointPath(shape, { closed: false, drawingScale }); -- const element = document.createElementNS(SVG_NS, roundedPath ? "path" : "polyline"); -- const points = this.svgPointList(shape.geometry.points, drawingScale); -- if (roundedPath) { -- element.setAttribute("d", roundedPath); -- element.dataset.roundedPointRender = "path"; -- } -- element.setAttribute("points", points); -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- if (geometryTool === "arc") { -- const element = document.createElementNS(SVG_NS, "path"); -- element.setAttribute("d", this.arcPath({ -- ...shape.geometry, -- cx: this.scaleDrawingValue(shape.geometry.cx, drawingScale), -- cy: this.scaleDrawingValue(shape.geometry.cy, drawingScale), -- r: this.scaleDrawingValue(shape.geometry.r, drawingScale) -- })); -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- if (geometryTool === "text") { -- const element = document.createElementNS(SVG_NS, "text"); -- element.setAttribute("x", this.scaleDrawingValue(shape.geometry.x, drawingScale)); -- element.setAttribute("y", this.scaleDrawingValue(shape.geometry.y, drawingScale)); -- element.setAttribute("font-size", this.scaleDrawingValue(shape.geometry.fontSize, drawingScale)); -- element.textContent = shape.geometry.text; -- this.applySvgStyle(element, shape, { drawingScale, transformOrigin }); -- return element; -- } -- throw new Error(`unsupported shape tool ${shapeTool(shape)}`); -- } -- -- svgPointList(points, drawingScale = 1) { -- return points.map((point) => `${this.scaleDrawingValue(point.x, drawingScale)},${this.scaleDrawingValue(point.y, drawingScale)}`).join(" "); -- } -- -- roundedPointPath(shape, { closed, drawingScale = 1 } = {}) { -- const geometryTool = shapeGeometryTool(shape); -- if (!["polygon", "polyline", "rectangle"].includes(geometryTool)) { -- return ""; -- } -- const sourcePoints = geometryTool === "rectangle" ? this.shapeGeometryPoints(shape) : shape.geometry.points; -- if (!Array.isArray(sourcePoints)) { -- return ""; -- } -- const pointCount = sourcePoints.length; -- if (pointCount < (closed ? 3 : 3)) { -- return ""; -- } -- const pointRounding = this.shapePointRoundingValues(shape); -- const roundedIndexes = pointRounding -- .map((isRounded, index) => isRounded === true ? index : -1) -- .filter((index) => index >= 0 && (closed || (index > 0 && index < pointCount - 1))); -- if (!roundedIndexes.length) { -- return ""; -- } -- const points = sourcePoints.map((point) => ({ -- x: Number(this.scaleDrawingValue(point.x, drawingScale)), -- y: Number(this.scaleDrawingValue(point.y, drawingScale)) -- })); -- const preferredRadius = this.shapeRoundingRadius(shape) * drawingScale; -- if (preferredRadius <= 0) { -- return ""; -- } -- const roundedVertex = (index) => { -- if (pointRounding[index] !== true || (!closed && (index === 0 || index === pointCount - 1))) { -- return null; -- } -- const previous = points[index === 0 ? pointCount - 1 : index - 1]; -- const current = points[index]; -- const next = points[index === pointCount - 1 ? 0 : index + 1]; -- if (!previous || !current || !next) { -- return null; -- } -- const previousDistance = Math.hypot(previous.x - current.x, previous.y - current.y); -- const nextDistance = Math.hypot(next.x - current.x, next.y - current.y); -- if (previousDistance <= 0 || nextDistance <= 0) { -- return null; -- } -- const radius = Math.min(preferredRadius, previousDistance / 2, nextDistance / 2); -- if (radius <= 0) { -- return null; -- } -- return { -- after: { -- x: current.x + (next.x - current.x) / nextDistance * radius, -- y: current.y + (next.y - current.y) / nextDistance * radius -- }, -- before: { -- x: current.x + (previous.x - current.x) / previousDistance * radius, -- y: current.y + (previous.y - current.y) / previousDistance * radius -- }, -- current -- }; -- }; -- const commandPoint = (point) => `${this.formatViewportNumber(point.x)} ${this.formatViewportNumber(point.y)}`; -- const appendVertex = (commands, index) => { -- const rounded = roundedVertex(index); -- if (!rounded) { -- commands.push(`L ${commandPoint(points[index])}`); -- return; -- } -- commands.push(`L ${commandPoint(rounded.before)}`); -- commands.push(`Q ${commandPoint(rounded.current)} ${commandPoint(rounded.after)}`); -- }; -- if (closed) { -- const start = roundedVertex(0)?.after || points[0]; -- const commands = [`M ${commandPoint(start)}`]; -- for (let index = 1; index < pointCount; index += 1) { -- appendVertex(commands, index); -- } -- appendVertex(commands, 0); -- commands.push("Z"); -- return commands.join(" "); -- } -- const commands = [`M ${commandPoint(points[0])}`]; -- for (let index = 1; index < pointCount - 1; index += 1) { -- appendVertex(commands, index); -- } -- commands.push(`L ${commandPoint(points[pointCount - 1])}`); -- return commands.join(" "); -- } -- -- applySvgStyle(element, shape, { drawingScale = 1, transformOrigin = { x: 0, y: 0 } } = {}) { -- const geometryTool = shapeGeometryTool(shape); -- element.setAttribute("fill", shape.style.fill); -- element.setAttribute("stroke", shape.style.stroke); -- element.setAttribute("stroke-width", shape.style.strokeWidth); -- element.setAttribute("fill-opacity", shape.style.fillOpacity); -- element.setAttribute("stroke-opacity", shape.style.strokeOpacity); -- if (OPEN_POINT_STYLE_TOOLS.has(geometryTool)) { -- const lineCap = this.openShapeUsesSplitPointStyles(shape) ? "butt" : this.shapeStartPointStyle(shape); -- element.setAttribute("stroke-linecap", lineCap); -- element.setAttribute("stroke-linejoin", this.strokeLinejoinValue(this.shapeUnifiedPointStyle(shape))); -- element.dataset.pointStyle = this.shapeUnifiedPointStyle(shape); -- element.dataset.startPointStyle = this.shapeStartPointStyle(shape); -- element.dataset.endPointStyle = this.shapeEndPointStyle(shape); -- } else if (CLOSED_POINT_STYLE_TOOLS.has(geometryTool)) { -- const pointStyle = this.shapeUnifiedPointStyle(shape); -- element.setAttribute("stroke-linecap", pointStyle); -- element.setAttribute("stroke-linejoin", this.strokeLinejoinValue(pointStyle)); -- element.dataset.pointStyle = pointStyle; -- } -- const transform = this.scaledDrawingTransform(this.shapeTransform(shape), drawingScale); -- element.setAttribute("transform", this.svgTransformAttribute(transform, this.scaledDrawingPoint(transformOrigin, drawingScale))); -- } -- -- appendShapePointStyleCaps(parent, shape, { drawingScale = 1, transformOrigin = { x: 0, y: 0 } } = {}) { -- const endpoints = this.openShapeEndpointPoints(shape); -- const pointMarkers = this.shapePointStyleMarkers(shape); -- if (!this.openShapeUsesSplitPointStyles(shape) && !pointMarkers.length) { -- return; -- } -- const group = document.createElementNS(SVG_NS, "g"); -- group.classList.add("object-vector-studio-v2__point-style-caps"); -- group.dataset.pointStyleCaps = shapeTool(shape); -- group.setAttribute("aria-hidden", "true"); -- group.setAttribute("pointer-events", "none"); -- group.setAttribute("transform", this.svgTransformAttribute( -- this.scaledDrawingTransform(this.shapeTransform(shape), drawingScale), -- this.scaledDrawingPoint(transformOrigin, drawingScale) -- )); -- if (this.openShapeUsesSplitPointStyles(shape) && endpoints) { -- [ -- ["start", endpoints.start.point, this.shapeStartPointStyle(shape), endpoints.start.angle], -- ["end", endpoints.end.point, this.shapeEndPointStyle(shape), endpoints.end.angle] -- ].forEach(([endpoint, point, pointStyle, angle]) => { -- group.append(this.createPointStyleCapMarker(point, pointStyle, shape, endpoint, drawingScale, angle)); -- }); -- } -- pointMarkers.forEach(({ angle = 0, markerId, point, pointStyle }) => { -- group.append(this.createPointStyleCapMarker(point, pointStyle, shape, markerId, drawingScale, angle)); -- }); -- parent.append(group); -- } -- -- shapePointStyleMarkers(shape) { -- const geometryTool = shapeGeometryTool(shape); -- if (!["polygon", "polyline", "rectangle"].includes(geometryTool)) { -- return []; -- } -- const points = this.shapeGeometryPoints(shape); -- const styles = this.shapePointStyleValues(shape); -- return points -- .map((point, index) => ({ index, point, pointStyle: styles[index] })) -- .filter(({ index, pointStyle }) => pointStyle === "round" -- && !(geometryTool === "polyline" && (index === 0 || index === points.length - 1))) -- .map(({ index, point, pointStyle }) => ({ -- markerId: `point-${index}`, -- point, -- pointStyle -- })); -- } -- -- createPointStyleCapMarker(point, pointStyle, shape, endpoint, drawingScale, angle = 0) { -- const strokeWidth = Math.max(1, Number(shape.style?.strokeWidth) || 1); -- const size = strokeWidth; -- const x = this.scaleDrawingValue(point.x, drawingScale); -- const y = this.scaleDrawingValue(point.y, drawingScale); -- const marker = pointStyle === "square" -- ? document.createElementNS(SVG_NS, "rect") -- : document.createElementNS(SVG_NS, "circle"); -- marker.classList.add("object-vector-studio-v2__point-style-cap"); -- marker.dataset.pointStyleCap = endpoint; -- marker.dataset.pointStyle = pointStyle; -- marker.setAttribute("fill", shape.style.stroke); -- marker.setAttribute("fill-opacity", shape.style.strokeOpacity); -- marker.setAttribute("stroke", "none"); -- if (pointStyle === "square") { -- marker.setAttribute("x", x - size / 2); -- marker.setAttribute("y", y - size / 2); -- marker.setAttribute("width", size); -- marker.setAttribute("height", size); -- marker.setAttribute("transform", `rotate(${this.formatViewportNumber(angle)} ${this.formatViewportNumber(x)} ${this.formatViewportNumber(y)})`); -- return marker; -- } -- marker.setAttribute("cx", x); -- marker.setAttribute("cy", y); -- marker.setAttribute("r", size / 2); -- return marker; -- } -- -- openShapeEndpointPoints(shape) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "line") { -- const start = linePoint(shape.geometry, "point1"); -- const end = linePoint(shape.geometry, "point2"); -- const angle = this.angleBetweenPoints(start, end); -- return { -- end: { angle, point: end }, -- start: { angle, point: start } -- }; -- } -- if (geometryTool === "polyline" && Array.isArray(shape.geometry?.points) && shape.geometry.points.length >= 2) { -- const points = shape.geometry.points; -- return { -- end: { angle: this.angleBetweenPoints(points.at(-2), points.at(-1)), point: points.at(-1) }, -- start: { angle: this.angleBetweenPoints(points[0], points[1]), point: points[0] } -- }; -- } -- if (geometryTool === "arc") { -- const start = this.polarPoint(shape.geometry.cx, shape.geometry.cy, shape.geometry.r, shape.geometry.startAngle); -- const end = this.polarPoint(shape.geometry.cx, shape.geometry.cy, shape.geometry.r, shape.geometry.endAngle); -- return { -- end: { angle: Number(shape.geometry.endAngle) + 90, point: end }, -- start: { angle: Number(shape.geometry.startAngle) + 90, point: start } -- }; -- } -- return null; -- } -- -- angleBetweenPoints(start, end) { -- if (!start || !end) { -- return 0; -- } -- return Math.atan2(Number(end.y) - Number(start.y), Number(end.x) - Number(start.x)) * 180 / Math.PI; -- } -- -- shapeTransform(shape) { -- return this.ensureShapeTransform(shape || {}); -- } -- -- effectiveShape(shape, shapeIndex = this.selectedShapeIndex) { -- return this.effectiveShapeForFrame(shape, this.activeFrame(), shapeIndex); -- } -- -- effectiveShapeForFrame(shape, frame, shapeIndex) { -- const effective = JSON.parse(JSON.stringify(shape)); -- const override = frame?.shapeOverrides?.find((entry) => entry.shapeIndex === shapeIndex) || null; -- if (override && Object.prototype.hasOwnProperty.call(override, "visible")) { -- effective.visible = override.visible; -- } -- if (override?.transform) { -- effective.transform = { ...override.transform }; -- } -- return effective; -- } -- -- svgTransformAttribute(transform, origin = { x: 0, y: 0 }) { -- return objectVectorSvgTransformAttribute(transform, origin); -- } -- -- scaleDrawingValue(value, drawingScale = 1) { -- return drawingScale === 1 ? value : this.formatViewportNumber(value * drawingScale); -- } -- -- scaledDrawingTransform(transform, drawingScale = 1) { -- if (drawingScale === 1) { -- return transform; -- } -- return { -- ...transform, -- x: this.scaleDrawingValue(transform.x, drawingScale), -- y: this.scaleDrawingValue(transform.y, drawingScale) -- }; -- } -- -- scaledDrawingPoint(point, drawingScale = 1) { -- if (drawingScale === 1) { -- return point; -- } -- return { -- x: this.scaleDrawingValue(point.x, drawingScale), -- y: this.scaleDrawingValue(point.y, drawingScale) -- }; -- } -- -- scaledDrawingBounds(bounds, drawingScale = 1) { -- if (drawingScale === 1) { -- return bounds; -- } -- return { -- height: this.scaleDrawingValue(bounds.height, drawingScale), -- originX: this.scaleDrawingValue(bounds.originX, drawingScale), -- originY: this.scaleDrawingValue(bounds.originY, drawingScale), -- width: this.scaleDrawingValue(bounds.width, drawingScale), -- x: this.scaleDrawingValue(bounds.x, drawingScale), -- y: this.scaleDrawingValue(bounds.y, drawingScale) -- }; -- } -- -- transformedPoint(point, transform, origin = { x: 0, y: 0 }) { -- return transformObjectVectorShapePoint(point, transform, origin); -- } -- -- localPointFromTransformedPoint(point, transform, origin = { x: 0, y: 0 }) { -- const localPoint = inverseTransformObjectVectorShapePoint(point, transform, origin); -- return { -- x: this.formatViewportNumber(localPoint.x), -- y: this.formatViewportNumber(localPoint.y) -- }; -- } -- -- transformedBounds(shape, { drawingScale = 1, transformOrigin = { x: 0, y: 0 } } = {}) { -- return this.scaledDrawingBounds( -- transformedObjectVectorShapeBounds(shapeBoundsPoints(shape), this.shapeTransform(shape), transformOrigin), -- drawingScale -- ); -- } -- -- objectBounds(object, { drawingScale = 1, includeInvisible = true } = {}) { -- const activeFrame = object?.id === this.selectedObjectId ? this.activeFrame() : null; -- const transformOrigin = this.objectTransformOrigin(object); -- const objectScale = this.objectScalePreviewValueForObject(object); -- const shapes = sortedShapes(object) -- .map((shape, shapeIndex) => ({ shape, shapeIndex })) -- .map(({ shape, shapeIndex }) => ({ -- shape: this.shapeWithObjectScalePreview(this.effectiveShapeForFrame(shape, activeFrame, shapeIndex), objectScale), -- shapeIndex -- })) -- .filter((entry) => includeInvisible || entry.shape.visible !== false); -- if (!shapes.length) { -- return { -- height: 80, -- width: 120, -- x: -60, -- y: -40 -- }; -- } -- -- return combineObjectVectorBounds(shapes.map(({ shape }) => this.transformedBounds(shape, { drawingScale, transformOrigin }))); -- } -- -- rawVisibleObjectGeometryBounds(object) { -- const activeFrame = object?.id === this.selectedObjectId ? this.activeFrame() : null; -- const bounds = sortedShapes(object) -- .map((shape, shapeIndex) => this.effectiveShapeForFrame(shape, activeFrame, shapeIndex)) -- .filter((shape) => shape.visible !== false) -- .map((shape) => shapeBounds(shape)); -- if (!bounds.length) { -- return null; -- } -- return combineObjectVectorBounds(bounds); -- } -- -- objectBoundsForFrame(object, frame) { -- const transformOrigin = this.objectTransformOrigin(object); -- const shapes = sortedShapes(object) -- .map((shape, shapeIndex) => this.effectiveShapeForFrame(shape, frame, shapeIndex)) -- .filter((shape) => shape.visible); -- if (!shapes.length) { -- return { -- height: 80, -- width: 120, -- x: -60, -- y: -40 -- }; -- } -- return combineObjectVectorBounds(shapes.map((shape) => this.transformedBounds(shape, { transformOrigin }))); -- } -- -- shapeSetBounds(object, shapeIndexes, { drawingScale = 1, includeInvisible = false } = {}) { -- const activeFrame = object?.id === this.selectedObjectId ? this.activeFrame() : null; -- const objectShapes = sortedShapes(object); -- const transformOrigin = this.objectTransformOrigin(object); -- const objectScale = this.objectScalePreviewValueForObject(object); -- const targetIndexes = Array.from(new Set(Array.from(shapeIndexes || []) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0 && shapeIndex < objectShapes.length))) -- .sort((left, right) => left - right); -- const shapes = targetIndexes -- .map((shapeIndex) => this.shapeWithObjectScalePreview(this.effectiveShapeForFrame(objectShapes[shapeIndex], activeFrame, shapeIndex), objectScale)) -- .filter((shape) => includeInvisible || shape.visible !== false); -- if (!shapes.length) { -- return null; -- } -- return combineObjectVectorBounds(shapes.map((shape) => this.transformedBounds(shape, { drawingScale, transformOrigin }))); -- } -- -- renderSelectionOverlay(object) { -- const selectedShape = this.selectedShape(); -- if (!selectedShape || selectedShape.visible === false) { -- return; -- } -- try { -- const selectedIndexes = this.selectedPreviewDragIndexes(object, this.selectedShapeIndex); -- const transformOrigin = this.objectTransformOrigin(object); -- const objectScale = this.objectScalePreviewValueForObject(object); -- const bounds = selectedIndexes.length > 1 -- ? this.shapeSetBounds(object, selectedIndexes, { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE }) -- : this.transformedBounds(this.shapeWithObjectScalePreview(this.effectiveShape(selectedShape), objectScale), { drawingScale: OBJECT_PREVIEW_DRAWING_SCALE, transformOrigin }); -- if (!bounds) { -- return; -- } -- const dragArea = document.createElementNS(SVG_NS, "rect"); -- dragArea.classList.add("object-vector-studio-v2__selection-drag-area"); -- dragArea.dataset.selectionDragBounds = selectedIndexes.join(","); -- dragArea.setAttribute("x", bounds.x - 4); -- dragArea.setAttribute("y", bounds.y - 4); -- dragArea.setAttribute("width", bounds.width + 8); -- dragArea.setAttribute("height", bounds.height + 8); -- dragArea.addEventListener("pointerdown", (event) => this.startPreviewSelectionBoundsMove(event)); -- dragArea.addEventListener("click", (event) => this.handleSelectionDragAreaClick(event)); -- dragArea.addEventListener("contextmenu", (event) => this.handleSelectionDragAreaContextMenu(event)); -- this.elements.renderSurface.insertBefore(dragArea, this.elements.renderSurface.querySelector(".object-vector-studio-v2__shape")); -- -- const box = document.createElementNS(SVG_NS, "rect"); -- box.classList.add("object-vector-studio-v2__selection-box"); -- box.dataset.selectionBounds = String(this.selectedShapeIndex); -- box.setAttribute("x", bounds.x - 4); -- box.setAttribute("y", bounds.y - 4); -- box.setAttribute("width", bounds.width + 8); -- box.setAttribute("height", bounds.height + 8); -- this.elements.renderSurface.append(box); -- -- [ -- [bounds.x - 4, bounds.y - 4, "nw"], -- [bounds.x + bounds.width + 4, bounds.y - 4, "ne"], -- [bounds.x - 4, bounds.y + bounds.height + 4, "sw"], -- [bounds.x + bounds.width + 4, bounds.y + bounds.height + 4, "se"] -- ].forEach(([cx, cy, handle]) => { -- const point = document.createElementNS(SVG_NS, "rect"); -- point.classList.add("object-vector-studio-v2__resize-handle"); -- point.dataset.resizeHandle = handle; -- point.dataset.resizeShapeIndex = String(this.selectedShapeIndex); -- if (shapeGeometryTool(selectedShape) === "rectangle") { -- point.classList.add("object-vector-studio-v2__geometry-point-handle"); -- point.dataset.geometryPointHandle = `rectangle-${handle}`; -- point.dataset.geometryPointKind = "rectangle-corner"; -- } -- const handleSize = shapeGeometryTool(selectedShape) === "circle" ? 5 : 3; -- point.setAttribute("x", cx - handleSize / 2); -- point.setAttribute("y", cy - handleSize / 2); -- point.setAttribute("width", handleSize); -- point.setAttribute("height", handleSize); -- point.addEventListener("pointerdown", (event) => this.startPreviewHandleEdit(event, this.selectedShapeIndex, { handle, mode: "resize" })); -- this.elements.renderSurface.append(point); -- }); -- -- this.renderGeometryPointHandles(selectedShape); -- } catch (error) { -- this.statusLog.write(`FAIL Selection overlay render failed for ${object.name}/shape-${this.selectedShapeIndex} (${shapeTool(selectedShape)}): ${error.message}`); -- } -- } -- -- selectedPreviewDragIndexes(object, preferredShapeIndex = this.selectedShapeIndex) { -- const objectShapes = sortedShapes(object); -- const preferredIndex = normalizeShapeIndex(preferredShapeIndex); -- const selectedIndexes = Array.from(this.selectedShapeIndexes || []) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0 && shapeIndex < objectShapes.length && objectShapes[shapeIndex]?.visible !== false); -- if (selectedIndexes.length) { -- return Array.from(new Set(selectedIndexes)).sort((left, right) => left - right); -- } -- if (preferredIndex >= 0 && preferredIndex < objectShapes.length && objectShapes[preferredIndex]?.visible !== false) { -- return [preferredIndex]; -- } -- return []; -- } -- -- selectedPreviewDragGroupId(objectShapes, targetIndexes) { -- if (!Array.isArray(targetIndexes) || targetIndexes.length < 2) { -- return ""; -- } -- const groupIds = new Set(targetIndexes -- .map((shapeIndex) => String(objectShapes[shapeIndex]?.groupId || "").trim()) -- .filter(Boolean)); -- return groupIds.size === 1 ? Array.from(groupIds)[0] : ""; -- } -- -- selectedPreviewHitShapeIndex(object, shapeIndexes, point) { -- const objectShapes = sortedShapes(object); -- const activeFrame = object?.id === this.selectedObjectId ? this.activeFrame() : null; -- const transformOrigin = this.objectTransformOrigin(object); -- const objectScale = this.objectScalePreviewValueForObject(object); -- return Array.from(shapeIndexes || []) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0 && shapeIndex < objectShapes.length) -- .reverse() -- .find((shapeIndex) => { -- const effectiveShape = this.shapeWithObjectScalePreview(this.effectiveShapeForFrame(objectShapes[shapeIndex], activeFrame, shapeIndex), objectScale); -- if (!effectiveShape || effectiveShape.visible === false) { -- return false; -- } -- const bounds = this.transformedBounds(effectiveShape, { transformOrigin }); -- return point.x >= bounds.x - 4 -- && point.x <= bounds.x + bounds.width + 4 -- && point.y >= bounds.y - 4 -- && point.y <= bounds.y + bounds.height + 4; -- }); -- } -- -- handleSelectionDragAreaContextMenu(event) { -- const object = this.selectedObject(); -- const targetIndexes = this.selectedPreviewDragIndexes(object, this.selectedShapeIndex); -- const shapeIndex = this.selectedPreviewHitShapeIndex(object, targetIndexes, this.pointerPreviewPoint(event)); -- if (!Number.isInteger(shapeIndex)) { -- event.preventDefault(); -- return; -- } -- this.handleShapeContextMenu(event, sortedShapes(object)[shapeIndex], shapeIndex); -- } -- -- handleSelectionDragAreaClick(event) { -- event.stopPropagation(); -- if (Date.now() < (this.previewClickSuppressedUntil || 0)) { -- event.preventDefault(); -- return; -- } -- const object = this.selectedObject(); -- const targetIndexes = this.selectedPreviewDragIndexes(object, this.selectedShapeIndex); -- const shapeIndex = this.selectedPreviewHitShapeIndex(object, targetIndexes, this.pointerPreviewPoint(event)); -- if (!Number.isInteger(shapeIndex)) { -- return; -- } -- this.handleShapePointer(event, sortedShapes(object)[shapeIndex], shapeIndex, { source: "selection bounds click" }); -- } -- -- renderGeometryPointHandles(shape) { -- const effectiveShape = this.effectiveShape(shape); -- const renderShape = this.shapeWithObjectScalePreview(effectiveShape, this.objectScalePreviewValueForObject(this.selectedObject())); -- const renderTransform = this.shapeTransform(renderShape); -- const transformOrigin = this.objectTransformOrigin(this.selectedObject()); -- this.geometryPointHandleDefinitions(effectiveShape).forEach((definition) => { -- const position = this.transformedPoint(definition.point, renderTransform, transformOrigin); -- const point = document.createElementNS(SVG_NS, "rect"); -- point.classList.add("object-vector-studio-v2__resize-handle", "object-vector-studio-v2__geometry-point-handle"); -- if (definition.className) { -- point.classList.add(definition.className); -- } -- point.dataset.geometryPointHandle = definition.id; -- point.dataset.geometryPointKind = definition.kind; -- point.dataset.resizeShapeIndex = String(this.selectedShapeIndex); -- if (definition.endpoint) { -- point.dataset.lineEndpoint = definition.endpoint; -- } -- point.setAttribute("x", this.scaleDrawingValue(position.x, OBJECT_PREVIEW_DRAWING_SCALE) - 2); -- point.setAttribute("y", this.scaleDrawingValue(position.y, OBJECT_PREVIEW_DRAWING_SCALE) - 2); -- point.setAttribute("width", 4); -- point.setAttribute("height", 4); -- point.setAttribute("aria-label", definition.label); -- point.addEventListener("pointerdown", (event) => this.startPreviewHandleEdit(event, this.selectedShapeIndex, definition.editOptions)); -- this.elements.renderSurface.append(point); -- }); -- } -- -- geometryPointHandleDefinitions(shape) { -- const geometryTool = shapeGeometryTool(shape); -- const geometry = shape.geometry; -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- return geometry.points.map((point, index) => ({ -- editOptions: { -- mode: "geometry-point", -- point: { ...point }, -- pointIndex: index -- }, -- id: `${geometryTool}-${index}`, -- kind: this.isTriangleShape(shape) ? "triangle-point" : `${geometryTool}-point`, -- label: `Move point ${index + 1}`, -- point -- })); -- } -- if (geometryTool === "line") { -- return [ -- ["start", "point1", linePoint(geometry, "point1")], -- ["end", "point2", linePoint(geometry, "point2")] -- ].map(([endpoint, key, point], index) => ({ -- className: "object-vector-studio-v2__line-endpoint-handle", -- editOptions: { -- endpoint, -- mode: "line-endpoint", -- point: { ...point } -- }, -- endpoint, -- id: `line-${endpoint}`, -- kind: key, -- label: `Move line point ${index + 1}`, -- point -- })); -- } -- if (geometryTool === "circle") { -- return [ -- { -- control: "center", -- id: "circle-center", -- kind: "center", -- label: "Move circle center", -- point: { x: geometry.cx, y: geometry.cy } -- }, -- { -- control: "radius", -- id: "circle-radius", -- kind: "radius", -- label: "Adjust circle radius", -- point: { x: geometry.cx + geometry.r, y: geometry.cy } -- } -- ].map((definition) => ({ -- ...definition, -- editOptions: { -- control: definition.control, -- mode: "geometry-point", -- point: { ...definition.point } -- } -- })); -- } -- if (geometryTool === "ellipse") { -- return [ -- { -- control: "center", -- id: "ellipse-center", -- kind: "center", -- label: "Move ellipse center", -- point: { x: geometry.cx, y: geometry.cy } -- }, -- { -- control: "rx", -- id: "ellipse-rx", -- kind: "rx", -- label: "Adjust ellipse rx", -- point: { x: geometry.cx + geometry.rx, y: geometry.cy } -- }, -- { -- control: "ry", -- id: "ellipse-ry", -- kind: "ry", -- label: "Adjust ellipse ry", -- point: { x: geometry.cx, y: geometry.cy + geometry.ry } -- } -- ].map((definition) => ({ -- ...definition, -- editOptions: { -- control: definition.control, -- mode: "geometry-point", -- point: { ...definition.point } -- } -- })); -- } -- if (geometryTool === "arc") { -- return [ -- { -- control: "center", -- id: "arc-center", -- kind: "center", -- label: "Move arc center", -- point: { x: geometry.cx, y: geometry.cy } -- }, -- { -- control: "start", -- id: "arc-start", -- kind: "startAngle", -- label: "Move arc start point", -- point: this.polarPoint(geometry.cx, geometry.cy, geometry.r, geometry.startAngle) -- }, -- { -- control: "end", -- id: "arc-end", -- kind: "endAngle", -- label: "Move arc end point", -- point: this.polarPoint(geometry.cx, geometry.cy, geometry.r, geometry.endAngle) -- } -- ].map((definition) => ({ -- ...definition, -- editOptions: { -- control: definition.control, -- mode: "geometry-point", -- point: { ...definition.point } -- } -- })); -- } -- if (geometryTool === "text") { -- const point = { x: geometry.x, y: geometry.y }; -- return [ -- { -- editOptions: { -- control: "anchor", -- mode: "geometry-point", -- point: { ...point } -- }, -- id: "text-anchor", -- kind: "anchor", -- label: "Move text anchor", -- point -- } -- ]; -- } -- return []; -- } -- -- arcPath(geometry) { -- const start = this.polarPoint(geometry.cx, geometry.cy, geometry.r, geometry.startAngle); -- const end = this.polarPoint(geometry.cx, geometry.cy, geometry.r, geometry.endAngle); -- const largeArc = Math.abs(geometry.endAngle - geometry.startAngle) > 180 ? 1 : 0; -- return `M ${start.x} ${start.y} A ${geometry.r} ${geometry.r} 0 ${largeArc} 1 ${end.x} ${end.y}`; -- } -- -- polarPoint(cx, cy, radius, degrees) { -- const radians = (degrees - 90) * Math.PI / 180; -- return { -- x: Number((cx + radius * Math.cos(radians)).toFixed(3)), -- y: Number((cy + radius * Math.sin(radians)).toFixed(3)) -- }; -- } -- -- updateViewport() { -- const width = DEFAULT_VIEWPORT.width / this.viewport.zoom; -- const height = DEFAULT_VIEWPORT.height / this.viewport.zoom; -- const viewX = this.formatViewportNumber(this.viewport.x - width / 2); -- const viewY = this.formatViewportNumber(this.viewport.y - height / 2); -- this.elements.renderSurface.setAttribute("viewBox", `${viewX} ${viewY} ${this.formatViewportNumber(width)} ${this.formatViewportNumber(height)}`); -- this.elements.coordinateDisplay.textContent = `Origin: ${this.formatLogicalCoordinate(this.viewport.x)}, ${this.formatLogicalCoordinate(this.viewport.y)} | ${this.canvasOriginDisplayText()} | Zoom ${this.formatZoomPercentage()}%`; -- } -- -- formatViewportNumber(value) { -- const normalized = Number(Number(value).toFixed(3)); -- return Object.is(normalized, -0) ? 0 : normalized; -- } -- -- formatCoordinateInputValue(value) { -- return this.formatViewportNumber(value).toFixed(3); -- } -- -- canvasOriginDisplayText() { -- const x = this.formatLogicalCoordinate(-this.viewport.x); -- const y = this.formatLogicalCoordinate(-this.viewport.y); -- if (x === 0 && y === 0) { -- return "Canvas origin 0,0 centered"; -- } -- return `Canvas origin ${x},${y} from center`; -- } -- -- formatLogicalCoordinate(value) { -- return this.formatViewportNumber(value / GRID_STEP); -- } -- -- formatLogicalPointerCoordinate(value) { -- return Math.round(this.formatLogicalCoordinate(value)); -- } -- -- formatZoomPercentage() { -- return Math.round(this.viewport.zoom * 100); -- } -- -- zoomViewportByStep(step) { -- const nextZoom = Number((this.viewport.zoom + step).toFixed(3)); -- this.zoomViewport(nextZoom); -- } -- -- zoomViewportByStepAtPointer(step, event) { -- const nextZoom = Number((this.viewport.zoom + step).toFixed(3)); -- this.zoomViewportAtPointer(nextZoom, event); -- } -- -- zoomViewport(nextZoom) { -- if (!Number.isFinite(nextZoom)) { -- this.statusLog.write("WARN Viewport zoom skipped: zoom value is invalid."); -- return; -- } -- this.viewport.zoom = this.clampedViewportZoom(nextZoom); -- this.updateViewport(); -- this.renderWorkSurface(); -- this.statusLog.write(`OK Viewport zoom set to ${this.formatZoomPercentage()}%.`); -- } -- -- zoomViewportAtPointer(nextZoom, event) { -- if (!Number.isFinite(nextZoom)) { -- this.statusLog.write("WARN Viewport zoom skipped: zoom value is invalid."); -- return; -- } -- const anchor = this.viewportPointFromEvent(event); -- if (!anchor) { -- this.zoomViewport(nextZoom); -- return; -- } -- const zoom = this.clampedViewportZoom(nextZoom); -- const viewWidth = DEFAULT_VIEWPORT.width / zoom; -- const viewHeight = DEFAULT_VIEWPORT.height / zoom; -- this.viewport.zoom = zoom; -- this.viewport.x = Number((anchor.x - (anchor.ratioX - 0.5) * viewWidth).toFixed(6)); -- this.viewport.y = Number((anchor.y - (anchor.ratioY - 0.5) * viewHeight).toFixed(6)); -- this.updateViewport(); -- this.renderWorkSurface(); -- this.updateCoordinateDisplay(event); -- this.statusLog.write(`OK Viewport zoom set to ${this.formatZoomPercentage()}%.`); -- } -- -- clampedViewportZoom(zoom) { -- return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Number(zoom.toFixed(3)))); -- } -- -- panViewport(x, y) { -- this.viewport.x = Number((this.viewport.x + x).toFixed(3)); -- this.viewport.y = Number((this.viewport.y + y).toFixed(3)); -- this.updateViewport(); -- this.statusLog.write(`OK Viewport pan set to ${this.viewport.x}, ${this.viewport.y}.`); -- } -- -- toggleCenterOriginMarker() { -- this.centerOriginVisible = !this.centerOriginVisible; -- this.window.sessionStorage?.setItem(CENTER_ORIGIN_SESSION_KEY, this.centerOriginVisible ? "1" : "0"); -- this.applySnapState(); -- this.renderWorkSurface(); -- this.statusLog.write(`OK Center dot ${this.centerOriginVisible ? "visible" : "hidden"}.`); -- } -- -- resetViewport() { -- this.viewport = { ...DEFAULT_VIEWPORT }; -- this.updateViewport(); -- this.statusLog.write(`OK Viewport reset to ${this.formatZoomPercentage()}% at origin 0,0.`); -- } -- -- updateCoordinateDisplay(event) { -- const point = this.viewportPointFromEvent(event); -- if (!point) { -- return; -- } -- this.elements.coordinateDisplay.textContent = `Pointer ${this.formatLogicalPointerCoordinate(point.x)}, ${this.formatLogicalPointerCoordinate(point.y)} | ${this.canvasOriginDisplayText()} | Zoom ${this.formatZoomPercentage()}%`; -- } -- -- viewportPointFromEvent(event) { -- return this.viewportPointFromClient(event.clientX, event.clientY); -- } -- -- viewportPointFromClient(clientX, clientY, zoom = this.viewport.zoom, viewport = this.viewport) { -- const bounds = this.elements.renderSurface.getBoundingClientRect(); -- if (!bounds.width || !bounds.height) { -- return null; -- } -- const ratioX = (clientX - bounds.left) / bounds.width; -- const ratioY = (clientY - bounds.top) / bounds.height; -- const viewWidth = DEFAULT_VIEWPORT.width / zoom; -- const viewHeight = DEFAULT_VIEWPORT.height / zoom; -- return { -- ratioX, -- ratioY, -- x: viewport.x - viewWidth / 2 + ratioX * viewWidth, -- y: viewport.y - viewHeight / 2 + ratioY * viewHeight -- }; -- } -- -- pointerPreviewPoint(event) { -- const point = this.viewportPointFromEvent(event); -- if (!point) { -- return { x: 0, y: 0 }; -- } -- return { -- x: this.formatViewportNumber(point.x / OBJECT_PREVIEW_DRAWING_SCALE), -- y: this.formatViewportNumber(point.y / OBJECT_PREVIEW_DRAWING_SCALE) -- }; -- } -- -- startDrawingMode(tool) { -- if (!PRIMITIVE_TOOLS.includes(tool)) { -- this.statusLog.write(`WARN Drawing mode skipped: ${tool || "unknown"} is not a drawable shape tool.`); -- return; -- } -- this.activeTool = tool; -- this.previewPointerEdit = null; -- this.activeDrawing = this.createDrawingState(tool); -- this.drawingPreviewPoint = null; -- this.drawingHintClientPoint = null; -- this.renderWorkSurface(); -- const objectHint = this.selectedObject() ? "" : " Select a schema-valid object before committing geometry."; -- const drawingHelp = ["polygon", "polyline"].includes(tool) -- ? "Click to add points.\n\nDouble-click to finish." -- : "Click once to start, move to preview, then click again to finish."; -- this.statusLog.write(`OK Drawing mode selected: ${shapeTypeLabel(tool)}. ${drawingHelp}${objectHint}`); -- } -- -- createDrawingState(tool) { -- const flow = ["polygon", "polyline"].includes(tool) -- ? "points" -- : "two-click"; -- return { -- flow, -- points: [], -- preview: null, -- start: null, -- style: this.drawingStyleFromActivePalette(), -- tool -- }; -- } -- -- drawingStyleFromActivePalette() { -- const { pointStyle, startPointStyle, endPointStyle, strokeLinecap, ...styleDefault } = this.schemaDefault("style"); -- const strokeWidth = Number(this.elements.strokeWidth?.value); -- return { -- ...styleDefault, -- fill: TRANSPARENT_STYLE_COLOR, -- fillOpacity: this.selectedFillOpacity, -- stroke: this.selectedStrokeColor || this.currentTargetColor() || "#ffffff", -- strokeOpacity: this.selectedStrokeOpacity, -- strokeWidth: Number.isFinite(strokeWidth) && strokeWidth > 0 ? strokeWidth : styleDefault.strokeWidth -- }; -- } -- -- cancelActiveDrawing(sourceLabel = "cancel", { log = true } = {}) { -- if (!this.activeDrawing) { -- return; -- } -- const tool = this.activeDrawing.tool; -- this.activeDrawing = null; -- this.drawingPreviewPoint = null; -- this.drawingHintClientPoint = null; -- this.activeTool = "select"; -- const selectButton = this.elements.toolToggles.find((candidate) => candidate.dataset.shapeTool === "select") || null; -- this.setActiveToolButton(selectButton); -- this.renderWorkSurface(); -- if (log) { -- this.statusLog.write(`OK Canceled ${shapeTypeLabel(tool)} drawing from ${sourceLabel}; no invalid geometry was committed.`); -- } -- } -- -- handleRenderSurfacePointerDown(event) { -- if (this.activeDrawing) { -- this.handleDrawingPointerDown(event); -- return; -- } -- if (event.button !== 0 || this.previewPointerEdit) { -- return; -- } -- if (this.isEmptyCanvasPointerTarget(event.target)) { -- if (this.startPreviewObjectMove(event)) { -- return; -- } -- if (this.startViewportPan(event)) { -- return; -- } -- this.deselectShape("empty canvas"); -- } -- } -- -- isEmptyCanvasPointerTarget(target) { -- if (target === this.elements.renderSurface) { -- return true; -- } -- return Boolean(target?.classList?.contains("object-vector-studio-v2__svg-grid") -- || target?.closest?.(".object-vector-studio-v2__svg-grid") -- || target?.classList?.contains("object-vector-studio-v2__center-origin-dot")); -- } -- -- canDragSelectedObjectFromEmptyCanvas() { -- const object = this.selectedObject(); -- if (!object || this.activeDrawing || this.activeTool !== "select") { -- return false; -- } -- if (this.selectedShapeIndex >= 0 || this.selectedShapeIndexes.size || this.directSelectedShapeIndexes.size) { -- return false; -- } -- const targetIndexes = this.selectedObjectShapeIndexes(object); -- if (!targetIndexes.length || this.isObjectLocked(object.id)) { -- return false; -- } -- return !targetIndexes.some((shapeIndex) => sortedShapes(object)[shapeIndex]?.locked); -- } -- -- canPanViewportFromEmptyCanvas() { -- return !this.activeDrawing -- && !this.previewPointerEdit -- && this.activeTool === "select" -- && this.selectedShapeIndex < 0 -- && !this.selectedShapeIndexes.size -- && !this.directSelectedShapeIndexes.size -- && !this.canDragSelectedObjectFromEmptyCanvas(); -- } -- -- updateRenderSurfaceCursorState() { -- const draggingObject = this.previewPointerEdit?.mode === "move-object"; -- const panningViewport = Boolean(this.viewportPan); -- this.elements.renderSurface.classList.toggle("is-empty-canvas-object-drag-ready", this.canDragSelectedObjectFromEmptyCanvas()); -- this.elements.renderSurface.classList.toggle("is-empty-canvas-object-dragging", draggingObject); -- this.elements.renderSurface.classList.toggle("is-canvas-pan-ready", this.canPanViewportFromEmptyCanvas()); -- this.elements.renderSurface.classList.toggle("is-canvas-panning", panningViewport); -- } -- -- deselectShape(sourceLabel = "selection") { -- if (this.selectedShapeIndex < 0 && !this.selectedShapeIndexes.size && !this.directSelectedShapeIndexes.size) { -- return; -- } -- this.selectedShapeIndex = -1; -- this.selectedShapeIndexes.clear(); -- this.directSelectedShapeIndexes.clear(); -- this.renderObjectTiles(); -- this.renderSelectedObject(); -- this.renderWorkSurface(); -- this.updateObjectActionState(); -- this.statusLog.write(`OK Deselected shape from ${sourceLabel}.`); -- } -- -- handleDrawingPointerDown(event) { -- const drawing = this.activeDrawing; -- if (!drawing || event.button !== 0) { -- return; -- } -- event.preventDefault(); -- event.stopPropagation(); -- const point = this.snapDrawingPoint(this.pointerPreviewPoint(event), drawing); -- this.drawingPreviewPoint = point; -- this.drawingHintClientPoint = { x: event.clientX, y: event.clientY }; -- if (drawing.flow === "points") { -- if (!drawing.points.length) { -- drawing.style = this.drawingStyleFromActivePalette(); -- } -- drawing.points.push(point); -- drawing.preview = point; -- this.renderWorkSurface(); -- this.statusLog.write(`OK Added ${shapeTypeLabel(drawing.tool)} point ${drawing.points.length}.`); -- return; -- } -- if (drawing.flow === "two-click") { -- if (!drawing.start) { -- drawing.style = this.drawingStyleFromActivePalette(); -- drawing.start = point; -- drawing.preview = null; -- this.renderWorkSurface(); -- this.statusLog.write(`OK Started ${shapeTypeLabel(drawing.tool)} drawing.`); -- return; -- } -- drawing.preview = point; -- const geometry = this.geometryFromDrawingPoints(drawing.tool, [drawing.start, point], point); -- this.commitDrawnShape(drawing.tool, geometry, "canvas click"); -- return; -- } -- this.renderWorkSurface(); -- } -- -- updateDrawingPreview(event) { -- const drawing = this.activeDrawing; -- if (!drawing) { -- return; -- } -- if (drawing.flow === "two-click" && !drawing.start) { -- return; -- } -- const point = this.snapDrawingPoint(this.pointerPreviewPoint(event), drawing); -- if (drawing.preview && Math.abs(point.x - drawing.preview.x) < 0.001 && Math.abs(point.y - drawing.preview.y) < 0.001) { -- return; -- } -- drawing.preview = point; -- this.drawingPreviewPoint = point; -- this.drawingHintClientPoint = { x: event.clientX, y: event.clientY }; -- this.renderWorkSurface(); -- } -- -- finishMultiPointDrawing(sourceLabel = "finish", event = null) { -- const drawing = this.activeDrawing; -- if (!drawing || drawing.flow !== "points" || !["polygon", "polyline"].includes(drawing.tool)) { -- return; -- } -- if (event) { -- event.preventDefault(); -- event.stopPropagation(); -- } -- const geometry = this.geometryFromDrawingPoints(drawing.tool, drawing.points, drawing.preview || drawing.points.at(-1)); -- this.commitDrawnShape(drawing.tool, geometry, sourceLabel); -- } -- -- previewShapeFromDrawing() { -- const drawing = this.activeDrawing; -- if (!drawing) { -- return null; -- } -- const points = [...drawing.points]; -- if (drawing.flow === "points" && drawing.preview && points.length) { -- points.push(drawing.preview); -- } -- if (drawing.flow === "two-click" && drawing.start && drawing.preview) { -- points.push(drawing.start, drawing.preview); -- } -- if (!points.length) { -- return null; -- } -- const geometry = this.geometryFromDrawingPoints(drawing.tool === "polygon" && points.length < 4 ? "polyline" : drawing.tool, points, drawing.preview); -- if (!geometry) { -- return null; -- } -- const tool = drawing.tool === "polygon" && points.length < 4 ? "polyline" : drawing.tool; -- const shape = { -- geometry, -- locked: false, -- order: 0, -- style: { -- ...drawing.style -- }, -- tool, -- transform: defaultShapeTransform(), -- visible: true -- }; -- return shape; -- } -- -- geometryFromDrawingPoints(tool, points, previewPoint = null) { -- const cleanPoints = points -- .filter(Boolean) -- .map((point) => ({ -- x: this.formatViewportNumber(point.x), -- y: this.formatViewportNumber(point.y) -- })); -- if (tool === "line") { -- if (cleanPoints.length < 2) { -- return null; -- } -- return { point1: cleanPoints[0], point2: cleanPoints[1] }; -- } -- if (tool === "polygon" || tool === "polyline") { -- return { points: cleanPoints }; -- } -- if (tool === "rectangle" || tool === "square") { -- if (cleanPoints.length < 2) { -- return null; -- } -- const [start, end] = cleanPoints; -- const width = Math.abs(end.x - start.x); -- const height = Math.abs(end.y - start.y); -- if (tool === "square") { -- const size = Math.max(width, height); -- return { -- height: size, -- width: size, -- x: end.x < start.x ? start.x - size : start.x, -- y: end.y < start.y ? start.y - size : start.y -- }; -- } -- return { -- height, -- width, -- x: Math.min(start.x, end.x), -- y: Math.min(start.y, end.y) -- }; -- } -- if (tool === "triangle") { -- if (cleanPoints.length < 2) { -- return null; -- } -- const [start, end] = cleanPoints; -- const minX = Math.min(start.x, end.x); -- const maxX = Math.max(start.x, end.x); -- const minY = Math.min(start.y, end.y); -- const maxY = Math.max(start.y, end.y); -- return { -- points: [ -- { x: Number(((minX + maxX) / 2).toFixed(3)), y: minY }, -- { x: maxX, y: maxY }, -- { x: minX, y: maxY } -- ] -- }; -- } -- if (tool === "circle") { -- if (cleanPoints.length < 2) { -- return null; -- } -- const [center, edge] = cleanPoints; -- return { -- cx: center.x, -- cy: center.y, -- r: this.radiusFromControlPoint(center, edge) -- }; -- } -- if (tool === "ellipse") { -- if (cleanPoints.length < 2) { -- return null; -- } -- const [start, end] = cleanPoints; -- return { -- cx: Number(((start.x + end.x) / 2).toFixed(3)), -- cy: Number(((start.y + end.y) / 2).toFixed(3)), -- rx: Number((Math.max(0, Math.abs(end.x - start.x)) / 2).toFixed(3)), -- ry: Number((Math.max(0, Math.abs(end.y - start.y)) / 2).toFixed(3)) -- }; -- } -- if (tool === "arc") { -- if (cleanPoints.length < 2) { -- return null; -- } -- const [center, edge] = cleanPoints; -- return { -- cx: center.x, -- cy: center.y, -- endAngle: 135, -- r: this.radiusFromControlPoint(center, edge), -- startAngle: -135 -- }; -- } -- if (tool === "text") { -- if (cleanPoints.length < 2) { -- return null; -- } -- const [start, end] = cleanPoints; -- const width = Math.abs(end.x - start.x); -- const height = Math.abs(end.y - start.y); -- if (width <= 0 && height <= 0) { -- return null; -- } -- const fontSize = Number(Math.max(1, height || width * 0.5).toFixed(3)); -- return { -- fontSize, -- text: "Text", -- x: Math.min(start.x, end.x), -- y: Number((Math.min(start.y, end.y) + fontSize).toFixed(3)) -- }; -- } -- return null; -- } -- -- validateDrawnGeometry(tool, geometry) { -- if (!geometry) { -- return `${shapeTypeLabel(tool)} drawing has no geometry.`; -- } -- if (tool === "line") { -- const distance = Math.hypot(geometry.point2.x - geometry.point1.x, geometry.point2.y - geometry.point1.y); -- return distance > 0 ? "" : "line requires two distinct points."; -- } -- if (tool === "polyline") { -- return geometry.points.length >= 2 ? "" : "polyline requires at least two points."; -- } -- if (tool === "polygon") { -- return geometry.points.length >= 4 ? "" : "polygon requires at least four points."; -- } -- if (tool === "triangle") { -- return geometry.points.length === 3 ? "" : "triangle requires exactly three points."; -- } -- if (tool === "rectangle" || tool === "square") { -- return geometry.width > 0 && geometry.height > 0 ? "" : `${shapeTypeLabel(tool)} requires non-zero width and height.`; -- } -- if (tool === "circle" || tool === "arc") { -- return geometry.r > 0 ? "" : `${shapeTypeLabel(tool)} requires a non-zero radius.`; -- } -- if (tool === "ellipse") { -- return geometry.rx > 0 && geometry.ry > 0 ? "" : "ellipse requires non-zero rx and ry."; -- } -- if (tool === "text") { -- return geometry.text && geometry.fontSize > 0 ? "" : "text requires two distinct points, content, and a positive font size."; -- } -- return ""; -- } -- -- commitDrawnShape(type, geometry, sourceLabel) { -- const geometryError = this.validateDrawnGeometry(type, geometry); -- if (geometryError) { -- this.statusLog.write(`FAIL Create ${type} blocked: ${geometryError}`); -- return; -- } -- this.createShape(type, { geometry, sourceLabel, style: this.activeDrawing?.style || null }); -- } -- -- startPreviewObjectMove(event) { -- if (event.button !== 0 || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey || !this.canDragSelectedObjectFromEmptyCanvas()) { -- return false; -- } -- const object = this.selectedObject(); -- const objectShapes = sortedShapes(object); -- const targetIndexes = this.selectedObjectShapeIndexes(object); -- const activeFrame = this.activeFrame(); -- event.preventDefault(); -- event.stopPropagation(); -- this.previewPointerEdit = { -- ...this.previewPointerEditStartMetadata(event), -- mode: "move-object", -- objectId: object.id, -- objectName: object.name, -- originalTransforms: Object.fromEntries(targetIndexes.map((shapeIndex) => { -- const effectiveShape = this.effectiveShapeForFrame(objectShapes[shapeIndex], activeFrame, shapeIndex); -- return [shapeIndex, this.ensureShapeTransform(effectiveShape)]; -- })), -- shapeIndex: -1, -- start: this.snapCanvasPoint(this.pointerPreviewPoint(event), { excludeShapeIndex: -1 }), -- targetIndexes -- }; -- this.updateRenderSurfaceCursorState(); -- return true; -- } -- -- startViewportPan(event) { -- if (event.button !== 0 || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey || !this.canPanViewportFromEmptyCanvas()) { -- return false; -- } -- event.preventDefault(); -- event.stopPropagation(); -- this.viewportPan = { -- dragThresholdMet: false, -- pointerId: event.pointerId, -- startClient: { -- x: Number(event.clientX) || 0, -- y: Number(event.clientY) || 0 -- }, -- startViewport: { ...this.viewport } -- }; -- try { -- this.elements.renderSurface.setPointerCapture?.(event.pointerId); -- } catch { -- // Pointer capture is best-effort for synthetic validation events. -- } -- this.updateRenderSurfaceCursorState(); -- return true; -- } -- -- updateViewportPan(event) { -- const pan = this.viewportPan; -- if (!pan || event.buttons !== 1) { -- return; -- } -- const clientX = Number(event.clientX) || 0; -- const clientY = Number(event.clientY) || 0; -- const distance = Math.hypot(clientX - pan.startClient.x, clientY - pan.startClient.y); -- if (!pan.dragThresholdMet && distance < PREVIEW_DRAG_START_THRESHOLD_PX) { -- return; -- } -- pan.dragThresholdMet = true; -- const bounds = this.elements.renderSurface.getBoundingClientRect(); -- if (!bounds.width || !bounds.height) { -- return; -- } -- const viewWidth = DEFAULT_VIEWPORT.width / pan.startViewport.zoom; -- const viewHeight = DEFAULT_VIEWPORT.height / pan.startViewport.zoom; -- const viewportX = Number((pan.startViewport.x - ((clientX - pan.startClient.x) / bounds.width) * viewWidth).toFixed(3)); -- const viewportY = Number((pan.startViewport.y - ((clientY - pan.startClient.y) / bounds.height) * viewHeight).toFixed(3)); -- if (viewportX === this.viewport.x && viewportY === this.viewport.y) { -- return; -- } -- this.viewport.x = viewportX; -- this.viewport.y = viewportY; -- this.updateViewport(); -- this.updateCoordinateDisplay(event); -- } -- -- finishViewportPan(event) { -- const pan = this.viewportPan; -- if (!pan) { -- return false; -- } -- this.viewportPan = null; -- try { -- this.elements.renderSurface.releasePointerCapture?.(pan.pointerId); -- } catch { -- // Pointer capture is best-effort for synthetic validation events. -- } -- this.updateRenderSurfaceCursorState(); -- if (!pan.dragThresholdMet) { -- this.deselectShape("empty canvas"); -- return true; -- } -- this.updateCoordinateDisplay(event); -- this.statusLog.write(`OK Viewport drag-pan set to ${this.viewport.x}, ${this.viewport.y}.`); -- return true; -- } -- -- startPreviewSelectionBoundsMove(event) { -- this.startPreviewShapeMove(event, this.selectedShapeIndex, { fromSelectionBounds: true }); -- } -- -- startPreviewShapeMove(event, shapeIndex, options = {}) { -- const normalizedIndex = normalizeShapeIndex(shapeIndex); -- if (event.button !== 0 || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) { -- return; -- } -- const object = this.selectedObject(); -- const objectShapes = sortedShapes(object); -- const targetIndexes = this.selectedPreviewDragIndexes(object, normalizedIndex); -- const dragAnchorIndex = options.fromSelectionBounds ? this.selectedShapeIndex : normalizedIndex; -- if (!targetIndexes.includes(dragAnchorIndex)) { -- return; -- } -- const selected = objectShapes[dragAnchorIndex] || null; -- if (!selected || selected.visible === false) { -- return; -- } -- event.preventDefault(); -- event.stopPropagation(); -- const groupId = this.selectedPreviewDragGroupId(objectShapes, targetIndexes); -- if (targetIndexes.length > 1) { -- const lockedIndex = targetIndexes.find((targetIndex) => objectShapes[targetIndex]?.locked); -- if (Number.isInteger(lockedIndex)) { -- this.statusLog.write(`WARN Drag selection skipped: shape row ${lockedIndex} is locked.`); -- return; -- } -- const activeFrame = this.activeFrame(); -- const directSelectedShapeIndexes = this.directSelectedShapeIndexes.size -- ? new Set(Array.from(this.directSelectedShapeIndexes).filter((targetIndex) => targetIndexes.includes(targetIndex))) -- : new Set(targetIndexes); -- this.selectedShapeIndexes = new Set(targetIndexes); -- this.directSelectedShapeIndexes = directSelectedShapeIndexes.size ? directSelectedShapeIndexes : new Set(targetIndexes); -- this.previewPointerEdit = { -- ...this.previewPointerEditStartMetadata(event), -- mode: "move-group", -- groupId, -- directSelectedShapeIndexes: new Set(this.directSelectedShapeIndexes), -- originalTransforms: Object.fromEntries(targetIndexes.map((targetIndex) => { -- const effectiveShape = this.effectiveShapeForFrame(objectShapes[targetIndex], activeFrame, targetIndex); -- return [targetIndex, this.ensureShapeTransform(effectiveShape)]; -- })), -- shapeIndex: dragAnchorIndex, -- start: this.snapCanvasPoint(this.pointerPreviewPoint(event), { excludeShapeIndex: dragAnchorIndex }), -- targetIndexes -- }; -- return; -- } -- if (this.selectedShapeIndex !== dragAnchorIndex) { -- return; -- } -- this.previewPointerEdit = { -- ...this.previewPointerEditStartMetadata(event), -- mode: "move", -- originalGeometry: JSON.parse(JSON.stringify(selected.geometry)), -- originalTransform: { ...this.shapeTransform(selected) }, -- shapeIndex: normalizedIndex, -- start: this.snapCanvasPoint(this.pointerPreviewPoint(event), { excludeShapeIndex: normalizedIndex }) -- }; -- } -- -- startPreviewHandleEdit(event, shapeIndex, options) { -- const normalizedIndex = normalizeShapeIndex(shapeIndex); -- if (event.button !== 0) { -- return; -- } -- const selected = this.selectedShape(); -- if (!selected || this.selectedShapeIndex !== normalizedIndex) { -- return; -- } -- event.preventDefault(); -- event.stopPropagation(); -- const effectiveSelected = this.effectiveShape(selected, normalizedIndex); -- const originalTransform = { ...this.shapeTransform(effectiveSelected) }; -- const transformOrigin = this.objectTransformOrigin(this.selectedObject()); -- const rawPointer = this.pointerPreviewPoint(event); -- const transformedPoint = options?.point -- ? this.transformedPoint(options.point, originalTransform, transformOrigin) -- : null; -- this.previewPointerEdit = { -- ...this.previewPointerEditStartMetadata(event), -- ...options, -- originalGeometry: JSON.parse(JSON.stringify(selected.geometry)), -- originalTransform, -- transformOrigin, -- pointerOffset: transformedPoint -- ? { -- x: this.formatViewportNumber(rawPointer.x - transformedPoint.x), -- y: this.formatViewportNumber(rawPointer.y - transformedPoint.y) -- } -- : null, -- shapeIndex: normalizedIndex, -- start: transformedPoint || this.snapCanvasPoint(rawPointer, { excludeShapeIndex: normalizedIndex }) -- }; -- } -- -- previewPointerEditStartMetadata(event) { -- return { -- dragThresholdMet: false, -- historyRecorded: false, -- historySnapshot: this.cloneCurrentPayload(), -- lastDelta: { x: 0, y: 0 }, -- startClient: { -- x: Number(event.clientX) || 0, -- y: Number(event.clientY) || 0 -- } -- }; -- } -- -- previewPointerEditPastDragThreshold(edit, event) { -- if (edit.dragThresholdMet) { -- return true; -- } -- if (!edit.startClient) { -- edit.dragThresholdMet = true; -- return true; -- } -- const distance = Math.hypot((Number(event.clientX) || 0) - edit.startClient.x, (Number(event.clientY) || 0) - edit.startClient.y); -- if (distance < PREVIEW_DRAG_START_THRESHOLD_PX) { -- return false; -- } -- edit.dragThresholdMet = true; -- return true; -- } -- -- previewPointerEditDelta(edit, event) { -- const end = this.previewPointerEditTargetPoint(edit, event); -- edit.currentTargetPoint = end; -- return { -- x: this.formatViewportNumber(end.x - edit.start.x), -- y: this.formatViewportNumber(end.y - edit.start.y) -- }; -- } -- -- previewPointerEditTargetPoint(edit, event) { -- const rawEnd = this.pointerPreviewPoint(event); -- const target = edit.pointerOffset -- ? { -- x: this.formatViewportNumber(rawEnd.x - edit.pointerOffset.x), -- y: this.formatViewportNumber(rawEnd.y - edit.pointerOffset.y) -- } -- : rawEnd; -- return this.snapCanvasPoint(target, { excludeShapeIndex: edit.shapeIndex }); -- } -- -- previewPointerEditLocalTarget(edit) { -- const target = edit.currentTargetPoint || edit.start; -- if (!edit.originalTransform) { -- return this.normalizeDrawingPoint(target); -- } -- return this.localPointFromTransformedPoint(target, edit.originalTransform, edit.transformOrigin || { x: 0, y: 0 }); -- } -- -- updatePreviewPointerEdit(event) { -- const edit = this.previewPointerEdit; -- if (!edit || event.buttons !== 1) { -- return; -- } -- if (!this.previewPointerEditPastDragThreshold(edit, event)) { -- return; -- } -- const delta = this.previewPointerEditDelta(edit, event); -- if (Math.abs(delta.x - edit.lastDelta.x) < 0.001 && Math.abs(delta.y - edit.lastDelta.y) < 0.001) { -- return; -- } -- this.recordPreviewPointerEditStart(edit); -- edit.lastDelta = delta; -- edit.didMove = true; -- this.applyPreviewPointerEdit(edit, delta, { live: true }); -- } -- -- finishPreviewPointerEdit(event) { -- const edit = this.previewPointerEdit; -- if (!edit) { -- return; -- } -- this.previewPointerEdit = null; -- this.updateRenderSurfaceCursorState(); -- if (!edit.dragThresholdMet && !this.previewPointerEditPastDragThreshold(edit, event)) { -- return; -- } -- const delta = this.previewPointerEditDelta(edit, event); -- if (Math.abs(delta.x) < 0.001 && Math.abs(delta.y) < 0.001) { -- return; -- } -- this.previewClickSuppressedUntil = Date.now() + 500; -- this.recordPreviewPointerEditStart(edit); -- this.applyPreviewPointerEdit(edit, delta, { live: false }); -- } -- -- recordPreviewPointerEditStart(edit) { -- if (edit.historyRecorded || !edit.historySnapshot) { -- return; -- } -- this.previewUndoStack.push(this.clonePayloadValue(edit.historySnapshot)); -- if (this.previewUndoStack.length > PREVIEW_HISTORY_LIMIT) { -- this.previewUndoStack.shift(); -- } -- this.previewRedoStack = []; -- edit.historyRecorded = true; -- this.updatePreviewEditActionState(); -- } -- -- applyPreviewPointerEdit(edit, delta, { live = false } = {}) { -- if (edit.mode === "move-object") { -- this.applyPreviewObjectMove(edit, delta, { live }); -- return; -- } -- -- if (edit.mode === "move") { -- this.updateSelectedShapeTransform("preview move", (shape) => { -- shape.transform = this.ensureShapeTransform(shape); -- shape.transform.x = Number((edit.originalTransform.x + delta.x).toFixed(3)); -- shape.transform.y = Number((edit.originalTransform.y + delta.y).toFixed(3)); -- }, `OK ${live ? "Live " : ""}Dragged shape row ${edit.shapeIndex} by ${delta.x}, ${delta.y}.`, { skipPreviewHistory: true }); -- return; -- } -- -- if (edit.mode === "move-group") { -- this.applyPreviewGroupMove(edit, delta, { live }); -- return; -- } -- -- if (edit.mode === "line-endpoint") { -- this.updateSelectedShapeGeometry("preview line endpoint", (shape) => { -- shape.geometry = this.previewLineEndpointGeometry(shape, edit, delta); -- }, `OK ${live ? "Live " : ""}Moved line ${edit.endpoint} for shape row ${edit.shapeIndex}.`, { skipPreviewHistory: true }); -- return; -- } -- -- if (edit.mode === "geometry-point") { -- this.updateSelectedShapeGeometry("preview point handle", (shape) => { -- shape.geometry = this.previewGeometryPointGeometry(shape, edit, delta); -- }, `OK ${live ? "Live " : ""}Moved geometry point ${edit.control || edit.pointIndex + 1 || "handle"} for shape row ${edit.shapeIndex}.`, { skipPreviewHistory: true }); -- return; -- } -- -- if (edit.mode === "resize") { -- this.updateSelectedShapeGeometry("preview resize", (shape) => { -- shape.geometry = this.previewResizeGeometry(shape, edit, delta); -- }, `OK ${live ? "Live " : ""}Resized shape row ${edit.shapeIndex} with ${edit.handle} handle.`, { skipPreviewHistory: true }); -- } -- } -- -- applyPreviewObjectMove(edit, delta, { live = false } = {}) { -- const object = this.selectedObject(); -- const objectShapes = sortedShapes(object); -- const targetIndexes = Array.from(new Set((edit.targetIndexes || []) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0 && shapeIndex < objectShapes.length))) -- .sort((left, right) => left - right); -- if (!object || object.id !== edit.objectId || !targetIndexes.length) { -- this.statusLog.write("WARN Drag object skipped: selected object is not available."); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const activeFrame = this.activeFrame(); -- try { -- targetIndexes.forEach((shapeIndex) => { -- const originalTransform = edit.originalTransforms?.[shapeIndex]; -- if (!originalTransform) { -- throw new Error(`shape row ${shapeIndex} original transform is not available.`); -- } -- const override = this.frameOverrideInPayload(nextPayload, shapeIndex, { create: true }); -- const baseShape = this.findShapeInPayload(nextPayload, shapeIndex); -- const shape = override -- ? this.effectiveShapeForFrame(baseShape, activeFrame, shapeIndex) -- : baseShape; -- if (!shape) { -- throw new Error(`shape row ${shapeIndex} is not available.`); -- } -- shape.transform = this.ensureShapeTransform({ -- transform: { -- ...originalTransform, -- x: Number((originalTransform.x + delta.x).toFixed(3)), -- y: Number((originalTransform.y + delta.y).toFixed(3)) -- } -- }); -- const transformErrors = this.validateShapeForTransform(shape); -- if (transformErrors.length) { -- throw new Error(`shape row ${shapeIndex}: ${transformErrors.join(" ")}`); -- } -- if (override) { -- override.transform = this.ensureShapeTransform(shape); -- } -- }); -- } catch (error) { -- this.statusLog.write(`FAIL Invalid object drag rejected for ${object.name}: ${error.message}`); -- return; -- } -- -- this.commitPayloadUpdate(nextPayload, object.id, -1, `OK ${live ? "Live " : ""}Dragged object ${object.name} by ${delta.x}, ${delta.y}.`, "Drag object failed schema validation", { -- directSelectedShapeIndexes: new Set(), -- preserveEmptyShapeSelection: true, -- selectedShapeIndexes: new Set(), -- skipPreviewHistory: true -- }); -- } -- -- applyPreviewGroupMove(edit, delta, { live = false } = {}) { -- const object = this.selectedObject(); -- const objectShapes = sortedShapes(object); -- const targetIndexes = Array.from(new Set((edit.targetIndexes || []) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0 && shapeIndex < objectShapes.length))) -- .sort((left, right) => left - right); -- if (!object || targetIndexes.length < 2) { -- this.statusLog.write("WARN Drag selection skipped: selected shape set is not available."); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const activeFrame = this.activeFrame(); -- try { -- targetIndexes.forEach((shapeIndex) => { -- const originalTransform = edit.originalTransforms?.[shapeIndex]; -- if (!originalTransform) { -- throw new Error(`shape row ${shapeIndex} original transform is not available.`); -- } -- const override = this.frameOverrideInPayload(nextPayload, shapeIndex, { create: true }); -- const baseShape = this.findShapeInPayload(nextPayload, shapeIndex); -- const shape = override -- ? this.effectiveShapeForFrame(baseShape, activeFrame, shapeIndex) -- : baseShape; -- if (!shape) { -- throw new Error(`shape row ${shapeIndex} is not available.`); -- } -- shape.transform = this.ensureShapeTransform({ -- transform: { -- ...originalTransform, -- x: Number((originalTransform.x + delta.x).toFixed(3)), -- y: Number((originalTransform.y + delta.y).toFixed(3)) -- } -- }); -- const transformErrors = this.validateShapeForTransform(shape); -- if (transformErrors.length) { -- throw new Error(`shape row ${shapeIndex}: ${transformErrors.join(" ")}`); -- } -- if (override) { -- override.transform = this.ensureShapeTransform(shape); -- } -- }); -- } catch (error) { -- this.statusLog.write(`FAIL Invalid drag rejected for ${edit.groupId ? `group ${edit.groupId}` : "selected shapes"}: ${error.message}`); -- return; -- } -- -- const dragSubject = edit.groupId ? `group ${edit.groupId}` : "selected shapes"; -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK ${live ? "Live " : ""}Dragged ${dragSubject} (${targetIndexes.length} shapes) by ${delta.x}, ${delta.y}.`, "Drag selection failed schema validation", { -- directSelectedShapeIndexes: edit.directSelectedShapeIndexes || new Set([edit.shapeIndex]), -- selectedShapeIndexes: new Set(targetIndexes), -- skipPreviewHistory: true -- }); -- } -- -- previewLineEndpointGeometry(shape, edit, delta) { -- const geometry = JSON.parse(JSON.stringify(edit.originalGeometry)); -- if (shapeGeometryTool(shape) !== "line") { -- return geometry; -- } -- const movedPoint = this.previewPointerEditLocalTarget(edit); -- if (edit.endpoint === "start") { -- geometry.point1 = movedPoint; -- } else { -- geometry.point2 = movedPoint; -- } -- return geometry; -- } -- -- previewResizeGeometry(shape, edit, delta) { -- const geometry = JSON.parse(JSON.stringify(edit.originalGeometry)); -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "rectangle") { -- if (edit.handle.includes("w")) { -- geometry.x = Number((edit.originalGeometry.x + delta.x).toFixed(3)); -- geometry.width = Number((edit.originalGeometry.width - delta.x).toFixed(3)); -- } -- if (edit.handle.includes("e")) { -- geometry.width = Number((edit.originalGeometry.width + delta.x).toFixed(3)); -- } -- if (edit.handle.includes("n")) { -- geometry.y = Number((edit.originalGeometry.y + delta.y).toFixed(3)); -- geometry.height = Number((edit.originalGeometry.height - delta.y).toFixed(3)); -- } -- if (edit.handle.includes("s")) { -- geometry.height = Number((edit.originalGeometry.height + delta.y).toFixed(3)); -- } -- return geometry; -- } -- if (geometryTool === "ellipse") { -- geometry.rx = Number(Math.max(1, edit.originalGeometry.rx + (edit.handle.includes("e") ? delta.x : -delta.x)).toFixed(3)); -- geometry.ry = Number(Math.max(1, edit.originalGeometry.ry + (edit.handle.includes("s") ? delta.y : -delta.y)).toFixed(3)); -- return geometry; -- } -- if (geometryTool === "circle") { -- const bounds = shapeBounds({ ...shape, geometry: edit.originalGeometry }); -- const nextBounds = this.resizedGeometryBounds(bounds, edit.handle, delta); -- const diameter = Math.max(1, Math.min(Math.abs(nextBounds.width), Math.abs(nextBounds.height))); -- geometry.cx = Number((nextBounds.x + nextBounds.width / 2).toFixed(3)); -- geometry.cy = Number((nextBounds.y + nextBounds.height / 2).toFixed(3)); -- geometry.r = Number((diameter / 2).toFixed(3)); -- return geometry; -- } -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- const bounds = shapeBounds({ ...shape, geometry: edit.originalGeometry }); -- const nextBounds = this.resizedGeometryBounds(bounds, edit.handle, delta); -- geometry.points = edit.originalGeometry.points.map((point) => this.mapPointBetweenBounds(point, bounds, nextBounds)); -- return geometry; -- } -- if (geometryTool === "line") { -- const bounds = shapeBounds({ ...shape, geometry: edit.originalGeometry }); -- const nextBounds = this.resizedGeometryBounds(bounds, edit.handle, delta); -- geometry.point1 = this.mapPointBetweenBounds(edit.originalGeometry.point1, bounds, nextBounds); -- geometry.point2 = this.mapPointBetweenBounds(edit.originalGeometry.point2, bounds, nextBounds); -- return geometry; -- } -- if (geometryTool === "arc") { -- const bounds = shapeBounds({ ...shape, geometry: edit.originalGeometry }); -- const nextBounds = this.resizedGeometryBounds(bounds, edit.handle, delta); -- geometry.cx = Number((nextBounds.x + nextBounds.width / 2).toFixed(3)); -- geometry.cy = Number((nextBounds.y + nextBounds.height / 2).toFixed(3)); -- geometry.r = Number((Math.max(1, Math.min(nextBounds.width, nextBounds.height) / 2)).toFixed(3)); -- return geometry; -- } -- if (geometryTool === "text") { -- const bounds = shapeBounds({ ...shape, geometry: edit.originalGeometry }); -- const nextBounds = this.resizedGeometryBounds(bounds, edit.handle, delta); -- const anchor = this.mapPointBetweenBounds({ x: edit.originalGeometry.x, y: edit.originalGeometry.y }, bounds, nextBounds); -- const scaleY = nextBounds.height / Math.max(1, bounds.height); -- geometry.x = anchor.x; -- geometry.y = anchor.y; -- geometry.fontSize = Number(Math.max(1, edit.originalGeometry.fontSize * scaleY).toFixed(3)); -- return geometry; -- } -- return geometry; -- } -- -- resizedGeometryBounds(bounds, handle, delta) { -- const next = { ...bounds }; -- if (handle.includes("w")) { -- next.x = Number((bounds.x + delta.x).toFixed(3)); -- next.width = Number((bounds.width - delta.x).toFixed(3)); -- } -- if (handle.includes("e")) { -- next.width = Number((bounds.width + delta.x).toFixed(3)); -- } -- if (handle.includes("n")) { -- next.y = Number((bounds.y + delta.y).toFixed(3)); -- next.height = Number((bounds.height - delta.y).toFixed(3)); -- } -- if (handle.includes("s")) { -- next.height = Number((bounds.height + delta.y).toFixed(3)); -- } -- if (next.width <= 0 || next.height <= 0) { -- throw new Error("resize would invert or collapse geometry bounds."); -- } -- return next; -- } -- -- mapPointBetweenBounds(point, bounds, nextBounds) { -- const xRatio = bounds.width <= 0 ? 0 : (point.x - bounds.x) / bounds.width; -- const yRatio = bounds.height <= 0 ? 0 : (point.y - bounds.y) / bounds.height; -- return { -- x: Number((nextBounds.x + xRatio * nextBounds.width).toFixed(3)), -- y: Number((nextBounds.y + yRatio * nextBounds.height).toFixed(3)) -- }; -- } -- -- previewGeometryPointGeometry(shape, edit, delta) { -- const geometry = JSON.parse(JSON.stringify(edit.originalGeometry)); -- const geometryTool = shapeGeometryTool(shape); -- const movedPoint = this.previewPointerEditLocalTarget(edit); -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- const pointIndex = normalizeShapeIndex(edit.pointIndex); -- if (pointIndex >= 0 && pointIndex < geometry.points.length) { -- geometry.points[pointIndex] = movedPoint; -- } -- return geometry; -- } -- if (geometryTool === "circle") { -- if (edit.control === "center") { -- geometry.cx = Number((edit.originalGeometry.cx + delta.x).toFixed(3)); -- geometry.cy = Number((edit.originalGeometry.cy + delta.y).toFixed(3)); -- return geometry; -- } -- if (edit.control === "radius") { -- geometry.r = this.radiusFromControlPoint({ x: edit.originalGeometry.cx, y: edit.originalGeometry.cy }, movedPoint); -- } -- return geometry; -- } -- if (geometryTool === "ellipse") { -- if (edit.control === "center") { -- geometry.cx = Number((edit.originalGeometry.cx + delta.x).toFixed(3)); -- geometry.cy = Number((edit.originalGeometry.cy + delta.y).toFixed(3)); -- return geometry; -- } -- if (edit.control === "rx") { -- geometry.rx = this.axisRadiusFromControlPoint(edit.originalGeometry.cx, movedPoint.x); -- } -- if (edit.control === "ry") { -- geometry.ry = this.axisRadiusFromControlPoint(edit.originalGeometry.cy, movedPoint.y); -- } -- return geometry; -- } -- if (geometryTool === "arc") { -- if (edit.control === "center") { -- geometry.cx = Number((edit.originalGeometry.cx + delta.x).toFixed(3)); -- geometry.cy = Number((edit.originalGeometry.cy + delta.y).toFixed(3)); -- return geometry; -- } -- if (edit.control === "start" || edit.control === "end") { -- const center = { x: edit.originalGeometry.cx, y: edit.originalGeometry.cy }; -- geometry.r = this.radiusFromControlPoint(center, movedPoint); -- geometry[edit.control === "start" ? "startAngle" : "endAngle"] = this.angleFromControlPoint(center, movedPoint); -- } -- return geometry; -- } -- if (geometryTool === "text") { -- geometry.x = movedPoint.x; -- geometry.y = movedPoint.y; -- return geometry; -- } -- return geometry; -- } -- -- radiusFromControlPoint(center, point) { -- return Number(Math.max(1, Math.hypot(point.x - center.x, point.y - center.y)).toFixed(3)); -- } -- -- axisRadiusFromControlPoint(centerValue, pointValue) { -- return Number(Math.max(1, Math.abs(pointValue - centerValue)).toFixed(3)); -- } -- -- angleFromControlPoint(center, point) { -- const degrees = (Math.atan2(point.y - center.y, point.x - center.x) * 180) / Math.PI + 90; -- return this.normalizeRotationDegrees(degrees); -- } -- -- selectObject(objectId, sourceLabel) { -- if (!this.currentPayload?.objects.some((object) => object.id === objectId)) { -- this.statusLog.write(`WARN Select object skipped: object id ${objectId || "unknown"} is not available.`); -- return; -- } -- -- const scrollState = this.captureLeftPanelScrollState(); -- this.selectedObjectId = objectId; -- this.transformInputValues.set("objectVectorStudioV2ObjectScaleInput", "1"); -- const selectedObject = this.selectedObject(); -- this.selectedShapeIndex = sortedShapes(selectedObject).length ? 0 : -1; -- this.selectedShapeIndexes = new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : []); -- this.directSelectedShapeIndexes = new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : []); -- const selectedState = this.objectStates(selectedObject)[0] || null; -- this.selectedStateId = selectedState?.id || ""; -- this.stateControlStateId = this.selectedStateId || OBJECT_STATE_IDS[0]; -- this.selectedFrameId = selectedState ? sortedFrames(selectedState)[0]?.id || "" : ""; -- this.syncPaletteSelectionFromCurrentShape({ logMissing: true }); -- this.setPaletteTarget("stroke", false); -- this.renderPayload(); -- this.restoreLeftPanelScrollState(scrollState); -- const selected = this.selectedObject(); -- this.statusLog.write(`OK Selected object from ${sourceLabel}: ${selected.name}.`); -- } -- -- selectShape(shapeIndex, sourceLabel, options = {}) { -- const object = this.selectedObject(); -- const normalizedIndex = normalizeShapeIndex(shapeIndex); -- const shapes = sortedShapes(object); -- if (normalizedIndex < 0 || normalizedIndex >= shapes.length) { -- this.statusLog.write(`WARN Select shape skipped: shape row ${shapeIndex ?? "unknown"} is not available.`); -- return; -- } -- -- const scrollState = this.captureLeftPanelScrollState(); -- this.selectedShapeIndex = normalizedIndex; -- if (options.additive) { -- if (this.selectedShapeIndexes.has(normalizedIndex) && this.selectedShapeIndexes.size > 1) { -- this.selectedShapeIndexes.delete(normalizedIndex); -- this.directSelectedShapeIndexes = new Set(this.selectedShapeIndexes); -- this.selectedShapeIndex = Array.from(this.selectedShapeIndexes).at(-1) ?? -1; -- } else { -- this.selectedShapeIndexes.add(normalizedIndex); -- this.directSelectedShapeIndexes = new Set(this.selectedShapeIndexes); -- } -- } else { -- this.selectedShapeIndexes = new Set([normalizedIndex]); -- this.directSelectedShapeIndexes = new Set([normalizedIndex]); -- } -- this.syncPaletteSelectionFromCurrentShape({ logMissing: true }); -- this.setPaletteTarget("stroke", false); -- this.renderPayload(); -- this.restoreLeftPanelScrollState(scrollState); -- const shape = this.selectedShape(); -- this.statusLog.write(`OK Selected shape from ${sourceLabel}: row ${this.selectedShapeIndex} (${shapeTool(shape)}). Multi-select count: ${this.selectedShapeIndexes.size}.`); -- } -- -- selectShapeGroup(groupId, shapeIndex, sourceLabel = "shape group") { -- const object = this.selectedObject(); -- const shapes = sortedShapes(object); -- const normalizedGroupId = String(groupId || "").trim(); -- if (!normalizedGroupId) { -- this.statusLog.write("WARN Select group skipped: shape group id is not available."); -- return; -- } -- const groupIndexes = shapes -- .map((shape, index) => String(shape?.groupId || "").trim() === normalizedGroupId ? index : -1) -- .filter((index) => index >= 0); -- if (groupIndexes.length < 2) { -- this.statusLog.write(`WARN Select group skipped: group ${normalizedGroupId} has fewer than two shapes.`); -- return; -- } -- -- const normalizedIndex = normalizeShapeIndex(shapeIndex); -- const nextSelectedIndex = groupIndexes.includes(normalizedIndex) ? normalizedIndex : groupIndexes[0]; -- const scrollState = this.captureLeftPanelScrollState(); -- this.selectedShapeIndex = nextSelectedIndex; -- this.selectedShapeIndexes = new Set(groupIndexes); -- this.directSelectedShapeIndexes = new Set(groupIndexes); -- this.syncPaletteSelectionFromCurrentShape({ logMissing: true }); -- this.setPaletteTarget("stroke", false); -- this.renderPayload(); -- this.restoreLeftPanelScrollState(scrollState); -- this.statusLog.write(`OK Selected group ${normalizedGroupId} from ${sourceLabel}: ${groupIndexes.length} shapes.`); -- } -- -- shapeSelectionGroupIndexes(shapes, shapeIndex) { -- const shape = shapes[shapeIndex]; -- const groupId = String(shape?.groupId || "").trim(); -- if (!groupId) { -- return [shapeIndex]; -- } -- return shapes -- .map((candidate, index) => String(candidate?.groupId || "").trim() === groupId ? index : -1) -- .filter((index) => index >= 0); -- } -- -- shapeBelongsToValidGroup(object, shapeIndex) { -- const shapes = sortedShapes(object); -- const shape = shapes[normalizeShapeIndex(shapeIndex)]; -- const groupId = String(shape?.groupId || "").trim(); -- if (!groupId) { -- return false; -- } -- return shapes.filter((candidate) => String(candidate?.groupId || "").trim() === groupId).length >= 2; -- } -- -- selectState(stateId, sourceLabel) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN State selection skipped: no object is selected."); -- return; -- } -- if (!stateId) { -- this.selectedStateId = ""; -- this.stateControlStateId = ""; -- this.selectedFrameId = ""; -- this.stopPlaybackTimer(); -- this.renderPayload(); -- this.statusLog.write(`OK State selection cleared from ${sourceLabel}.`); -- return; -- } -- const state = this.objectStates(object).find((candidate) => candidate.id === stateId); -- if (!state) { -- this.statusLog.write(`WARN State selection pending: ${stateId} is not created for ${object.name}. Use Add to create it.`); -- this.selectedStateId = ""; -- this.stateControlStateId = stateId; -- this.selectedFrameId = ""; -- this.renderFrameTimeline(); -- this.updateAnimationActionState(); -- return; -- } -- this.selectedStateId = state.id; -- this.stateControlStateId = state.id; -- this.selectedFrameId = sortedFrames(state)[0]?.id || ""; -- this.stopPlaybackTimer(); -- this.renderPayload(); -- this.statusLog.write(`OK Selected state ${OBJECT_STATE_LABELS[state.id] || state.id} from ${sourceLabel}; active object remains ${object.name}.`); -- } -- -- selectFrame(frameId, sourceLabel) { -- const state = this.selectedState(); -- if (!state) { -- this.statusLog.write("WARN Frame selection skipped: no state is selected."); -- return; -- } -- const frame = sortedFrames(state).find((candidate) => candidate.id === frameId); -- if (!frame) { -- this.statusLog.write(`WARN Frame selection skipped: frame id ${frameId || "unknown"} is not available.`); -- return; -- } -- this.selectedFrameId = frame.id; -- this.renderPayload(); -- this.statusLog.write(`OK Selected frame ${frame.id} from ${sourceLabel}.`); -- } -- -- createSelectedState(stateId = this.stateControlSelectionId(this.selectedObject())) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Create state skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Create state")) { -- return; -- } -- if (!OBJECT_STATE_IDS.includes(stateId)) { -- this.statusLog.write(`FAIL Create state blocked: choose ${OBJECT_STATE_IDS.join(", ")}.`); -- return; -- } -- if (this.objectStates(object).some((state) => state.id === stateId)) { -- this.statusLog.write(`WARN Create state skipped: ${OBJECT_STATE_LABELS[stateId] || stateId} already exists for ${object.name}.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- nextObject.states = this.objectStates(nextObject); -- const frame = this.createFrameSnapshot(nextObject, stateId, "frame-1", 1); -- nextObject.states.push({ -- frames: [frame], -- id: stateId, -- name: OBJECT_STATE_LABELS[stateId] || stateId -- }); -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Created state ${OBJECT_STATE_LABELS[stateId] || stateId} with frame ${frame.id}.`, "Create state failed schema validation", { -- selectedFrameId: frame.id, -- selectedStateId: stateId -- }); -- } -- -- deleteSelectedState(stateId = this.selectedStateId) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Delete state skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Delete state")) { -- return; -- } -- const states = this.objectStates(object); -- const stateIndex = states.findIndex((state) => state.id === stateId); -- if (stateIndex < 0) { -- this.statusLog.write(`WARN Delete state skipped: ${stateId || "unknown"} is not created for ${object.name}.`); -- return; -- } -- if (states.length <= 1) { -- this.statusLog.write(`WARN Delete state skipped: ${stateId} is the only state for ${object.name}.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- nextObject.states = this.objectStates(nextObject).filter((state) => state.id !== stateId); -- const nextState = nextObject.states[Math.min(stateIndex, nextObject.states.length - 1)] || nextObject.states[0] || null; -- const nextFrame = sortedFrames(nextState)[0] || null; -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Deleted state ${stateId} from ${object.name}.`, "Delete state failed schema validation", { -- selectedFrameId: nextFrame?.id || "", -- selectedStateId: nextState?.id || "" -- }); -- } -- -- duplicateSelectedFrame() { -- const object = this.selectedObject(); -- const state = this.selectedState(); -- const frame = this.activeFrame(); -- if (!object || !state || !frame) { -- this.statusLog.write("WARN Duplicate frame skipped: select an object state and frame first."); -- return; -- } -- if (this.guardSelectedObjectMutation("Duplicate frame")) { -- return; -- } -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- const nextState = this.objectStates(nextObject).find((candidate) => candidate.id === state.id); -- const frameCopy = JSON.parse(JSON.stringify(frame)); -- frameCopy.id = this.uniqueFrameId(nextState); -- frameCopy.order = sortedFrames(nextState).length + 1; -- nextState.frames.push(frameCopy); -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Duplicated frame ${frame.id} as ${frameCopy.id}.`, "Duplicate frame failed schema validation", { -- selectedFrameId: frameCopy.id, -- selectedStateId: state.id -- }); -- } -- -- deleteSelectedFrame() { -- const object = this.selectedObject(); -- const state = this.selectedState(); -- const frame = this.activeFrame(); -- if (!object || !state || !frame) { -- this.statusLog.write("WARN Delete frame skipped: select an object state and frame first."); -- return; -- } -- if (this.guardSelectedObjectMutation("Delete frame")) { -- return; -- } -- const frames = sortedFrames(state); -- if (frames.length <= 1) { -- this.statusLog.write(`WARN Delete frame skipped: frame ${frame.id} is the only frame in ${OBJECT_STATE_LABELS[state.id] || state.id}.`); -- return; -- } -- const index = frames.findIndex((candidate) => candidate.id === frame.id); -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- const nextState = this.objectStates(nextObject).find((candidate) => candidate.id === state.id); -- const nextFrames = sortedFrames(nextState).filter((candidate) => candidate.id !== frame.id); -- nextFrames.forEach((candidate, frameIndex) => { -- candidate.order = frameIndex + 1; -- }); -- nextState.frames = nextFrames; -- const nextSelectedFrame = nextFrames[Math.min(index, nextFrames.length - 1)] || nextFrames[0]; -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Deleted frame ${frame.id} from ${OBJECT_STATE_LABELS[state.id] || state.id}.`, "Delete frame failed schema validation", { -- selectedFrameId: nextSelectedFrame.id, -- selectedStateId: state.id -- }); -- } -- -- moveSelectedFrame(direction, controlLabel = direction) { -- const object = this.selectedObject(); -- const state = this.selectedState(); -- const frame = this.activeFrame(); -- if (!object || !state || !frame) { -- this.statusLog.write(`WARN Move frame ${controlLabel} skipped: select an object state and frame first.`); -- return; -- } -- if (this.guardSelectedObjectMutation("Move frame")) { -- return; -- } -- const frames = sortedFrames(state); -- const index = frames.findIndex((candidate) => candidate.id === frame.id); -- const nextIndex = direction === "earlier" ? index - 1 : index + 1; -- if (nextIndex < 0 || nextIndex >= frames.length) { -- this.statusLog.write(`WARN Move frame ${controlLabel} skipped: frame ${frame.id} cannot move ${controlLabel}.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- const nextState = this.objectStates(nextObject).find((candidate) => candidate.id === state.id); -- const nextFrames = sortedFrames(nextState); -- const [movedFrame] = nextFrames.splice(index, 1); -- nextFrames.splice(nextIndex, 0, movedFrame); -- nextFrames.forEach((candidate, frameIndex) => { -- candidate.order = frameIndex + 1; -- }); -- nextState.frames = nextFrames; -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Moved frame ${frame.id} ${controlLabel}.`, "Move frame failed schema validation", { -- selectedFrameId: frame.id, -- selectedStateId: state.id -- }); -- } -- -- playAnimation() { -- const state = this.selectedState(); -- const frames = sortedFrames(state); -- if (!state || !frames.length) { -- this.statusLog.write("FAIL Playback blocked: create and select an animation state with at least one frame."); -- return; -- } -- const fps = Number(this.elements.fpsInput.value); -- if (!Number.isInteger(fps) || fps < 1 || fps > 60) { -- this.statusLog.write("FAIL Playback blocked: FPS must be an integer from 1 to 60."); -- return; -- } -- this.stopPlaybackTimer(false); -- this.isAnimationPlaying = true; -- this.updateAnimationActionState(); -- this.playbackTimerId = this.window.setInterval(() => this.advancePlaybackFrame(), Math.round(1000 / fps)); -- this.statusLog.write(`OK Playback started for state ${OBJECT_STATE_LABELS[state.id] || state.id} at ${fps} FPS.`); -- } -- -- pauseAnimation() { -- if (!this.isAnimationPlaying) { -- this.statusLog.write("WARN Playback pause skipped: animation is not playing."); -- return; -- } -- this.stopPlaybackTimer(false); -- this.statusLog.write(`OK Playback paused at frame ${this.selectedFrameId || "none"}.`); -- } -- -- stopAnimation() { -- const state = this.selectedState(); -- this.stopPlaybackTimer(false); -- if (state) { -- this.selectedFrameId = sortedFrames(state)[0]?.id || ""; -- this.renderPayload(); -- } -- this.statusLog.write("OK Playback stopped."); -- } -- -- stopPlaybackTimer(shouldRender = true) { -- if (this.playbackTimerId) { -- this.window.clearInterval(this.playbackTimerId); -- this.playbackTimerId = 0; -- } -- this.isAnimationPlaying = false; -- if (shouldRender) { -- this.updateAnimationActionState(); -- } -- } -- -- advancePlaybackFrame() { -- const state = this.selectedState(); -- const frames = sortedFrames(state); -- if (!state || !frames.length) { -- this.stopPlaybackTimer(); -- this.statusLog.write("FAIL Playback stopped: selected state or frame timeline is missing."); -- return; -- } -- const index = Math.max(0, frames.findIndex((frame) => frame.id === this.selectedFrameId)); -- const nextFrame = frames[index + 1]; -- if (nextFrame) { -- this.selectedFrameId = nextFrame.id; -- this.renderPayload(); -- return; -- } -- if (this.elements.loopToggle.checked) { -- this.selectedFrameId = frames[0].id; -- this.renderPayload(); -- return; -- } -- this.stopPlaybackTimer(); -- this.statusLog.write(`OK Playback completed for state ${OBJECT_STATE_LABELS[state.id] || state.id}.`); -- } -- -- addObject() { -- const name = this.elements.objectNameInput.value.trim(); -- this.pendingAddObjectClick = false; -- if (!this.currentPayload) { -- this.statusLog.write("FAIL Add object blocked: load a schema-valid Object Vector Studio V2 payload before adding objects."); -- return; -- } -- if (!name) { -- this.statusLog.write("FAIL Add object blocked: enter an object name."); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const id = this.uniqueObjectId(name, nextPayload.objects); -- let objectDefault; -- try { -- objectDefault = this.schemaDefault("object"); -- } catch (error) { -- this.statusLog.write(`FAIL Add object blocked: ${error.message}`); -- return; -- } -- nextPayload.objects.push({ -- ...objectDefault, -- id, -- name, -- tags: tagList(this.elements.objectTagInput.value) -- }); -- this.commitPayloadUpdate(nextPayload, id, "", `OK Added object ${name} with object/game/name id ${id}.`, "Add object failed schema validation"); -- } -- -- addTagToSelectedObject() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.statusLog.write("WARN Tag add skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Tag add")) { -- return; -- } -- const tags = tagList(this.elements.objectTagInput.value); -- if (!tags.length) { -- this.statusLog.write("FAIL Tag add blocked: enter a tag name."); -- return; -- } -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((object) => object.id === selected.id); -- const nextTags = tagList(nextObject.tags); -- const addedTags = []; -- tags.forEach((tag) => { -- const nextTag = this.uniqueTag(tag, nextTags); -- nextTags.push(nextTag); -- addedTags.push(nextTag); -- }); -- nextObject.tags = nextTags.sort(); -- this.elements.objectTagInput.value = ""; -- this.commitPayloadUpdate(nextPayload, selected.id, this.selectedShapeIndex, `OK Added tag ${addedTags.join(", ")} to ${selected.name}.`, "Object tag update failed schema validation"); -- } -- -- removeTagFromSelectedObject(tag) { -- const selected = this.selectedObject(); -- if (!selected) { -- this.statusLog.write("WARN Tag remove skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Tag remove")) { -- return; -- } -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((object) => object.id === selected.id); -- nextObject.tags = tagList(nextObject.tags).filter((candidate) => candidate !== tag); -- this.commitPayloadUpdate(nextPayload, selected.id, this.selectedShapeIndex, `OK Removed tag ${tag} from ${selected.name}.`, "Object tag update failed schema validation"); -- } -- -- renameSelectedObject() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.statusLog.write("WARN Rename object skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Rename object")) { -- return; -- } -- -- const name = this.elements.objectNameInput.value.trim(); -- if (!name) { -- this.statusLog.write("FAIL Rename object blocked: enter an object name."); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((object) => object.id === selected.id); -- const oldId = nextObject.id; -- const siblingObjects = nextPayload.objects.filter((object) => object.id !== oldId); -- const nextId = this.uniqueObjectId(name, siblingObjects, objectGameSegment(oldId) || this.payloadGameKey()); -- nextPayload.objects.forEach((object) => { -- if (object.baseObjectId === oldId) { -- object.baseObjectId = nextId; -- } -- }); -- nextObject.id = nextId; -- nextObject.name = name; -- this.commitPayloadUpdate(nextPayload, nextId, this.selectedShapeIndex, `OK Renamed object ${oldId} to ${name} and updated object/game/name id to ${nextId}.`, "Rename object failed schema validation"); -- } -- -- duplicateSelectedObject() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.statusLog.write("WARN Duplicate object skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Duplicate object")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const objectCopy = JSON.parse(JSON.stringify(selected)); -- objectCopy.id = this.uniqueObjectId(`${selected.name} Copy`, nextPayload.objects, objectGameSegment(selected.id) || this.payloadGameKey()); -- objectCopy.name = `${selected.name} Copy`; -- nextPayload.objects.push(objectCopy); -- const selectedShapeIndex = sortedShapes(objectCopy).length ? 0 : -1; -- this.commitPayloadUpdate(nextPayload, objectCopy.id, selectedShapeIndex, `OK Duplicated object ${selected.name} as ${objectCopy.name}.`, "Duplicate object failed schema validation"); -- } -- -- deleteSelectedObject() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.statusLog.write("WARN Delete object skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Delete object")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- nextPayload.objects = nextPayload.objects.filter((object) => object.id !== selected.id); -- this.removeDeletedObjectReferences(nextPayload, selected.id); -- this.removeDanglingShapeOverrideReferences(nextPayload); -- const selectedObjectId = nextPayload.objects[0]?.id || ""; -- const selectedShapeIndex = nextPayload.objects[0] && sortedShapes(nextPayload.objects[0]).length ? 0 : -1; -- this.commitPayloadUpdate(nextPayload, selectedObjectId, selectedShapeIndex, `OK Deleted object ${selected.name}.`, "Delete object failed schema validation"); -- } -- -- deleteObjectById(objectId, sourceLabel) { -- const selected = this.currentPayload?.objects.find((object) => object.id === objectId); -- if (!selected) { -- this.statusLog.write(`WARN Delete object skipped: object id ${objectId || "unknown"} is not available.`); -- return; -- } -- if (this.isObjectLocked(selected.id)) { -- this.statusLog.write(`WARN Delete object blocked: object ${selected.name} is locked for this runtime session.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- nextPayload.objects = nextPayload.objects.filter((object) => object.id !== selected.id); -- this.removeDeletedObjectReferences(nextPayload, selected.id); -- this.removeDanglingShapeOverrideReferences(nextPayload); -- const selectedObjectStillExists = nextPayload.objects.some((object) => object.id === this.selectedObjectId); -- const nextSelectedObject = selectedObjectStillExists -- ? nextPayload.objects.find((object) => object.id === this.selectedObjectId) -- : nextPayload.objects[0] || null; -- const selectedObjectId = nextSelectedObject?.id || ""; -- const selectedShapeStillExists = Number.isInteger(this.selectedShapeIndex) && this.selectedShapeIndex >= 0 && this.selectedShapeIndex < sortedShapes(nextSelectedObject).length; -- const selectedShapeIndex = selectedShapeStillExists ? this.selectedShapeIndex : nextSelectedObject && sortedShapes(nextSelectedObject).length ? 0 : -1; -- this.hiddenObjectIds.delete(selected.id); -- this.lockedObjectIds.delete(selected.id); -- this.commitPayloadUpdate(nextPayload, selectedObjectId, selectedShapeIndex, `OK Deleted object ${selected.name} from ${sourceLabel}.`, "Delete object failed schema validation"); -- } -- -- removeDeletedObjectReferences(payload, objectId) { -- payload.objects.forEach((object) => { -- if (object.baseObjectId === objectId) { -- delete object.baseObjectId; -- } -- }); -- this.removeDirectReferenceEntries(payload, "objectId", objectId); -- } -- -- removeDirectReferenceEntries(value, referenceKey, targetId) { -- if (!value || typeof value !== "object") { -- return; -- } -- if (Array.isArray(value)) { -- for (let index = value.length - 1; index >= 0; index -= 1) { -- const item = value[index]; -- if (item && typeof item === "object" && item[referenceKey] === targetId) { -- value.splice(index, 1); -- } else { -- this.removeDirectReferenceEntries(item, referenceKey, targetId); -- } -- } -- return; -- } -- Object.entries(value).forEach(([key, child]) => { -- if (key === referenceKey && child === targetId) { -- delete value[key]; -- return; -- } -- this.removeDirectReferenceEntries(child, referenceKey, targetId); -- }); -- } -- -- removeDanglingShapeOverrideReferences(payload) { -- const objectsById = new Map(payload.objects.map((object) => [object.id, object])); -- const collectShapeCount = (object, seen = new Set()) => { -- if (!object || seen.has(object.id)) { -- return 0; -- } -- seen.add(object.id); -- const shapeByOrder = object.baseObjectId -- ? new Map(collectShapeRows(objectsById.get(object.baseObjectId), seen)) -- : new Map(); -- object.shapes.forEach((shape) => shapeByOrder.set(shape.order, shape)); -- return shapeByOrder.size; -- }; -- const collectShapeRows = (object, seen = new Set()) => { -- if (!object || seen.has(object.id)) { -- return []; -- } -- seen.add(object.id); -- const shapeByOrder = object.baseObjectId -- ? new Map(collectShapeRows(objectsById.get(object.baseObjectId), seen)) -- : new Map(); -- object.shapes.forEach((shape) => shapeByOrder.set(shape.order, shape)); -- return Array.from(shapeByOrder.entries()); -- }; -- payload.objects.forEach((object) => { -- const shapeCount = collectShapeCount(object); -- this.objectStates(object).forEach((state) => { -- state.frames.forEach((frame) => { -- frame.shapeOverrides = frame.shapeOverrides.filter((override) => override.shapeIndex < shapeCount); -- }); -- }); -- }); -- } -- -- flattenSelectedObject() { -- const selected = this.selectedObject(); -- if (!selected) { -- this.statusLog.write("WARN Flatten object skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Flatten object")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((object) => object.id === selected.id); -- if (!nextObject.shapes.length) { -- this.statusLog.write(`WARN Flatten object skipped: ${selected.name} has no shapes to flatten.`); -- return; -- } -- const rotatedShape = sortedShapes(nextObject).find((shape) => this.shapeTransform(shape).rotation !== 0); -- if (rotatedShape) { -- this.statusLog.write(`FAIL Flatten object blocked: shape ${rotatedShape.id} has rotation ${this.shapeTransform(rotatedShape).rotation}; rotate-safe flatten is not available in this schema pass.`); -- return; -- } -- let flattenedCount = 0; -- const origin = this.objectTransformOrigin(nextObject); -- nextObject.shapes = sortedShapes(nextObject).map((shape, index) => { -- const flattened = this.flattenShapeTransform(shape, origin); -- flattened.order = index + 1; -- flattenedCount += 1; -- return flattened; -- }); -- const selectedShapeIndex = sortedShapes(nextObject).length ? 0 : -1; -- this.commitPayloadUpdate(nextPayload, selected.id, selectedShapeIndex, `OK Flattened object ${selected.name}: baked transforms into ${flattenedCount} shapes.`, "Flatten object failed schema validation"); -- } -- -- schemaDefault(definitionName) { -- if (typeof this.schemaService.getDefinitionDefault !== "function") { -- throw new Error("Object Vector Studio V2 schema default reader is unavailable."); -- } -- return this.schemaService.getDefinitionDefault(definitionName); -- } -- -- createShapeStyleDefault(type, color, styleOverride = null) { -- const { pointStyle, startPointStyle, endPointStyle, strokeLinecap, ...style } = this.schemaDefault("style"); -- const strokeColor = styleOverride?.stroke || this.selectedStrokeColor || color; -- return { -- ...style, -- ...(styleOverride || {}), -- fill: TRANSPARENT_STYLE_COLOR, -- fillOpacity: styleOverride?.fillOpacity ?? this.selectedFillOpacity, -- stroke: strokeColor, -- strokeOpacity: styleOverride?.strokeOpacity ?? this.selectedStrokeOpacity, -- strokeWidth: styleOverride?.strokeWidth ?? style.strokeWidth -- }; -- } -- -- createShapeTransformDefault() { -- const transform = this.schemaDefault("transform"); -- return { -- ...defaultShapeTransform(), -- ...transform -- }; -- } -- -- createShape(type, options = {}) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write(`FAIL Create ${type} blocked: select a schema-valid object first.`); -- return; -- } -- if (this.guardSelectedObjectMutation(`Create ${type}`)) { -- return; -- } -- -- const color = this.firstPaletteColor(); -- if (!color) { -- this.statusLog.write(`FAIL Create ${type} blocked: palette swatches do not provide a usable color value.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- const order = nextObject.shapes.length ? Math.max(...nextObject.shapes.map((shape) => shape.order)) + 1 : 1; -- let shape; -- try { -- shape = this.createPrimitiveShape(type, order, color, options.geometry, options.style); -- } catch (error) { -- this.statusLog.write(`FAIL Create ${type} blocked: ${error.message}`); -- return; -- } -- nextObject.shapes.push(shape); -- this.activeDrawing = PRIMITIVE_TOOLS.includes(type) ? this.createDrawingState(type) : null; -- this.drawingPreviewPoint = null; -- this.commitPayloadUpdate(nextPayload, nextObject.id, sortedShapes(nextObject).length - 1, `OK Created ${type} shape on ${nextObject.name} from ${options.sourceLabel || "canvas drawing"}.`, `Create ${type} failed schema validation`); -- } -- -- createPrimitiveShape(type, order, color, geometry = null, styleOverride = null) { -- if (!PRIMITIVE_TOOLS.includes(type)) { -- throw new Error(`unsupported shape tool ${type}.`); -- } -- if (!geometry) { -- throw new Error(`${shapeTypeLabel(type)} requires committed canvas placement geometry.`); -- } -- -- const base = this.schemaDefault("shapeCommon"); -- const shape = { -- ...base, -- geometry: JSON.parse(JSON.stringify(geometry)), -- order, -- style: this.createShapeStyleDefault(type, color, styleOverride), -- tool: type, -- visible: base.visible -- }; -- if (type === "square") { -- const size = Math.min(shape.geometry.width, shape.geometry.height); -- shape.geometry.width = size; -- shape.geometry.height = size; -- } -- shape.transform = this.createShapeTransformDefault(); -- return shape; -- } -- -- flattenShapeTransform(shape, origin = { x: 0, y: 0 }) { -- const nextShape = JSON.parse(JSON.stringify(shape)); -- const transform = this.shapeTransform(nextShape); -- const applyX = (value) => Number((transform.x + origin.x + (value - origin.x) * transform.scaleX).toFixed(3)); -- const applyY = (value) => Number((transform.y + origin.y + (value - origin.y) * transform.scaleY).toFixed(3)); -- const geometryTool = shapeGeometryTool(nextShape); -- if (geometryTool === "rectangle") { -- nextShape.geometry.x = applyX(nextShape.geometry.x); -- nextShape.geometry.y = applyY(nextShape.geometry.y); -- nextShape.geometry.width = Number((nextShape.geometry.width * transform.scaleX).toFixed(3)); -- nextShape.geometry.height = Number((nextShape.geometry.height * transform.scaleY).toFixed(3)); -- } else if (geometryTool === "circle") { -- nextShape.geometry.cx = applyX(nextShape.geometry.cx); -- nextShape.geometry.cy = applyY(nextShape.geometry.cy); -- nextShape.geometry.r = Number((nextShape.geometry.r * Math.max(transform.scaleX, transform.scaleY)).toFixed(3)); -- } else if (geometryTool === "ellipse") { -- nextShape.geometry.cx = applyX(nextShape.geometry.cx); -- nextShape.geometry.cy = applyY(nextShape.geometry.cy); -- nextShape.geometry.rx = Number((nextShape.geometry.rx * transform.scaleX).toFixed(3)); -- nextShape.geometry.ry = Number((nextShape.geometry.ry * transform.scaleY).toFixed(3)); -- } else if (geometryTool === "line") { -- nextShape.geometry.point1.x = applyX(nextShape.geometry.point1.x); -- nextShape.geometry.point1.y = applyY(nextShape.geometry.point1.y); -- nextShape.geometry.point2.x = applyX(nextShape.geometry.point2.x); -- nextShape.geometry.point2.y = applyY(nextShape.geometry.point2.y); -- } else if (geometryTool === "polygon" || geometryTool === "polyline") { -- nextShape.geometry.points = nextShape.geometry.points.map((point) => ({ -- x: applyX(point.x), -- y: applyY(point.y) -- })); -- } else if (geometryTool === "arc") { -- nextShape.geometry.cx = applyX(nextShape.geometry.cx); -- nextShape.geometry.cy = applyY(nextShape.geometry.cy); -- nextShape.geometry.r = Number((nextShape.geometry.r * Math.max(transform.scaleX, transform.scaleY)).toFixed(3)); -- } else if (geometryTool === "text") { -- nextShape.geometry.x = applyX(nextShape.geometry.x); -- nextShape.geometry.y = applyY(nextShape.geometry.y); -- nextShape.geometry.fontSize = Number((nextShape.geometry.fontSize * Math.max(transform.scaleX, transform.scaleY)).toFixed(3)); -- } -- nextShape.transform = defaultShapeTransform(); -- return nextShape; -- } -- -- currentTargetColor() { -- return this.paletteTarget === "stroke" ? this.selectedStrokeColor : this.selectedFillColor; -- } -- -- paletteSwatchForColor(color) { -- const normalized = String(color || "").trim().toLowerCase(); -- if (!normalized || !this.runtimePalette?.swatches?.length) { -- return null; -- } -- return this.runtimePalette.swatches.find((swatch) => String(swatchColor(swatch)).trim().toLowerCase() === normalized) || null; -- } -- -- directStyleColorLabel(color) { -- const normalized = String(color || "").trim().toLowerCase(); -- if (normalized === TRANSPARENT_STYLE_COLOR || normalized === "transparent") { -- return "transparent"; -- } -- if (normalized === "none") { -- return "none"; -- } -- return ""; -- } -- -- paletteLabelForColor(color, fallbackLabel) { -- const swatch = this.paletteSwatchForColor(color); -- return swatch?.name || swatch?.id || swatch?.symbol || this.directStyleColorLabel(color) || fallbackLabel; -- } -- -- syncPaletteSelectionFromCurrentShape(options = {}) { -- this.syncPaletteSelectionFromShape(this.selectedShape(), options); -- } -- -- syncPaletteSelectionFromShape(shape, options = {}) { -- if (!shape || !this.runtimePalette) { -- return; -- } -- const { logMissing = false, render = true } = options; -- const effectiveShape = this.effectiveShape(shape); -- let changed = false; -- const syncTarget = (target, color) => { -- const normalizedColor = String(color || "").trim(); -- if (!normalizedColor || normalizedColor === "none" || this.directStyleColorLabel(normalizedColor)) { -- return; -- } -- const swatch = this.paletteSwatchForColor(normalizedColor); -- if (!swatch) { -- if (logMissing) { -- this.statusLog.write(`WARN Palette sync skipped for selected ${shapeTool(shape)} ${target} color ${normalizedColor}: color is not in the loaded palette.`); -- } -- return; -- } -- const label = swatch.name || swatch.id || swatch.symbol || shapeDisplayLabel(shape); -- const paletteColor = swatchColor(swatch) || normalizedColor; -- if (target === "stroke") { -- changed = changed || this.selectedStrokeColor !== paletteColor || this.selectedStrokeLabel !== label; -- this.selectedStrokeColor = paletteColor; -- this.selectedStrokeLabel = label; -- return; -- } -- changed = changed || this.selectedFillColor !== paletteColor || this.selectedFillLabel !== label; -- this.selectedFillColor = paletteColor; -- this.selectedFillLabel = label; -- }; -- syncTarget("paint", effectiveShape.style?.fill); -- syncTarget("stroke", effectiveShape.style?.stroke); -- if (changed && render) { -- this.renderPalette(); -- } -- } -- -- opacityInputValue(target) { -- const element = target === "stroke" ? this.elements.strokeOpacity : this.elements.fillOpacity; -- const rawValue = element?.value?.trim() || ""; -- const value = Number(rawValue); -- if (!Number.isInteger(value) || value < 0 || value > 255) { -- const error = `${target === "stroke" ? "Stroke" : "Fill"} opacity must be a whole number between 0 and 255.`; -- this.markInputInvalid(element, error); -- return { ok: false, error }; -- } -- this.clearInputValidity(element); -- return { ok: true, value: this.opacityFromInputByte(value) }; -- } -- -- opacityFromInputByte(value) { -- return Number((value / 255).toFixed(3)); -- } -- -- opacityInputDisplayValue(value) { -- if (!Number.isFinite(value)) { -- return "255"; -- } -- const clamped = Math.min(1, Math.max(0, value)); -- return String(Math.round(clamped * 255)); -- } -- -- changePaletteOpacity(target) { -- const normalizedTarget = target === "stroke" ? "stroke" : "fill"; -- const opacity = this.opacityInputValue(normalizedTarget); -- if (!opacity.ok) { -- this.statusLog.write(`FAIL Palette ${normalizedTarget} opacity rejected: ${opacity.error}`); -- return; -- } -- if (normalizedTarget === "stroke") { -- this.selectedStrokeOpacity = opacity.value; -- } else { -- this.selectedFillOpacity = opacity.value; -- } -- this.updatePaletteModeSwatches(); -- this.statusLog.write(`OK Selected ${normalizedTarget} opacity ${opacity.value}.`); -- } -- -- selectPaletteColor(color, label) { -- if (!color) { -- this.statusLog.write(`FAIL Palette color selection blocked: swatch ${label} has no usable color value.`); -- return false; -- } -- const swatch = this.paletteSwatchForColor(color); -- if (!swatch) { -- this.statusLog.write(`FAIL Palette color selection rejected: ${color} is not in the loaded palette.`); -- return false; -- } -- const paletteLabel = swatch.name || swatch.id || swatch.symbol || label; -- if (this.paletteTarget === "stroke") { -- this.selectedStrokeColor = color; -- this.selectedStrokeLabel = paletteLabel; -- } else { -- this.selectedFillColor = color; -- this.selectedFillLabel = paletteLabel; -- } -- if (this.runtimePalette) { -- this.renderPalette(); -- } -- this.statusLog.write(`OK Selected ${this.paletteTarget === "stroke" ? "stroke" : "paint"} color ${color} from ${paletteLabel}.`); -- return true; -- } -- -- applyPaletteColor(color, label) { -- this.selectPaletteColor(color, label); -- } -- -- applySelectedPaletteColorToShape(shapeIndex, target, sourceLabel) { -- const selectedIndex = normalizeShapeIndex(shapeIndex); -- const selected = sortedShapes(this.selectedObject())[selectedIndex] || null; -- if (!selected) { -- this.statusLog.write("FAIL Palette color application blocked: select a shape first."); -- return; -- } -- if (selected.locked) { -- this.statusLog.write(`WARN Palette color application skipped: shape row ${selectedIndex} is locked.`); -- return; -- } -- if (this.guardSelectedObjectMutation("Palette color application")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const shape = this.findShapeInPayload(nextPayload, selectedIndex); -- const strokeWidth = Number(this.elements.strokeWidth.value); -- const shouldApplyStroke = target === "stroke"; -- const color = shouldApplyStroke ? this.selectedStrokeColor : this.selectedFillColor; -- const label = shouldApplyStroke ? this.selectedStrokeLabel : this.selectedFillLabel; -- if (!color) { -- this.statusLog.write(`FAIL Palette color application blocked: no ${shouldApplyStroke ? "stroke" : "paint"} color is selected.`); -- return; -- } -- const swatch = this.paletteSwatchForColor(color); -- const directLabel = this.directStyleColorLabel(color); -- if (!swatch && !directLabel) { -- this.statusLog.write(`FAIL Palette color application rejected: ${color} is not in the loaded palette.`); -- return; -- } -- const paletteLabel = swatch?.name || swatch?.id || swatch?.symbol || directLabel || label; -- if (shouldApplyStroke) { -- shape.style.stroke = color; -- shape.style.strokeWidth = Number.isFinite(strokeWidth) && strokeWidth > 0 ? strokeWidth : 2; -- shape.style.strokeOpacity = this.selectedStrokeOpacity; -- } else { -- shape.style.fill = color; -- shape.style.fillOpacity = this.selectedFillOpacity; -- } -- const targetLabel = shouldApplyStroke ? `Target: stroke width ${shape.style.strokeWidth}, opacity ${shape.style.strokeOpacity}.` : `Target: paint opacity ${shape.style.fillOpacity}.`; -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, selectedIndex, `OK Applied palette color ${color} from ${paletteLabel} to shape row ${selectedIndex} by ${sourceLabel}. ${targetLabel}`, "Palette color application failed schema validation", { -- syncPaletteSelection: false -- }); -- } -- -- applyTransparentStyleToShape(shapeIndex, target, sourceLabel) { -- const selectedIndex = normalizeShapeIndex(shapeIndex); -- const selected = sortedShapes(this.selectedObject())[selectedIndex] || null; -- if (!selected) { -- this.statusLog.write(`WARN Transparent style skipped: shape row ${shapeIndex ?? "unknown"} is not available.`); -- return; -- } -- if (selected.locked) { -- this.statusLog.write(`WARN Transparent style skipped: shape row ${selectedIndex} is locked.`); -- return; -- } -- if (this.guardSelectedObjectMutation("Transparent style application")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const shape = this.findShapeInPayload(nextPayload, selectedIndex); -- const shouldApplyStroke = target === "stroke"; -- if (shouldApplyStroke) { -- shape.style.stroke = TRANSPARENT_STYLE_COLOR; -- } else { -- shape.style.fill = TRANSPARENT_STYLE_COLOR; -- } -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, `OK Applied transparent ${shouldApplyStroke ? "stroke" : "fill"} to shape row ${selectedIndex} by ${sourceLabel}.`, "Transparent style application failed schema validation", { -- syncPaletteSelection: false -- }); -- } -- -- sampleShapeStyle(shapeIndex) { -- const selectedIndex = normalizeShapeIndex(shapeIndex); -- const selected = sortedShapes(this.selectedObject())[selectedIndex] || null; -- if (!selected) { -- this.statusLog.write(`WARN Picker skipped: shape row ${shapeIndex ?? "unknown"} is not available.`); -- return; -- } -- const effectiveShape = this.effectiveShape(selected, selectedIndex); -- const style = effectiveShape.style || {}; -- if (style.fill) { -- this.selectedFillColor = style.fill; -- this.selectedFillLabel = this.paletteLabelForColor(style.fill, shapeDisplayLabel(selected)); -- } -- if (style.stroke) { -- this.selectedStrokeColor = style.stroke; -- this.selectedStrokeLabel = this.paletteLabelForColor(style.stroke, shapeDisplayLabel(selected)); -- } -- if (Number.isFinite(style.fillOpacity)) { -- this.selectedFillOpacity = style.fillOpacity; -- this.elements.fillOpacity.value = this.opacityInputDisplayValue(style.fillOpacity); -- this.clearInputValidity(this.elements.fillOpacity); -- } -- if (Number.isFinite(style.strokeOpacity)) { -- this.selectedStrokeOpacity = style.strokeOpacity; -- this.elements.strokeOpacity.value = this.opacityInputDisplayValue(style.strokeOpacity); -- this.clearInputValidity(this.elements.strokeOpacity); -- } -- if (Number.isFinite(style.strokeWidth) && style.strokeWidth > 0) { -- this.elements.strokeWidth.value = String(style.strokeWidth); -- } -- this.updatePaletteModeSwatches(); -- if (this.runtimePalette) { -- this.renderPalette(); -- } -- this.statusLog.write(`OK Picker sampled shape row ${selectedIndex}: fill ${style.fill || "none"}, stroke ${style.stroke || "none"}, fill opacity ${Number.isFinite(style.fillOpacity) ? style.fillOpacity : "n/a"}, stroke opacity ${Number.isFinite(style.strokeOpacity) ? style.strokeOpacity : "n/a"}, stroke width ${Number.isFinite(style.strokeWidth) ? style.strokeWidth : "n/a"}.`); -- } -- -- swapFillStrokeColors() { -- const nextFill = this.selectedStrokeColor; -- const nextFillLabel = this.selectedStrokeLabel; -- this.selectedStrokeColor = this.selectedFillColor; -- this.selectedStrokeLabel = this.selectedFillLabel; -- this.selectedFillColor = nextFill; -- this.selectedFillLabel = nextFillLabel; -- if (this.runtimePalette) { -- this.renderPalette(); -- } -- this.statusLog.write(`OK Swapped fill and stroke colors: fill ${this.selectedFillColor}, stroke ${this.selectedStrokeColor}.`); -- } -- -- restoreDefaultColors() { -- const fillColor = this.firstPaletteColor(); -- const strokeColor = this.secondPaletteColor(fillColor); -- if (!fillColor || !strokeColor) { -- this.statusLog.write("FAIL Default colors blocked: a loaded palette color is required."); -- return; -- } -- this.selectedFillColor = fillColor; -- this.selectedStrokeColor = strokeColor; -- this.selectedFillLabel = this.paletteLabelForColor(fillColor, "default fill"); -- this.selectedStrokeLabel = this.paletteLabelForColor(strokeColor, "default stroke"); -- this.renderPalette(); -- this.statusLog.write(`OK Restored default paint/stroke colors from loaded palette: fill ${fillColor}, stroke ${strokeColor}.`); -- } -- -- toggleSelectedShapeVisibility() { -- this.toggleShapeVisibilityByIndex(this.selectedShapeIndex, this.selectedObjectId); -- } -- -- toggleShapeVisibilityByIndex(shapeIndex, objectId = this.selectedObjectId) { -- const normalizedIndex = normalizeShapeIndex(shapeIndex); -- const object = this.currentPayload?.objects.find((candidate) => candidate.id === objectId) || null; -- const shape = sortedShapes(object)[normalizedIndex] || null; -- if (!object || !shape) { -- this.statusLog.write("WARN Shape visibility skipped: no shape is selected."); -- return; -- } -- if (shape.locked) { -- this.statusLog.write(`WARN Shape visibility skipped: shape row ${normalizedIndex} is locked.`); -- return; -- } -- if (this.isObjectLocked(object.id)) { -- this.statusLog.write(`WARN Shape visibility blocked: object ${object.name} is locked for this runtime session.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- const targetIsSelectedObject = object.id === this.selectedObjectId; -- const state = targetIsSelectedObject ? this.objectStates(nextObject).find((candidate) => candidate.id === this.selectedStateId) : null; -- const frame = targetIsSelectedObject ? sortedFrames(state).find((candidate) => candidate.id === this.selectedFrameId) : null; -- if (frame && !Array.isArray(frame.shapeOverrides)) { -- frame.shapeOverrides = []; -- } -- let override = frame?.shapeOverrides.find((entry) => entry.shapeIndex === normalizedIndex) || null; -- if (frame) { -- const baseShape = sortedShapes(nextObject)[normalizedIndex]; -- if (!override) { -- override = { -- shapeIndex: normalizedIndex, -- transform: this.shapeTransform(baseShape), -- visible: baseShape.visible -- }; -- frame.shapeOverrides.push(override); -- } -- const nextVisible = !this.effectiveShapeForFrame(shape, this.activeFrame(), normalizedIndex).visible; -- override.visible = nextVisible; -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, `OK State ${this.selectedStateId} frame ${this.selectedFrameId} shape row ${normalizedIndex} visibility set to ${nextVisible ? "visible" : "hidden"}.`, "State frame visibility failed schema validation", { -- directSelectedShapeIndexes: this.directSelectedShapeIndexes, -- selectedShapeIndexes: this.selectedShapeIndexes -- }); -- return; -- } -- const nextShape = sortedShapes(nextObject)[normalizedIndex]; -- nextShape.visible = !nextShape.visible; -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, `OK Shape row ${normalizedIndex} visibility set to ${nextShape.visible ? "visible" : "hidden"}.`, "Shape visibility failed schema validation", { -- directSelectedShapeIndexes: this.directSelectedShapeIndexes, -- selectedShapeIndexes: this.selectedShapeIndexes -- }); -- } -- -- toggleSelectedShapeLock() { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Shape lock skipped: no shape is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Shape lock")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const shape = this.findShapeInPayload(nextPayload, this.selectedShapeIndex); -- shape.locked = !shape.locked; -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, `OK Shape row ${this.selectedShapeIndex} lock set to ${shape.locked ? "locked" : "unlocked"}.`, "Shape lock failed schema validation"); -- } -- -- changeSelectedShapeOrder(action) { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Shape order skipped: no shape is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Shape order")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const object = nextPayload.objects.find((candidate) => candidate.id === this.selectedObjectId); -- const shapes = sortedShapes(object); -- const selectedIndexes = Array.from(new Set(Array.from(this.selectedShapeIndexes || []) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0 && shapeIndex < shapes.length))) -- .sort((left, right) => left - right); -- if (!selectedIndexes.length && this.selectedShapeIndex >= 0 && this.selectedShapeIndex < shapes.length) { -- selectedIndexes.push(this.selectedShapeIndex); -- } -- const lockedIndex = selectedIndexes.find((shapeIndex) => shapes[shapeIndex]?.locked); -- if (Number.isInteger(lockedIndex)) { -- this.statusLog.write(`WARN Shape order skipped: shape row ${lockedIndex} is locked.`); -- return; -- } -- if (shapes.length < 2) { -- this.statusLog.write(`WARN Shape order skipped: shape row ${this.selectedShapeIndex} is the only shape in ${object.name}.`); -- return; -- } -- -- const selectedIndexSet = new Set(selectedIndexes); -- const oldIndexByShape = new Map(shapes.map((shape, shapeIndex) => [shape, shapeIndex])); -- const reorderedShapes = shapes.slice(); -- if (action === "back") { -- reorderedShapes.sort((left, right) => { -- const leftIndex = oldIndexByShape.get(left); -- const rightIndex = oldIndexByShape.get(right); -- const leftSelected = selectedIndexSet.has(leftIndex); -- const rightSelected = selectedIndexSet.has(rightIndex); -- if (leftSelected === rightSelected) { -- return leftIndex - rightIndex; -- } -- return leftSelected ? -1 : 1; -- }); -- } else if (action === "front") { -- reorderedShapes.sort((left, right) => { -- const leftIndex = oldIndexByShape.get(left); -- const rightIndex = oldIndexByShape.get(right); -- const leftSelected = selectedIndexSet.has(leftIndex); -- const rightSelected = selectedIndexSet.has(rightIndex); -- if (leftSelected === rightSelected) { -- return leftIndex - rightIndex; -- } -- return leftSelected ? 1 : -1; -- }); -- } else if (action === "backward") { -- for (let index = 1; index < reorderedShapes.length; index += 1) { -- const currentOldIndex = oldIndexByShape.get(reorderedShapes[index]); -- const previousOldIndex = oldIndexByShape.get(reorderedShapes[index - 1]); -- if (selectedIndexSet.has(currentOldIndex) && !selectedIndexSet.has(previousOldIndex)) { -- [reorderedShapes[index - 1], reorderedShapes[index]] = [reorderedShapes[index], reorderedShapes[index - 1]]; -- } -- } -- } else if (action === "forward") { -- for (let index = reorderedShapes.length - 2; index >= 0; index -= 1) { -- const currentOldIndex = oldIndexByShape.get(reorderedShapes[index]); -- const nextOldIndex = oldIndexByShape.get(reorderedShapes[index + 1]); -- if (selectedIndexSet.has(currentOldIndex) && !selectedIndexSet.has(nextOldIndex)) { -- [reorderedShapes[index], reorderedShapes[index + 1]] = [reorderedShapes[index + 1], reorderedShapes[index]]; -- } -- } -- } else { -- this.statusLog.write(`WARN Shape z-order skipped: unsupported action ${action}.`); -- return; -- } -- -- const oldShapes = shapes.slice(); -- if (oldShapes.every((shape, shapeIndex) => reorderedShapes[shapeIndex] === shape)) { -- const subject = selectedIndexes.length > 1 ? `selected shape rows ${selectedIndexes.join(", ")}` : `shape row ${this.selectedShapeIndex}`; -- this.statusLog.write(`WARN Shape z-order skipped: ${subject} cannot move ${action}.`); -- return; -- } -- const oldIndexToNextIndex = new Map(); -- oldShapes.forEach((shape, oldIndex) => { -- oldIndexToNextIndex.set(oldIndex, reorderedShapes.indexOf(shape)); -- }); -- reorderedShapes.forEach((shape, shapeIndex) => { -- shape.order = shapeIndex + 1; -- }); -- object.shapes = reorderedShapes; -- this.remapShapeOverrideIndexes(object, oldShapes, reorderedShapes); -- const selectedShapeIndexes = new Set(selectedIndexes -- .map((shapeIndex) => oldIndexToNextIndex.get(shapeIndex)) -- .filter((shapeIndex) => Number.isInteger(shapeIndex) && shapeIndex >= 0)); -- const directSelectedShapeIndexes = new Set(Array.from(this.directSelectedShapeIndexes || []) -- .map((shapeIndex) => oldIndexToNextIndex.get(normalizeShapeIndex(shapeIndex))) -- .filter((shapeIndex) => Number.isInteger(shapeIndex) && shapeIndex >= 0)); -- if (!directSelectedShapeIndexes.size) { -- selectedShapeIndexes.forEach((shapeIndex) => directSelectedShapeIndexes.add(shapeIndex)); -- } -- const nextSelectedShapeIndex = oldIndexToNextIndex.get(this.selectedShapeIndex); -- const primaryShapeIndex = Number.isInteger(nextSelectedShapeIndex) && nextSelectedShapeIndex >= 0 -- ? nextSelectedShapeIndex -- : Math.min(...selectedShapeIndexes); -- const okMessage = selectedIndexes.length > 1 -- ? `OK Shape rows ${selectedIndexes.join(", ")} z-order ${action}.` -- : `OK Shape row ${this.selectedShapeIndex} z-order ${action}.`; -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, primaryShapeIndex, okMessage, "Shape z-order failed schema validation", { -- directSelectedShapeIndexes, -- selectedShapeIndexes -- }); -- } -- -- objectTransformOrigin(object) { -- const origin = normalizeObjectVectorOrigin(object?.objectOrigin); -- return { -- x: this.formatViewportNumber(origin.x), -- y: this.formatViewportNumber(origin.y) -- }; -- } -- -- updateSelectedObjectOrigin(origin, okMessage) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Object Transform origin skipped: no object is selected."); -- return false; -- } -- if (this.guardSelectedObjectMutation("Object Transform origin")) { -- return false; -- } -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- nextObject.objectOrigin = { -- x: Number(origin.x.toFixed(3)), -- y: Number(origin.y.toFixed(3)) -- }; -- return this.commitPayloadUpdate( -- nextPayload, -- object.id, -- this.selectedShapeIndex, -- okMessage, -- "Object Transform origin failed schema validation", -- { -- directSelectedShapeIndexes: this.directSelectedShapeIndexes, -- selectedShapeIndexes: this.selectedShapeIndexes -- } -- ); -- } -- -- selectedObjectShapeIndexes(object = this.selectedObject()) { -- return sortedShapes(object).map((shape, shapeIndex) => shapeIndex); -- } -- -- readObjectOriginInputs() { -- const originX = this.numberInputValue("objectVectorStudioV2ObjectOriginXInput", "Object Origin X"); -- const originY = this.numberInputValue("objectVectorStudioV2ObjectOriginYInput", "Object Origin Y"); -- if (!originX.ok || !originY.ok) { -- return { -- error: originX.error || originY.error, -- ok: false, -- value: { x: 0, y: 0 } -- }; -- } -- return { -- error: "", -- ok: true, -- value: { -- x: Number(originX.value.toFixed(3)), -- y: Number(originY.value.toFixed(3)) -- } -- }; -- } -- -- updateSelectedObjectTransforms(operation, updater, okMessage, failPrefix = `Object Transform ${operation} failed schema validation`, options = {}) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write(`WARN Object Transform ${operation} skipped: no object is selected.`); -- return false; -- } -- if (this.guardSelectedObjectMutation(`Object Transform ${operation}`)) { -- return false; -- } -- -- const targetIndexes = this.selectedObjectShapeIndexes(object); -- if (!targetIndexes.length) { -- this.statusLog.write(`WARN Object Transform ${operation} skipped: object ${object.name} has no shapes.`); -- return false; -- } -- const lockedIndex = targetIndexes.find((shapeIndex) => sortedShapes(object)[shapeIndex]?.locked); -- if (Number.isInteger(lockedIndex)) { -- this.statusLog.write(`WARN Object Transform ${operation} skipped: shape row ${lockedIndex} is locked.`); -- return false; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const activeFrame = this.activeFrame(); -- try { -- targetIndexes.forEach((shapeIndex) => { -- const override = this.frameOverrideInPayload(nextPayload, shapeIndex, { create: true }); -- const baseShape = this.findShapeInPayload(nextPayload, shapeIndex); -- const shape = override -- ? this.effectiveShapeForFrame(baseShape, activeFrame, shapeIndex) -- : baseShape; -- if (!shape) { -- throw new Error(`shape row ${shapeIndex} is not available.`); -- } -- updater(shape, shapeIndex); -- shape.transform = this.ensureShapeTransform(shape); -- const transformErrors = this.validateShapeForTransform(shape); -- if (transformErrors.length) { -- throw new Error(`shape row ${shapeIndex}: ${transformErrors.join(" ")}`); -- } -- if (override) { -- override.transform = this.ensureShapeTransform(shape); -- } -- }); -- } catch (error) { -- this.statusLog.write(`FAIL Invalid object transform rejected for ${object.name}: ${error.message}`); -- return false; -- } -- -- return this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, okMessage, failPrefix, { -- directSelectedShapeIndexes: this.directSelectedShapeIndexes, -- dirtyReason: options.dirtyReason, -- selectedShapeIndexes: this.selectedShapeIndexes -- }); -- } -- -- rotateSelectedObject() { -- const input = this.objectRotateInputValue(); -- if (!input.ok) { -- this.statusLog.write(`FAIL Invalid object transform rejected: ${input.error}`); -- return; -- } -- const origin = this.readObjectOriginInputs(); -- if (!origin.ok) { -- this.statusLog.write(`FAIL Invalid object transform rejected: ${origin.error}`); -- return; -- } -- const rotation = this.snapAngle(input.value); -- const snapAngleStatus = this.formatSnapAngleRotateStatus(input.value, rotation); -- const object = this.selectedObject(); -- const activeFrame = this.activeFrame(); -- const targetIndexes = this.selectedObjectShapeIndexes(object); -- const nextTransforms = new Map(targetIndexes.map((shapeIndex) => { -- const effectiveShape = this.effectiveShapeForFrame(sortedShapes(object)[shapeIndex], activeFrame, shapeIndex); -- const transform = this.ensureShapeTransform(effectiveShape); -- const translatedOrigin = { -- x: this.formatViewportNumber(origin.value.x + transform.x), -- y: this.formatViewportNumber(origin.value.y + transform.y) -- }; -- const rotatedTranslatedOrigin = this.rotatePointAround(translatedOrigin, origin.value, rotation); -- return [shapeIndex, { -- ...transform, -- rotation: this.normalizeRotationDegrees(transform.rotation + rotation), -- x: this.formatViewportNumber(rotatedTranslatedOrigin.x - origin.value.x), -- y: this.formatViewportNumber(rotatedTranslatedOrigin.y - origin.value.y) -- }]; -- })); -- this.updateSelectedObjectTransforms("rotate", (shape, shapeIndex) => { -- shape.transform = this.ensureShapeTransform({ transform: nextTransforms.get(shapeIndex) }); -- }, `OK Rotated object ${object?.name || "selected object"} (${targetIndexes.length} shapes) by ${rotation} degrees around origin ${origin.value.x}, ${origin.value.y}.${snapAngleStatus}`); -- } -- -- applySelectedObjectOrigin() { -- const origin = this.readObjectOriginInputs(); -- if (!origin.ok) { -- this.statusLog.write(`FAIL Invalid object transform rejected: ${origin.error}`); -- return; -- } -- const object = this.selectedObject(); -- this.updateSelectedObjectOrigin(origin.value, `OK Updated object ${object?.name || "selected object"} origin/pivot to ${origin.value.x}, ${origin.value.y}.`); -- } -- -- autoOriginSelectedObjectPivot() { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Auto Origin skipped: no object is selected."); -- return; -- } -- const visibleShapes = sortedShapes(object) -- .map((shape, shapeIndex) => this.effectiveShapeForFrame(shape, this.activeFrame(), shapeIndex)) -- .filter((shape) => shape.visible !== false); -- if (!visibleShapes.length) { -- this.statusLog.write(`FAIL Auto Origin blocked: object ${object.name} has no visible geometry.`); -- return; -- } -- const bounds = this.rawVisibleObjectGeometryBounds(object); -- if (!bounds) { -- this.statusLog.write(`FAIL Auto Origin blocked: object ${object.name} has no raw visible geometry bounds.`); -- return; -- } -- const balancedCenter = { -- x: this.formatViewportNumber(bounds.x + bounds.width / 2), -- y: this.formatViewportNumber(bounds.y + bounds.height / 2) -- }; -- this.transformInputValues.set("objectVectorStudioV2ObjectOriginXInput", String(balancedCenter.x)); -- this.transformInputValues.set("objectVectorStudioV2ObjectOriginYInput", String(balancedCenter.y)); -- this.updateSelectedObjectOrigin(balancedCenter, `OK Auto Origin updated object ${object.name} origin/pivot from raw visible geometry bounds ${balancedCenter.x}, ${balancedCenter.y}.`); -- } -- -- moveSelectedShape() { -- if (!this.canUseShapeTransform()) { -- this.statusLog.write("WARN Shape Transform move skipped: select exactly one shape."); -- return; -- } -- const x = this.numberInputValue("objectVectorStudioV2MoveXInput", "Move X"); -- const y = this.numberInputValue("objectVectorStudioV2MoveYInput", "Move Y"); -- if (!x.ok || !y.ok) { -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: ${x.error || y.error}`); -- return; -- } -- const moveX = this.snapDistance(x.value); -- const moveY = this.snapDistance(y.value); -- const object = this.selectedObject(); -- if (this.shapeBelongsToValidGroup(object, this.selectedShapeIndex)) { -- const targetIndexes = this.shapeSelectionGroupIndexes(sortedShapes(object), this.selectedShapeIndex); -- const groupId = String(sortedShapes(object)[this.selectedShapeIndex]?.groupId || "").trim(); -- this.moveSelectedShapeGroup(targetIndexes, moveX, moveY, groupId); -- return; -- } -- this.updateSelectedShapeTransform("move", (shape) => { -- shape.transform = this.ensureShapeTransform(shape); -- shape.transform.x = Number((shape.transform.x + moveX).toFixed(3)); -- shape.transform.y = Number((shape.transform.y + moveY).toFixed(3)); -- }, `OK Moved shape row ${this.selectedShapeIndex} by ${moveX}, ${moveY}.`); -- } -- -- moveSelectedShapeGroup(shapeIndexes, moveX, moveY, groupId) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Transform move skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Transform move")) { -- return; -- } -- -- const objectShapes = sortedShapes(object); -- const targetIndexes = Array.from(new Set(shapeIndexes -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0 && shapeIndex < objectShapes.length))) -- .sort((left, right) => left - right); -- if (targetIndexes.length < 2) { -- this.updateSelectedShapeTransform("move", (shape) => { -- shape.transform = this.ensureShapeTransform(shape); -- shape.transform.x = Number((shape.transform.x + moveX).toFixed(3)); -- shape.transform.y = Number((shape.transform.y + moveY).toFixed(3)); -- }, `OK Moved shape row ${this.selectedShapeIndex} by ${moveX}, ${moveY}.`); -- return; -- } -- -- const lockedIndex = targetIndexes.find((shapeIndex) => objectShapes[shapeIndex]?.locked); -- if (Number.isInteger(lockedIndex)) { -- this.statusLog.write(`WARN Transform move skipped: shape row ${lockedIndex} is locked.`); -- return; -- } -- -- this.selectedShapeIndexes = new Set(targetIndexes); -- const directShapeIndexes = this.directSelectedShapeIndexes.size -- ? new Set(Array.from(this.directSelectedShapeIndexes).filter((shapeIndex) => targetIndexes.includes(shapeIndex))) -- : new Set([this.selectedShapeIndex]); -- if (!directShapeIndexes.size && this.selectedShapeIndex >= 0) { -- directShapeIndexes.add(this.selectedShapeIndex); -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- try { -- targetIndexes.forEach((shapeIndex) => { -- const override = this.frameOverrideInPayload(nextPayload, shapeIndex, { create: true }); -- const shape = override -- ? this.effectiveShapeForFrame(this.findShapeInPayload(nextPayload, shapeIndex), this.activeFrame(), shapeIndex) -- : this.findShapeInPayload(nextPayload, shapeIndex); -- if (!shape) { -- throw new Error(`shape row ${shapeIndex} is not available.`); -- } -- shape.transform = this.ensureShapeTransform(shape); -- shape.transform.x = Number((shape.transform.x + moveX).toFixed(3)); -- shape.transform.y = Number((shape.transform.y + moveY).toFixed(3)); -- const transformErrors = this.validateShapeForTransform(shape); -- if (transformErrors.length) { -- throw new Error(`shape row ${shapeIndex}: ${transformErrors.join(" ")}`); -- } -- if (override) { -- override.transform = this.ensureShapeTransform(shape); -- } -- }); -- } catch (error) { -- this.statusLog.write(`FAIL Invalid transform rejected for group ${groupId || "selected group"}: ${error.message}`); -- return; -- } -- -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, `OK Moved group ${groupId || "selected group"} (${targetIndexes.length} shapes) by ${moveX}, ${moveY}.`, "Transform move failed schema validation", { -- directSelectedShapeIndexes: directShapeIndexes, -- selectedShapeIndexes: new Set(targetIndexes) -- }); -- } -- -- rotateSelectedShape() { -- if (!this.canUseShapeTransform()) { -- this.statusLog.write("WARN Shape Transform rotate skipped: select exactly one shape."); -- return; -- } -- const input = this.rotateInputValue(); -- if (!input.ok) { -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: ${input.error}`); -- return; -- } -- const rotation = this.snapAngle(input.value); -- const snapAngleStatus = this.formatSnapAngleRotateStatus(input.value, rotation); -- const object = this.selectedObject(); -- const origin = this.objectTransformOrigin(object); -- this.updateSelectedShapeGeometry("rotate", (shape) => { -- this.rotateShapeGeometryAroundOrigin(shape, origin, rotation); -- shape.transform = { -- ...this.ensureShapeTransform(shape), -- rotation: 0 -- }; -- }, `OK Rotated shape row ${this.selectedShapeIndex} geometry by ${rotation} degrees around object origin ${origin.x}, ${origin.y}.${snapAngleStatus}`); -- } -- -- formatSnapAngleRotateStatus(rawRotation, appliedRotation) { -- if (this.angleSnapEnabled) { -- return ` Snap Angle active: ${rawRotation} -> ${appliedRotation}.`; -- } -- return " Snap Angle disabled: raw rotation applied."; -- } -- -- rotateInputValue() { -- if (!this.angleSnapEnabled) { -- const input = this.numberInputValue("objectVectorStudioV2RotateInput", "Rotate"); -- return { ...input, rawValue: String(input.value) }; -- } -- const select = this.window.document.getElementById("objectVectorStudioV2RotateSnapSelect"); -- const rawValue = select?.value?.trim() || ""; -- const value = Number(rawValue); -- if (!select || rawValue === "" || !Number.isFinite(value)) { -- const error = "Rotate Snap Angle must be a valid dropdown value."; -- this.markInputInvalid(select, error); -- return { error, ok: false, rawValue, value: 0 }; -- } -- this.clearInputValidity(select); -- return { error: "", ok: true, rawValue, value }; -- } -- -- objectRotateInputValue() { -- if (!this.angleSnapEnabled) { -- const input = this.numberInputValue("objectVectorStudioV2ObjectRotateInput", "Object Rotate"); -- return { ...input, rawValue: String(input.value) }; -- } -- const select = this.window.document.getElementById("objectVectorStudioV2ObjectRotateSnapSelect"); -- const rawValue = select?.value?.trim() || ""; -- const value = Number(rawValue); -- if (!select || rawValue === "" || !Number.isFinite(value)) { -- const error = "Object Rotate Snap Angle must be a valid dropdown value."; -- this.markInputInvalid(select, error); -- return { error, ok: false, rawValue, value: 0 }; -- } -- this.clearInputValidity(select); -- return { error: "", ok: true, rawValue, value }; -- } -- -- rotatePointAround(point, pivot, rotation) { -- return rotateSharedPointAround(point, pivot, rotation, "degrees"); -- } -- -- rotateGeometryPoint(point, origin, rotation) { -- const rotated = this.rotatePointAround(point, origin, rotation); -- return { -- x: this.formatViewportNumber(rotated.x), -- y: this.formatViewportNumber(rotated.y) -- }; -- } -- -- rotateShapeGeometryAroundOrigin(shape, origin, rotation) { -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "polygon" || geometryTool === "polyline") { -- shape.geometry.points = shape.geometry.points.map((point) => this.rotateGeometryPoint(point, origin, rotation)); -- return; -- } -- if (geometryTool === "line") { -- shape.geometry.point1 = this.rotateGeometryPoint(shape.geometry.point1, origin, rotation); -- shape.geometry.point2 = this.rotateGeometryPoint(shape.geometry.point2, origin, rotation); -- return; -- } -- if (geometryTool === "rectangle") { -- const center = this.rotateGeometryPoint({ -- x: shape.geometry.x + shape.geometry.width / 2, -- y: shape.geometry.y + shape.geometry.height / 2 -- }, origin, rotation); -- shape.geometry.x = this.formatViewportNumber(center.x - shape.geometry.width / 2); -- shape.geometry.y = this.formatViewportNumber(center.y - shape.geometry.height / 2); -- return; -- } -- if (geometryTool === "circle") { -- const center = this.rotateGeometryPoint({ x: shape.geometry.cx, y: shape.geometry.cy }, origin, rotation); -- shape.geometry.cx = center.x; -- shape.geometry.cy = center.y; -- return; -- } -- if (geometryTool === "ellipse") { -- const center = this.rotateGeometryPoint({ x: shape.geometry.cx, y: shape.geometry.cy }, origin, rotation); -- shape.geometry.cx = center.x; -- shape.geometry.cy = center.y; -- return; -- } -- if (geometryTool === "arc") { -- const center = this.rotateGeometryPoint({ x: shape.geometry.cx, y: shape.geometry.cy }, origin, rotation); -- shape.geometry.cx = center.x; -- shape.geometry.cy = center.y; -- shape.geometry.startAngle = this.normalizeRotationDegrees(shape.geometry.startAngle + rotation); -- shape.geometry.endAngle = this.normalizeRotationDegrees(shape.geometry.endAngle + rotation); -- return; -- } -- if (geometryTool === "text") { -- const anchor = this.rotateGeometryPoint({ x: shape.geometry.x, y: shape.geometry.y }, origin, rotation); -- shape.geometry.x = anchor.x; -- shape.geometry.y = anchor.y; -- } -- } -- -- normalizeRotationDegrees(value) { -- return normalizeSharedRotationDegrees(value); -- } -- -- formatScaleInputValue(value) { -- return String(Number(Number(value).toFixed(3))); -- } -- -- readScaleInput(inputId = "objectVectorStudioV2ScaleInput", label = "Scale") { -- const input = this.numberInputValue(inputId, label); -- if (!input.ok) { -- return input; -- } -- if (input.value <= 0) { -- this.markInputInvalid(this.window.document.getElementById(inputId), `${label} must be greater than 0.`); -- return { error: "scale must be greater than 0.", ok: false, value: input.value }; -- } -- return { error: "", ok: true, value: Number(input.value.toFixed(3)) }; -- } -- -- applyScaleInputLive(inputElement) { -- const rawValue = inputElement?.value?.trim() || ""; -- if (!rawValue) { -- this.markInputInvalid(inputElement, "Scale must be a finite number."); -- return; -- } -- const input = this.readScaleInput(); -- if (!input.ok) { -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: ${input.error}`); -- return; -- } -- this.applySelectedShapeScaleValue(input.value, { -- okMessage: `OK Scale preview set to ${this.formatScaleInputValue(input.value)} for shape row ${this.selectedShapeIndex}.`, -- renderControls: false -- }); -- } -- -- adjustSelectedShapeScale(delta) { -- const input = this.readScaleInput(); -- if (!input.ok) { -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: ${input.error}`); -- return; -- } -- const nextScale = Number((input.value + delta).toFixed(3)); -- if (nextScale <= 0) { -- this.markInputInvalid(this.window.document.getElementById("objectVectorStudioV2ScaleInput"), "Scale must be greater than 0."); -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: scale must be greater than 0.`); -- return; -- } -- const inputElement = this.window.document.getElementById("objectVectorStudioV2ScaleInput"); -- if (inputElement) { -- inputElement.value = this.formatScaleInputValue(nextScale); -- this.transformInputValues.set(inputElement.id, inputElement.value); -- } -- this.applySelectedShapeScaleValue(nextScale, { -- okMessage: `OK Scale preview set to ${this.formatScaleInputValue(nextScale)} for shape row ${this.selectedShapeIndex}.`, -- renderControls: false -- }); -- } -- -- resetSelectedShapeScale() { -- const inputElement = this.window.document.getElementById("objectVectorStudioV2ScaleInput"); -- if (inputElement) { -- inputElement.value = "1"; -- this.transformInputValues.set(inputElement.id, "1"); -- this.clearInputValidity(inputElement); -- } -- this.applySelectedShapeScaleValue(1, { -- okMessage: `OK Scale reset to 1 for shape row ${this.selectedShapeIndex}.`, -- renderControls: false -- }); -- } -- -- applySelectedShapeScaleValue(scale, { okMessage, renderControls = false } = {}) { -- if (!this.canUseShapeTransform()) { -- this.statusLog.write("WARN Transform scale skipped: select exactly one shape."); -- return false; -- } -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Transform scale skipped: no shape is selected."); -- return false; -- } -- if (selected.locked) { -- this.statusLog.write(`WARN Transform scale skipped: shape row ${this.selectedShapeIndex} is locked.`); -- return false; -- } -- if (this.guardSelectedObjectMutation("Transform scale")) { -- return false; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const override = this.frameOverrideInPayload(nextPayload, this.selectedShapeIndex, { create: true }); -- const shape = override -- ? this.effectiveShapeForFrame(this.findShapeInPayload(nextPayload, this.selectedShapeIndex), this.activeFrame(), this.selectedShapeIndex) -- : this.findShapeInPayload(nextPayload, this.selectedShapeIndex); -- shape.transform = this.ensureShapeTransform(shape); -- shape.transform.scaleX = Number(scale.toFixed(3)); -- shape.transform.scaleY = Number(scale.toFixed(3)); -- const transformErrors = this.validateShapeForTransform(shape); -- if (transformErrors.length) { -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: ${transformErrors.join(" ")}`); -- return false; -- } -- if (override) { -- override.transform = this.ensureShapeTransform(shape); -- } -- -- const validation = this.schemaService.validatePayload(nextPayload); -- if (!validation.ok) { -- this.statusLog.write(`FAIL Transform scale failed schema validation: ${validation.errors.join(" ")}`); -- return false; -- } -- const hasPayloadChange = this.hasPayloadChanged(validation.payload); -- const workspaceSync = this.syncWorkspaceToolSessionPayload(validation.payload, { -- changedKeys: ["data.objects"], -- reason: "object-vector-transform-scale" -- }); -- if (!workspaceSync.ok) { -- this.statusLog.write(`FAIL Transform scale failed schema validation: ${workspaceSync.message}`); -- return false; -- } -- -- this.recordPreviewHistorySnapshot(hasPayloadChange); -- this.currentPayload = validation.payload; -- this.actionNav.setJsonPayloadActionsEnabled(true); -- this.ensureSelectedFrame(); -- if (renderControls) { -- this.renderPayload(); -- } else { -- this.renderObjectTiles(); -- this.renderShapeTiles(); -- this.renderWorkSurface(); -- this.updateTransformSummaryText(); -- this.updateSelectedObjectJsonDetails(); -- this.updateObjectActionState(); -- } -- if (workspaceSync.changed) { -- this.statusLog.write(`OK Object Vector Studio V2 workspace dirty state: true; reason=${workspaceSync.reason}; changedKeys=${workspaceSync.changedKeys.join(", ")}.`); -- } -- if (okMessage) { -- this.statusLog.write(okMessage); -- } -- return true; -- } -- -- applyObjectScaleInputLive(inputElement) { -- const rawValue = inputElement?.value?.trim() || ""; -- if (!rawValue) { -- this.markInputInvalid(inputElement, "Object Scale must be a finite number."); -- return; -- } -- const input = this.readScaleInput("objectVectorStudioV2ObjectScaleInput", "Object Scale"); -- if (!input.ok) { -- this.statusLog.write(`FAIL Invalid object transform rejected: ${input.error}`); -- return; -- } -- this.applySelectedObjectScaleValue(input.value, { -- okMessage: `OK Object resize scale set to ${this.formatScaleInputValue(input.value)} for ${this.selectedObject()?.name || "selected object"}; object transform scale remains 1 until Resize rewrites geometry.` -- }); -- } -- -- adjustSelectedObjectScale(delta) { -- const input = this.readScaleInput("objectVectorStudioV2ObjectScaleInput", "Object Scale"); -- if (!input.ok) { -- this.statusLog.write(`FAIL Invalid object transform rejected: ${input.error}`); -- return; -- } -- const nextScale = Number((input.value + delta).toFixed(3)); -- if (nextScale <= 0) { -- this.markInputInvalid(this.window.document.getElementById("objectVectorStudioV2ObjectScaleInput"), "Object Scale must be greater than 0."); -- this.statusLog.write("FAIL Invalid object transform rejected: scale must be greater than 0."); -- return; -- } -- const inputElement = this.window.document.getElementById("objectVectorStudioV2ObjectScaleInput"); -- if (inputElement) { -- inputElement.value = this.formatScaleInputValue(nextScale); -- this.transformInputValues.set(inputElement.id, inputElement.value); -- } -- this.applySelectedObjectScaleValue(nextScale, { -- okMessage: `OK Object resize scale set to ${this.formatScaleInputValue(nextScale)} for ${this.selectedObject()?.name || "selected object"}; object transform scale remains 1 until Resize rewrites geometry.` -- }); -- } -- -- resetSelectedObjectScale() { -- const inputElement = this.window.document.getElementById("objectVectorStudioV2ObjectScaleInput"); -- if (inputElement) { -- inputElement.value = "1"; -- this.transformInputValues.set(inputElement.id, "1"); -- this.clearInputValidity(inputElement); -- } -- this.applySelectedObjectScaleValue(1, { -- okMessage: `OK Object resize scale reset to 1 for ${this.selectedObject()?.name || "selected object"}; object transform scale remains 1 until Resize rewrites geometry.` -- }); -- } -- -- applySelectedObjectScaleValue(scale, { okMessage } = {}) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Object scale skipped: no object is selected."); -- return false; -- } -- this.transformInputValues.set("objectVectorStudioV2ObjectScaleInput", this.formatScaleInputValue(scale)); -- this.renderWorkSurface(); -- this.updateTransformSummaryText(); -- this.updateSelectedObjectJsonDetails(); -- if (okMessage) { -- this.statusLog.write(okMessage); -- } else { -- this.statusLog.write(`OK Object resize scale set to ${this.formatScaleInputValue(scale)} for ${object.name}; object transform scale remains 1 until Resize rewrites geometry.`); -- } -- return true; -- } -- -- resizeSelectedObject() { -- const input = this.readScaleInput("objectVectorStudioV2ObjectScaleInput", "Object Scale"); -- if (!input.ok) { -- this.statusLog.write(`FAIL Resize Geometry rejected for selected object: ${input.error}`); -- return; -- } -- const origin = this.readObjectOriginInputs(); -- if (!origin.ok) { -- this.statusLog.write(`FAIL Resize Geometry rejected for selected object: ${origin.error}`); -- return; -- } -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Resize Geometry skipped: no object is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Resize Geometry")) { -- return; -- } -- -- const targetIndexes = this.selectedObjectShapeIndexes(object); -- const lockedIndex = targetIndexes.find((shapeIndex) => sortedShapes(object)[shapeIndex]?.locked); -- if (Number.isInteger(lockedIndex)) { -- this.statusLog.write(`WARN Resize Geometry skipped: shape row ${lockedIndex} is locked.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- try { -- targetIndexes.forEach((shapeIndex) => { -- const baseShape = this.findShapeInPayload(nextPayload, shapeIndex); -- const override = this.frameOverrideInPayload(nextPayload, shapeIndex, { create: false }); -- const transformTarget = override?.transform ? { transform: override.transform, geometry: baseShape.geometry } : baseShape; -- const transform = this.ensureShapeTransform(transformTarget); -- transform.scaleX = input.value; -- transform.scaleY = input.value; -- this.resizeShapeGeometryByTransformScale(baseShape, transform, origin.value); -- transform.scaleX = 1; -- transform.scaleY = 1; -- if (override?.transform) { -- override.transform = transform; -- } else { -- baseShape.transform = transform; -- } -- const transformErrors = this.validateShapeForTransform(baseShape); -- if (transformErrors.length) { -- throw new Error(`shape row ${shapeIndex}: ${transformErrors.join(" ")}`); -- } -- }); -- } catch (error) { -- this.statusLog.write(`FAIL Resize Geometry rejected for selected object: ${error.message}`); -- return; -- } -- -- this.transformInputValues.set("objectVectorStudioV2ObjectScaleInput", "1"); -- this.commitPayloadUpdate( -- nextPayload, -- object.id, -- this.selectedShapeIndex, -- `OK Resize Geometry applied scale ${this.formatScaleInputValue(input.value)} to object ${object.name} (${targetIndexes.length} shapes); transform scale reset to 1.`, -- "Resize Geometry failed schema validation", -- { -- directSelectedShapeIndexes: this.directSelectedShapeIndexes, -- selectedShapeIndexes: this.selectedShapeIndexes -- } -- ); -- } -- -- resizeSelectedShape() { -- if (!this.canUseShapeTransform()) { -- this.statusLog.write("WARN Resize Geometry skipped: select exactly one shape."); -- return; -- } -- const input = this.readScaleInput(); -- if (!input.ok) { -- this.statusLog.write(`FAIL Resize Geometry rejected for shape row ${this.selectedShapeIndex}: ${input.error}`); -- return; -- } -- -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Resize Geometry skipped: no shape is selected."); -- return; -- } -- if (selected.locked) { -- this.statusLog.write(`WARN Resize Geometry skipped: shape row ${this.selectedShapeIndex} is locked.`); -- return; -- } -- if (this.guardSelectedObjectMutation("Resize Geometry")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const baseShape = this.findShapeInPayload(nextPayload, this.selectedShapeIndex); -- const override = this.frameOverrideInPayload(nextPayload, this.selectedShapeIndex, { create: false }); -- const transformTarget = override?.transform ? { transform: override.transform, geometry: baseShape.geometry } : baseShape; -- const transform = this.ensureShapeTransform(transformTarget); -- transform.scaleX = input.value; -- transform.scaleY = input.value; -- -- try { -- this.resizeShapeGeometryByTransformScale(baseShape, transform, this.objectTransformOrigin(this.selectedObject())); -- transform.scaleX = 1; -- transform.scaleY = 1; -- if (override?.transform) { -- override.transform = transform; -- } else { -- baseShape.transform = transform; -- } -- const transformErrors = this.validateShapeForTransform(baseShape); -- if (transformErrors.length) { -- this.statusLog.write(`FAIL Resize Geometry rejected for shape row ${this.selectedShapeIndex}: ${transformErrors.join(" ")}`); -- return; -- } -- } catch (error) { -- this.statusLog.write(`FAIL Resize Geometry rejected for shape row ${this.selectedShapeIndex}: ${error.message}`); -- return; -- } -- -- this.transformInputValues.set("objectVectorStudioV2ScaleInput", "1"); -- this.commitPayloadUpdate( -- nextPayload, -- this.selectedObjectId, -- this.selectedShapeIndex, -- `OK Resize Geometry applied scale ${this.formatScaleInputValue(input.value)} to shape row ${this.selectedShapeIndex}; transform scale reset to 1.`, -- "Resize Geometry failed schema validation" -- ); -- } -- -- groupSelectedShapes() { -- const object = this.selectedObject(); -- if (!object || this.selectedShapeIndexes.size < 2) { -- this.statusLog.write("WARN Group shapes skipped: select at least two shapes."); -- return; -- } -- if (this.guardSelectedObjectMutation("Group shapes")) { -- return; -- } -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- const groupId = this.uniqueGroupId(nextObject); -- const directIndexes = Array.from(this.directSelectedShapeIndexes) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => this.selectedShapeIndexes.has(shapeIndex)); -- const selectedIndexes = new Set((directIndexes.length >= 2 ? directIndexes : Array.from(this.selectedShapeIndexes)) -- .map((shapeIndex) => normalizeShapeIndex(shapeIndex)) -- .filter((shapeIndex) => shapeIndex >= 0)); -- if (selectedIndexes.size < 2) { -- this.statusLog.write("WARN Group shapes skipped: select at least two shapes."); -- return; -- } -- sortedShapes(nextObject).forEach((shape, shapeIndex) => { -- if (selectedIndexes.has(shapeIndex)) { -- shape.groupId = groupId; -- } -- }); -- this.pruneSingleMemberShapeGroups(nextObject); -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Grouped ${selectedIndexes.size} shapes into ${groupId}.`, "Group shapes failed schema validation", { -- directSelectedShapeIndexes: selectedIndexes, -- selectedShapeIndexes: selectedIndexes -- }); -- } -- -- ungroupSelectedShapes() { -- const object = this.selectedObject(); -- const selected = this.selectedShape(); -- if (!object || !selected) { -- this.statusLog.write("WARN Ungroup skipped: no shape is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Ungroup shapes")) { -- return; -- } -- const selectedIndexes = new Set(this.selectedShapeIndexes); -- const selectedGroupIds = new Set(sortedShapes(object) -- .filter((shape, shapeIndex) => selectedIndexes.has(shapeIndex) && shape.groupId) -- .map((shape) => shape.groupId)); -- if (!selectedGroupIds.size) { -- this.statusLog.write("WARN Ungroup skipped: selected shapes are not grouped."); -- return; -- } -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- sortedShapes(nextObject).forEach((shape, shapeIndex) => { -- if (selectedIndexes.has(shapeIndex) && selectedGroupIds.has(shape.groupId)) { -- delete shape.groupId; -- } -- }); -- this.pruneSingleMemberShapeGroups(nextObject); -- this.commitPayloadUpdate(nextPayload, object.id, this.selectedShapeIndex, `OK Ungrouped ${selectedIndexes.size} selected shapes from ${Array.from(selectedGroupIds).join(", ")}.`, "Ungroup shapes failed schema validation", { -- directSelectedShapeIndexes: selectedIndexes, -- selectedShapeIndexes: selectedIndexes -- }); -- } -- -- pruneSingleMemberShapeGroups(object) { -- const groupCounts = new Map(); -- sortedShapes(object).forEach((shape) => { -- const groupId = String(shape.groupId || "").trim(); -- if (!groupId) { -- return; -- } -- groupCounts.set(groupId, (groupCounts.get(groupId) || 0) + 1); -- }); -- sortedShapes(object).forEach((shape) => { -- const groupId = String(shape.groupId || "").trim(); -- if (groupId && groupCounts.get(groupId) < 2) { -- delete shape.groupId; -- } -- }); -- } -- -- applyShapeGeometryEdits(options = {}) { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Geometry edit skipped: no shape is selected."); -- return false; -- } -- if (!this.canUseShapeGeometry()) { -- this.statusLog.write("WARN Geometry edit skipped: select exactly one shape."); -- return false; -- } -- if (this.guardSelectedObjectMutation("Geometry edit")) { -- return false; -- } -- const fields = Array.from(this.elements.shapeGeometryDetails.querySelectorAll("[data-shape-geometry-field]")); -- const geometry = this.readShapeGeometryFields(selected, fields); -- if (!geometry.ok) { -- this.statusLog.write(`FAIL Invalid geometry rejected for shape row ${this.selectedShapeIndex}: ${geometry.error}`); -- return false; -- } -- return this.updateSelectedShapeGeometry("geometry edit", (shape) => { -- shape.geometry = geometry.value; -- if (shapeGeometryTool(shape) === "polygon" || shapeGeometryTool(shape) === "polyline") { -- const pointRounding = this.currentPointRoundingRows(geometry.value.points.length); -- this.applyPointRoundingToShape(shape, pointRounding); -- } -- shape.transform = this.ensureShapeTransform(shape); -- }, options.okMessage || `OK Auto-applied geometry edits to shape row ${this.selectedShapeIndex}.`); -- } -- -- addPolygonPointAfterRow(pointIndex, sourceButton = null) { -- const selected = this.selectedShape(); -- if (!selected || !["polygon", "polyline"].includes(shapeGeometryTool(selected))) { -- this.statusLog.write("WARN Add point skipped: no editable point-list shape is selected."); -- return; -- } -- if (!this.canUseShapeGeometry()) { -- this.statusLog.write("WARN Add point skipped: select exactly one shape."); -- return; -- } -- if (!this.isEditablePolygonShape(selected)) { -- this.statusLog.write(`WARN Add point skipped for shape row ${this.selectedShapeIndex}: triangle point count is fixed.`); -- return; -- } -- const geometry = this.readCurrentPolygonGeometry(selected); -- if (!geometry.ok) { -- this.markPolygonPointAddInvalid(sourceButton, geometry.error); -- this.statusLog.write(`FAIL Add point rejected for shape row ${this.selectedShapeIndex}: ${geometry.error}`); -- return; -- } -- const points = geometry.value.points; -- const pointRounding = this.currentPointRoundingRows(points.length); -- const insertAfterIndex = Number(pointIndex); -- if (!Number.isInteger(insertAfterIndex) || insertAfterIndex < 0 || insertAfterIndex >= points.length) { -- const message = `point ${insertAfterIndex + 1} is not available.`; -- this.markPolygonPointAddInvalid(sourceButton, message); -- this.statusLog.write(`FAIL Add point rejected for shape row ${this.selectedShapeIndex}: ${message}`); -- return; -- } -- const currentPoint = points[insertAfterIndex]; -- const nextPoint = { -- x: Number(currentPoint.x), -- y: Number(currentPoint.y) -- }; -- points.splice(insertAfterIndex + 1, 0, nextPoint); -- pointRounding.splice(insertAfterIndex + 1, 0, pointRounding[insertAfterIndex] === true); -- this.rebuildPolygonPointList(points, pointRounding); -- this.clearPolygonPointSelections(); -- this.clearPolygonPointAddValidity(); -- this.clearPolygonPointDeleteValidity(); -- this.applyShapeGeometryEdits({ -- okMessage: `OK Added copied point after point ${insertAfterIndex + 1} for shape row ${this.selectedShapeIndex}.` -- }); -- } -- -- deletePolygonPointRow(pointIndex, sourceButton = null) { -- const selected = this.selectedShape(); -- if (!selected || !["polygon", "polyline"].includes(shapeGeometryTool(selected))) { -- this.statusLog.write("WARN Delete point skipped: no editable point-list shape is selected."); -- return; -- } -- if (!this.canUseShapeGeometry()) { -- this.statusLog.write("WARN Delete point skipped: select exactly one shape."); -- return; -- } -- if (!this.isEditablePolygonShape(selected)) { -- this.statusLog.write(`WARN Delete point skipped for shape row ${this.selectedShapeIndex}: triangle point count is fixed.`); -- return; -- } -- const geometry = this.readCurrentPolygonGeometry(selected); -- if (!geometry.ok) { -- this.markPolygonPointDeleteInvalid(sourceButton, geometry.error); -- this.statusLog.write(`FAIL Delete point rejected for shape row ${this.selectedShapeIndex}: ${geometry.error}`); -- return; -- } -- const normalizedIndex = Number(pointIndex); -- if (!Number.isInteger(normalizedIndex) || normalizedIndex < 0 || normalizedIndex >= geometry.value.points.length) { -- const message = `point ${normalizedIndex + 1} is not available.`; -- this.markPolygonPointDeleteInvalid(sourceButton, message); -- this.statusLog.write(`FAIL Delete point rejected for shape row ${this.selectedShapeIndex}: ${message}`); -- return; -- } -- const minPointCount = shapeGeometryTool(selected) === "polyline" ? 2 : 4; -- if (geometry.value.points.length - 1 < minPointCount) { -- const message = `${shapeGeometryTool(selected)} must keep at least ${minPointCount} points.`; -- this.markPolygonPointDeleteInvalid(sourceButton, message); -- this.statusLog.write(`FAIL Delete point rejected for shape row ${this.selectedShapeIndex}: ${message}`); -- return; -- } -- const nextPoints = geometry.value.points.filter((_, index) => index !== normalizedIndex); -- const nextPointRounding = this.currentPointRoundingRows(geometry.value.points.length).filter((_, index) => index !== normalizedIndex); -- if (nextPoints.length < minPointCount) { -- const message = `${shapeGeometryTool(selected)} must keep at least ${minPointCount} points.`; -- this.markPolygonPointDeleteInvalid(sourceButton, message); -- this.statusLog.write(`FAIL Delete point rejected for shape row ${this.selectedShapeIndex}: ${message}`); -- return; -- } -- this.rebuildPolygonPointList(nextPoints, nextPointRounding); -- this.clearPolygonPointSelections(); -- this.clearPolygonPointAddValidity(); -- this.clearPolygonPointDeleteValidity(); -- this.applyShapeGeometryEdits({ -- okMessage: `OK Deleted point ${normalizedIndex + 1} from shape row ${this.selectedShapeIndex}.` -- }); -- } -- -- readCurrentPolygonGeometry(shape) { -- const fields = Array.from(this.elements.shapeGeometryDetails.querySelectorAll("[data-shape-geometry-field='points']")); -- return this.readShapeGeometryFields(shape, fields); -- } -- -- clearPolygonPointSelections() { -- this.elements.shapeGeometryDetails.querySelectorAll("[data-polygon-point-action-selected='true']").forEach((row) => { -- delete row.dataset.polygonPointActionSelected; -- row.classList.remove("is-action-selected"); -- row.setAttribute("aria-pressed", "false"); -- }); -- } -- -- markPolygonPointAddInvalid(sourceButton, message) { -- if (!sourceButton) { -- return; -- } -- sourceButton.dataset.validationState = "invalid"; -- sourceButton.setAttribute("aria-invalid", "true"); -- sourceButton.title = message; -- } -- -- clearPolygonPointAddValidity() { -- this.elements.shapeGeometryDetails.querySelectorAll("[data-polygon-point-add='true']").forEach((button) => { -- delete button.dataset.validationState; -- button.removeAttribute("aria-invalid"); -- button.title = `Add point after point ${Number(button.dataset.polygonPointIndex) + 1}`; -- }); -- } -- -- markPolygonPointDeleteInvalid(sourceButton, message) { -- if (!sourceButton) { -- return; -- } -- sourceButton.dataset.validationState = "invalid"; -- sourceButton.setAttribute("aria-invalid", "true"); -- sourceButton.title = message; -- } -- -- clearPolygonPointDeleteValidity() { -- this.elements.shapeGeometryDetails.querySelectorAll("[data-polygon-point-delete='true']").forEach((button) => { -- delete button.dataset.validationState; -- button.removeAttribute("aria-invalid"); -- button.title = `Delete point ${Number(button.dataset.polygonPointIndex) + 1}`; -- }); -- } -- -- currentPointRoundingRows(expectedCount = null) { -- const checkboxes = Array.from(this.elements.shapeGeometryDetails.querySelectorAll("[data-polygon-point-round='true']")); -- const pointRounding = checkboxes -- .sort((left, right) => Number(left.dataset.polygonPointIndex) - Number(right.dataset.polygonPointIndex)) -- .map((checkbox) => checkbox.checked === true); -- const count = Number.isInteger(expectedCount) ? expectedCount : pointRounding.length; -- return Array.from({ length: count }, (_, index) => pointRounding[index] === true); -- } -- -- normalizedPointRounding(shape, pointCount = this.shapeGeometryPointCount(shape)) { -- const values = this.shapePointRoundingValues(shape); -- return Array.from({ length: pointCount }, (_, index) => values[index] === true); -- } -- -- applyPointRoundingToShape(shape, pointRounding) { -- const normalized = Array.from({ length: this.shapeGeometryPointCount(shape) || pointRounding.length }, (_, index) => pointRounding[index] === true); -- shape.style = { ...shape.style, pointRounding: normalized }; -- delete shape.style.strokeLinecap; -- shape.style.startPointStyle = normalized[0] ? "round" : "square"; -- shape.style.endPointStyle = normalized.at(-1) ? "round" : "square"; -- shape.style.pointStyle = "square"; -- } -- -- rebuildPolygonPointList(points, pointRounding = null) { -- const list = this.elements.shapeGeometryDetails.querySelector(".object-vector-studio-v2__polygon-point-list"); -- if (!list) { -- return; -- } -- const selected = this.selectedShape(); -- const selectable = this.isEditablePolygonShape(selected); -- const rounding = pointRounding || this.normalizedPointRounding(selected, points.length); -- list.replaceChildren(this.createPointRowHeader({ addable: selectable, deletable: selectable }), ...points.map((point, index) => this.createPolygonPointRow(point, index, { -- addable: selectable, -- deletable: selectable, -- rounded: rounding[index] === true, -- selectable: false -- }))); -- } -- -- reindexPolygonPointRows() { -- this.elements.shapeGeometryDetails.querySelectorAll(".object-vector-studio-v2__polygon-point-field").forEach((row, index) => { -- const caption = row.querySelector(".object-vector-studio-v2__polygon-point-label"); -- if (caption) { -- caption.textContent = `Point ${index + 1}`; -- } -- row.querySelectorAll("[data-polygon-point-index]").forEach((input) => { -- input.dataset.polygonPointIndex = String(index); -- }); -- row.dataset.polygonPointIndex = String(index); -- const roundCheckbox = row.querySelector("[data-polygon-point-round='true']"); -- if (roundCheckbox) { -- roundCheckbox.setAttribute("aria-label", `Round point ${index + 1}`); -- } -- const addButton = row.querySelector("[data-polygon-point-add='true']"); -- if (addButton) { -- addButton.setAttribute("aria-label", `Add point after point ${index + 1}`); -- addButton.title = `Add point after point ${index + 1}`; -- } -- const deleteButton = row.querySelector("[data-polygon-point-delete='true']"); -- if (deleteButton) { -- deleteButton.setAttribute("aria-label", `Delete point ${index + 1}`); -- deleteButton.title = `Delete point ${index + 1}`; -- } -- }); -- } -- -- readShapeGeometryFields(shape, fields) { -- try { -- if (shapeGeometryTool(shape) === "polygon" || shapeGeometryTool(shape) === "polyline") { -- const pointInputs = fields.filter((input) => input.dataset.shapeGeometryField === "points"); -- const geometryTool = shapeGeometryTool(shape); -- const requiredPointCount = this.isTriangleShape(shape) ? 3 : geometryTool === "polyline" ? 2 : 4; -- const shapeLabel = this.isTriangleShape(shape) ? "triangle" : geometryTool; -- if (pointInputs.length < requiredPointCount) { -- throw new Error(`${shapeLabel} points must contain at least ${requiredPointCount} point rows.`); -- } -- const pointRows = new Map(); -- pointInputs.forEach((input) => { -- const index = Number(input.dataset.polygonPointIndex); -- const axis = input.dataset.polygonPointAxis; -- if (!pointRows.has(index)) { -- pointRows.set(index, {}); -- } -- pointRows.get(index)[axis] = input; -- }); -- if (pointRows.size < requiredPointCount) { -- throw new Error(`${shapeLabel} points must contain at least ${requiredPointCount} point rows.`); -- } -- if (this.isTriangleShape(shape) && pointRows.size !== 3) { -- throw new Error("triangle points must contain exactly three point rows."); -- } -- const points = []; -- for (const index of [...pointRows.keys()].sort((left, right) => left - right)) { -- const row = pointRows.get(index); -- const pointNumber = index + 1; -- const x = this.numberInputElementValue(row.x, `Point ${pointNumber} X`); -- const y = this.numberInputElementValue(row.y, `Point ${pointNumber} Y`); -- if (!x.ok || !y.ok) { -- return { error: x.error || y.error, ok: false, value: null }; -- } -- points.push({ x: x.value, y: y.value }); -- } -- return { ok: true, value: { points } }; -- } -- const geometry = {}; -- for (const input of fields) { -- const key = input.dataset.shapeGeometryField; -- if (input.type === "number") { -- const parsed = this.numberInputElementValue(input, key); -- if (!parsed.ok) { -- return { error: parsed.error, ok: false, value: null }; -- } -- this.writeGeometryFieldValue(geometry, key, parsed.value); -- } else { -- this.clearInputValidity(input); -- this.writeGeometryFieldValue(geometry, key, input.value); -- } -- } -- if (shapeTool(shape) === "square") { -- const sizeInput = fields.find((input) => input.dataset.shapeGeometryField === "size") || null; -- if (!(geometry.size > 0)) { -- const message = "Size must be greater than 0."; -- if (sizeInput) { -- this.markInputInvalid(sizeInput, message); -- } -- return { error: message, ok: false, value: null }; -- } -- return { -- ok: true, -- value: { -- height: geometry.size, -- width: geometry.size, -- x: geometry.x, -- y: geometry.y -- } -- }; -- } -- return { ok: true, value: geometry }; -- } catch (error) { -- if (error.input) { -- this.markInputInvalid(error.input, error.message); -- } else { -- const pointsInput = fields.find((input) => input.dataset.shapeGeometryField === "points"); -- if (pointsInput) { -- this.markInputInvalid(pointsInput, error.message); -- } -- } -- return { error: error.message, ok: false, value: null }; -- } -- } -- -- writeGeometryFieldValue(geometry, key, value) { -- if (!key.includes(".")) { -- geometry[key] = value; -- return; -- } -- const [parentKey, childKey] = key.split("."); -- geometry[parentKey] = { -- ...(geometry[parentKey] || {}), -- [childKey]: value -- }; -- } -- -- updateSelectedShapeGeometry(operation, updater, okMessage, options = {}) { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write(`WARN Geometry ${operation} skipped: no shape is selected.`); -- return false; -- } -- if (selected.locked) { -- this.statusLog.write(`WARN Geometry ${operation} skipped: shape row ${this.selectedShapeIndex} is locked.`); -- return false; -- } -- if (this.guardSelectedObjectMutation(`Geometry ${operation}`)) { -- return false; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const shape = this.findShapeInPayload(nextPayload, this.selectedShapeIndex); -- try { -- updater(shape); -- shape.transform = this.ensureShapeTransform(shape); -- const transformErrors = this.validateShapeForTransform(shape); -- if (transformErrors.length) { -- this.statusLog.write(`FAIL Invalid geometry rejected for shape row ${this.selectedShapeIndex}: ${transformErrors.join(" ")}`); -- return false; -- } -- } catch (error) { -- this.statusLog.write(`FAIL Invalid geometry rejected for shape row ${this.selectedShapeIndex}: ${error.message}`); -- return false; -- } -- return this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, okMessage, `Geometry ${operation} failed schema validation`, options); -- } -- -- updateSelectedShapeTransform(operation, updater, okMessage, options = {}) { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write(`WARN Transform ${operation} skipped: no shape is selected.`); -- return; -- } -- if (selected.locked) { -- this.statusLog.write(`WARN Transform ${operation} skipped: shape row ${this.selectedShapeIndex} is locked.`); -- return; -- } -- if (this.guardSelectedObjectMutation(`Transform ${operation}`)) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const override = this.frameOverrideInPayload(nextPayload, this.selectedShapeIndex, { create: true }); -- const shape = override -- ? this.effectiveShapeForFrame(this.findShapeInPayload(nextPayload, this.selectedShapeIndex), this.activeFrame(), this.selectedShapeIndex) -- : this.findShapeInPayload(nextPayload, this.selectedShapeIndex); -- try { -- updater(shape); -- const transformErrors = this.validateShapeForTransform(shape); -- if (transformErrors.length) { -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: ${transformErrors.join(" ")}`); -- return; -- } -- } catch (error) { -- this.statusLog.write(`FAIL Invalid transform rejected for shape row ${this.selectedShapeIndex}: ${error.message}`); -- return; -- } -- if (override) { -- override.transform = this.ensureShapeTransform(shape); -- } -- this.commitPayloadUpdate(nextPayload, this.selectedObjectId, this.selectedShapeIndex, okMessage, `Transform ${operation} failed schema validation`, options); -- } -- -- duplicateSelectedShape() { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write("WARN Duplicate shape skipped: no shape is selected."); -- return; -- } -- if (this.guardSelectedObjectMutation("Duplicate shape")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const object = nextPayload.objects.find((candidate) => candidate.id === this.selectedObjectId); -- const shape = JSON.parse(JSON.stringify(selected)); -- shape.order = object.shapes.length ? Math.max(...object.shapes.map((candidate) => candidate.order)) + 1 : 1; -- shape.transform = this.ensureShapeTransform(shape); -- shape.transform.x = Number((shape.transform.x + 10).toFixed(3)); -- shape.transform.y = Number((shape.transform.y + 10).toFixed(3)); -- object.shapes.push(shape); -- this.commitPayloadUpdate(nextPayload, object.id, sortedShapes(object).length - 1, `OK Duplicated shape row ${this.selectedShapeIndex}.`, "Duplicate shape failed schema validation"); -- } -- -- deleteSelectedShape(sourceLabel) { -- const selected = this.selectedShape(); -- if (!selected) { -- this.statusLog.write(`WARN Delete shape skipped from ${sourceLabel}: no shape is selected.`); -- return; -- } -- if (selected.locked) { -- this.statusLog.write(`WARN Delete shape skipped: shape row ${this.selectedShapeIndex} is locked.`); -- return; -- } -- if (this.guardSelectedObjectMutation("Delete shape")) { -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const object = nextPayload.objects.find((candidate) => candidate.id === this.selectedObjectId); -- const deleteIndex = this.selectedShapeIndex; -- object.shapes = sortedShapes(object).filter((shape, shapeIndex) => shapeIndex !== deleteIndex) -- .map((shape, index) => ({ ...shape, order: index + 1 })); -- this.pruneSingleMemberShapeGroups(object); -- this.removeDeletedShapeReferences(object, deleteIndex); -- this.removeDanglingShapeOverrideReferences(nextPayload); -- const selectedShapeIndex = sortedShapes(object).length ? 0 : -1; -- this.commitPayloadUpdate(nextPayload, object.id, selectedShapeIndex, `OK Deleted shape row ${deleteIndex} from ${sourceLabel}.`, "Delete shape failed schema validation"); -- } -- -- deleteShapeByIndex(shapeIndex, sourceLabel, objectId = this.selectedObjectId) { -- const deleteIndex = normalizeShapeIndex(shapeIndex); -- const object = this.currentPayload?.objects.find((candidate) => candidate.id === objectId) || null; -- const shape = sortedShapes(object)[deleteIndex] || null; -- if (!object || !shape) { -- this.statusLog.write(`WARN Delete shape skipped from ${sourceLabel}: shape row ${shapeIndex ?? "unknown"} is not available.`); -- return; -- } -- if (shape.locked) { -- this.statusLog.write(`WARN Delete shape skipped: shape row ${deleteIndex} is locked.`); -- return; -- } -- if (this.isObjectLocked(object.id)) { -- this.statusLog.write(`WARN Delete shape blocked: object ${object.name} is locked for this runtime session.`); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- nextObject.shapes = sortedShapes(nextObject).filter((candidate, index) => index !== deleteIndex) -- .map((candidate, index) => ({ ...candidate, order: index + 1 })); -- this.pruneSingleMemberShapeGroups(nextObject); -- this.removeDeletedShapeReferences(nextObject, deleteIndex); -- this.removeDanglingShapeOverrideReferences(nextPayload); -- const selectedShapeStillExists = this.selectedShapeIndex >= 0 && this.selectedShapeIndex < sortedShapes(nextObject).length; -- const selectedShapeIndex = selectedShapeStillExists ? this.selectedShapeIndex : sortedShapes(nextObject).length ? 0 : -1; -- this.commitPayloadUpdate(nextPayload, object.id, selectedShapeIndex, `OK Deleted shape row ${deleteIndex} from ${sourceLabel}.`, "Delete shape failed schema validation"); -- } -- -- removeDeletedShapeReferences(object, deletedShapeIndex) { -- this.objectStates(object).forEach((state) => { -- state.frames.forEach((frame) => { -- frame.shapeOverrides = frame.shapeOverrides -- .filter((override) => override.shapeIndex !== deletedShapeIndex) -- .map((override) => ({ -- ...override, -- shapeIndex: override.shapeIndex > deletedShapeIndex ? override.shapeIndex - 1 : override.shapeIndex -- })); -- }); -- }); -- } -- -- remapShapeOverrideIndexes(object, oldShapes, nextShapes) { -- const oldIndexToNextIndex = new Map(); -- oldShapes.forEach((shape, oldIndex) => { -- oldIndexToNextIndex.set(oldIndex, nextShapes.indexOf(shape)); -- }); -- this.objectStates(object).forEach((state) => { -- state.frames.forEach((frame) => { -- frame.shapeOverrides = frame.shapeOverrides -- .map((override) => { -- const nextIndex = oldIndexToNextIndex.get(override.shapeIndex); -- return Number.isInteger(nextIndex) && nextIndex >= 0 -- ? { ...override, shapeIndex: nextIndex } -- : null; -- }) -- .filter(Boolean); -- }); -- }); -- } -- -- numberInputValue(id, label) { -- const element = this.window.document.getElementById(id); -- return this.numberInputElementValue(element, label); -- } -- -- numberInputElementValue(element, label) { -- const rawValue = element?.value?.trim() || ""; -- const value = Number(rawValue); -- if (!element || rawValue === "" || !Number.isFinite(value)) { -- const error = `${label} must be a finite number.`; -- this.markInputInvalid(element, error); -- return { error, ok: false, value: 0 }; -- } -- this.clearInputValidity(element); -- const normalizedValue = this.formatViewportNumber(value); -- element.value = String(normalizedValue); -- return { error: "", ok: true, value: normalizedValue }; -- } -- -- markInputInvalid(element, message) { -- if (!element) { -- return; -- } -- element.classList.add("is-invalid"); -- element.setAttribute("aria-invalid", "true"); -- element.setCustomValidity(message); -- element.reportValidity(); -- } -- -- clearInputValidity(element) { -- if (!element) { -- return; -- } -- element.classList.remove("is-invalid"); -- element.removeAttribute("aria-invalid"); -- element.setCustomValidity(""); -- } -- -- snapDistance(value) { -- return value; -- } -- -- snapAngle(value) { -- if (!this.angleSnapEnabled) { -- return value; -- } -- const step = ANGLE_SNAP_STEPS.includes(this.angleSnapStep) ? this.angleSnapStep : 15; -- return Math.round(value / step) * step; -- } -- -- ensureShapeTransform(shape) { -- if (!shape.transform) { -- return defaultShapeTransform(); -- } -- const transform = shape.transform; -- return { -- rotation: this.formatViewportNumber(Number(transform.rotation ?? 0)), -- scaleX: this.formatViewportNumber(Number(transform.scaleX ?? 1)), -- scaleY: this.formatViewportNumber(Number(transform.scaleY ?? 1)), -- x: this.formatViewportNumber(Number(transform.x ?? 0)), -- y: this.formatViewportNumber(Number(transform.y ?? 0)) -- }; -- } -- -- resizeShapeGeometryByTransformScale(shape, transform, origin = { x: 0, y: 0 }) { -- if (!Number.isFinite(transform.scaleX) || !Number.isFinite(transform.scaleY) || transform.scaleX <= 0 || transform.scaleY <= 0) { -- throw new Error("scale must be greater than 0."); -- } -- const scaleX = transform.scaleX; -- const scaleY = transform.scaleY; -- const uniformScale = Number(((scaleX + scaleY) / 2).toFixed(3)); -- const requiresUniformScale = (label) => { -- if (Math.abs(scaleX - scaleY) > 0.001) { -- throw new Error(`${label} geometry cannot bake non-uniform scale; set matching X/Y scale first.`); -- } -- return uniformScale; -- }; -- const applyX = (value) => Number((origin.x + (value - origin.x) * scaleX).toFixed(3)); -- const applyY = (value) => Number((origin.y + (value - origin.y) * scaleY).toFixed(3)); -- const geometryTool = shapeGeometryTool(shape); -- if (geometryTool === "rectangle") { -- shape.geometry.x = applyX(shape.geometry.x); -- shape.geometry.y = applyY(shape.geometry.y); -- shape.geometry.width = Number((shape.geometry.width * scaleX).toFixed(3)); -- shape.geometry.height = Number((shape.geometry.height * scaleY).toFixed(3)); -- return; -- } -- if (geometryTool === "circle") { -- const scale = requiresUniformScale("circle"); -- shape.geometry.cx = applyX(shape.geometry.cx); -- shape.geometry.cy = applyY(shape.geometry.cy); -- shape.geometry.r = Number((shape.geometry.r * scale).toFixed(3)); -- return; -- } -- if (geometryTool === "ellipse") { -- shape.geometry.cx = applyX(shape.geometry.cx); -- shape.geometry.cy = applyY(shape.geometry.cy); -- shape.geometry.rx = Number((shape.geometry.rx * scaleX).toFixed(3)); -- shape.geometry.ry = Number((shape.geometry.ry * scaleY).toFixed(3)); -- return; -- } -- if (geometryTool === "line") { -- shape.geometry.point1.x = applyX(shape.geometry.point1.x); -- shape.geometry.point1.y = applyY(shape.geometry.point1.y); -- shape.geometry.point2.x = applyX(shape.geometry.point2.x); -- shape.geometry.point2.y = applyY(shape.geometry.point2.y); -- return; -- } -- if (geometryTool === "arc") { -- const scale = requiresUniformScale("arc"); -- shape.geometry.cx = applyX(shape.geometry.cx); -- shape.geometry.cy = applyY(shape.geometry.cy); -- shape.geometry.r = Number((shape.geometry.r * scale).toFixed(3)); -- return; -- } -- if (geometryTool === "text") { -- const scale = requiresUniformScale("text"); -- shape.geometry.x = applyX(shape.geometry.x); -- shape.geometry.y = applyY(shape.geometry.y); -- shape.geometry.fontSize = Number((shape.geometry.fontSize * scale).toFixed(3)); -- return; -- } -- shape.geometry.points = shape.geometry.points.map((point) => ({ -- x: applyX(point.x), -- y: applyY(point.y) -- })); -- } -- -- validateShapeForTransform(shape) { -- const errors = []; -- try { -- const bounds = shapeBounds(shape); -- if (bounds.width <= 0 || bounds.height <= 0) { -- errors.push("shape bounds must remain positive."); -- } -- } catch (error) { -- errors.push(error.message); -- } -- const transform = shape.transform; -- if (!transform) { -- return errors; -- } -- ["x", "y", "rotation", "scaleX", "scaleY"].forEach((key) => { -- if (!Number.isFinite(transform[key])) { -- errors.push(`transform.${key} must be a finite number.`); -- } -- }); -- if (Number.isFinite(transform.scaleX) && transform.scaleX <= 0) { -- errors.push("scaleX must be greater than 0."); -- } -- if (Number.isFinite(transform.scaleY) && transform.scaleY <= 0) { -- errors.push("scaleY must be greater than 0."); -- } -- return errors; -- } -- -- commitPayloadUpdate(nextPayload, selectedObjectId, selectedShapeIndex, okMessage, failPrefix, options = {}) { -- const validation = this.schemaService.validatePayload(nextPayload); -- if (!validation.ok) { -- this.statusLog.write(`FAIL ${failPrefix}: ${validation.errors.join(" ")}`); -- return false; -- } -- const hasPayloadChange = this.hasPayloadChanged(validation.payload); -- -- const workspaceSync = this.syncWorkspaceToolSessionPayload(validation.payload, { -- changedKeys: options.changedKeys, -- reason: options.dirtyReason -- }); -- if (!workspaceSync.ok) { -- this.statusLog.write(`FAIL ${failPrefix}: ${workspaceSync.message}`); -- return false; -- } -- -- this.recordPreviewHistorySnapshot(hasPayloadChange, options); -- this.currentPayload = validation.payload; -- this.selectedObjectId = selectedObjectId; -- this.selectedShapeIndex = normalizeShapeIndex(selectedShapeIndex); -- this.selectedShapeIndexes = options.selectedShapeIndexes -- ? new Set(Array.from(options.selectedShapeIndexes).map((index) => normalizeShapeIndex(index)).filter((index) => index >= 0)) -- : new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : []); -- this.directSelectedShapeIndexes = options.directSelectedShapeIndexes -- ? new Set(Array.from(options.directSelectedShapeIndexes).map((index) => normalizeShapeIndex(index)).filter((index) => index >= 0)) -- : (options.selectedShapeIndexes -- ? new Set(this.selectedShapeIndexes) -- : new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : [])); -- this.selectedStateId = options.selectedStateId ?? this.selectedStateId; -- this.stateControlStateId = this.selectedStateId || this.stateControlStateId; -- this.selectedFrameId = options.selectedFrameId ?? this.selectedFrameId; -- this.ensureSelectedFrame(); -- this.actionNav.setJsonPayloadActionsEnabled(true); -- this.renderPayload({ -- preserveEmptyShapeSelection: options.preserveEmptyShapeSelection === true, -- syncPaletteSelection: options.syncPaletteSelection !== false -- }); -- if (workspaceSync.changed) { -- this.statusLog.write(`OK Object Vector Studio V2 workspace dirty state: true; reason=${workspaceSync.reason}; changedKeys=${workspaceSync.changedKeys.join(", ")}.`); -- } -- this.statusLog.write(okMessage); -- return true; -- } -- -- syncWorkspaceToolSessionPayload(payload, { changedKeys = ["data.objects"], reason = "object-vector-updated" } = {}) { -- if (!this.actionNav.isWorkspaceLaunch()) { -- return { changed: false, ok: true }; -- } -- const rawSession = this.window.sessionStorage?.getItem(WORKSPACE_TOOL_SESSION_KEY) || ""; -- if (!rawSession) { -- return { ok: false, message: `workspace dirty tracking failed: missing ${WORKSPACE_TOOL_SESSION_KEY}.` }; -- } -- -- let session; -- try { -- session = JSON.parse(rawSession); -- } catch (error) { -- return { ok: false, message: `workspace dirty tracking failed: ${WORKSPACE_TOOL_SESSION_KEY} is invalid JSON: ${error.message}.` }; -- } -- if (!isPlainObject(session) -- || !isPlainObject(session.schema) -- || !isPlainObject(session.workspace) -- || !Object.prototype.hasOwnProperty.call(session, "data") -- || !isPlainObject(session.dirty)) { -- return { ok: false, message: `workspace dirty tracking failed: ${WORKSPACE_TOOL_SESSION_KEY} must use the normalized schema/workspace/data/dirty object shape.` }; -- } -- -- const dataChanged = JSON.stringify(session.data) !== JSON.stringify(payload); -- if (!dataChanged) { -- return { changed: false, ok: true }; -- } -- -- const normalizedKeys = this.normalizedWorkspaceDirtyKeys(changedKeys); -- const previousKeys = session.dirty.isDirty === true -- ? this.normalizedWorkspaceDirtyKeys(session.dirty.changedKeys) -- : []; -- const mergedChangedKeys = Array.from(new Set([...previousKeys, ...normalizedKeys])); -- const dirtyReason = String(reason || "object-vector-updated").trim() || "object-vector-updated"; -- const nextSession = { -- ...session, -- data: payload, -- dirty: { -- isDirty: true, -- reason: dirtyReason, -- changedAt: new Date().toISOString(), -- changedKeys: mergedChangedKeys -- } -- }; -- try { -- this.window.sessionStorage?.setItem(WORKSPACE_TOOL_SESSION_KEY, JSON.stringify(nextSession)); -- } catch (error) { -- return { ok: false, message: `workspace dirty tracking failed: unable to write ${WORKSPACE_TOOL_SESSION_KEY}: ${error.message}.` }; -- } -- return { -- changed: true, -- changedKeys: mergedChangedKeys, -- ok: true, -- reason: dirtyReason, -- session: nextSession -- }; -- } -- -- normalizedWorkspaceDirtyKeys(changedKeys) { -- const keys = Array.isArray(changedKeys) ? changedKeys : ["data.objects"]; -- const normalized = keys -- .map((key) => String(key || "").trim()) -- .filter(Boolean); -- return normalized.length ? normalized : ["data.objects"]; -- } -- -- hasPayloadChanged(nextPayload) { -- return Boolean(this.currentPayload) && JSON.stringify(this.currentPayload) !== JSON.stringify(nextPayload); -- } -- -- recordPreviewHistorySnapshot(hasPayloadChange, options = {}) { -- if (!hasPayloadChange || options.skipPreviewHistory || !this.currentPayload) { -- return; -- } -- this.previewUndoStack.push(this.cloneCurrentPayload()); -- if (this.previewUndoStack.length > PREVIEW_HISTORY_LIMIT) { -- this.previewUndoStack.shift(); -- } -- this.previewRedoStack = []; -- } -- -- cloneCurrentPayload() { -- return this.clonePayloadValue(this.currentPayload); -- } -- -- clonePayloadValue(payload) { -- if (typeof this.window.structuredClone === "function") { -- return this.window.structuredClone(payload); -- } -- return JSON.parse(JSON.stringify(payload)); -- } -- -- selectedObject() { -- return this.currentPayload?.objects.find((object) => object.id === this.selectedObjectId) || null; -- } -- -- guardSelectedObjectMutation(actionLabel) { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write(`WARN ${actionLabel} skipped: no object is selected.`); -- return true; -- } -- if (!this.isObjectLocked(object.id)) { -- return false; -- } -- this.statusLog.write(`WARN ${actionLabel} blocked: object ${object.name} is locked for this runtime session.`); -- return true; -- } -- -- resolveObjectForPayload(payload, objectId, chain = []) { -- const object = payload?.objects.find((candidate) => candidate.id === objectId) || null; -- if (!object) { -- return null; -- } -- if (chain.includes(object.id)) { -- return null; -- } -- if (!object.baseObjectId) { -- return JSON.parse(JSON.stringify(object)); -- } -- const baseObject = this.resolveObjectForPayload(payload, object.baseObjectId, [...chain, object.id]); -- if (!baseObject) { -- return null; -- } -- const shapeByOrder = new Map(baseObject.shapes.map((shape) => [shape.order, JSON.parse(JSON.stringify(shape))])); -- object.shapes.forEach((shape) => { -- shapeByOrder.set(shape.order, JSON.parse(JSON.stringify(shape))); -- }); -- const stateById = new Map((baseObject.states || []).map((state) => [state.id, JSON.parse(JSON.stringify(state))])); -- (object.states || []).forEach((state) => { -- stateById.set(state.id, JSON.parse(JSON.stringify(state))); -- }); -- const resolved = { -- ...baseObject, -- ...JSON.parse(JSON.stringify(object)), -- inheritedFrom: object.baseObjectId, -- shapes: Array.from(shapeByOrder.values()).sort((left, right) => left.order - right.order) -- }; -- if (stateById.size) { -- resolved.states = Array.from(stateById.values()); -- } else { -- delete resolved.states; -- } -- return resolved; -- } -- -- selectedShape() { -- return sortedShapes(this.selectedObject())[this.selectedShapeIndex] || null; -- } -- -- ensureSelectedShape() { -- const object = this.selectedObject(); -- if (!object) { -- this.selectedShapeIndex = -1; -- this.selectedShapeIndexes.clear(); -- this.directSelectedShapeIndexes.clear(); -- return; -- } -- if (this.selectedShapeIndex >= 0 && this.selectedShapeIndex < sortedShapes(object).length) { -- if (!this.selectedShapeIndexes.size) { -- this.selectedShapeIndexes = new Set([this.selectedShapeIndex]); -- } -- this.directSelectedShapeIndexes = new Set(Array.from(this.directSelectedShapeIndexes) -- .map((index) => normalizeShapeIndex(index)) -- .filter((index) => index >= 0 && index < sortedShapes(object).length)); -- if (!this.directSelectedShapeIndexes.size) { -- this.directSelectedShapeIndexes = new Set([this.selectedShapeIndex]); -- } -- return; -- } -- this.selectedShapeIndex = sortedShapes(object).length ? 0 : -1; -- this.selectedShapeIndexes = new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : []); -- this.directSelectedShapeIndexes = new Set(this.selectedShapeIndex >= 0 ? [this.selectedShapeIndex] : []); -- } -- -- ensureSelectedFrame() { -- const object = this.selectedObject(); -- if (!object) { -- this.selectedStateId = ""; -- this.selectedFrameId = ""; -- return; -- } -- const states = this.objectStates(object); -- const state = states.find((candidate) => candidate.id === this.selectedStateId) || states[0] || null; -- this.selectedStateId = state?.id || ""; -- if (!OBJECT_STATE_IDS.includes(this.stateControlStateId)) { -- this.stateControlStateId = this.selectedStateId || OBJECT_STATE_IDS[0]; -- } -- if (!state) { -- this.selectedFrameId = ""; -- return; -- } -- const frames = sortedFrames(state); -- const frame = frames.find((candidate) => candidate.id === this.selectedFrameId) || frames[0] || null; -- this.selectedFrameId = frame?.id || ""; -- } -- -- objectStates(object) { -- return Array.isArray(object?.states) ? object.states : []; -- } -- -- groupColor(groupId) { -- const text = String(groupId || ""); -- const hash = Array.from(text).reduce((total, character) => total + character.codePointAt(0), 0); -- return GROUP_COLOR_SWATCHES[hash % GROUP_COLOR_SWATCHES.length]; -- } -- -- selectedState() { -- return this.objectStates(this.selectedObject()).find((state) => state.id === this.selectedStateId) || null; -- } -- -- activeFrame() { -- return sortedFrames(this.selectedState()).find((frame) => frame.id === this.selectedFrameId) || null; -- } -- -- previousFrame() { -- const state = this.selectedState(); -- const frame = this.activeFrame(); -- if (!state || !frame) { -- return null; -- } -- const frames = sortedFrames(state); -- const index = frames.findIndex((candidate) => candidate.id === frame.id); -- return index > 0 ? frames[index - 1] : null; -- } -- -- createFrameSnapshot(object, stateId, frameId, order) { -- return { -- durationFrames: 1, -- id: frameId, -- order, -- shapeOverrides: sortedShapes(object).map((shape, shapeIndex) => ({ -- shapeIndex, -- transform: this.shapeTransform(shape), -- visible: shape.visible -- })) -- }; -- } -- -- frameOverrideInPayload(payload, shapeIndex, { create = false } = {}) { -- const normalizedIndex = normalizeShapeIndex(shapeIndex); -- const object = payload.objects.find((candidate) => candidate.id === this.selectedObjectId); -- const state = this.objectStates(object).find((candidate) => candidate.id === this.selectedStateId); -- const frame = sortedFrames(state).find((candidate) => candidate.id === this.selectedFrameId); -- if (!object || !state || !frame) { -- return null; -- } -- let override = frame.shapeOverrides.find((entry) => entry.shapeIndex === normalizedIndex); -- if (!override && create) { -- const baseShape = sortedShapes(object)[normalizedIndex]; -- if (!baseShape) { -- return null; -- } -- override = { -- shapeIndex: normalizedIndex, -- transform: this.shapeTransform(baseShape), -- visible: baseShape.visible -- }; -- frame.shapeOverrides.push(override); -- } -- return override || null; -- } -- -- findShapeInPayload(payload, shapeIndex) { -- const object = payload.objects.find((candidate) => candidate.id === this.selectedObjectId); -- return sortedShapes(object)[normalizeShapeIndex(shapeIndex)] || null; -- } -- -- firstPaletteColor() { -- const swatch = this.runtimePalette?.swatches.find((candidate) => swatchColor(candidate)); -- return swatchColor(swatch); -- } -- -- secondPaletteColor(firstColor) { -- const swatch = this.runtimePalette?.swatches.find((candidate) => swatchColor(candidate) && swatchColor(candidate) !== firstColor); -- return swatchColor(swatch) || firstColor; -- } -- -- uniqueObjectId(name, objects, gameKey = this.payloadGameKey()) { -- const baseId = `object.${gameKey}.${canonicalObjectSlug(name, gameKey)}`; -- const usedIds = new Set(objects.map((object) => object.id)); -- if (!usedIds.has(baseId)) { -- return baseId; -- } -- -- let suffix = 2; -- let candidate = `${baseId}-${suffix}`; -- while (usedIds.has(candidate)) { -- suffix += 1; -- candidate = `${baseId}-${suffix}`; -- } -- return candidate; -- } -- -- payloadGameKey() { -- const counts = new Map(); -- (this.currentPayload?.objects || []).forEach((object) => { -- const segment = objectGameSegment(object.id); -- if (segment) { -- counts.set(segment, (counts.get(segment) || 0) + 1); -- } -- }); -- const existingGame = [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0]?.[0]; -- return existingGame || payloadGameSlugFromName(this.currentPayload?.name); -- } -- -- uniqueGroupId(object) { -- const usedIds = new Set((object?.shapes || []).map((shape) => shape.groupId).filter(Boolean)); -- let suffix = usedIds.size + 1; -- let candidate = `group-${suffix}`; -- while (usedIds.has(candidate)) { -- suffix += 1; -- candidate = `group-${suffix}`; -- } -- return candidate; -- } -- -- uniqueTag(tag, tags) { -- const baseTag = slugifyObjectName(tag); -- const usedTags = new Set(tags); -- if (!usedTags.has(baseTag)) { -- return baseTag; -- } -- -- let suffix = 2; -- let candidate = `${baseTag}-${suffix}`; -- while (usedTags.has(candidate)) { -- suffix += 1; -- candidate = `${baseTag}-${suffix}`; -- } -- return candidate; -- } -- -- uniqueFrameId(state) { -- const usedIds = new Set(sortedFrames(state).map((frame) => frame.id)); -- let suffix = 1; -- let candidate = `frame-${suffix}`; -- while (usedIds.has(candidate)) { -- suffix += 1; -- candidate = `frame-${suffix}`; -- } -- return candidate; -- } -- -- clearPreviewEditState({ clipboard = false } = {}) { -- this.previewUndoStack = []; -- this.previewRedoStack = []; -- if (clipboard) { -- this.previewClipboardShape = null; -- } -- } -- -- copyPreviewSelection() { -- const shape = this.selectedShape(); -- if (!shape) { -- this.statusLog.write("WARN Copy skipped: no Object Preview shape is selected."); -- this.updatePreviewEditActionState(); -- return; -- } -- this.previewClipboardShape = this.clonePayloadValue(shape); -- this.updatePreviewEditActionState(); -- this.statusLog.write(`OK Copied shape row ${this.selectedShapeIndex} to Object Preview clipboard.`); -- } -- -- pastePreviewClipboard() { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("WARN Paste skipped: no object is selected."); -- this.updatePreviewEditActionState(); -- return; -- } -- if (!this.previewClipboardShape) { -- this.statusLog.write("WARN Paste skipped: Object Preview clipboard is empty."); -- this.updatePreviewEditActionState(); -- return; -- } -- if (this.guardSelectedObjectMutation("Paste shape")) { -- this.updatePreviewEditActionState(); -- return; -- } -- -- const nextPayload = this.cloneCurrentPayload(); -- const nextObject = nextPayload.objects.find((candidate) => candidate.id === object.id); -- const shape = this.clonePayloadValue(this.previewClipboardShape); -- shape.order = nextObject.shapes.length ? Math.max(...nextObject.shapes.map((candidate) => candidate.order)) + 1 : 1; -- shape.transform = this.ensureShapeTransform(shape); -- shape.transform.x = Number((shape.transform.x + 10).toFixed(3)); -- shape.transform.y = Number((shape.transform.y + 10).toFixed(3)); -- nextObject.shapes.push(shape); -- this.commitPayloadUpdate(nextPayload, nextObject.id, sortedShapes(nextObject).length - 1, `OK Pasted copied shape into ${nextObject.name}.`, "Paste shape failed schema validation"); -- } -- -- undoPreviewEdit() { -- if (!this.previewUndoStack.length) { -- this.statusLog.write("WARN Undo skipped: no Object Preview edit history is available."); -- this.updatePreviewEditActionState(); -- return; -- } -- const targetPayload = this.previewUndoStack.at(-1); -- const currentPayload = this.cloneCurrentPayload(); -- if (!this.applyPreviewHistoryPayload(targetPayload, "Undo")) { -- this.updatePreviewEditActionState(); -- return; -- } -- this.previewUndoStack.pop(); -- this.previewRedoStack.push(currentPayload); -- if (this.previewRedoStack.length > PREVIEW_HISTORY_LIMIT) { -- this.previewRedoStack.shift(); -- } -- this.updatePreviewEditActionState(); -- } -- -- redoPreviewEdit() { -- if (!this.previewRedoStack.length) { -- this.statusLog.write("WARN Redo skipped: no Object Preview redo history is available."); -- this.updatePreviewEditActionState(); -- return; -- } -- const targetPayload = this.previewRedoStack.at(-1); -- const currentPayload = this.cloneCurrentPayload(); -- if (!this.applyPreviewHistoryPayload(targetPayload, "Redo")) { -- this.updatePreviewEditActionState(); -- return; -- } -- this.previewRedoStack.pop(); -- this.previewUndoStack.push(currentPayload); -- if (this.previewUndoStack.length > PREVIEW_HISTORY_LIMIT) { -- this.previewUndoStack.shift(); -- } -- this.updatePreviewEditActionState(); -- } -- -- applyPreviewHistoryPayload(payload, actionLabel) { -- const validation = this.schemaService.validatePayload(payload); -- if (!validation.ok) { -- this.statusLog.write(`FAIL ${actionLabel} failed schema validation: ${validation.errors.join(" ")}`); -- return false; -- } -- const workspaceSync = this.syncWorkspaceToolSessionPayload(validation.payload, { -- changedKeys: ["data.objects"], -- reason: `object-vector-preview-${actionLabel.toLowerCase()}` -- }); -- if (!workspaceSync.ok) { -- this.statusLog.write(`FAIL ${actionLabel} failed schema validation: ${workspaceSync.message}`); -- return false; -- } -- this.currentPayload = validation.payload; -- if (!this.currentPayload.objects.some((object) => object.id === this.selectedObjectId)) { -- this.selectedObjectId = this.currentPayload.objects[0]?.id || ""; -- this.selectedShapeIndex = -1; -- this.selectedShapeIndexes.clear(); -- this.directSelectedShapeIndexes.clear(); -- } -- this.actionNav.setJsonPayloadActionsEnabled(true); -- this.renderPayload({ syncPaletteSelection: false }); -- if (workspaceSync.changed) { -- this.statusLog.write(`OK Object Vector Studio V2 workspace dirty state: true; reason=${workspaceSync.reason}; changedKeys=${workspaceSync.changedKeys.join(", ")}.`); -- } -- this.statusLog.write(`OK ${actionLabel} applied to Object Preview edits.`); -- return true; -- } -- -- updatePreviewEditActionState() { -- const object = this.selectedObject(); -- const shape = this.selectedShape(); -- const noShapeReason = "Disabled until a schema-valid shape is selected."; -- const noObjectReason = "Disabled until a schema-valid object is selected."; -- const lockedReason = object ? `Disabled because ${object.name} is locked for this runtime session.` : noObjectReason; -- const isLocked = Boolean(object && this.isObjectLocked(object.id)); -- this.setControlDisabled(this.elements.previewUndoButton, !this.previewUndoStack.length, "Disabled until an Object Preview edit can be undone.", "Undo the last Object Preview edit."); -- this.setControlDisabled(this.elements.previewRedoButton, !this.previewRedoStack.length, "Disabled until an Object Preview edit can be redone.", "Redo the last undone Object Preview edit."); -- this.setControlDisabled(this.elements.previewCopyButton, !shape, noShapeReason, "Copy the selected shape."); -- this.setControlDisabled(this.elements.previewPasteButton, !object || !this.previewClipboardShape || isLocked, !object ? noObjectReason : (isLocked ? lockedReason : "Disabled until a shape has been copied."), "Paste the copied shape into the selected object."); -- } -- -- updateObjectActionState() { -- const hasSelectedObject = Boolean(this.selectedObject()); -- const selectedObject = this.selectedObject(); -- const noObjectReason = "Disabled until a schema-valid object is selected."; -- const lockedReason = selectedObject ? `Disabled because ${selectedObject.name} is locked for this runtime session.` : noObjectReason; -- const isLocked = Boolean(selectedObject && this.isObjectLocked(selectedObject.id)); -- this.setControlDisabled(this.elements.renameObjectButton, !hasSelectedObject || isLocked, lockedReason, "Rename the selected object."); -- this.setControlDisabled(this.elements.duplicateObjectButton, !hasSelectedObject || isLocked, lockedReason, "Duplicate the selected object."); -- if (this.elements.deleteObjectButton) { -- this.setControlDisabled(this.elements.deleteObjectButton, !hasSelectedObject || isLocked, lockedReason, "Delete the selected object."); -- } -- this.setControlDisabled(this.elements.exportSvgButton, !hasSelectedObject, noObjectReason, "Export the selected object as SVG."); -- this.setControlDisabled(this.elements.runtimePreviewButton, !hasSelectedObject, noObjectReason, "Preview the selected object through the runtime asset renderer."); -- this.updatePreviewEditActionState(); -- this.updateAnimationActionState(); -- } -- -- updateAnimationActionState() { -- const state = this.selectedState(); -- const frames = sortedFrames(state); -- const hasFrame = Boolean(state && this.activeFrame()); -- const frameIndex = frames.findIndex((frame) => frame.id === this.selectedFrameId); -- const noFrameReason = "Disabled until the selected object has an active state frame."; -- const object = this.selectedObject(); -- const isLocked = Boolean(object && this.isObjectLocked(object.id)); -- const lockedReason = object ? `Disabled because ${object.name} is locked for this runtime session.` : noFrameReason; -- this.setControlDisabled(this.elements.duplicateFrameButton, !hasFrame || isLocked, isLocked ? lockedReason : noFrameReason, "Duplicate the active animation frame."); -- this.setControlDisabled(this.elements.deleteFrameButton, !hasFrame || isLocked || frames.length <= 1, isLocked ? lockedReason : (hasFrame ? "Disabled because the selected state needs at least one frame." : noFrameReason), "Delete the selected animation frame."); -- this.setControlDisabled(this.elements.frameLeftButton, !hasFrame || isLocked || frameIndex <= 0, isLocked ? lockedReason : (hasFrame ? "Disabled because the selected frame is already first." : noFrameReason), "Move the selected frame left."); -- this.setControlDisabled(this.elements.frameEarlierButton, !hasFrame || isLocked || frameIndex <= 0, isLocked ? lockedReason : (hasFrame ? "Disabled because the selected frame is already first." : noFrameReason), "Move the selected frame earlier."); -- this.setControlDisabled(this.elements.frameLaterButton, !hasFrame || isLocked || frameIndex >= frames.length - 1, isLocked ? lockedReason : (hasFrame ? "Disabled because the selected frame is already last." : noFrameReason), "Move the selected frame later."); -- this.setControlDisabled(this.elements.frameRightButton, !hasFrame || isLocked || frameIndex >= frames.length - 1, isLocked ? lockedReason : (hasFrame ? "Disabled because the selected frame is already last." : noFrameReason), "Move the selected frame right."); -- this.setControlDisabled(this.elements.playButton, !hasFrame || this.isAnimationPlaying, this.isAnimationPlaying ? "Disabled while playback is already running." : noFrameReason, "Play the selected state timeline."); -- this.setControlDisabled(this.elements.pauseButton, !this.isAnimationPlaying, "Disabled until playback starts.", "Pause animation playback."); -- this.setControlDisabled(this.elements.stopButton, !hasFrame, noFrameReason, "Stop animation playback."); -- } -- -- setControlDisabled(element, isDisabled, disabledReason, enabledTitle) { -- element.disabled = isDisabled; -- element.title = isDisabled ? disabledReason : enabledTitle; -- element.setAttribute("aria-disabled", String(isDisabled)); -- if (isDisabled) { -- element.dataset.disabledReason = disabledReason; -- return; -- } -- delete element.dataset.disabledReason; -- } -- -- async copyJson() { -- const payload = this.validatedJsonActionPayload("Copy JSON"); -- if (!payload) { -- return; -- } -- -- const json = JSON.stringify(payload, null, 2); -- if (typeof this.window.navigator?.clipboard?.writeText !== "function") { -- this.statusLog.write("WARN Clipboard API unavailable. JSON Details remains read-only on the active object."); -- return; -- } -- -- try { -- await this.window.navigator.clipboard.writeText(json); -- this.statusLog.write(`OK Object Vector Studio V2 JSON copied with ${payload.objects.length} objects.`); -- } catch (error) { -- this.statusLog.write(`FAIL Copy JSON failed: ${error.message}`); -- } -- } -- -- async previewRuntimeAsset() { -- const object = this.selectedObject(); -- const payload = this.validatedJsonActionPayload("Runtime Preview"); -- if (!object || !payload) { -- this.statusLog.write("FAIL Runtime Preview blocked: select a schema-valid Object Vector Studio V2 object first."); -- return; -- } -- if (!this.runtimePalette) { -- this.loadPaletteFromSessionKey(RUNTIME_PALETTE_SESSION_KEY) || this.loadPaletteFromWorkspaceSession(); -- } -- if (!this.runtimePalette) { -- this.statusLog.write("FAIL Runtime Preview blocked: runtime palette is required from workspace/session and must not be embedded in object JSON."); -- return; -- } -- -- const assetSet = await this.runtimeAssetService.loadPayload(payload, { -- sourceLabel: "Object Vector Studio V2 runtime preview" -- }); -- if (!assetSet) { -- return; -- } -- const result = this.runtimeAssetService.createSvgString(assetSet, { -- elapsedMs: 0, -- frameId: this.selectedFrameId, -- objectId: object.id, -- stateId: this.selectedStateId -- }); -- if (!result.ok) { -- return; -- } -- -- this.elements.renderSurface.dataset.runtimePreview = "true"; -- this.elements.renderSurface.dataset.runtimePreviewFrame = this.selectedFrameId || ""; -- this.elements.renderSurface.dataset.runtimePreviewState = this.selectedStateId || ""; -- const message = `Runtime preview launched for ${object.name}${this.selectedStateId ? ` state ${this.selectedStateId}` : ""}${this.selectedFrameId ? ` frame ${this.selectedFrameId}` : ""}.`; -- this.statusLog.write(`OK ${message} Runtime palette source ${this.runtimePaletteSource}; object JSON remains palette-free.`); -- } -- -- exportJson() { -- const payload = this.validatedJsonActionPayload("Export"); -- if (!payload) { -- return; -- } -- -- downloadTextFile(JSON.stringify(payload, null, 2), "object-vector-studio-v2.json", { -- documentRef: this.window.document, -- windowRef: this.window -- }); -- this.statusLog.write(`OK Export JSON prepared for ${payload.objects.length} Object Vector Studio V2 objects.`); -- } -- -- exportSelectedObjectSvg() { -- const object = this.selectedObject(); -- if (!object) { -- this.statusLog.write("FAIL Export SVG blocked: no Object Vector Studio V2 object is selected."); -- return; -- } -- -- try { -- const svg = this.createObjectSvgExport(object); -- downloadTextFile(svg, `${slugifyObjectName(object.name)}.svg`, { -- documentRef: this.window.document, -- mimeType: "image/svg+xml", -- windowRef: this.window -- }); -- const visibleShapeCount = sortedShapes(object).filter((shape) => shape.visible).length; -- this.statusLog.write(`OK Export SVG generated for ${object.name}: ${visibleShapeCount} visible shapes.`); -- } catch (error) { -- this.statusLog.write(`FAIL Export SVG failed for ${object.name}: ${error.message}`); -- } -- } -- -- createObjectSvgExport(object) { -- const validation = this.schemaService.validatePayload(this.currentPayload); -- if (!validation.ok) { -- throw new Error(`schema validation failed: ${validation.errors.join(" ")}`); -- } -- const visibleShapes = sortedShapes(object).filter((shape) => this.effectiveShape(shape).visible); -- if (!visibleShapes.length) { -- throw new Error("selected object has no visible shapes."); -- } -- const bounds = this.objectBounds(object, { includeInvisible: false }); -- const padding = 12; -- const svg = this.window.document.createElementNS(SVG_NS, "svg"); -- svg.setAttribute("xmlns", SVG_NS); -- svg.setAttribute("role", "img"); -- svg.setAttribute("aria-label", `${object.name} Object Vector Studio V2 export`); -- if (this.selectedStateId) { -- svg.dataset.objectState = this.selectedStateId; -- } -- if (this.selectedFrameId) { -- svg.dataset.objectFrame = this.selectedFrameId; -- } -- svg.setAttribute("viewBox", `${bounds.x - padding} ${bounds.y - padding} ${bounds.width + padding * 2} ${bounds.height + padding * 2}`); -- const transformOrigin = this.objectTransformOrigin(object); -- if (this.selectedStateId || this.selectedFrameId) { -- const metadata = this.window.document.createElementNS(SVG_NS, "metadata"); -- metadata.textContent = JSON.stringify({ -- frameId: this.selectedFrameId || null, -- stateId: this.selectedStateId || null, -- toolId: "object-vector-studio-v2" -- }); -- svg.append(metadata); -- } -- visibleShapes.forEach((shape) => { -- const element = this.createSvgShape(this.effectiveShape(shape), { transformOrigin }); -- element.removeAttribute("tabindex"); -- element.removeAttribute("class"); -- svg.append(element); -- this.appendShapePointStyleCaps(svg, this.effectiveShape(shape), { transformOrigin }); -- }); -- return new this.window.XMLSerializer().serializeToString(svg); -- } -- -- validatedJsonActionPayload(actionLabel) { -- if (!this.currentPayload) { -- this.statusLog.write(`FAIL ${actionLabel} blocked: no schema-valid Object Vector Studio V2 payload is loaded.`); -- return null; -- } -- const payload = this.cloneCurrentPayload(); -- const validation = this.schemaService.validatePayload(payload); -- if (!validation.ok) { -- this.statusLog.write(`FAIL ${actionLabel} blocked by Object Vector Studio V2 schema: ${validation.errors.join(" ")}`); -- return null; -- } -- return validation.payload; -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/bootstrap.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/bootstrap.js -deleted file mode 100644 -index 9f52e34a9..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/bootstrap.js -+++ /dev/null -@@ -1,114 +0,0 @@ --import { ToolStarterApp } from "./ToolStarterApp.js"; --import { ActionNavControl } from "./controls/ActionNavControl.js"; --import { AccordionSection } from "./controls/AccordionSection.js"; --import { StatusLogControl } from "./controls/StatusLogControl.js"; --import { ToolStarterShellControl } from "./controls/ToolStarterShellControl.js"; --import { ObjectVectorStudioV2SchemaService } from "./services/ObjectVectorStudioV2SchemaService.js"; -- --function requireElement(selector) { -- const element = document.querySelector(selector); -- if (!element) { -- throw new Error(`Missing required starter template element: ${selector}`); -- } -- return element; --} -- --function optionalElement(selector) { -- return document.querySelector(selector); --} -- --window.addEventListener("DOMContentLoaded", () => { -- const accordions = Array.from(document.querySelectorAll(".accordion-v2"), (section) => new AccordionSection(section)); -- const statusLog = new StatusLogControl({ -- log: requireElement("#statusLog"), -- clearButton: requireElement("#clearStatusButton") -- }); -- const app = new ToolStarterApp({ -- accordions, -- actionNav: new ActionNavControl({ -- returnToWorkspaceButton: requireElement("#returnToWorkspaceButton"), -- toolCopyJsonButton: requireElement("#objectVectorStudioV2CopyJsonButton"), -- toolExportJsonButton: requireElement("#objectVectorStudioV2ExportJsonButton"), -- toolImportJsonButton: requireElement("#objectVectorStudioV2ImportJsonButton"), -- toolImportJsonInput: requireElement("#objectVectorStudioV2ImportJsonInput"), -- toolNav: requireElement(".tool-starter__tool__menu"), -- workspaceNav: requireElement(".tool-starter__workspace__menu") -- }), -- elements: { -- addObjectButton: requireElement("#objectVectorStudioV2AddObjectButton"), -- addTagButton: requireElement("#objectVectorStudioV2AddTagButton"), -- angleSnapButton: requireElement("#objectVectorStudioV2AngleSnapButton"), -- centerDotButton: requireElement("#objectVectorStudioV2CenterDotButton"), -- coordinateDisplay: requireElement("#objectVectorStudioV2CoordinateDisplay"), -- deleteFrameButton: requireElement("#objectVectorStudioV2DeleteFrameButton"), -- deleteObjectButton: optionalElement("#objectVectorStudioV2DeleteObjectButton"), -- dependencyGraph: requireElement("#objectVectorStudioV2DependencyGraph"), -- duplicateFrameButton: requireElement("#objectVectorStudioV2DuplicateFrameButton"), -- duplicateObjectButton: requireElement("#objectVectorStudioV2DuplicateObjectButton"), -- exportSvgButton: requireElement("#objectVectorStudioV2ExportSvgButton"), -- fillOpacity: requireElement("#objectVectorStudioV2FillOpacity"), -- fpsInput: requireElement("#objectVectorStudioV2FpsInput"), -- frameEarlierButton: requireElement("#objectVectorStudioV2FrameEarlierButton"), -- frameLaterButton: requireElement("#objectVectorStudioV2FrameLaterButton"), -- frameLeftButton: requireElement("#objectVectorStudioV2FrameLeftButton"), -- frameRightButton: requireElement("#objectVectorStudioV2FrameRightButton"), -- frameTimeline: requireElement("#objectVectorStudioV2FrameTimeline"), -- gridRenderButton: requireElement("#objectVectorStudioV2GridRenderButton"), -- jsonDetails: requireElement("#objectVectorStudioV2JsonDetails"), -- leftPanel: requireElement(".tool-starter__panel--left"), -- loopToggle: requireElement("#objectVectorStudioV2LoopToggle"), -- shapeGeometryDetails: requireElement("#objectVectorStudioV2ShapeGeometryDetails"), -- shapeGeometryName: requireElement("#objectVectorStudioV2ShapeGeometryName"), -- shapeGeometrySummary: requireElement("#objectVectorStudioV2ShapeGeometrySummary"), -- shapeTransform: requireElement("#objectVectorStudioV2ShapeTransform"), -- objectNameInput: requireElement("#objectVectorStudioV2ObjectNameInput"), -- objectPreviewFooter: requireElement("#objectVectorStudioV2ObjectPreviewFooter"), -- objectTagInput: requireElement("#objectVectorStudioV2ObjectTagInput"), -- objectTagList: requireElement("#objectVectorStudioV2ObjectTagList"), -- objectTransform: requireElement("#objectVectorStudioV2ObjectTransform"), -- objectsContent: requireElement("#objectVectorStudioV2ObjectsContent"), -- objectsCount: requireElement("#objectVectorStudioV2ObjectsCount"), -- objectTiles: requireElement("#objectVectorStudioV2ObjectTiles"), -- paintModeButton: requireElement("#objectVectorStudioV2PaintModeButton"), -- paletteSortButtons: Array.from(document.querySelectorAll("[data-palette-sort]")), -- paletteSummary: requireElement("#objectVectorStudioV2PaletteSummary"), -- paletteSwatchCount: requireElement("#objectVectorStudioV2PaletteSwatchCount"), -- panDownButton: requireElement("#objectVectorStudioV2PanDownButton"), -- panLeftButton: requireElement("#objectVectorStudioV2PanLeftButton"), -- panRightButton: requireElement("#objectVectorStudioV2PanRightButton"), -- panUpButton: requireElement("#objectVectorStudioV2PanUpButton"), -- onionSkinToggle: requireElement("#objectVectorStudioV2OnionSkinToggle"), -- pauseButton: requireElement("#objectVectorStudioV2PauseButton"), -- playButton: requireElement("#objectVectorStudioV2PlayButton"), -- previewCopyButton: requireElement("#objectVectorStudioV2PreviewCopyButton"), -- previewPasteButton: requireElement("#objectVectorStudioV2PreviewPasteButton"), -- previewRedoButton: requireElement("#objectVectorStudioV2PreviewRedoButton"), -- previewUndoButton: requireElement("#objectVectorStudioV2PreviewUndoButton"), -- copyJsonDetailsButton: requireElement("#objectVectorStudioV2CopyJsonDetailsButton"), -- renameObjectButton: requireElement("#objectVectorStudioV2RenameObjectButton"), -- renderSurface: requireElement("#objectVectorStudioV2RenderSurface"), -- resetViewButton: requireElement("#objectVectorStudioV2ResetViewButton"), -- runtimePreviewButton: requireElement("#objectVectorStudioV2RuntimePreviewButton"), -- searchFilter: requireElement("#objectVectorStudioV2SearchFilter"), -- snapModeButton: requireElement("#objectVectorStudioV2SnapModeButton"), -- stopButton: requireElement("#objectVectorStudioV2StopButton"), -- strokeModeButton: requireElement("#objectVectorStudioV2StrokeModeButton"), -- strokeOpacity: requireElement("#objectVectorStudioV2StrokeOpacity"), -- strokeWidth: requireElement("#objectVectorStudioV2StrokeWidth"), -- tagFilter: requireElement("#objectVectorStudioV2TagFilter"), -- toolLabelModeButton: requireElement("#objectVectorStudioV2ToolLabelModeButton"), -- toolToggleGrid: requireElement("#objectVectorStudioV2ToolToggleGrid"), -- toolToggles: Array.from(document.querySelectorAll("[data-shape-tool]")), -- zoomInButton: requireElement("#objectVectorStudioV2ZoomInButton"), -- zoomOutButton: requireElement("#objectVectorStudioV2ZoomOutButton") -- }, -- schemaService: new ObjectVectorStudioV2SchemaService(), -- shell: new ToolStarterShellControl(), -- statusLog, -- windowRef: window -- }); -- -- app.start().catch((error) => { -- statusLog.write(`FAIL Object Vector Studio V2 schema contract failed to load: ${error.message}`); -- }); --}); -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/AccordionSection.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/AccordionSection.js -deleted file mode 100644 -index 1e03257e1..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/AccordionSection.js -+++ /dev/null -@@ -1,30 +0,0 @@ --export class AccordionSection { -- constructor(section) { -- this.section = section; -- this.header = section?.querySelector(".accordion-v2__header") || null; -- this.content = section?.querySelector(".accordion-v2__content") || null; -- this.icon = this.header?.querySelector(".accordion-v2__icon") || null; -- } -- -- mount() { -- if (!this.section || !this.header || !this.content || this.header.dataset.accordionV2Bound === "true") { -- return; -- } -- -- this.header.dataset.accordionV2Bound = "true"; -- this.setOpen(this.section.dataset.accordionV2Open !== "false"); -- this.header.addEventListener("click", () => { -- this.setOpen(!this.section.classList.contains("is-open")); -- }); -- } -- -- setOpen(isOpen) { -- this.section.classList.toggle("is-open", isOpen); -- this.section.dataset.accordionV2Open = String(isOpen); -- this.header.setAttribute("aria-expanded", String(isOpen)); -- this.content.hidden = !isOpen; -- if (this.icon) { -- this.icon.dataset.accordionV2IconState = isOpen ? "open" : "closed"; -- } -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ActionNavControl.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ActionNavControl.js -deleted file mode 100644 -index 8d704b10c..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ActionNavControl.js -+++ /dev/null -@@ -1,84 +0,0 @@ --export class ActionNavControl { -- constructor({ -- locationRef = window.location, -- returnToWorkspaceButton, -- windowRef = window, -- toolCopyJsonButton, -- toolExportJsonButton, -- toolImportJsonButton, -- toolImportJsonInput, -- toolNav, -- workspaceNav -- }) { -- this.location = locationRef; -- this.returnToWorkspaceButton = returnToWorkspaceButton; -- this.toolCopyJsonButton = toolCopyJsonButton; -- this.toolExportJsonButton = toolExportJsonButton; -- this.toolImportJsonButton = toolImportJsonButton; -- this.toolImportJsonInput = toolImportJsonInput; -- this.toolNav = toolNav; -- this.workspaceNav = workspaceNav; -- this.window = windowRef; -- } -- -- mount({ -- onCopyJson, -- onExportJson, -- onImportJson, -- onReturnToWorkspace -- }) { -- this.applyLaunchMode(); -- this.toolImportJsonButton.addEventListener("click", () => { -- this.toolImportJsonInput.click(); -- }); -- this.toolImportJsonInput.addEventListener("change", () => { -- const file = this.toolImportJsonInput.files?.[0] || null; -- onImportJson(file); -- this.toolImportJsonInput.value = ""; -- }); -- this.toolCopyJsonButton.addEventListener("click", onCopyJson); -- this.toolExportJsonButton.addEventListener("click", onExportJson); -- this.returnToWorkspaceButton.addEventListener("click", () => { -- onReturnToWorkspace(this.workspaceManagerUrl()); -- }); -- } -- -- applyLaunchMode() { -- const params = new URLSearchParams(this.location.search); -- const isWorkspaceManagerLaunch = params.get("launch") === "workspace" -- && params.get("fromTool") === "workspace-manager-v2" -- && Boolean(params.get("hostContextId")); -- this.toolNav.hidden = isWorkspaceManagerLaunch; -- this.workspaceNav.hidden = !isWorkspaceManagerLaunch; -- } -- -- setJsonPayloadActionsEnabled(isEnabled) { -- this.toolCopyJsonButton.disabled = !isEnabled; -- this.toolExportJsonButton.disabled = !isEnabled; -- this.toolCopyJsonButton.title = isEnabled ? "Copy schema-valid Object Vector Studio V2 JSON." : "Disabled until a schema-valid Object Vector Studio V2 payload is loaded."; -- this.toolExportJsonButton.title = isEnabled ? "Export schema-valid Object Vector Studio V2 JSON." : "Disabled until a schema-valid Object Vector Studio V2 payload is loaded."; -- } -- -- setImportEnabled(isEnabled) { -- this.toolImportJsonButton.disabled = !isEnabled; -- this.toolImportJsonInput.disabled = !isEnabled; -- this.toolImportJsonButton.title = isEnabled ? "Import Object Vector Studio V2 JSON." : "Disabled while schema is loading or workspace launch owns the payload source."; -- } -- -- isWorkspaceLaunch() { -- return this.workspaceNav.hidden === false; -- } -- -- workspaceManagerUrl() { -- const url = new URL("../workspace-manager-v2/index.html", this.location.href); -- const params = new URLSearchParams(this.location.search); -- const hostContextId = params.get("hostContextId") || ""; -- if (hostContextId) { -- url.searchParams.set("hostContextId", hostContextId); -- } -- if (params.get("workspaceMode")?.toLowerCase() === "uat") { -- url.searchParams.set("workspace", "uat"); -- } -- return url.href; -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/InspectorControl.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/InspectorControl.js -deleted file mode 100644 -index d89b7abb4..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/InspectorControl.js -+++ /dev/null -@@ -1,10 +0,0 @@ --export class InspectorControl { -- constructor(output) { -- this.output = output; -- } -- -- showObject(value) { -- this.output.textContent = JSON.stringify(value, null, 2); -- } --} -- -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/PreviewPanelControl.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/PreviewPanelControl.js -deleted file mode 100644 -index 56f3eca72..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/PreviewPanelControl.js -+++ /dev/null -@@ -1,30 +0,0 @@ --export class PreviewPanelControl { -- constructor(output) { -- this.output = output; -- } -- -- clear() { -- this.output.replaceChildren(this.createEmptyState()); -- } -- -- render(payload) { -- const card = document.createElement("article"); -- card.className = "tool-starter__preview-card"; -- -- const title = document.createElement("h2"); -- title.textContent = "Rendered Preview"; -- -- const body = document.createElement("p"); -- body.textContent = payload.sourceValue; -- -- card.append(title, body); -- this.output.replaceChildren(card); -- } -- -- createEmptyState() { -- const emptyState = document.createElement("p"); -- emptyState.textContent = "No preview rendered yet."; -- return emptyState; -- } --} -- -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/SourceInputControl.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/SourceInputControl.js -deleted file mode 100644 -index 42990f590..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/SourceInputControl.js -+++ /dev/null -@@ -1,40 +0,0 @@ --export class SourceInputControl { -- constructor({ input, validationMessage }) { -- this.input = input; -- this.validationMessage = validationMessage; -- } -- -- mount({ onChange }) { -- this.input.addEventListener("input", onChange); -- } -- -- clear() { -- this.input.value = ""; -- this.showMessage("Input is required before Export can process.", false); -- } -- -- getValue() { -- return this.input.value.trim(); -- } -- -- hasValue() { -- return this.getValue().length > 0; -- } -- -- validate() { -- const value = this.getValue(); -- if (!value) { -- const message = "Source value is required."; -- this.showMessage(message, true); -- return { message, valid: false, value: "" }; -- } -- -- this.showMessage("Source value is ready.", false); -- return { message: "Source value is ready.", valid: true, value }; -- } -- -- showMessage(message, isError) { -- this.validationMessage.textContent = message; -- this.validationMessage.classList.toggle("is-error", isError); -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/StatusLogControl.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/StatusLogControl.js -deleted file mode 100644 -index d5be2f9e9..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/StatusLogControl.js -+++ /dev/null -@@ -1,28 +0,0 @@ --export class StatusLogControl { -- constructor({ log, clearButton }) { -- this.log = log; -- this.clearButton = clearButton; -- } -- -- mount() { -- this.clearButton.addEventListener("click", (event) => { -- event.stopPropagation(); -- this.clear(); -- }); -- } -- -- clear() { -- this.log.value = ""; -- } -- -- error(message) { -- this.write(`FAIL ${message}`); -- } -- -- write(message) { -- const timestamp = new Date().toLocaleTimeString(); -- const nextLine = `[${timestamp}] ${message}`; -- this.log.value = this.log.value ? `${this.log.value}\n${nextLine}` : nextLine; -- this.log.scrollTop = this.log.scrollHeight; -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ToolStarterShellControl.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ToolStarterShellControl.js -deleted file mode 100644 -index 7c9b039c8..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ToolStarterShellControl.js -+++ /dev/null -@@ -1,130 +0,0 @@ --export class ToolStarterShellControl { -- constructor({ documentRef = document } = {}) { -- this.document = documentRef; -- this.toolId = "object-vector-studio-v2"; -- this.toolName = "Object Vector Studio V2"; -- this.toolDescription = "First-Class Tools Surface V2"; -- this.headerText = `${this.toolName} - ${this.toolDescription}`; -- this.introText = this.toolDescription; -- this.suppressFullscreenSync = false; -- } -- -- mount() { -- this.document.body.classList.add("tools-platform-surface", "tool-starter-local-shell"); -- this.applyFullscreenState(Boolean(this.document.fullscreenElement)); -- this.bindHeaderDetails(); -- this.updateSummary(); -- } -- -- getHeaderDetails() { -- return this.document.querySelector(".is-collapsible"); -- } -- -- getSummaryElement() { -- return this.document.querySelector("[data-tool-starter-summary]"); -- } -- -- applyFullscreenState(isActive) { -- this.document.body.classList.toggle("tools-platform-fullscreen-active", isActive); -- this.document.documentElement.classList.toggle("tools-platform-fullscreen-active", isActive); -- if (isActive) { -- this.document.body.setAttribute("data-tools-platform-fullscreen", "1"); -- this.document.documentElement.setAttribute("data-tools-platform-fullscreen", "1"); -- return; -- } -- -- this.document.body.removeAttribute("data-tools-platform-fullscreen"); -- this.document.documentElement.removeAttribute("data-tools-platform-fullscreen"); -- } -- -- updateSummary() { -- const details = this.getHeaderDetails(); -- const summary = this.getSummaryElement(); -- if (!(details instanceof HTMLDetailsElement) || !(summary instanceof HTMLElement)) { -- return; -- } -- -- const isFullscreen = Boolean(this.document.fullscreenElement) -- || this.document.body.classList.contains("tools-platform-fullscreen-active"); -- const isExpanded = details.open === true; -- summary.textContent = isExpanded -- ? "Hide Header and Details" -- : (isFullscreen ? this.headerText : "Show Header and Details"); -- -- if (isFullscreen) { -- summary.style.maxWidth = "calc(100vw - 24px)"; -- summary.style.whiteSpace = "nowrap"; -- summary.style.overflow = "hidden"; -- summary.style.textOverflow = "ellipsis"; -- } else { -- summary.style.removeProperty("max-width"); -- summary.style.removeProperty("white-space"); -- summary.style.removeProperty("overflow"); -- summary.style.removeProperty("text-overflow"); -- } -- -- summary.setAttribute("data-tools-platform-summary-active", "1"); -- summary.setAttribute("data-tools-platform-summary-error", "0"); -- summary.setAttribute("data-tools-platform-summary-state", isExpanded ? "expanded" : "collapsed"); -- summary.setAttribute("data-tools-platform-summary-mode", isFullscreen ? "fullscreen" : "normal"); -- summary.setAttribute("data-tool-id", this.toolId); -- summary.setAttribute("title", `${this.headerText}\n${this.introText}`); -- } -- -- async enterFullscreenIfAvailable() { -- if (this.document.fullscreenElement || !this.document.fullscreenEnabled) { -- return; -- } -- if (typeof this.document.documentElement?.requestFullscreen !== "function") { -- return; -- } -- try { -- await this.document.documentElement.requestFullscreen(); -- } catch { -- // Collapsed header remains usable when fullscreen is unavailable. -- } -- } -- -- async exitFullscreenIfActive() { -- if (!this.document.fullscreenElement || typeof this.document.exitFullscreen !== "function") { -- return; -- } -- try { -- await this.document.exitFullscreen(); -- } catch { -- // The fullscreenchange listener will resync if the browser exits later. -- } -- } -- -- bindHeaderDetails() { -- const details = this.getHeaderDetails(); -- if (!(details instanceof HTMLDetailsElement)) { -- return; -- } -- -- details.addEventListener("toggle", () => { -- this.updateSummary(); -- if (this.suppressFullscreenSync) { -- this.suppressFullscreenSync = false; -- return; -- } -- -- if (details.open) { -- void this.exitFullscreenIfActive(); -- return; -- } -- -- void this.enterFullscreenIfAvailable(); -- }); -- -- this.document.addEventListener("fullscreenchange", () => { -- const isFullscreen = Boolean(this.document.fullscreenElement); -- this.applyFullscreenState(isFullscreen); -- if (!isFullscreen && !details.open) { -- this.suppressFullscreenSync = true; -- details.open = true; -- } -- this.updateSummary(); -- }); -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js -deleted file mode 100644 -index 644653b13..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js -+++ /dev/null -@@ -1,545 +0,0 @@ --import { isPlainObject } from "../../../../src/shared/object/objects.js"; --import { deepClone } from "../../../../src/shared/json/clone.js"; -- --const SCHEMA_URL = new URL("../../../schemas/tools/object-vector-studio-v2.schema.json", import.meta.url); -- --function isObjectIdentityId(value) { -- return /^object\.[a-z0-9-]+\.[a-z0-9][a-z0-9-]*$/.test(String(value || "")); --} -- --function withoutShapeOrigin(transform) { -- if (!isPlainObject(transform)) { -- return transform; -- } -- const { shapeOrigin, ...nextTransform } = transform; -- return nextTransform; --} -- --function typeMatches(expectedType, value) { -- if (expectedType === "array") { -- return Array.isArray(value); -- } -- if (expectedType === "object") { -- return isPlainObject(value); -- } -- if (expectedType === "integer") { -- return Number.isInteger(value); -- } -- if (expectedType === "number") { -- return typeof value === "number" && Number.isFinite(value); -- } -- return typeof value === expectedType; --} -- --function typeDescription(expectedType) { -- if (expectedType === "boolean") { -- return "true or false"; -- } -- return expectedType === "object" ? "an object" : `a ${expectedType}`; --} -- --function resolveSchemaRef(schemaRoot, ref) { -- if (typeof ref !== "string" || !ref.startsWith("#/")) { -- throw new Error(`unsupported schema reference ${ref || "unknown"}`); -- } -- return ref.slice(2).split("/").reduce((current, segment) => current?.[segment], schemaRoot); --} -- --export class ObjectVectorStudioV2SchemaService { -- constructor({ fetchRef = (...args) => fetch(...args), schemaUrl = SCHEMA_URL } = {}) { -- this.fetchRef = fetchRef; -- this.schema = null; -- this.schemaUrl = schemaUrl; -- } -- -- get schemaPath() { -- return this.schemaUrl.pathname || String(this.schemaUrl); -- } -- -- async loadSchema() { -- const response = await this.fetchRef(this.schemaUrl, { cache: "no-store" }); -- if (!response.ok) { -- throw new Error(`${this.schemaPath} returned ${response.status}`); -- } -- const schema = await response.json(); -- const schemaErrors = []; -- this.validateSchemaShape(schema, schemaErrors); -- if (schemaErrors.length) { -- throw new Error(schemaErrors.join(" ")); -- } -- this.schema = schema; -- return schema; -- } -- -- getDefinitionDefault(definitionName) { -- if (!this.schema) { -- throw new Error("Object Vector Studio V2 schema is not loaded."); -- } -- const definition = this.schema.$defs?.[definitionName]; -- if (!isPlainObject(definition) || !Object.prototype.hasOwnProperty.call(definition, "default")) { -- throw new Error(`Object Vector Studio V2 schema default is missing for $defs.${definitionName}.default.`); -- } -- return deepClone(definition.default); -- } -- -- validateSchemaShape(schema, errors) { -- if (!isPlainObject(schema)) { -- errors.push("Object Vector Studio V2 schema root must be an object."); -- return; -- } -- if (schema.$id !== "object-vector-studio-v2.schema.json") { -- errors.push("Object Vector Studio V2 schema id must be object-vector-studio-v2.schema.json."); -- } -- if (schema.additionalProperties !== false) { -- errors.push("Object Vector Studio V2 schema root must reject unknown properties."); -- } -- const requiredRootFields = ["version", "toolId", "name", "objects"]; -- if (!Array.isArray(schema.required) || requiredRootFields.some((key) => !schema.required.includes(key))) { -- errors.push("Object Vector Studio V2 schema root must require version, toolId, name, and objects."); -- } -- ["palette", "selection", "viewport", "export", "vectorMaps"].forEach((key) => { -- if (Object.prototype.hasOwnProperty.call(schema.properties || {}, key)) { -- errors.push(`Object Vector Studio V2 schema root must not define ${key}.`); -- } -- if (Object.prototype.hasOwnProperty.call(schema.$defs || {}, key === "export" ? "exportPayload" : key)) { -- errors.push(`Object Vector Studio V2 schema definitions must not define ${key}.`); -- } -- }); -- if (schema.properties?.toolId?.const !== "object-vector-studio-v2") { -- errors.push("Object Vector Studio V2 schema toolId must be object-vector-studio-v2."); -- } -- if (!isPlainObject(schema.$defs?.shape) || !Array.isArray(schema.$defs.shape.oneOf)) { -- errors.push("Object Vector Studio V2 schema must define shape variants."); -- } -- if (Object.prototype.hasOwnProperty.call(schema.$defs?.object?.properties || {}, "type")) { -- errors.push("Object Vector Studio V2 object schema must not define object type."); -- } -- if (Object.prototype.hasOwnProperty.call(schema.properties || {}, "assetLibrary")) { -- errors.push("Object Vector Studio V2 schema must not define assetLibrary."); -- } -- ["id", "shapeKey", "label", "type"].forEach((key) => { -- if (Object.prototype.hasOwnProperty.call(schema.$defs?.shapeCommon?.properties || {}, key)) { -- errors.push(`Object Vector Studio V2 shape schema must not define deprecated shape field ${key}.`); -- } -- }); -- const requiredShapeFields = schema.$defs?.shapeCommon?.required || []; -- if (!requiredShapeFields.includes("tool")) { -- errors.push("Object Vector Studio V2 shape schema must require tool."); -- } -- const overrideFields = schema.$defs?.shapeFrameOverride?.required || []; -- if (!overrideFields.includes("shapeIndex")) { -- errors.push("Object Vector Studio V2 shape frame override schema must require shapeIndex."); -- } -- [ -- "object", -- "shapeCommon", -- "style", -- "transform" -- ].forEach((definitionName) => { -- if (!Object.prototype.hasOwnProperty.call(schema.$defs?.[definitionName] || {}, "default")) { -- errors.push(`Object Vector Studio V2 schema must define $defs.${definitionName}.default.`); -- } -- }); -- [ -- "rectangleGeometry", -- "circleGeometry", -- "ellipseGeometry", -- "lineGeometry", -- "triangleGeometry", -- "polygonGeometry", -- "polylineGeometry", -- "arcGeometry", -- "textGeometry" -- ].forEach((definitionName) => { -- if (Object.prototype.hasOwnProperty.call(schema.$defs?.[definitionName] || {}, "default")) { -- errors.push(`Object Vector Studio V2 schema must not define pre-positioned $defs.${definitionName}.default geometry.`); -- } -- }); -- } -- -- validatePayload(payload) { -- if (!this.schema) { -- return { -- errors: ["Object Vector Studio V2 schema is not loaded."], -- ok: false, -- payload: null -- }; -- } -- -- const payloadForValidation = this.stripLegacyShapeOrigins(payload); -- const errors = []; -- this.validateLegacyRootFields(payloadForValidation, errors); -- this.validateValue(this.schema, payloadForValidation, "root", errors); -- if (!errors.length) { -- this.validatePayloadReferences(payloadForValidation, errors); -- } -- if (errors.length) { -- return { errors, ok: false, payload: null }; -- } -- -- return { -- errors: [], -- ok: true, -- payload: this.normalizePayload(payloadForValidation) -- }; -- } -- -- validateLegacyRootFields(payload, errors) { -- if (!isPlainObject(payload)) { -- return; -- } -- if (Object.prototype.hasOwnProperty.call(payload, "vectorMaps")) { -- errors.push("root.vectorMaps is deprecated legacy vector-map data. Remove root.vectorMaps and import Object Vector Studio V2 payloads with root.objects[].tags and root.objects[].shapes only."); -- } -- } -- -- validatePayloadReferences(payload, errors) { -- const objects = Array.isArray(payload?.objects) ? payload.objects : []; -- const objectsById = new Map(); -- objects.forEach((object, index) => { -- if (!isObjectIdentityId(object.id)) { -- errors.push(`root.objects[${index}].id ${object.id} must follow object.game.name.`); -- } -- if (objectsById.has(object.id)) { -- errors.push(`root.objects[${index}].id ${object.id} duplicates an existing object id.`); -- return; -- } -- objectsById.set(object.id, object); -- }); -- -- objects.forEach((object, index) => { -- this.validateInheritanceChain(object, objectsById, `root.objects[${index}]`, errors); -- const localShapeOrders = new Set(); -- object.shapes.forEach((shape, shapeIndex) => { -- if (localShapeOrders.has(shape.order)) { -- errors.push(`root.objects[${index}].shapes[${shapeIndex}].order ${shape.order} duplicates a local shape order.`); -- } -- localShapeOrders.add(shape.order); -- }); -- }); -- -- objects.forEach((object, index) => { -- const shapeCount = this.collectInheritedShapeCount(object, objectsById, new Set(), errors, `root.objects[${index}]`); -- (object.states || []).forEach((state, stateIndex) => { -- state.frames.forEach((frame, frameIndex) => { -- frame.shapeOverrides.forEach((override, overrideIndex) => { -- if (override.shapeIndex >= shapeCount) { -- errors.push(`root.objects[${index}].states[${stateIndex}].frames[${frameIndex}].shapeOverrides[${overrideIndex}].shapeIndex ${override.shapeIndex} must reference a local or inherited sorted shape row.`); -- } -- }); -- }); -- }); -- }); -- -- } -- -- validateInheritanceChain(object, objectsById, path, errors) { -- const seen = new Set([object.id]); -- let current = object; -- while (current?.baseObjectId) { -- const baseId = current.baseObjectId; -- if (seen.has(baseId)) { -- errors.push(`${path}.baseObjectId creates a circular inheritance chain at ${baseId}.`); -- return; -- } -- const baseObject = objectsById.get(baseId); -- if (!baseObject) { -- errors.push(`${path}.baseObjectId ${baseId} must reference an existing base object.`); -- return; -- } -- seen.add(baseId); -- current = baseObject; -- } -- } -- -- collectInheritedShapeCount(object, objectsById, seen, errors, path) { -- return this.collectInheritedShapesByOrder(object, objectsById, seen, errors, path).size; -- } -- -- collectInheritedShapesByOrder(object, objectsById, seen, errors, path) { -- const shapeByOrder = new Map(); -- if (!object || seen.has(object.id)) { -- if (object) { -- errors.push(`${path}.baseObjectId creates a circular shape inheritance chain at ${object.id}.`); -- } -- return shapeByOrder; -- } -- seen.add(object.id); -- if (object.baseObjectId) { -- const baseObject = objectsById.get(object.baseObjectId); -- if (baseObject) { -- this.collectInheritedShapesByOrder(baseObject, objectsById, seen, errors, path) -- .forEach((shape, order) => shapeByOrder.set(order, shape)); -- } -- } -- object.shapes.forEach((shape) => shapeByOrder.set(shape.order, shape)); -- return shapeByOrder; -- } -- -- validateRuntimePalette(palette, sourceLabel = "runtime palette") { -- const errors = []; -- if (!isPlainObject(palette)) { -- errors.push(`${sourceLabel} must be a palette object.`); -- return { errors, ok: false, palette: null }; -- } -- if (!Array.isArray(palette.swatches) || !palette.swatches.length) { -- errors.push(`${sourceLabel}.swatches must contain at least one swatch.`); -- return { errors, ok: false, palette: null }; -- } -- -- palette.swatches.forEach((swatch, index) => { -- if (!isPlainObject(swatch)) { -- errors.push(`${sourceLabel}.swatches[${index}] must be an object.`); -- return; -- } -- const label = swatch.id || swatch.name || swatch.symbol; -- if (typeof label !== "string" || !label.trim()) { -- errors.push(`${sourceLabel}.swatches[${index}] must provide id, name, or symbol.`); -- } -- const color = swatch.value || swatch.hex || swatch.color; -- if (typeof color !== "string" || !color.trim()) { -- errors.push(`${sourceLabel}.swatches[${index}] must provide value, hex, or color.`); -- } -- }); -- -- if (errors.length) { -- return { errors, ok: false, palette: null }; -- } -- return { -- errors: [], -- ok: true, -- palette: deepClone(palette) -- }; -- } -- -- stripLegacyShapeOrigins(payload) { -- if (!isPlainObject(payload)) { -- return payload; -- } -- const normalized = deepClone(payload); -- if (!Array.isArray(normalized?.objects)) { -- return normalized; -- } -- normalized.objects.forEach((object) => { -- if (Array.isArray(object.shapes)) { -- object.shapes.forEach((shape) => { -- shape.transform = withoutShapeOrigin(shape.transform); -- }); -- } -- if (Array.isArray(object.states)) { -- object.states.forEach((state) => { -- (state.frames || []).forEach((frame) => { -- (frame.shapeOverrides || []).forEach((override) => { -- if (isPlainObject(override.transform)) { -- override.transform = withoutShapeOrigin(override.transform); -- } -- }); -- }); -- }); -- } -- }); -- return normalized; -- } -- -- validateValue(schemaNode, value, path, errors) { -- const schema = schemaNode?.$ref ? resolveSchemaRef(this.schema, schemaNode.$ref) : schemaNode; -- if (!schema) { -- errors.push(`${path} references an unavailable Object Vector Studio V2 schema definition.`); -- return; -- } -- -- if (Array.isArray(schema.allOf)) { -- schema.allOf.forEach((entry) => this.validateValue(entry, value, path, errors)); -- return; -- } -- -- if (Array.isArray(schema.anyOf)) { -- const matched = schema.anyOf.some((entry) => { -- const optionErrors = []; -- this.validateValue(entry, value, path, optionErrors); -- return optionErrors.length === 0; -- }); -- if (!matched) { -- errors.push(`${path} must match at least one allowed schema option.`); -- } -- return; -- } -- -- if (Array.isArray(schema.oneOf)) { -- const matchedCount = schema.oneOf.reduce((count, entry) => { -- const optionErrors = []; -- this.validateValue(entry, value, path, optionErrors); -- return optionErrors.length === 0 ? count + 1 : count; -- }, 0); -- if (matchedCount !== 1) { -- errors.push(`${path} must match exactly one Object Vector Studio V2 shape schema.`); -- } -- return; -- } -- -- this.validateConst(schema, value, path, errors); -- this.validateEnum(schema, value, path, errors); -- if (schema.type && !this.validateType(schema, value, path, errors)) { -- return; -- } -- this.validateString(schema, value, path, errors); -- this.validateNumber(schema, value, path, errors); -- this.validateArray(schema, value, path, errors); -- this.validateObject(schema, value, path, errors); -- } -- -- validateConst(schema, value, path, errors) { -- if (!Object.prototype.hasOwnProperty.call(schema, "const")) { -- return; -- } -- if (value !== schema.const) { -- errors.push(`${path} must be ${schema.const}.`); -- } -- } -- -- validateEnum(schema, value, path, errors) { -- if (!Array.isArray(schema.enum)) { -- return; -- } -- if (!schema.enum.includes(value)) { -- errors.push(`${path} must be one of ${schema.enum.join(", ")}.`); -- } -- } -- -- validateType(schema, value, path, errors) { -- if (Array.isArray(schema.type)) { -- const matched = schema.type.some((expectedType) => typeMatches(expectedType, value)); -- if (!matched) { -- errors.push(`${path} must be one of ${schema.type.join(", ")}.`); -- } -- return matched; -- } -- const matched = typeMatches(schema.type, value); -- if (!matched) { -- errors.push(`${path} must be ${typeDescription(schema.type)}.`); -- } -- return matched; -- } -- -- validateString(schema, value, path, errors) { -- if (typeof value !== "string") { -- return; -- } -- if (Number.isInteger(schema.minLength) && value.trim().length < schema.minLength) { -- errors.push(`${path} must contain at least ${schema.minLength} characters.`); -- } -- } -- -- validateNumber(schema, value, path, errors) { -- if (typeof value !== "number" || !Number.isFinite(value)) { -- return; -- } -- if (typeof schema.minimum === "number" && value < schema.minimum) { -- errors.push(`${path} must be at least ${schema.minimum}.`); -- } -- if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum) { -- errors.push(`${path} must be greater than ${schema.exclusiveMinimum}.`); -- } -- if (typeof schema.maximum === "number" && value > schema.maximum) { -- errors.push(`${path} must be at most ${schema.maximum}.`); -- } -- } -- -- validateArray(schema, value, path, errors) { -- if (!Array.isArray(value)) { -- return; -- } -- if (Number.isInteger(schema.minItems) && value.length < schema.minItems) { -- errors.push(`${path} must contain at least ${schema.minItems} items.`); -- } -- if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems) { -- errors.push(`${path} must contain no more than ${schema.maxItems} items.`); -- } -- if (schema.items) { -- value.forEach((item, index) => this.validateValue(schema.items, item, `${path}[${index}]`, errors)); -- } -- } -- -- validateObject(schema, value, path, errors) { -- if (!isPlainObject(value)) { -- return; -- } -- if (Array.isArray(schema.required)) { -- schema.required.forEach((key) => { -- if (!Object.prototype.hasOwnProperty.call(value, key)) { -- errors.push(`${path}.${key} is required.`); -- } -- }); -- } -- const properties = isPlainObject(schema.properties) ? schema.properties : {}; -- Object.entries(properties).forEach(([key, childSchema]) => { -- if (Object.prototype.hasOwnProperty.call(value, key)) { -- this.validateValue(childSchema, value[key], `${path}.${key}`, errors); -- } -- }); -- if (isPlainObject(schema.additionalProperties)) { -- Object.entries(value).forEach(([key, child]) => { -- if (!Object.prototype.hasOwnProperty.call(properties, key)) { -- this.validateValue(schema.additionalProperties, child, `${path}.${key}`, errors); -- } -- }); -- } -- if (schema.additionalProperties === false) { -- Object.keys(value).forEach((key) => { -- if (!Object.prototype.hasOwnProperty.call(properties, key)) { -- errors.push(`${path}.${key} is not allowed.`); -- } -- }); -- } -- } -- -- normalizePayload(payload) { -- const normalized = deepClone(payload); -- normalized.name = normalized.name.trim(); -- normalized.objects = normalized.objects.map((object) => ({ -- ...object, -- baseObjectId: typeof object.baseObjectId === "string" ? object.baseObjectId.trim() : undefined, -- id: object.id.trim(), -- name: object.name.trim(), -- objectOrigin: isPlainObject(object.objectOrigin) -- ? { x: Number(object.objectOrigin.x), y: Number(object.objectOrigin.y) } -- : undefined, -- tags: Array.isArray(object.tags) ? object.tags.map((tag) => tag.trim()).filter(Boolean) : undefined, -- states: Array.isArray(object.states) -- ? object.states.map((state) => ({ -- ...state, -- id: state.id.trim().toLowerCase(), -- name: state.name.trim(), -- frames: state.frames.map((frame) => ({ -- ...frame, -- id: frame.id.trim(), -- shapeOverrides: frame.shapeOverrides.map((override) => ({ -- ...override, -- shapeIndex: override.shapeIndex -- })) -- })) -- })) -- : undefined, -- shapes: object.shapes.map((shape) => ({ -- ...shape, -- tool: shape.tool.trim().toLowerCase() -- })) -- })); -- normalized.objects = normalized.objects.map((object) => { -- if (object.states === undefined) { -- delete object.states; -- } -- if (object.baseObjectId === undefined) { -- delete object.baseObjectId; -- } -- if (object.objectOrigin === undefined) { -- delete object.objectOrigin; -- } -- if (object.tags === undefined) { -- delete object.tags; -- } -- return object; -- }); -- return normalized; -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ToolStateSerializer.js b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ToolStateSerializer.js -deleted file mode 100644 -index 61ff8202f..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ToolStateSerializer.js -+++ /dev/null -@@ -1,15 +0,0 @@ --export class ToolStateSerializer { -- constructor(toolId) { -- this.toolId = toolId; -- } -- -- createToolState({ sourceValue }) { -- return { -- toolId: this.toolId, -- payload: { -- sourceValue -- } -- }; -- } --} -- -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/playwright.config.mjs b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/playwright.config.mjs -deleted file mode 100644 -index 3a5ab3bf7..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/playwright.config.mjs -+++ /dev/null -@@ -1,10 +0,0 @@ --export default { -- testDir: "../../tests/playwright/tools", -- testMatch: "ObjectVectorStudioV2FirstClassToolStarter.spec.mjs", -- timeout: 30000, -- outputDir: "./tests/results", -- use: { -- headless: true, -- trace: "retain-on-failure" -- } --}; -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/possible.future.adds.txt b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/possible.future.adds.txt -deleted file mode 100644 -index ff08fbe09..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/possible.future.adds.txt -+++ /dev/null -@@ -1,31 +0,0 @@ --Object Vector Studio V2 possible future geometry additions -- --Architecture notes from recent discussion: -- --- Object Vector Studio V2 should stay focused on reusable atomic vector objects. --- Future World Vector or Scene layers should instance Object Vector objects instead of absorbing Object Vector editing concerns. --- Keep Object Vector and World Vector separate so object authoring, scene composition, and runtime placement do not collapse into one contract. --- Future 3D vector workflows can build from Object Vector concepts without changing the 2D object contract. --- Future 3D concepts to explore separately: -- - point3d -- - camera -- - projection -- - mesh -- --- polyline -- - Most likely next geometry addition. -- - Would support connected open point sequences without requiring polygon closure. -- --- spline -- - Possible future addition for smooth point-driven curves. -- - Would need explicit control-point, interpolation, and runtime tessellation contracts before implementation. -- --- path -- - Possible future addition for richer vector authoring. -- - Would require a stricter schema/editor contract before implementation. -- --- point/marker -- - Possible future addition for anchors, control points, or lightweight markers. -- - Would need clear runtime/editor semantics before implementation. -- --Tracking note: this file is informational only. No new geometry shape is implemented by this PR. -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/styles/toolStarter.css b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/styles/toolStarter.css -deleted file mode 100644 -index d13d86d72..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/styles/toolStarter.css -+++ /dev/null -@@ -1,2408 +0,0 @@ --@font-face { -- font-family: "0xProto Nerd Font"; -- src: url("../../../src/assets/fonts/0xProtoNerdFont/0xProtoNerdFontMono-Regular.ttf") format("truetype"); -- font-display: swap; -- font-style: normal; -- font-weight: 400; --} -- --:root { -- --tool-starter-bg: var(--bg-gradient, linear-gradient(180deg, #c4b5fd 0%, #a78bfa 18%, #8b5cf6 36%, #7c3aed 54%, #6d28d9 74%, #5b21b6 88%, #4c1d95 100%)); -- --tool-starter-panel: var(--panel, rgba(30, 24, 48, 0.92)); -- --tool-starter-surface: var(--surface-inline, rgba(18, 16, 28, 0.78)); -- --tool-starter-panel-strong: rgba(0, 0, 0, 0.24); -- --tool-starter-input-surface: var(--surface-inline, rgba(43, 16, 91, 0.9)); -- --tool-starter-line: var(--line, rgba(221, 214, 254, 0.26)); -- --tool-starter-surface-border: var(--surface-border, rgba(221, 214, 254, 0.22)); -- --tool-starter-text: var(--text, #f7f4ff); -- --tool-starter-muted: var(--muted, #c4b5fd); -- --tool-starter-header-muted: var(--muted, #e9ddff); -- --tool-starter-accent: var(--accent, #ddd6fe); -- --tool-starter-danger: #ffb4b4; -- --tool-starter-shadow: 0 18px 36px rgba(0, 0, 0, 0.22); -- --tool-starter-shadow-strong: 0 20px 44px rgba(0, 0, 0, 0.26); -- color-scheme: dark; -- font-family: Inter, Segoe UI, system-ui, sans-serif; --} -- --* { -- box-sizing: border-box; --} -- --body { -- min-height: 100vh; -- min-height: 100dvh; -- margin: 0; -- background: var(--tool-starter-bg); -- color: var(--tool-starter-text); -- overflow-x: hidden; --} -- --button, --input, --select, --textarea { -- font: inherit; --} -- --button { -- border: 1px solid var(--tool-starter-line) !important; -- border-radius: 10px !important; -- background: ButtonFace; -- color: ButtonText; -- cursor: pointer; -- padding: 2px 6px; --} -- --.object-vector-studio-v2 { -- --object-vector-studio-v2-icon-scale: 1.75; -- --object-vector-studio-v2-snap-disabled-icon-color: #ef4444; -- --object-vector-studio-v2-snap-point-color: #fbbf24; --} -- --button:disabled { -- cursor: not-allowed; -- opacity: 0.62; -- filter: saturate(0.72); -- box-shadow: none; --} -- --button:focus-visible, --input:focus-visible, --select:focus-visible, --textarea:focus-visible { -- outline: 2px solid var(--tool-starter-accent); -- outline-offset: 2px; --} -- --input, --select, --textarea { -- border: 1px solid var(--tool-starter-surface-border); -- border-radius: 10px !important; -- background: var(--tool-starter-input-surface); -- color: var(--tool-starter-text); --} -- --input:hover, --select:hover, --textarea:hover { -- border-color: var(--card-hover-border, #c4b5fd); --} -- --.is-collapsible { -- border-bottom: 1px solid var(--tool-starter-line); -- background: transparent; --} -- --.is-collapsible__summary { -- min-height: 36px; -- display: flex; -- align-items: center; -- cursor: pointer; -- color: var(--tool-starter-header-muted); -- font-weight: 700; -- padding: 8px 18px; --} -- --.is-collapsible__content { -- display: grid; -- gap: 0; --} -- --#shared-theme-header { -- min-height: 0; --} -- --.tool-starter__header { -- background: transparent; -- padding: 10px 16px; --} -- --.tool-starter__local-shell-frame { -- --tools-platform-frame-surface: var(--tool-starter-panel); -- --tools-platform-frame-line: var(--tool-starter-line); -- border: 1px solid var(--tool-starter-line); -- border-radius: 18px; -- background: var(--tool-starter-bg); -- box-shadow: var(--tool-starter-shadow); -- color: var(--tool-starter-text); --} -- --.tool-starter__local-shell-frame .tools-platform-frame__accordion-content { -- display: grid; -- gap: 10px; -- padding: 12px 14px; --} -- --.tool-starter__local-shell-frame .tools-platform-frame__accordion-summary { -- display: flex; -- align-items: center; -- justify-content: space-between; -- gap: 12px; -- border-radius: 12px; -- background: #4c1d95; -- padding: 12px 14px; --} -- --.tool-starter__local-shell-frame .tools-platform-frame__title { -- margin: 0; -- color: #ffffff; --} -- --.tool-starter__local-shell-frame .tools-platform-frame__eyebrow, --.tool-starter__local-shell-frame .tools-platform-frame__meta, --.tool-starter__local-shell-frame .tools-platform-frame__description { -- margin: 0; -- color: var(--tool-starter-header-muted); --} -- --.tool-starter__menu { -- box-sizing: border-box; -- width: calc(100% - 32px); -- display: flex; -- align-items: center; -- justify-content: center; -- flex-wrap: wrap; -- gap: 12px; -- margin: 16px 16px 0; -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-panel); -- padding: 10px 14px; --} -- --.tool-starter__menu[hidden] { -- display: none; --} -- --.tool-starter.app-shell { -- height: calc(100vh - 137px); -- min-height: 0; -- width: min(1600px, calc(100% - 32px)); -- margin: 0 auto 16px; -- display: grid; -- grid-template-columns: 340px minmax(0, 1fr) 360px; -- gap: 10px; -- border: 1px solid var(--tool-starter-line); -- border-radius: 20px; -- box-shadow: var(--tool-starter-shadow-strong); -- overflow: hidden; -- padding: 12px; --} -- --.tool-starter__panel { -- min-width: 0; -- min-height: 0; -- display: flex; -- flex-direction: column; -- gap: 5px; -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-panel); -- overflow: hidden; -- padding: 8px; --} -- --.tool-starter__panel--center { -- min-height: 0; --} -- --.tool-starter__panel--left { -- overflow-y: auto; --} -- --.tool-starter__panel--right { -- overflow-y: auto; --} -- --.tool-starter__panel--left > .accordion-v2.is-open { -- flex: 1 0 220px; --} -- --.tool-starter__panel--left > .accordion-v2.tool-starter__accordion--compact.is-open { -- flex: 0 0 auto; --} -- --.tool-starter__panel--left > .accordion-v2.tool-starter__accordion--compact.is-open > .accordion-v2__content { -- flex: 0 0 auto; --} -- --.accordion-v2 { -- --accordion-v2-accent: var(--tool-starter-accent); -- --accordion-v2-line: var(--tool-starter-line); -- --accordion-v2-surface: var(--tool-starter-surface); -- min-width: 0; -- min-height: 0; -- display: flex; -- flex-direction: column; -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-surface); -- box-shadow: 0 14px 30px rgba(0, 0, 0, 0.12); -- overflow: hidden; --} -- --.accordion-v2.is-open { -- flex: 1 1 0; --} -- --.accordion-v2:not(.is-open) { -- flex: 0 0 auto; --} -- --.accordion-v2__header { -- width: 100%; -- min-height: 36px; -- display: flex; -- align-items: center; -- justify-content: space-between; -- gap: 8px; -- border: 0; -- border-radius: 0; -- background: transparent; -- color: inherit; -- font-weight: 700; -- padding: 6px 10px; -- text-align: left; -- cursor: pointer; --} -- --.accordion-v2__icon { -- width: 22px; -- height: 22px; -- display: inline-grid; -- place-items: center; -- border: 1px solid var(--tool-starter-line); -- border-radius: 999px; -- line-height: 1; --} -- --.accordion-v2.is-open .accordion-v2__icon { -- transform: rotate(45deg); --} -- --.accordion-v2__content { -- min-height: 0; -- display: flex; -- flex: 1 1 auto; -- flex-direction: column; -- gap: 6px; -- border-top: 1px solid var(--tool-starter-line); -- overflow: auto; -- padding: 8px; --} -- --.accordion-v2__content[hidden] { -- display: none; --} -- --.tool-starter__status-accordion-header { -- flex: 0 0 36px; -- min-width: 0; --} -- --.tool-starter__status-header-actions { -- flex: 0 0 auto; -- display: inline-flex; -- align-items: center; -- gap: 8px; --} -- --.tool-starter__status-clear-button { -- flex: 0 0 auto; -- margin-right: 0; -- border-color: var(--tool-starter-line) !important; -- border-radius: 10px !important; -- background: ButtonFace; -- color: ButtonText; -- padding: 2px 6px; --} -- --.tool-starter__status-clear-button:hover { -- border-color: var(--tool-starter-line) !important; -- background: ButtonFace; -- color: ButtonText; --} -- --.tool-starter__accordion--fill { -- flex: 1 1 auto; --} -- --.tool-starter__accordion--compact.is-open { -- flex: 0 0 auto; --} -- --.tool-starter__accordion--compact .accordion-v2__content { -- flex: 0 0 auto; --} -- --.tool-starter__field { -- min-width: 0; -- display: grid; -- grid-template-columns: 74px minmax(0, 1fr); -- align-items: center; -- gap: 8px; -- font-weight: 700; --} -- --.tool-starter__field--compact { -- grid-template-columns: 96px minmax(0, 1fr); --} -- --.tool-starter__field--stacked { -- grid-template-columns: 1fr; -- gap: 6px; --} -- --.tool-starter__field-patterns { -- display: grid; -- gap: 8px; --} -- --.tool-starter__field input { -- width: 100%; -- min-width: 0; -- padding: 7px 9px; --} -- --#objectVectorStudioV2ObjectContent .tool-starter__field--compact > span { -- white-space: nowrap; --} -- --.tool-starter__field input[readonly] { -- color: var(--tool-starter-muted); -- cursor: default; --} -- --.tool-starter__field--not-editable input { -- padding-block: 0; --} -- --.tool-starter__field--editable input { -- background: var(--tool-starter-input-surface); -- color: var(--tool-starter-text); --} -- --.tool-starter__editable-tags-box { -- min-height: 36px; -- border-radius: 8px !important; -- background: var(--tool-starter-panel-strong); -- padding: 6px; --} -- --.tool-starter__hint { -- margin: 0; -- color: var(--tool-starter-muted); -- font-size: 0.9rem; --} -- --.tool-starter__hint.is-error { -- color: var(--tool-starter-danger); --} -- --.object-vector-studio-v2__callout, --.object-vector-studio-v2__details { -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-panel-strong); -- color: var(--tool-starter-text); -- padding: 10px; --} -- --.object-vector-studio-v2__scroll-content { -- min-height: 0; -- overflow: auto; --} -- --#objectVectorStudioV2ObjectsContent { -- overflow-y: auto; --} -- --#objectVectorStudioV2ShapeToolsContent, --#objectVectorStudioV2PaletteContent { -- flex: 0 1 auto; -- max-height: 260px; --} -- --#objectVectorStudioV2PaletteContent { -- flex: 0 0 auto; -- max-height: none; --} -- --#objectVectorStudioV2ShapeToolsContent { -- flex: 0 0 auto; -- max-height: none; -- min-height: 0; --} -- --.tool-starter__panel--right > .accordion-v2.is-open { -- flex: 0 0 auto; --} -- --.tool-starter__panel--right > .accordion-v2.is-open > .accordion-v2__content { -- flex: 0 0 auto; --} -- --.tool-starter__panel--right > .accordion-v2.is-open > .accordion-v2__content:not(#objectVectorStudioV2ShapeToolsContent) { -- max-height: 260px; --} -- --.tool-starter__panel--right > .object-vector-studio-v2__shape-geometry-accordion.is-open { -- flex: 1 1 320px; -- min-height: 220px; --} -- --.tool-starter__panel--right > .accordion-v2.object-vector-studio-v2__shape-geometry-accordion.is-open > .accordion-v2__content#objectVectorStudioV2ShapeGeometryContent { -- flex: 1 1 auto; -- max-height: none; --} -- --#objectVectorStudioV2ShapeGeometryDetails { -- flex: 1 1 auto; -- min-height: 0; --} -- --#objectVectorStudioV2ShapeGeometryDetails > .object-vector-studio-v2__object-detail-stack { -- min-height: 100%; --} -- --.object-vector-studio-v2__palette-swatch-list { -- display: grid; -- flex: 0 0 auto; -- grid-template-columns: repeat(auto-fill, 32px); -- gap: 8px; -- align-content: start; -- max-height: 176px; -- min-height: 0; -- overflow-y: auto; --} -- --.object-vector-studio-v2__palette-swatch-list:not(:has(.object-vector-studio-v2__palette-swatch)) { -- display: block; -- max-height: none; -- overflow: visible; -- color: var(--tool-starter-muted); -- font-size: 0.85rem; -- line-height: 1.25; -- text-align: center; --} -- --.object-vector-studio-v2__palette-swatch { -- position: relative; -- width: 32px; -- height: 32px; -- min-height: 32px; -- aspect-ratio: 1; -- display: block; -- padding: 0 !important; -- overflow: visible; -- background: var(--object-vector-studio-v2-swatch, transparent) !important; -- border-radius: 4px !important; -- color: transparent !important; -- font-size: 0; --} -- --.object-vector-studio-v2__palette-swatch.is-selected { -- border-color: #ffffff !important; -- outline: 3px solid var(--tool-starter-accent-strong); -- outline-offset: 2px; -- box-shadow: 0 0 0 2px #020617, 0 0 0 6px rgba(20, 184, 166, 0.34); --} -- --.object-vector-studio-v2__palette-swatch::before { -- display: none; --} -- --.object-vector-studio-v2__palette-swatch.is-selected::before { -- content: ""; -- position: absolute; -- left: 11px; -- top: 7px; -- display: block; -- width: 8px; -- height: 14px; -- border: solid #ffffff; -- border-width: 0 3px 3px 0; -- filter: drop-shadow(0 1px 2px #020617); -- transform: rotate(45deg); --} -- --.object-vector-studio-v2__palette-swatch:hover::after, --.object-vector-studio-v2__palette-swatch:focus-visible::after { -- content: attr(data-palette-details); -- position: absolute; -- left: 50%; -- bottom: calc(100% + 6px); -- z-index: 5; -- min-width: 96px; -- transform: translateX(-50%); -- border: 1px solid var(--tool-starter-line); -- border-radius: 6px; -- background: rgba(15, 23, 42, 0.96); -- color: var(--tool-starter-text); -- font-size: 0.74rem; -- line-height: 1.25; -- padding: 5px 7px; -- text-align: center; -- white-space: pre-line; -- text-indent: 0; --} -- --.object-vector-studio-v2__object-actions, --.object-vector-studio-v2__tool-display-actions, --.object-vector-studio-v2__snap-actions, --.object-vector-studio-v2__z-order-actions, --.object-vector-studio-v2__viewport-controls, --.object-vector-studio-v2__preview-edit-toolbar, --.object-vector-studio-v2__palette-controls, --.object-vector-studio-v2__json-actions { -- display: flex; -- align-items: center; -- justify-content: center; -- flex-wrap: wrap; -- gap: 8px; --} -- --.object-vector-studio-v2__control-group { -- display: grid; -- gap: 8px; -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-panel-strong); -- padding: 10px; --} -- --#objectVectorStudioV2ShapeToolsContent .object-vector-studio-v2__snap-actions { -- display: grid; -- grid-template-columns: repeat(2, minmax(0, 1fr)); -- align-items: stretch; --} -- --#objectVectorStudioV2ObjectsContent .object-vector-studio-v2__z-order-actions { -- display: grid; -- grid-template-columns: repeat(auto-fit, 34px); -- align-items: stretch; -- justify-content: center; -- gap: 4px; -- border-top: 1px solid var(--tool-starter-line); -- margin-top: 3px; -- padding-top: 4px; --} -- --#objectVectorStudioV2ObjectsContent .object-vector-studio-v2__shape-list-actions { -- width: 100%; -- grid-column: 1 / -1; -- margin-top: 2px; --} -- --.object-vector-studio-v2__z-action { -- width: auto; -- min-height: 0; -- min-width: 86px; -- position: relative; -- display: inline-flex; -- flex-direction: column; -- align-items: center; -- justify-content: center; -- gap: 3px; -- overflow: hidden; -- padding: 4px !important; -- text-align: center; --} -- --.object-vector-studio-v2__control-group h3, --.object-vector-studio-v2__object-detail-stack h3 { -- margin: 0; -- font-size: 0.95rem; -- line-height: 1.25; --} -- --.object-vector-studio-v2__object-actions button { -- min-width: 68px; -- min-height: 32px; --} -- --.object-vector-studio-v2__tool-toggle-grid { -- display: grid; -- grid-template-columns: repeat(auto-fill, 76px); -- align-items: center; -- justify-content: center; -- gap: 8px; --} -- --.object-vector-studio-v2__tool-toggle { -- width: auto; -- min-height: 0; -- min-width: 76px; -- display: inline-flex; -- flex-direction: column; -- align-items: center; -- justify-content: center; -- gap: 4px; -- overflow: hidden; -- padding: 5px 4px !important; -- text-align: center; --} -- --.object-vector-studio-v2__tool-toggle--future { -- cursor: not-allowed; --} -- --.object-vector-studio-v2__tool-toggle--future .object-vector-studio-v2__shape-icon, --.object-vector-studio-v2__tool-toggle--future .object-vector-studio-v2__tool-label { -- color: var(--muted); --} -- --.object-vector-studio-v2__tool-label { -- max-width: 100%; -- overflow: hidden; -- font-size: 0.68rem; -- line-height: 1; -- text-overflow: ellipsis; -- white-space: nowrap; --} -- --.object-vector-studio-v2__z-label { -- max-width: 100%; -- overflow: hidden; -- font-size: 0.68rem; -- line-height: 1; -- text-overflow: ellipsis; -- white-space: nowrap; --} -- --.object-vector-studio-v2__shape-icon { -- width: 24px; -- height: 24px; -- display: inline-grid; -- place-items: center; -- font-size: 1.08rem; -- color: var(--tool-starter-text); --} -- --.object-vector-studio-v2__tool-toggle-grid.is-icon-only { -- grid-template-columns: repeat(auto-fill, 42px); --} -- --.object-vector-studio-v2__tool-toggle-grid.is-icon-only .object-vector-studio-v2__tool-toggle { -- width: 100%; -- min-width: 0; -- min-height: 0; -- aspect-ratio: 1; -- gap: 0; -- padding: 4px; --} -- --.object-vector-studio-v2__tool-toggle-grid.is-icon-only .object-vector-studio-v2__tool-label { -- width: 1px; -- height: 1px; -- position: absolute; -- overflow: hidden; -- clip: rect(0 0 0 0); -- white-space: nowrap; --} -- --.object-vector-studio-v2__z-order-actions.is-icon-only { -- grid-template-columns: repeat(auto-fill, 42px); --} -- --#objectVectorStudioV2ObjectsContent .object-vector-studio-v2__z-order-actions.is-icon-only { -- grid-template-columns: repeat(auto-fit, 34px); --} -- --.object-vector-studio-v2__z-order-actions.is-icon-only .object-vector-studio-v2__z-action { -- width: 100%; -- min-width: 0; -- min-height: 0; -- aspect-ratio: 1; -- padding: 2px !important; --} -- --.object-vector-studio-v2__z-order-actions.is-icon-only .object-vector-studio-v2__z-label { -- width: 1px; -- height: 1px; -- position: absolute; -- overflow: hidden; -- clip: rect(0 0 0 0); -- white-space: nowrap; --} -- --.object-vector-studio-v2__z-icon { -- width: 24px; -- height: 24px; -- position: relative; -- display: inline-grid; -- place-items: center; -- color: var(--tool-starter-text); --} -- --.object-vector-studio-v2__tool-toggle.is-active, --.object-vector-studio-v2__tool-toggle[aria-pressed="true"] { -- border-color: var(--tool-starter-accent) !important; -- box-shadow: 0 0 0 2px rgba(221, 214, 254, 0.25); --} -- --.object-vector-studio-v2__object-tiles { -- min-height: 0; -- flex: 1 0 auto; -- display: grid; -- align-content: start; -- gap: 8px; -- overflow: visible; -- padding-right: 2px; --} -- --.object-vector-studio-v2__object-actions { -- display: grid; -- grid-template-columns: repeat(3, minmax(0, 1fr)); -- gap: 4px; -- margin-top: 0; -- padding-top: 0; --} -- --.object-vector-studio-v2__object-actions button { -- height: 28px; -- min-width: 0; -- min-height: 28px; -- padding: 2px 5px !important; -- font-size: 0.78rem; -- line-height: 1; -- white-space: nowrap; --} -- --.object-vector-studio-v2__library-panel, --.object-vector-studio-v2__asset-library { -- min-height: 0; -- gap: 8px; --} -- --.object-vector-studio-v2__library-panel { -- display: grid; --} -- --.object-vector-studio-v2__asset-library { -- display: flex; -- flex-direction: column; -- overflow: visible; --} -- --.object-vector-studio-v2__object-tile { -- width: 100%; -- min-height: 52px; -- display: grid; -- grid-template-columns: minmax(0, 1fr) auto; -- align-items: center; -- gap: 6px; -- border: 1px solid var(--tool-starter-line); -- background: var(--tool-starter-panel-strong); -- border-radius: 8px !important; -- padding: 8px 10px; -- text-align: left; --} -- --.object-vector-studio-v2__asset-tile { -- position: relative; -- z-index: 1; -- width: 100%; -- flex: 0 0 auto; -- min-height: 54px; -- display: grid; -- gap: 4px; -- border-radius: 8px !important; -- padding: 8px 10px; -- text-align: left; --} -- --.object-vector-studio-v2__asset-tile--empty { -- color: var(--tool-starter-muted); --} -- --.object-vector-studio-v2__object-tile.is-selected, --.object-vector-studio-v2__object-tile[aria-pressed="true"] { -- border-color: var(--tool-starter-accent) !important; -- background: var(--tool-starter-input-surface); --} -- --.object-vector-studio-v2__object-tile.is-hidden { -- opacity: 0.62; --} -- --.object-vector-studio-v2__object-tile.is-locked { -- box-shadow: inset 0 0 0 1px rgba(251, 191, 36, 0.58); --} -- --.object-vector-studio-v2__object-tile--empty { -- grid-template-columns: 1fr; -- color: var(--tool-starter-muted); --} -- --.object-vector-studio-v2__object-tile-copy { -- min-width: 0; -- display: grid; -- gap: 2px; --} -- --.object-vector-studio-v2__object-select { -- min-width: 0; -- width: 100%; -- display: grid; -- grid-template-columns: 50px minmax(0, 1fr); -- align-items: center; -- gap: 8px; -- background: transparent; -- text-align: left; -- justify-items: start; --} -- --.object-vector-studio-v2__object-tile-controls { -- align-self: center; -- justify-self: end; -- position: relative; -- z-index: 2; -- display: flex; -- flex-direction: column; -- flex-wrap: nowrap; -- justify-content: center; -- align-items: center; -- gap: 2px; --} -- --.object-vector-studio-v2__object-tile-control { -- position: relative; -- z-index: 3; -- width: 26px; -- height: 26px; -- min-height: 26px; -- padding: 0 !important; --} -- --.object-vector-studio-v2__object-tile-control--delete { -- color: var(--tool-starter-danger); --} -- --.object-vector-studio-v2__object-state-panel { -- grid-column: 1 / -1; -- display: grid; -- gap: 4px; -- border-top: 1px solid var(--tool-starter-line); -- margin-top: 2px; -- padding-top: 4px; --} -- --.object-vector-studio-v2__object-state-controls { -- display: grid; -- grid-template-columns: auto minmax(74px, 1fr) auto 26px; -- align-items: center; -- gap: 4px; --} -- --.object-vector-studio-v2__object-state-controls button, --.object-vector-studio-v2__object-state-controls select { -- min-height: 26px; -- padding: 2px 5px !important; -- font-size: 0.72rem; -- line-height: 1; --} -- --.object-vector-studio-v2__object-state-controls select { -- min-width: 0; --} -- --.object-vector-studio-v2__object-state-help { -- width: 26px; -- min-width: 26px; -- padding-inline: 0 !important; -- font-weight: 900; --} -- --.object-vector-studio-v2__object-state-tiles { -- display: flex; -- flex-wrap: wrap; -- gap: 4px; --} -- --.object-vector-studio-v2__object-state-tiles button { -- min-height: 24px; -- min-width: 46px; -- padding: 2px 6px !important; -- font-size: 0.68rem; -- line-height: 1; --} -- --.object-vector-studio-v2__object-state-tiles button.is-selected, --.object-vector-studio-v2__object-state-tiles button[aria-pressed="true"] { -- border-color: var(--tool-starter-accent) !important; -- box-shadow: 0 0 0 2px rgba(221, 214, 254, 0.25); --} -- --.object-vector-studio-v2__object-tile-shapes { -- grid-column: 1 / -1; -- display: grid; -- gap: 1px; -- border-top: 1px solid var(--tool-starter-line); -- padding-top: 4px; --} -- --.object-vector-studio-v2__object-tile-shape-row { -- display: grid; -- grid-template-columns: minmax(0, 1fr) auto; -- align-items: center; -- gap: 4px; -- font-size: 0.7rem; -- line-height: 1.08; --} -- --.object-vector-studio-v2__shape-select { -- width: 100% !important; -- min-height: 22px; -- position: relative; -- z-index: 3; -- display: flex; -- align-items: center; -- justify-content: flex-start; -- gap: 6px; -- padding: 2px 5px !important; -- text-align: left; --} -- --.object-vector-studio-v2__shape-select-label { -- min-width: 0; -- display: inline-flex; -- align-items: center; -- gap: 4px; -- overflow-wrap: anywhere; --} -- --.object-vector-studio-v2__shape-inline-actions { -- display: inline-flex; -- align-items: center; -- justify-content: flex-end; -- gap: 2px; --} -- --.object-vector-studio-v2__shape-inline-button { -- width: 24px; -- height: 24px; -- min-height: 24px; -- display: inline-grid; -- place-items: center; -- padding: 0 !important; --} -- --.object-vector-studio-v2__shape-group-indicator[data-ovs-icon] { -- font-size: 0.78rem; --} -- --.object-vector-studio-v2__object-tile-shape-row.is-selected .object-vector-studio-v2__shape-select, --.object-vector-studio-v2__shape-select[aria-pressed="true"] { -- border-color: var(--tool-starter-accent) !important; -- box-shadow: 0 0 0 2px rgba(221, 214, 254, 0.2); --} -- --.object-vector-studio-v2__tile-icon { -- width: 14px; -- height: 14px; -- position: relative; -- display: inline-grid; -- place-items: center; -- color: var(--tool-starter-text); -- pointer-events: none; --} -- --.object-vector-studio-v2__tile-icon.is-off { -- opacity: 0.42; --} -- --.object-vector-studio-v2 [data-ovs-icon]::before { -- content: attr(data-ovs-icon); -- display: inline-block; -- font-family: "0xProto Nerd Font", "Cascadia Mono", monospace; -- font-size: calc(1em * var(--object-vector-studio-v2-icon-scale, 1.75)); -- font-style: normal; -- font-weight: 400; -- line-height: 1; -- text-rendering: geometricPrecision; --} -- --.object-vector-studio-v2 button[data-ovs-icon] { -- display: inline-flex; -- align-items: center; -- justify-content: center; -- gap: 4px; --} -- --.object-vector-studio-v2__shape-icon[data-ovs-icon], --.object-vector-studio-v2__z-icon[data-ovs-icon], --.object-vector-studio-v2__tile-icon[data-ovs-icon] { -- display: inline-grid; -- place-items: center; -- border: 0; -- outline: 0; -- background: transparent; -- clip-path: none; -- color: var(--tool-starter-text); -- transform: none; --} -- --.object-vector-studio-v2__snap-actions button[data-ovs-icon]::before { -- color: inherit; --} -- --#objectVectorStudioV2SnapModeButton[data-snap-mode="point"][data-ovs-icon]::before { -- color: var(--object-vector-studio-v2-snap-point-color); --} -- --#objectVectorStudioV2SnapModeButton[data-snap-mode="none"][data-ovs-icon]::before, --#objectVectorStudioV2AngleSnapButton[aria-pressed="false"][data-ovs-icon]::before, --#objectVectorStudioV2GridRenderButton[aria-pressed="false"][data-ovs-icon]::before { -- color: var(--object-vector-studio-v2-snap-disabled-icon-color); --} -- --.object-vector-studio-v2__shape-icon[data-ovs-icon] { -- width: 24px; -- height: 24px; -- font-size: 1.08rem; --} -- --.object-vector-studio-v2__tool-toggle--future .object-vector-studio-v2__shape-icon[data-ovs-icon], --.object-vector-studio-v2__tool-toggle--future .object-vector-studio-v2__tool-label { -- color: var(--muted); --} -- --.object-vector-studio-v2__shape-icon--select[data-ovs-icon]::before { -- font-size: calc(1em * var(--object-vector-studio-v2-icon-scale, 1.75) * 1.125); --} -- --.object-vector-studio-v2__shape-icon--triangle[data-ovs-icon]::before { -- font-size: calc(1em * var(--object-vector-studio-v2-icon-scale, 1.75) * 1.25); --} -- --.object-vector-studio-v2__shape-icon--rectangle[data-ovs-icon]::before { -- font-size: calc(1em * var(--object-vector-studio-v2-icon-scale, 1.75) * 1.25); --} -- --.object-vector-studio-v2__z-icon[data-ovs-icon] { -- width: 24px; -- height: 24px; -- position: relative; -- font-size: 1.05rem; --} -- --.object-vector-studio-v2__tile-icon[data-ovs-icon] { -- width: 14px; -- height: 14px; -- position: relative; -- font-size: 0.84rem; --} -- --.object-vector-studio-v2__shape-icon[data-ovs-icon]::before, --.object-vector-studio-v2__z-icon[data-ovs-icon]::before, --.object-vector-studio-v2__tile-icon[data-ovs-icon]::before { -- position: static; -- width: auto; -- height: auto; -- border: 0; -- background: transparent; -- border-radius: 0; -- transform-origin: center; --} -- --.object-vector-studio-v2__shape-icon[data-ovs-icon]::after, --.object-vector-studio-v2__z-icon[data-ovs-icon]::after, --.object-vector-studio-v2__tile-icon[data-ovs-icon]::after { -- content: none; -- display: none; --} -- --.object-vector-studio-v2__shape-group-indicator.object-vector-studio-v2__tile-icon[data-ovs-icon], --#objectVectorStudioV2ObjectsContent .object-vector-studio-v2__shape-group-indicator[data-ovs-icon] { -- color: var(--object-vector-studio-v2-shape-group-color, var(--tool-starter-accent)); --} -- --.object-vector-studio-v2__object-tile-control--delete .object-vector-studio-v2__tile-icon[data-ovs-icon], --.object-vector-studio-v2__shape-delete-inline .object-vector-studio-v2__tile-icon[data-ovs-icon] { -- color: var(--tool-starter-danger); --} -- --.object-vector-studio-v2__shape-delete-inline { -- color: var(--tool-starter-danger); --} -- --.object-vector-studio-v2__object-thumbnail { -- width: 44px; -- height: 36px; -- display: block; -- border: 1px solid var(--tool-starter-line); -- border-radius: 4px; -- background: rgba(0, 0, 0, 0.18); --} -- --.object-vector-studio-v2__object-thumbnail-shape { -- pointer-events: none; --} -- --.object-vector-studio-v2__object-thumbnail-empty { -- fill: none; -- stroke: var(--tool-starter-muted); -- stroke-dasharray: 4 3; -- stroke-width: 2; --} -- --.object-vector-studio-v2__object-tile-name { -- font-weight: 800; -- line-height: 1.14; --} -- --.object-vector-studio-v2__object-tile-meta { -- color: var(--tool-starter-muted); -- font-size: 0.78rem; -- line-height: 1.16; -- overflow-wrap: anywhere; --} -- --.object-vector-studio-v2__work-area { -- position: relative; -- min-height: 0; -- flex: 0 0 auto; -- display: grid; -- grid-template-rows: auto auto auto auto auto auto auto; -- place-items: center; -- gap: 8px; -- border: 1px dashed var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-panel-strong); -- color: var(--tool-starter-muted); -- padding: 12px; -- text-align: center; --} -- --.object-vector-studio-v2__separator { -- width: min(720px, 100%); -- border: 0; -- border-top: 1px solid var(--tool-starter-line); -- margin: 0; --} -- --.object-vector-studio-v2__viewport-controls button { -- min-height: 30px; --} -- --.object-vector-studio-v2__snap-actions button, --.object-vector-studio-v2__z-order-actions button, --.object-vector-studio-v2__palette-sort button { -- min-height: 42px; --} -- --#objectVectorStudioV2ObjectsContent .object-vector-studio-v2__z-order-actions button { -- min-height: 34px; --} -- --.object-vector-studio-v2__animation-controls { -- width: min(720px, 100%); -- display: flex; -- align-items: center; -- justify-content: center; -- flex-wrap: wrap; -- gap: 8px; --} -- --.object-vector-studio-v2__inline-field { -- display: inline-flex; -- align-items: center; -- gap: 6px; -- color: var(--tool-starter-muted); -- font-weight: 800; --} -- --.object-vector-studio-v2__inline-field input[type="number"] { -- width: 72px; -- padding: 5px 7px; --} -- --.object-vector-studio-v2__frame-timeline { -- width: min(720px, 100%); -- min-height: 52px; -- display: flex; -- align-items: stretch; -- gap: 8px; -- overflow-x: auto; -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-surface); -- padding: 8px; --} -- --.object-vector-studio-v2__frame-tile { -- min-width: 108px; -- display: grid; -- gap: 4px; -- justify-items: center; -- border: 1px solid var(--tool-starter-line) !important; -- border-radius: 8px !important; -- background: ButtonFace; -- color: ButtonText; -- cursor: pointer; -- padding: 6px; -- text-align: center; --} -- --.object-vector-studio-v2__frame-tile.is-selected, --.object-vector-studio-v2__frame-tile[aria-pressed="true"] { -- border-color: var(--tool-starter-accent) !important; -- box-shadow: 0 0 0 2px rgba(221, 214, 254, 0.25); --} -- --.object-vector-studio-v2__frame-thumbnail { -- width: 84px; -- height: 44px; -- border: 1px solid var(--tool-starter-line); -- border-radius: 4px; -- background: rgba(0, 0, 0, 0.18); --} -- --.object-vector-studio-v2__coordinate-display, --.object-vector-studio-v2__selection-metrics { -- width: min(520px, 100%); -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-surface); -- color: var(--tool-starter-muted); -- padding: 8px 10px; -- font-size: 0.9rem; --} -- --.object-vector-studio-v2__header-count { -- color: var(--tool-starter-muted); -- font-size: 0.82rem; -- font-weight: 700; -- overflow-wrap: anywhere; --} -- --.object-vector-studio-v2__geometry-header-title { -- min-width: 0; -- display: grid; -- gap: 1px; -- line-height: 1.05; --} -- --.object-vector-studio-v2__geometry-header-line { -- min-width: 0; --} -- --.object-vector-studio-v2__geometry-header-name { -- min-width: 0; -- color: var(--tool-starter-muted); -- font-size: 0.78rem; -- font-weight: 700; -- overflow: hidden; -- text-overflow: ellipsis; -- white-space: nowrap; --} -- --.object-vector-studio-v2__shape-list { -- display: grid; -- flex: 0 0 auto; -- gap: 6px; -- max-height: 132px; -- overflow-y: auto; --} -- --.object-vector-studio-v2__shape-list-button { -- min-height: 30px; -- text-align: left; --} -- --.object-vector-studio-v2__shape-list-button.is-selected, --.object-vector-studio-v2__shape-list-button[aria-pressed="true"], --.object-vector-studio-v2__palette-controls button.is-active, --.object-vector-studio-v2__palette-controls button[aria-pressed="true"] { -- border-color: var(--tool-starter-accent) !important; -- box-shadow: 0 0 0 2px rgba(221, 214, 254, 0.25); --} -- --.object-vector-studio-v2__shape-list-empty { -- color: var(--tool-starter-muted); -- font-size: 0.9rem; --} -- --.object-vector-studio-v2__palette-controls .object-vector-studio-v2__inline-field input { -- width: 46px; -- padding-inline: 5px; --} -- --#objectVectorStudioV2StrokeWidth { -- width: 58px; --} -- --.object-vector-studio-v2__palette-controls { -- display: grid; -- gap: 5px; -- justify-items: center; --} -- --.object-vector-studio-v2__palette-controls button { -- min-width: 0; -- min-height: 30px; -- padding: 3px 5px !important; --} -- --.object-vector-studio-v2__palette-picker-button { -- width: 30px; -- min-width: 30px; -- height: 30px; -- padding: 0 !important; --} -- --.object-vector-studio-v2__palette-picker-button .object-vector-studio-v2__shape-icon { -- width: 20px; -- height: 20px; --} -- --.object-vector-studio-v2__palette-primary-row, --.object-vector-studio-v2__palette-opacity-row { -- display: flex; -- align-items: center; -- justify-content: center; -- flex-wrap: nowrap; -- gap: 5px; -- width: 100%; --} -- --.object-vector-studio-v2__palette-controls .object-vector-studio-v2__inline-field { -- flex: 0 0 auto; -- gap: 3px; -- font-size: 0.78rem; -- white-space: nowrap; --} -- --.object-vector-studio-v2__palette-opacity-row .object-vector-studio-v2__inline-field { -- font-size: 0.72rem; --} -- --.object-vector-studio-v2__palette-opacity-heading { -- flex: 0 0 auto; -- font-size: 0.74rem; -- font-weight: 700; -- line-height: 1; --} -- --.object-vector-studio-v2__palette-opacity-row .object-vector-studio-v2__inline-field input { -- width: 54px; --} -- --.object-vector-studio-v2__palette-controls button[data-ovs-icon]::before { -- order: 2; --} -- --.object-vector-studio-v2__palette-mode-swatch { -- order: 1; -- width: 13px; -- height: 13px; -- flex: 0 0 13px; -- border: 1px solid rgba(255, 255, 255, 0.72); -- border-radius: 2px; -- background: var(--object-vector-studio-v2-mode-swatch, transparent); -- opacity: var(--object-vector-studio-v2-mode-opacity, 1); -- box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25); --} -- --.object-vector-studio-v2__palette-mode-label { -- order: 3; --} -- --.object-vector-studio-v2__palette-sort { -- display: grid; -- grid-template-columns: repeat(5, minmax(0, 1fr)); -- align-items: center; -- gap: 2px; -- width: 100%; -- border-top: 1px solid var(--tool-starter-line); -- padding-top: 4px; --} -- --.object-vector-studio-v2__palette-sort button { -- min-width: 0; -- min-height: 24px; -- gap: 1px; -- padding: 1px 2px !important; -- font-size: 0.61rem; -- line-height: 1; --} -- --.object-vector-studio-v2__palette-sort button[data-ovs-icon]::before { -- font-size: 1.05em; --} -- --.object-vector-studio-v2__palette-sort button[aria-pressed="true"] { -- border-color: var(--tool-starter-accent) !important; -- box-shadow: 0 0 0 2px rgba(221, 214, 254, 0.25); --} -- --.object-vector-studio-v2__palette-sort-caret { -- margin-left: 1px; -- font-size: 0.62rem; -- line-height: 1; --} -- --.object-vector-studio-v2__selection-metrics { -- color: var(--tool-starter-text); -- overflow-wrap: anywhere; --} -- --.object-vector-studio-v2__selected-visibility { -- width: min(520px, 100%); -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-surface); -- color: var(--tool-starter-text); -- padding: 18px; -- overflow-wrap: anywhere; --} -- --.object-vector-studio-v2__render-surface { -- width: 100%; -- max-width: 100%; -- height: auto; -- min-height: 0; -- aspect-ratio: 16 / 11; -- align-self: stretch; -- justify-self: stretch; -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: rgba(0, 0, 0, 0.16); -- background-size: 20px 20px; --} -- --.object-vector-studio-v2__render-surface.is-grid-visible { -- background: rgba(0, 0, 0, 0.16); --} -- --.object-vector-studio-v2__svg-grid { -- stroke: rgba(255, 255, 255, 0.18); -- stroke-width: 1; -- vector-effect: non-scaling-stroke; -- pointer-events: none; --} -- --.object-vector-studio-v2__shape { -- cursor: pointer; -- transition: filter 120ms ease, opacity 120ms ease; --} -- --.object-vector-studio-v2__shape-hit-layer { -- pointer-events: auto; --} -- --.object-vector-studio-v2__shape-hit-area { -- cursor: pointer; -- fill-opacity: 0; -- stroke-opacity: 0; -- pointer-events: all; --} -- --.object-vector-studio-v2__render-surface.is-drawing-mode { -- cursor: crosshair; --} -- --.object-vector-studio-v2__render-surface.is-empty-canvas-object-drag-ready, --.object-vector-studio-v2__render-surface.is-canvas-pan-ready { -- cursor: grab; --} -- --.object-vector-studio-v2__render-surface.is-empty-canvas-object-dragging, --.object-vector-studio-v2__render-surface.is-canvas-panning { -- cursor: grabbing; --} -- --.object-vector-studio-v2__drawing-preview { -- opacity: 0.78; -- stroke-dasharray: 5 4; -- vector-effect: non-scaling-stroke; -- pointer-events: none; --} -- --.object-vector-studio-v2__snap-line { -- fill: none; -- stroke: var(--object-vector-studio-v2-snap-point-color); -- stroke-opacity: 0.88; -- stroke-width: 1.35; -- vector-effect: non-scaling-stroke; -- pointer-events: none; --} -- --.object-vector-studio-v2__drawing-hint { -- position: absolute; -- z-index: 6; -- max-width: min(260px, calc(100% - 24px)); -- border: 1px solid rgba(221, 214, 254, 0.45); -- border-radius: 6px; -- background: rgba(2, 6, 23, 0.92); -- color: var(--tool-starter-text); -- font-family: "0xProto Nerd Font", "Cascadia Mono", monospace; -- font-size: 12px; -- line-height: 1.2; -- padding: 4px 7px; -- white-space: nowrap; -- pointer-events: none; --} -- --.object-vector-studio-v2__snap-targets { -- pointer-events: none; --} -- --.object-vector-studio-v2__snap-target { -- fill: none; -- stroke: var(--object-vector-studio-v2-snap-point-color); -- stroke-width: 1.25; -- vector-effect: non-scaling-stroke; --} -- --.object-vector-studio-v2__shape:hover, --.object-vector-studio-v2__shape.is-selected { -- filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.92)); --} -- --.object-vector-studio-v2__selection-box { -- fill: none; -- stroke: var(--tool-starter-accent); -- stroke-dasharray: 5 4; -- stroke-width: 0.75; -- vector-effect: non-scaling-stroke; -- pointer-events: none; --} -- --.object-vector-studio-v2__selection-drag-area { -- cursor: move; -- fill: transparent; -- pointer-events: all; -- stroke: none; --} -- --.object-vector-studio-v2__object-bounds { -- fill: none; -- stroke: #67e8f9; -- stroke-dasharray: 8 5; -- stroke-width: 1.5; -- vector-effect: non-scaling-stroke; -- pointer-events: none; --} -- --.object-vector-studio-v2__onion-skin { -- opacity: 0.28; -- pointer-events: none; --} -- --.object-vector-studio-v2__onion-skin * { -- stroke-dasharray: 5 4; -- vector-effect: non-scaling-stroke; --} -- --.object-vector-studio-v2__resize-handle { -- fill: var(--tool-starter-accent); -- stroke: #ffffff; -- stroke-width: 0.75; -- vector-effect: non-scaling-stroke; -- cursor: nwse-resize; -- pointer-events: auto; --} -- --.object-vector-studio-v2__line-endpoint-handle { -- cursor: crosshair; --} -- --.object-vector-studio-v2__geometry-point-handle:not([data-resize-handle]) { -- cursor: crosshair; --} -- --.object-vector-studio-v2__center-origin-dot { -- fill: var(--tool-starter-accent-strong, #fbbf24); -- stroke: #020617; -- stroke-width: 1.5; -- vector-effect: non-scaling-stroke; -- pointer-events: none; --} -- --.object-vector-studio-v2__object-origin line { -- stroke: #f8fafc; -- stroke-width: 1.2; -- pointer-events: none; -- vector-effect: non-scaling-stroke; --} -- --.object-vector-studio-v2__object-origin line { -- stroke: var(--tool-starter-accent-strong, #fbbf24); -- stroke-width: 1.5; --} -- --.object-vector-studio-v2__object-detail-grid { -- display: grid; -- grid-template-columns: 104px minmax(0, 1fr); -- gap: 8px 10px; --} -- --.object-vector-studio-v2__shape-group-summary { -- align-items: center; -- display: grid; -- grid-template-columns: 104px minmax(0, 1fr); -- gap: 8px 10px; --} -- --.object-vector-studio-v2__shape-group-summary-value { -- align-items: center; -- display: inline-flex; -- gap: 6px; -- min-width: 0; --} -- --.object-vector-studio-v2__object-detail-stack, --.object-vector-studio-v2__shape-panel, --.object-vector-studio-v2__edit-panel { -- display: grid; -- gap: 12px; -- margin-top: 0; --} -- --.object-vector-studio-v2__object-detail-stack { -- align-content: start; -- gap: 6px; --} -- --.object-vector-studio-v2 .object-vector-studio-v2__shape-panel, --.object-vector-studio-v2 .object-vector-studio-v2__edit-panel { -- margin-top: 0; --} -- --.object-vector-studio-v2__shape-panel { -- border-top: 1px solid var(--tool-starter-line); -- padding-top: 12px; --} -- --.object-vector-studio-v2__shape-panel h3, --.object-vector-studio-v2__shape-panel h4, --.object-vector-studio-v2__shape-panel p { -- margin: 0; --} -- --.object-vector-studio-v2__edit-grid { -- display: grid; -- grid-template-columns: repeat(2, minmax(0, 1fr)); -- gap: 8px; --} -- --.object-vector-studio-v2__polygon-point-list { -- display: grid; -- gap: 5px; -- max-height: none; -- overflow: visible; -- padding-right: 0; --} -- --.object-vector-studio-v2__edit-panel--polygon { -- gap: 5px; --} -- --.object-vector-studio-v2__edit-panel--polygon h4 { -- margin: 0; --} -- --.object-vector-studio-v2__edit-grid--ellipse { -- grid-template-columns: repeat(2, minmax(0, 1fr)); --} -- --.object-vector-studio-v2__edit-panel--geometry { -- gap: 5px; --} -- --.object-vector-studio-v2__edit-panel--geometry + .object-vector-studio-v2__shape-panel { -- padding-top: 6px; --} -- --.object-vector-studio-v2__edit-panel--geometry h4 { -- margin: 0; -- font-size: 0.86rem; -- line-height: 1.1; --} -- --.object-vector-studio-v2__edit-panel--geometry .object-vector-studio-v2__edit-grid { -- gap: 4px 6px; --} -- --.object-vector-studio-v2__edit-panel--geometry .object-vector-studio-v2__edit-field { -- grid-template-columns: max-content minmax(0, 1fr); -- align-items: center; -- gap: 4px; -- font-size: 0.78rem; --} -- --.object-vector-studio-v2__edit-field--wide { -- grid-column: 1 / -1; --} -- --.object-vector-studio-v2__edit-field { -- min-width: 0; -- display: grid; -- gap: 4px; -- color: var(--tool-starter-muted); -- font-size: 0.88rem; -- font-weight: 800; -- text-align: left; --} -- --.object-vector-studio-v2__polygon-point-header, --.object-vector-studio-v2__polygon-point-field { -- display: grid; -- grid-template-columns: 50px minmax(72px, 1fr) minmax(72px, 1fr) 32px 21px 21px; -- align-items: center; -- gap: 4px; --} -- --.object-vector-studio-v2__polygon-point-header { -- color: var(--tool-starter-muted); -- font-size: 0.66rem; -- font-weight: 900; -- line-height: 1; -- text-align: center; -- text-transform: uppercase; --} -- --.object-vector-studio-v2__polygon-point-field[data-polygon-point-selectable="true"] { -- cursor: pointer; -- border-radius: 6px; --} -- --.object-vector-studio-v2__polygon-point-field.is-action-selected { -- background: color-mix(in srgb, var(--tool-starter-accent) 16%, transparent); -- outline: 1px solid color-mix(in srgb, var(--tool-starter-accent) 48%, transparent); -- outline-offset: 2px; --} -- --.object-vector-studio-v2__polygon-point-label { -- color: var(--tool-starter-muted); -- font-size: 0.76rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__polygon-point-axis { -- min-width: 0; -- display: grid; -- grid-template-columns: auto minmax(0, 1fr); -- align-items: center; -- gap: 3px; -- color: var(--tool-starter-muted); -- font-size: 0.76rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__edit-field input { -- min-width: 0; -- width: 100%; -- padding: 6px 8px; --} -- --.object-vector-studio-v2__edit-panel--geometry .object-vector-studio-v2__edit-field input { -- min-height: 24px; -- padding: 3px 5px; --} -- --.object-vector-studio-v2__polygon-point-axis input { -- appearance: textfield; -- -moz-appearance: textfield; -- min-width: 0; -- width: 100%; -- padding: 3px 4px; --} -- --.object-vector-studio-v2__polygon-point-axis input::-webkit-inner-spin-button, --.object-vector-studio-v2__polygon-point-axis input::-webkit-outer-spin-button { -- margin: 0; -- -webkit-appearance: none; --} -- --.object-vector-studio-v2__rounding-radius-field { -- display: grid; -- grid-template-columns: max-content minmax(0, 74px); -- align-items: center; -- justify-content: start; -- gap: 6px; -- width: 100%; -- color: var(--tool-starter-muted); -- font-size: 0.74rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__rounding-radius-field input { -- min-width: 0; -- width: 100%; -- min-height: 22px; -- padding: 2px 5px; --} -- --.object-vector-studio-v2__polygon-point-axis--readonly { -- grid-template-columns: auto minmax(0, 1fr); --} -- --.object-vector-studio-v2__polygon-point-value { -- min-height: 24px; -- display: inline-flex; -- align-items: center; -- padding: 3px 5px; -- border: 1px solid var(--tool-starter-border); -- border-radius: 6px; -- background: var(--tool-starter-surface); -- color: var(--tool-starter-text); -- font-size: 0.78rem; --} -- --.object-vector-studio-v2__polygon-point-rounding { -- display: inline-grid; -- grid-template-columns: 14px; -- align-items: center; -- justify-content: center; -- color: var(--tool-starter-muted); -- font-size: 0.7rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__polygon-point-rounding span { -- display: none; --} -- --.object-vector-studio-v2__polygon-point-rounding input { -- width: 14px; -- height: 14px; -- margin: 0; -- accent-color: var(--tool-starter-accent); --} -- --.object-vector-studio-v2__polygon-point-add, --.object-vector-studio-v2__polygon-point-delete { -- width: 21px; -- height: 22px; -- min-height: 22px; -- padding: 0 !important; -- color: var(--tool-starter-danger); --} -- --.object-vector-studio-v2__polygon-point-add { -- color: var(--tool-starter-text); --} -- --.object-vector-studio-v2__polygon-point-add[data-ovs-icon]::before, --.object-vector-studio-v2__polygon-point-delete[data-ovs-icon]::before { -- font-size: 0.92rem; --} -- --.object-vector-studio-v2__edit-field--inline { -- grid-template-columns: 78px minmax(0, 1fr); -- align-items: center; -- gap: 8px; --} -- --.object-vector-studio-v2__edit-panel--transform { -- gap: 6px; --} -- --.object-vector-studio-v2__edit-panel--transform h4 { -- margin: 0; -- font-size: 0.86rem; -- line-height: 1.1; --} -- --.object-vector-studio-v2__transform-control-row { -- display: grid; -- grid-template-columns: 50px minmax(0, 1fr) minmax(0, 1fr) max-content max-content; -- align-items: center; -- gap: 3px; -- min-width: 0; -- width: 100%; -- white-space: nowrap; -- --object-vector-studio-v2-icon-scale: 1.05; --} -- --.object-vector-studio-v2__transform-control-row--rotate { -- grid-template-columns: 50px minmax(42px, 1fr) max-content; --} -- --.object-vector-studio-v2__transform-control-row--rotate.is-angle-snap-enabled { -- grid-template-columns: 50px minmax(54px, 1fr) minmax(54px, max-content) max-content; --} -- --.object-vector-studio-v2__transform-control-row--auto-origin { -- grid-template-columns: 50px minmax(0, 1fr); --} -- --.object-vector-studio-v2__transform-control-row--rotate #objectVectorStudioV2RotateInput, --.object-vector-studio-v2__transform-control-row--rotate #objectVectorStudioV2RotateSnapSelect, --.object-vector-studio-v2__transform-control-row--rotate #objectVectorStudioV2ObjectRotateInput, --.object-vector-studio-v2__transform-control-row--rotate #objectVectorStudioV2ObjectRotateSnapSelect { -- grid-column: 2; -- grid-row: 1; --} -- --.object-vector-studio-v2__transform-control-row--rotate .object-vector-studio-v2__snap-angle-step-field { -- grid-column: 3; -- grid-row: 1; --} -- --.object-vector-studio-v2__transform-control-row--rotate #objectVectorStudioV2RotateShapeButton, --.object-vector-studio-v2__transform-control-row--rotate #objectVectorStudioV2ObjectRotateButton { -- grid-column: 3; -- grid-row: 1; --} -- --.object-vector-studio-v2__transform-control-row--rotate.is-angle-snap-enabled #objectVectorStudioV2RotateShapeButton, --.object-vector-studio-v2__transform-control-row--rotate.is-angle-snap-enabled #objectVectorStudioV2ObjectRotateButton { -- grid-column: 4; --} -- --.object-vector-studio-v2__transform-control-label { -- color: var(--tool-starter-text); -- font-size: 0.78rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__transform-axis-field { -- min-width: 0; -- display: grid; -- grid-template-columns: max-content minmax(0, 1fr); -- align-items: center; -- gap: 3px; -- font-size: 0.72rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__transform-control-row input { -- min-width: 0; -- width: 100%; -- min-height: 22px; -- padding: 2px 3px; --} -- --.object-vector-studio-v2__transform-control-row select { -- min-width: 0; -- width: 100%; -- min-height: 22px; -- padding: 1px 2px; -- font-size: 0.72rem; --} -- --.object-vector-studio-v2__snap-angle-step-field { -- min-width: 0; -- display: grid; -- grid-template-columns: max-content minmax(0, 1fr); -- align-items: center; -- gap: 2px; -- font-size: 0.7rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__transform-control-row button { -- min-width: 0; -- min-height: 22px; -- padding: 1px 4px; -- font-size: 0.72rem; -- line-height: 1; --} -- --.object-vector-studio-v2__edit-panel--transform .object-vector-studio-v2__edit-grid { -- gap: 4px 6px; --} -- --.object-vector-studio-v2__edit-panel--transform .object-vector-studio-v2__edit-field--inline { -- grid-template-columns: 62px minmax(0, 1fr); -- gap: 4px; -- font-size: 0.78rem; --} -- --.object-vector-studio-v2__edit-panel--transform .object-vector-studio-v2__edit-field input { -- min-height: 24px; -- padding: 3px 5px; --} -- --.object-vector-studio-v2__edit-panel--transform .object-vector-studio-v2__shape-actions { -- gap: 4px; --} -- --.object-vector-studio-v2__scale-control-row { -- display: grid; -- grid-template-columns: max-content 22px 21px minmax(58px, 64px) 21px 22px minmax(58px, max-content) 22px; -- align-items: center; -- gap: 2px; -- min-width: 0; -- width: 100%; -- white-space: nowrap; -- --object-vector-studio-v2-icon-scale: 1.05; --} -- --.object-vector-studio-v2__scale-control-label { -- color: var(--tool-starter-text); -- font-size: 0.78rem; -- font-weight: 800; --} -- --.object-vector-studio-v2__scale-control-row button { -- min-width: 0; -- min-height: 22px; -- padding: 1px 2px; -- font-size: 0.72rem; -- line-height: 1; --} -- --.object-vector-studio-v2__scale-control-row .object-vector-studio-v2__scale-resize-button { -- gap: 2px; -- padding-inline: 4px; --} -- --.object-vector-studio-v2__scale-control-row .object-vector-studio-v2__scale-reset-button { -- font-weight: 900; --} -- --.object-vector-studio-v2__scale-input { -- appearance: textfield; -- -moz-appearance: textfield; -- min-width: 0; -- width: 100%; -- min-height: 22px; -- padding: 2px 3px; -- text-align: center; --} -- --.object-vector-studio-v2__scale-input::-webkit-inner-spin-button, --.object-vector-studio-v2__scale-input::-webkit-outer-spin-button { -- margin: 0; -- -webkit-appearance: none; --} -- --.object-vector-studio-v2__transform-summary { -- margin: 0; -- color: var(--tool-starter-text); -- font-size: 0.86rem; -- font-weight: 800; -- line-height: 1.2; -- text-align: center; -- width: 100%; --} -- --.object-vector-studio-v2__object-detail-stack.is-disabled, --.object-vector-studio-v2__shape-panel.is-disabled { -- opacity: 0.58; --} -- --.object-vector-studio-v2__object-detail-stack.is-disabled input, --.object-vector-studio-v2__object-detail-stack.is-disabled select, --.object-vector-studio-v2__object-detail-stack.is-disabled button, --.object-vector-studio-v2__shape-panel.is-disabled input, --.object-vector-studio-v2__shape-panel.is-disabled select, --.object-vector-studio-v2__shape-panel.is-disabled button { -- cursor: not-allowed; --} -- --.object-vector-studio-v2__edit-field input.is-invalid, --.object-vector-studio-v2__transform-control-row input.is-invalid, --.object-vector-studio-v2__scale-input.is-invalid, --.object-vector-studio-v2__polygon-point-axis input.is-invalid { -- border-color: var(--tool-starter-danger); -- box-shadow: 0 0 0 2px rgba(248, 113, 113, 0.22); --} -- --.object-vector-studio-v2 [data-validation-state="invalid"] { -- border-color: var(--tool-starter-danger) !important; -- box-shadow: 0 0 0 2px rgba(248, 113, 113, 0.22); --} -- --.object-vector-studio-v2__shape-actions { -- display: flex; -- flex-wrap: wrap; -- justify-content: center; -- gap: 8px; --} -- --.object-vector-studio-v2__details-actions { -- width: min(720px, 100%); -- min-height: 32px; -- display: flex; -- align-items: center; -- justify-content: center; -- flex-wrap: wrap; -- gap: 8px; -- color: var(--tool-starter-muted); --} -- --.object-vector-studio-v2__preview-footer { -- width: min(720px, 100%); -- color: var(--tool-starter-muted); -- font-size: 0.88rem; -- overflow-wrap: anywhere; --} -- --.object-vector-studio-v2__preview-edit-toolbar { -- width: min(720px, 100%); -- justify-content: center; --} -- --.object-vector-studio-v2__preview-edit-toolbar button { -- min-width: 72px; --} -- --.object-vector-studio-v2__tag-editor, --.object-vector-studio-v2__tag-list { -- display: grid; -- gap: 8px; --} -- --.object-vector-studio-v2__tag-editor { -- grid-template-columns: minmax(0, 1fr) auto; -- align-items: center; -- gap: 5px; --} -- --.object-vector-studio-v2__tag-editor input { -- width: 100%; --} -- --#objectVectorStudioV2AddTagButton { -- width: 77px; -- min-height: 28px; -- font-size: 0.78rem; -- line-height: 1; -- padding: 3px 7px !important; --} -- --.object-vector-studio-v2__tag-list { -- grid-column: 1 / -1; -- grid-template-columns: repeat(auto-fill, 77px); -- justify-content: start; --} -- --.object-vector-studio-v2__tag-list [data-object-tag] { -- width: 77px; -- max-width: 77px; -- min-width: 77px; -- overflow: hidden; -- text-overflow: ellipsis; -- white-space: nowrap; --} -- --.object-vector-studio-v2__detail-label { -- color: var(--tool-starter-muted); -- font-weight: 800; --} -- --.object-vector-studio-v2__detail-value { -- min-width: 0; -- overflow-wrap: anywhere; --} -- --.tool-starter__preview { -- min-height: 240px; -- flex: 1 1 auto; -- display: grid; -- place-items: center; -- border: 1px dashed var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-panel-strong); -- color: var(--tool-starter-muted); -- padding: 16px; -- text-align: center; --} -- --.tool-starter__preview-card { -- width: min(420px, 100%); -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px; -- background: var(--tool-starter-panel-strong); -- padding: 18px; -- text-align: left; --} -- --.tool-starter__preview-card h2, --.tool-starter__preview-card p { -- margin: 0; --} -- --.tool-starter__preview-card p { -- margin-top: 8px; -- color: var(--tool-starter-muted); -- overflow-wrap: anywhere; --} -- --.tool-starter__output, --#statusLog { -- width: 100%; -- min-height: 0; -- height: 100%; -- margin: 0; -- border: 1px solid var(--tool-starter-line); -- border-radius: 8px !important; -- background: var(--tool-starter-panel-strong); -- color: var(--tool-starter-text); -- overflow: auto; -- padding: 12px; -- white-space: pre-wrap; --} -- --#statusLog { -- flex: 1 1 auto; -- resize: none; --} -- --@media (max-width: 900px) { -- .tool-starter.app-shell { -- height: auto; -- min-height: calc(100vh - 137px); -- grid-template-columns: 1fr; -- overflow: visible; -- } -- -- .tool-starter__header { -- min-width: 0; -- } --} -- --html.tools-platform-fullscreen-active, --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"], --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] { -- height: 100vh; -- max-height: 100vh; -- min-height: 100vh; -- overflow-x: hidden; -- overflow-y: auto; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"], --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] { -- display: flex; -- flex-direction: column; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] > .is-collapsible, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] > .is-collapsible { -- flex: 0 0 auto; -- width: 100%; -- margin: 0; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] > .is-collapsible > .is-collapsible__summary[data-tools-platform-summary-active="1"], --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] > .is-collapsible > .is-collapsible__summary[data-tools-platform-summary-active="1"] { -- width: calc(100vw - 24px); -- max-width: calc(100vw - 24px); -- margin: 8px auto; -- overflow: hidden; -- text-overflow: ellipsis; -- white-space: nowrap; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] > .tool-starter__menu, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] > .tool-starter__menu { -- flex: 0 0 auto; -- margin: 8px auto; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] > .tool-starter.app-shell, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] > .tool-starter.app-shell { -- flex: 1 1 0; -- grid-template-columns: 340px minmax(0, 1fr) 360px; -- align-items: stretch; -- gap: 16px; -- height: 0; -- width: 100%; -- max-width: none; -- min-height: 0; -- margin: 0; -- padding: 8px 12px 12px; -- overflow: hidden; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .tool-starter__panel, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .tool-starter__panel { -- max-height: 100%; -- min-height: 0; -- overflow: hidden; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .tool-starter__panel--left, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .tool-starter__panel--left { -- justify-self: start; -- width: 340px; -- max-width: 340px; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .tool-starter__panel--right, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .tool-starter__panel--right { -- justify-self: end; -- width: 360px; -- max-width: 360px; -- overflow-x: hidden; -- overflow-y: auto; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .tool-starter__panel--center, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .tool-starter__panel--center { -- align-self: stretch; -- height: 100%; -- max-height: none; -- min-height: 0; -- overflow-x: hidden; -- overflow-y: auto; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__render-surface, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__render-surface { -- width: min(100%, calc(34vh * 16 / 11)); -- justify-self: center; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__work-area, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__work-area { -- gap: 6px; -- padding: 8px; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__animation-controls, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__animation-controls { -- gap: 6px; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__frame-timeline, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__frame-timeline { -- min-height: 48px; -- padding: 6px; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__frame-tile, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__frame-tile { -- min-width: 96px; -- gap: 2px; -- padding: 4px; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__frame-thumbnail, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .object-vector-studio-v2__frame-thumbnail { -- width: 72px; -- height: 34px; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .accordion-v2.is-open, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .accordion-v2.is-open { -- flex: 1 1 0; -- min-height: 0; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .accordion-v2:not(.is-open), --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .accordion-v2:not(.is-open) { -- flex: 0 0 auto; -- max-height: 48px; -- min-height: 0; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .accordion-v2__content, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .accordion-v2__content { -- min-height: 0; -- overflow: auto; --} -- --body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .tool-starter__accordion--compact.is-open, --html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v2"] .tool-starter__accordion--compact.is-open { -- flex: 0 0 auto; --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/README.md b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/README.md -deleted file mode 100644 -index dae3f2aac..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/README.md -+++ /dev/null -@@ -1,17 +0,0 @@ --# Vector Native Arcade Template -- --This template is the reusable vector-native starting point for future arcade-style games. -- --## Baseline -- --- first-class vector assets are the required visual contract --- strict validation, packaging, runtime, CI, export, and publishing boundaries remain active --- sprite fallback is not part of the runtime baseline --- runtime bootstrap remains compatible with `games/Asteroids/main.js#bootAsteroids` as a starter handoff seam -- --## Structure -- --- `assets/` contains palette, vector, tilemap, and parallax starter content --- `config/` contains starter project configuration --- `runtime/` contains starter runtime bootstrap metadata --- `docs/` contains starter guidance and rollback notes -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/palettes/vector-native-primary.palette.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/palettes/vector-native-primary.palette.json -deleted file mode 100644 -index 61897ec6b..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/palettes/vector-native-primary.palette.json -+++ /dev/null -@@ -1,29 +0,0 @@ --{ -- "$schema": "../../../../../schemas/palette.schema.json", -- "schema": "html-js-gaming.palette", -- "version": 1, -- "name": "Vector Native Primary Palette", -- "source": "custom", -- "swatches": [ -- { -- "symbol": "!", -- "hex": "#05070D", -- "name": "Swatch 1" -- }, -- { -- "symbol": "#", -- "hex": "#E8F0FF", -- "name": "Swatch 2" -- }, -- { -- "symbol": "$", -- "hex": "#6FD3FF", -- "name": "Swatch 3" -- }, -- { -- "symbol": "%", -- "hex": "#FFB347", -- "name": "Swatch 4" -- } -- ] --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.parallax.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.parallax.json -deleted file mode 100644 -index 330bee626..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.parallax.json -+++ /dev/null -@@ -1,5 +0,0 @@ --{ -- "name": "Template Backdrop Layer", -- "role": "backdrop", -- "imageId": "image.template.backdrop" --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.svg b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.svg -deleted file mode 100644 -index 77a8feb76..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.svg -+++ /dev/null -@@ -1,7 +0,0 @@ -- -- -- -- -- -- -- -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-arena.tilemap.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-arena.tilemap.json -deleted file mode 100644 -index 2135cd960..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-arena.tilemap.json -+++ /dev/null -@@ -1,5 +0,0 @@ --{ -- "name": "Vector Native Arcade Arena", -- "states": ["title", "start", "gameplay", "restart"], -- "extensionPoints": ["spawn-patterns", "score-rules", "victory-conditions"] --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-ui.tileset.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-ui.tileset.json -deleted file mode 100644 -index 917f694b3..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-ui.tileset.json -+++ /dev/null -@@ -1,6 +0,0 @@ --{ -- "name": "Template UI Tileset", -- "tileWidth": 8, -- "tileHeight": 8, -- "tiles": ["score-panel", "wave-panel", "status-panel"] --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-hud.vector.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-hud.vector.json -deleted file mode 100644 -index 5b82aa292..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-hud.vector.json -+++ /dev/null -@@ -1,23 +0,0 @@ --{ -- "id": "vector.template.ui.hud", -- "type": "vector", -- "source": { -- "kind": "svg", -- "path": "tools/templates-v2/vector-native-arcade/assets/data/vectors/template-hud.vector.json" -- }, -- "geometry": { -- "viewBox": "0 0 160 32", -- "paths": [ -- { -- "d": "M 4 4 L 156 4 L 156 28 L 4 28 Z" -- }, -- { -- "d": "M 18 10 L 28 10 L 28 22 L 18 22 Z" -- } -- ] -- }, -- "style": { -- "stroke": true, -- "fill": false -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-large.vector.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-large.vector.json -deleted file mode 100644 -index 2ef6a9f51..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-large.vector.json -+++ /dev/null -@@ -1,20 +0,0 @@ --{ -- "id": "vector.template.obstacle.large", -- "type": "vector", -- "source": { -- "kind": "svg", -- "path": "tools/templates-v2/vector-native-arcade/assets/data/vectors/template-obstacle-large.vector.json" -- }, -- "geometry": { -- "viewBox": "-34 -34 68 68", -- "paths": [ -- { -- "d": "M -20 -14 L 0 -24 L 22 -18 L 28 6 L 14 24 L -10 22 L -26 4 Z" -- } -- ] -- }, -- "style": { -- "stroke": true, -- "fill": false -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-small.vector.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-small.vector.json -deleted file mode 100644 -index feaf4fe2c..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-small.vector.json -+++ /dev/null -@@ -1,20 +0,0 @@ --{ -- "id": "vector.template.obstacle.small", -- "type": "vector", -- "source": { -- "kind": "svg", -- "path": "tools/templates-v2/vector-native-arcade/assets/data/vectors/template-obstacle-small.vector.json" -- }, -- "geometry": { -- "viewBox": "-18 -18 36 36", -- "paths": [ -- { -- "d": "M -8 -8 L 2 -12 L 10 -2 L 6 10 L -6 12 L -12 0 Z" -- } -- ] -- }, -- "style": { -- "stroke": true, -- "fill": false -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-player.vector.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-player.vector.json -deleted file mode 100644 -index 572784f53..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-player.vector.json -+++ /dev/null -@@ -1,23 +0,0 @@ --{ -- "id": "vector.template.player", -- "type": "vector", -- "source": { -- "kind": "svg", -- "path": "tools/templates-v2/vector-native-arcade/assets/data/vectors/template-player.vector.json" -- }, -- "geometry": { -- "viewBox": "-20 -20 40 40", -- "paths": [ -- { -- "d": "M 0 -16 L 12 12 L 0 6 L -12 12 Z" -- }, -- { -- "d": "M -4 10 L 0 4 L 4 10" -- } -- ] -- }, -- "style": { -- "stroke": true, -- "fill": false -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-title.vector.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-title.vector.json -deleted file mode 100644 -index 16bb56320..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-title.vector.json -+++ /dev/null -@@ -1,23 +0,0 @@ --{ -- "id": "vector.template.ui.title", -- "type": "vector", -- "source": { -- "kind": "svg", -- "path": "tools/templates-v2/vector-native-arcade/assets/data/vectors/template-title.vector.json" -- }, -- "geometry": { -- "viewBox": "0 0 220 48", -- "paths": [ -- { -- "d": "M 8 40 L 24 8 L 40 40 Z" -- }, -- { -- "d": "M 56 40 L 56 8 L 84 8 L 84 18 L 68 18 L 68 22 L 82 22 L 82 32 L 68 32 L 68 40 Z" -- } -- ] -- }, -- "style": { -- "stroke": true, -- "fill": false -- } --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/config/template.project.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/config/template.project.json -deleted file mode 100644 -index a451abbdd..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/config/template.project.json -+++ /dev/null -@@ -1,6 +0,0 @@ --{ -- "templateId": "vector-native-arcade", -- "schema": "toolbox.template/1", -- "visualContract": "vector-only", -- "runtimeEntry": "games/Asteroids/main.js#bootAsteroids" --} -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/ROLLBACK_NOTES.md b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/ROLLBACK_NOTES.md -deleted file mode 100644 -index c8028670d..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/ROLLBACK_NOTES.md -+++ /dev/null -@@ -1,7 +0,0 @@ --# Rollback Notes -- --Rollback guidance is documentation only. -- --- The vector-native template baseline does not package sprite fallback assets. --- If a project needs a temporary rollback during local experimentation, document that outside the active template runtime contract. --- Do not reintroduce sprite fallback as an active packaged dependency for the template baseline. -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/STARTER_GUIDE.md b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/STARTER_GUIDE.md -deleted file mode 100644 -index 02a428d54..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/STARTER_GUIDE.md -+++ /dev/null -@@ -1,10 +0,0 @@ --# Starter Guide -- --Use this template when a new game should begin with vector-first visuals and strict platform boundaries already in place. -- --## Starter flow -- --1. Replace the starter vectors under `assets/vectors/` with game-specific shapes. --2. Keep the registry IDs stable or update them consistently in the config/runtime files. --3. Validate, package, export, and publish through the accepted shared platform systems. --4. Extend gameplay logic from the runtime handoff seam without changing engine core APIs. -diff --git a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/runtime/bootstrap.runtime.json b/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/runtime/bootstrap.runtime.json -deleted file mode 100644 -index 0348549ef..000000000 ---- a/dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/runtime/bootstrap.runtime.json -+++ /dev/null -@@ -1,6 +0,0 @@ --{ -- "modulePath": "games/Asteroids/main.js", -- "exportName": "bootAsteroids", -- "canvasId": "game", -- "requiredVectorIds": ["vector.template.player", "vector.template.obstacle.large", "vector.template.obstacle.small", "vector.template.ui.title", "vector.template.ui.hud"] --} -diff --git a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md -index 1e65998c6..d9f4bf6ed 100644 ---- a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md -+++ b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md -@@ -117,7 +117,7 @@ Rules: - - ## Current Version/Date - --- Project Instructions Version: 2026.06.28.020 -+- Project Instructions Version: 2026.06.28.021 - - Date: 2026-06-28 - - Owner: OWNER - -diff --git a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md -index f809be6a3..e4f81134e 100644 ---- a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md -+++ b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md -@@ -1,6 +1,6 @@ - # Project Instructions Version - --Current Project Instructions Version: 2026.06.28.020 -+Current Project Instructions Version: 2026.06.28.021 - - Last Updated: 2026-06-28 - -@@ -28,6 +28,7 @@ Last Updated: 2026-06-28 - - Moved PR017-audited browser/www-owned legacy `src/` files into `www/src/` while preserving public `/src/...` browser route compatibility under the `www` web root. - - Moved PR019a-audited active browser/runtime shared contracts and tool schemas into `www/src/shared/` while preserving public `/src/shared/...` browser route compatibility. - - Removed obsolete root `src/` legacy placeholders, stale schema README files, and archived-only debug helpers while preserving active contracts and validation schemas. -+- Removed inactive manifest-era `www/src/tools/common` helpers and archived old Object Vector/Parallax tool samples while preserving currently referenced archived games/samples. - - Codex must read this version file and the latest repository copy of `PROJECT_INSTRUCTIONS.md` before performing work. - - Codex must discard previously remembered Project Instructions and treat the repository copy as authoritative. - - Codex must validate canonical report and ZIP paths, branching model, and legacy path avoidance before work proceeds. -diff --git a/dev/build/ProjectInstructions/PROJECT_STATE.md b/dev/build/ProjectInstructions/PROJECT_STATE.md -index 8bd72875d..87738d6f2 100644 ---- a/dev/build/ProjectInstructions/PROJECT_STATE.md -+++ b/dev/build/ProjectInstructions/PROJECT_STATE.md -@@ -1,7 +1,7 @@ - # Project State - - ```yaml --project_state_version: "2026.06.28.020" -+project_state_version: "2026.06.28.021" - last_updated: "2026-06-28" - current_main_commit: "40de767476d70cadfd1292c916844c2f31b6f185" - repository_status: -@@ -10,9 +10,9 @@ repository_status: - runtime_database: "Postgres" - creator_metadata: "API to Postgres" - creator_assets: "API to R2" --project_instructions_version: "2026.06.28.020" --repository_structure_version: "2026.06.28.020" --canonical_layout_version: "2026.06.28.020" -+project_instructions_version: "2026.06.28.021" -+repository_structure_version: "2026.06.28.021" -+canonical_layout_version: "2026.06.28.021" - active_teams: - - "Owner" - - "Alfa" -@@ -20,8 +20,8 @@ active_teams: - - "Charlie" - - "Delta" - - "Golf" --latest_owner_pr: "PR_26180_OWNER_020-src-legacy-teardown" --latest_structure_pr: "PR_26180_OWNER_020-src-legacy-teardown" -+latest_owner_pr: "PR_26180_OWNER_021-archive-legacy-games-samples-teardown" -+latest_structure_pr: "PR_26180_OWNER_021-archive-legacy-games-samples-teardown" - valid_top_level_folders: - - "api/" - - "deploy/" -@@ -42,5 +42,6 @@ valid_dev_folders: - known_technical_debt: - - "Remaining API/server-owned contracts, validation-only schemas, and project data store modules remain under src/shared/ until scoped follow-up PRs move them to api/ or dev/." - - "Remaining actively validated contracts, validation schemas, and project data store modules remain under src/shared/ until scoped follow-up PRs move them to api/ or dev/." -+ - "Archived games and samples under dev/archive/v1-v2 remain until active runtime/test/script references are migrated away." - - "repository_directory_standard.md and codex_project_instructions_startup.md are superseded pointers retained for compatibility." - ``` -diff --git a/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md b/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md -index cfbad05b6..15d68a7da 100644 ---- a/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md -+++ b/dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md -@@ -40,7 +40,7 @@ The backlog is the authoritative source for determining the next logical PRs. - - Product Area: Repository Architecture Simplification - - Status: Active - - Percent Complete: 99% --- Active PR: PR_26180_OWNER_020-src-legacy-teardown -+- Active PR: PR_26180_OWNER_021-archive-legacy-games-samples-teardown - - Next Milestone: Move remaining API/server-owned source to api/ - - Source / Reference: `dev/build/ProjectInstructions/repository/repository_layout_architecture_plan.md`; `dev/build/ProjectInstructions/repository/www_migration_map.md` - -diff --git a/dev/build/ProjectInstructions/repository/canonical_repository_structure.md b/dev/build/ProjectInstructions/repository/canonical_repository_structure.md -index d60ba6140..43cccd10f 100644 ---- a/dev/build/ProjectInstructions/repository/canonical_repository_structure.md -+++ b/dev/build/ProjectInstructions/repository/canonical_repository_structure.md -@@ -85,7 +85,6 @@ Legacy transition buckets: - - www/src/shared/ - - www/src/shared/contracts/ - - www/src/shared/schemas/ --- www/src/tools/ - - www/src/dev-runtime/admin/ - - src/shared/contracts/ - - src/shared/schemas/ -diff --git a/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_branch-validation.md b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_branch-validation.md +diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md new file mode 100644 -index 000000000..439a027f3 +index 000000000..1ef1dc138 --- /dev/null -+++ b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_branch-validation.md -@@ -0,0 +1,13 @@ -+# PR_26180_OWNER_021-archive-legacy-games-samples-teardown Branch Validation -+ -+| Check | Status | Notes | -+|-------|--------|-------| -+| Current branch | PASS | `PR_26180_OWNER_021-archive-legacy-games-samples-teardown` | -+| Stack base | PASS | Created from `PR_26180_OWNER_020-src-legacy-teardown`. | -+| Worktree scope | PASS | Deletes limited to inactive manifest-era helpers and archived old tool/sample folders, plus governance/report updates. | -+| Runtime/API/UI/database changes | PASS | No active implementation behavior changed. | -+| Protected generated workspace | PASS | `dev/workspace/generated/tool-images/**` untouched. | -+ -+## Branch Decision -+ -+PASS. Branch is valid for the stacked Owner Repository Architecture Simplification workstream. -diff --git a/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_manual-validation-notes.md b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_manual-validation-notes.md ++++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md +@@ -0,0 +1,8 @@ ++# PR_26180_OWNER_022-delete-remaining-root-src-files Branch Validation ++ ++- Expected branch: `PR_26180_OWNER_022-delete-remaining-root-src-files` ++- Actual branch: `PR_26180_OWNER_022-delete-remaining-root-src-files` ++- Branch validation: PASS ++- Worktree at branch start: clean after removal of previous PR022 hard-stop artifacts ++- Current action: HARD STOP report artifacts only ++- Source files modified: none +diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md new file mode 100644 -index 000000000..0eb228746 +index 000000000..739995467 --- /dev/null -+++ b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_manual-validation-notes.md -@@ -0,0 +1,19 @@ -+# PR_26180_OWNER_021-archive-legacy-games-samples-teardown Manual Validation Notes -+ -+## Manual Review -+ -+- Confirmed current branch is `PR_26180_OWNER_021-archive-legacy-games-samples-teardown`. -+- Confirmed `www/src/tools/common` had no active imports beyond shared-extraction baseline entries. -+- Confirmed archived `old_object-vector-studio-v2` and `old_Parallax Scene Studio` had no active runtime/test/script references outside archive/report/history. -+- Confirmed active archive game/sample paths remain referenced by current runtime/test/dev tooling and were not deleted. -+- Confirmed `dev/workspace/generated/tool-images/**` was not touched. -+ -+## Product Impact -+ -+No product behavior, public URL behavior, API behavior, database behavior, or active UI behavior was changed. -+ -+## ZIP -+ -+Repo-structured ZIP path: -+ -+`dev/workspace/zips/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_delta.zip` -diff --git a/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_report.md b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_report.md ++++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md +@@ -0,0 +1,13 @@ ++# PR_26180_OWNER_022-delete-remaining-root-src-files Manual Validation Notes ++ ++- Reviewed current Project Instructions version `2026.06.28.021`. ++- Confirmed canonical report path is `dev/reports/` and ZIP path is `dev/workspace/zips/`. ++- Confirmed current branch is `PR_26180_OWNER_022-delete-remaining-root-src-files`. ++- Confirmed 65 tracked root `src` files remain before attempted deletion. ++- Confirmed active references still point to root `src/shared` from dev tests, validation scripts, and tooling baselines. ++- No runtime, UI, API, database, product, `www/`, `api/`, or source files were modified. ++- Manual recommendation: create a follow-up migration PR that updates the active tests/scripts/tooling baselines away from root `src/shared` before deleting the remaining files. ++ ++- Final `git diff --check` and `git diff --cached --check`: PASS. ++- Final `npm run validate:canonical-structure`: PASS. ++- Targeted root `src` scan still reports active references and remains the hard-stop blocker. +diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md new file mode 100644 -index 000000000..36dcb6f94 +index 000000000..5f94761be --- /dev/null -+++ b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_report.md -@@ -0,0 +1,40 @@ -+# PR_26180_OWNER_021-archive-legacy-games-samples-teardown Report -+ -+## Executive Summary -+ -+PR021 removes legacy manifest-era and archived tool sample surfaces that have no active `www/`, `api/`, `dev/scripts`, `dev/tests`, `dev/tools`, package, or CI references. -+ -+Deleted groups: -+ -+- `www/src/tools/common/` (2 files): inactive manifest-era browser helpers. -+- `dev/archive/v1-v2/tools/old_object-vector-studio-v2/` (35 files): archived old Object Vector tool implementation superseded by current Object Vector runtime/schema/tests under `www/` and `dev/tests/`. -+- `dev/archive/v1-v2/tools/old_Parallax Scene Studio/` (22 files): archived old Parallax tool/sample folder with no active runtime/test/script references. -+ -+Preserved groups: -+ -+- `dev/archive/v1-v2/games/`: preserved because current `www/`, `dev/scripts`, and `dev/tests` still reference archived game paths. -+- `dev/archive/v1-v2/samples/`: preserved because current dev tooling and validation baselines still reference archived sample paths. -+- `dev/archive/v1-v2/tools/SpriteEditor_old_keep/`: preserved because governance/reference docs still mark it as keep-policy material. -+- `dev/workspace/generated/tool-images/**`: untouched. -+ -+## Evidence Review -+ -+| Candidate | Status | Decision | -+|-----------|--------|----------| -+| `old_games/**` | No exact tracked `old_games` folder found. | No deletion required. | -+| `old_samples/**` | Only exact tracked `old_samples` match is under `SpriteEditor_old_keep`. | Preserved due explicit keep-policy references. | -+| `dev/archive/v1-v2/games/**` | Active references remain in `www/src/shared/toolbox/*`, dev scripts, and dev tests. | Preserved; deletion blocked until references migrate. | -+| `dev/archive/v1-v2/samples/**` | Active references remain in dev tools, dev scripts, dev tests, and validation baselines. | Preserved; deletion blocked until references migrate. | -+| `www/src/tools/**` | `www/src/tools/common` had no active imports beyond extraction baseline entries. | Deleted. | -+| Old Object Vector archive | No active runtime/test/script references outside archive/report/history. | Deleted. | -+| Old Parallax archive samples | No active runtime/test/script references outside archive/report/history. | Deleted. | -+ -+## Runtime Boundary -+ -+No active `www/`, `api/`, `dev/scripts`, `dev/tests`, package, or GitHub workflow reference remains to the deleted PR021 paths. -+ -+The PR does not change product behavior, API behavior, database behavior, or active UI behavior. -+ -+## Follow-Up -+ -+Future PRs must migrate active archive game/sample references before deleting `dev/archive/v1-v2/games/**` or `dev/archive/v1-v2/samples/**`. -diff --git a/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_requirement-checklist.md b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_requirement-checklist.md ++++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md +@@ -0,0 +1,66 @@ ++# PR_26180_OWNER_022-delete-remaining-root-src-files Hard Stop Report ++ ++## Outcome ++ ++HARD STOP. No root `src/` files were deleted, archived, or moved. ++ ++## Reason ++ ++The required root `src/` reference audit found active current validation/test/tooling references that still require root `src/shared` files. The task explicitly requires a hard stop if runtime, test, package, or CI references still require root `src`. ++ ++## Branch ++ ++- Branch: `PR_26180_OWNER_022-delete-remaining-root-src-files` ++- HEAD: `2e38e9e31cf0b9a2718681ea95a6b8054f4a7a93` ++- Worktree before reports: clean ++- Base requested: `PR_26180_OWNER_021-archive-legacy-games-samples-teardown` ++ ++## Root Src Count ++ ++- Before: 65 tracked files under `src/` ++- After: 65 tracked files under `src/` ++- Active reference scan matches: 1284 ++ ++## High-Signal Evidence ++ ++- `dev\tests\shared\InMemoryProjectDataStore.test.mjs:14:} from "../../../src/shared/contracts/projectDataStoreContract.js";` ++- `dev\tests\tools\ToolManifestBoundary.test.mjs:11: "www/src/shared/schemas/tools/palette-browser.schema.json",` ++- `dev\scripts\validate-json-contracts.mjs:499: return `www/src/shared/schemas/tools/${normalized}.schema.json`;` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3100: "file": "www/src/shared/color/color.js",` ++- `dev\tools\toolbox-dev\checkBoundaryHardeningGuard.mjs:16: ['src/shared/', 'shared'],` ++- `dev\tests\shared\ContractIndexValidation.test.mjs:59: const actualRootContractFiles = fs.readdirSync(resolvePath("src/shared/contracts"))` ++- `dev\tests\validation\samples.shared.boundaries.report.json:2: "$schema": "../../../src/shared/schemas/samples/sample.tool-payload.schema.json",` ++- `dev\scripts\validate-json-contracts.mjs:653: const gameManifestSchemaPath = "src/shared/schemas/game.manifest.schema.json";` ++- `dev\scripts\validate-json-contracts.mjs:665: errors.push("src/shared/schemas/game.manifest.schema.json not found");` ++- `dev\scripts\validate-json-contracts.mjs:706: if (!schemaPath.startsWith("www/src/shared/schemas/tools/") || !schemaPath.endsWith(".schema.json")) {` ++- `dev\tests\validation\samples.runtime.validation.report.json:2: "$schema": "../../../src/shared/schemas/samples/sample.tool-payload.schema.json",` ++- `dev\tests\validation\samples.curriculum.validation.json:2: "$schema": "../../../src/shared/schemas/samples/sample.tool-payload.schema.json",` ++- `dev\tests\fixtures\workspace-v2\uat.manifest.json:2: "$schema": "src/shared/schemas/game.manifest.schema.json",` ++- `dev\tests\fixtures\workspace-v2\uat.manifest.json:17: "$schema": "src/shared/schemas/tools/palette-manager-v2.schema.json",` ++- `dev\tests\fixtures\workspace-v2\uat.manifest.json:59: "$schema": "src/shared/schemas/tools/asset-manager-v2.schema.json",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3105: "file": "src/shared/contracts/auditEventContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3110: "file": "src/shared/contracts/backupSnapshotContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3115: "file": "src/shared/contracts/collaborationRoleContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3120: "file": "src/shared/contracts/creatorProfileContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3125: "file": "src/shared/contracts/downloadGrantContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3130: "file": "src/shared/contracts/entitlementContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3135: "file": "src/shared/contracts/installReceiptContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3140: "file": "src/shared/contracts/libraryItemContract.js",` ++- `dev\tools\toolbox-dev\checkSharedExtractionGuard.baseline.json:3145: "file": "src/shared/contracts/marketplaceListingContract.js",` ++ ++## Targeted Validation Evidence ++ ++- `git diff --check`: PASS before report generation. ++- `npm run validate:canonical-structure`: PASS. ++- `node ./dev/scripts/run-node-test-files.mjs dev/tests/shared/InMemoryProjectDataStore.test.mjs dev/tests/tools/ToolManifestBoundary.test.mjs dev/tests/shared/ContractIndexValidation.test.mjs`: FAILED. ++ - `InMemoryProjectDataStore.test.mjs`: PASS. ++ - `ToolManifestBoundary.test.mjs`: PASS. ++ - `ContractIndexValidation.test.mjs`: FAILS on missing legacy `src/shared/contracts/replayContracts.js`. ++ ++## Decision ++ ++The PR stopped before deleting or archiving remaining root `src` files. The next scoped PR should first migrate or retire the active validation/test/tooling dependencies that still point at root `src/shared`, then retry root `src` deletion only after `git ls-files -- src` and active reference scans prove it is safe. ++ ++## Files Changed By This Outcome ++ ++Reports only. No runtime, API, UI, database, or product files were modified. +diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md new file mode 100644 -index 000000000..bf1ac59d1 +index 000000000..5aa376d1d --- /dev/null -+++ b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_requirement-checklist.md -@@ -0,0 +1,17 @@ -+# PR_26180_OWNER_021-archive-legacy-games-samples-teardown Requirement Checklist -+ -+| Requirement | Status | Evidence | -+|-------------|--------|----------| -+| Base from PR020 | PASS | Branch created from `PR_26180_OWNER_020-src-legacy-teardown`. | -+| Delete archived `old_games/**` if only archive/legacy | PASS | No exact tracked `old_games` path exists. Active archive games were preserved due current references. | -+| Delete archived `old_samples/**` if only archive/legacy | PASS WITH PRESERVATION | Only exact `old_samples` file is under `SpriteEditor_old_keep`, which has active keep-policy governance references. Old Parallax sample archive was deleted. | -+| Delete/archive old manifest-era code | PASS | Removed inactive `www/src/tools/common` manifest-era helpers. | -+| Remove references to non-existent `games/**` | PASS | No deleted active game path references introduced; active archive-game references remain intentionally preserved. | -+| Remove references to `samples/**` unless active use is proven | PASS | Active sample references remain only where current validation/dev tools prove use. | -+| Audit `www/src/tools/**` | PASS | Deleted inactive `www/src/tools/common`; `www/src/tools` no longer exists in working tree. | -+| Audit old Object Vector files | PASS | Deleted archived `old_object-vector-studio-v2`; current Object Vector runtime/schema/test paths remain. | -+| Preserve active `www/`, `api/`, `dev/tests`, Project Instructions behavior | PASS | No active references remain to deleted PR021 paths. | -+| Do not touch `dev/workspace/generated/tool-images/**` | PASS | Protected generated tool-image workspace untouched. | -+| Hard stop before deleting files used by active runtime/API/validation/tests | PASS | Active-reference scan passed before deletion scope was finalized. | -+| Required reports under `dev/reports/` | PASS | PR-specific report, checklist, validation report, manual notes, branch validation, diff, and changed-file report generated. | -+| Required ZIP under `dev/workspace/zips/` | PASS | Repo-structured delta ZIP generated. | -diff --git a/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_validation-report.md b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_validation-report.md ++++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md +@@ -0,0 +1,13 @@ ++# PR_26180_OWNER_022-delete-remaining-root-src-files Requirement Checklist ++ ++| Requirement | Status | Notes | ++|---|---:|---| ++| Work only on remaining tracked files under root `src/` | PASS | Audit inspected tracked `src/` files only. | ++| Do not touch `dev/workspace/generated/tool-images/**` | PASS | Protected developer workspace path was not touched. | ++| Preserve no root `src` file unless active refs prove it required | HARD STOP | Active refs still prove current validation/test dependencies require root `src`. | ++| Delete definitely obsolete files instead of archiving | BLOCKED | Deletion would occur while active test/tooling refs remain. | ++| Required reference audit before delete/archive | PASS | Scan found 1284 matches. | ++| Stop on active runtime/test/package/CI refs requiring root `src` | PASS | Hard stop triggered. | ++| Report `git ls-files -- src` count before and after | PASS | Before 65, after 65. | ++| Produce reports under `dev/reports/` | PASS | This checklist and companion reports were generated. | ++| Produce ZIP under `dev/workspace/zips/` | PASS | Hard-stop ZIP generated for this outcome. | +diff --git a/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md new file mode 100644 -index 000000000..3c6ca8111 +index 000000000..8731ff895 --- /dev/null -+++ b/dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_validation-report.md -@@ -0,0 +1,34 @@ -+# PR_26180_OWNER_021-archive-legacy-games-samples-teardown Validation Report -+ -+## Required Validation -+ -+| Command | Status | Notes | -+|---------|--------|-------| -+| `git diff --check` | PASS | No whitespace errors. | -+| `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed with 0 blocking violations. | -+| `node dev/tools/toolbox-dev/checkSharedExtractionGuard.mjs` | PASS | Baseline updated for deleted `www/src/tools/common` helper entries. | -+| Deleted path active-reference scan | PASS | No active runtime/script/test/package/CI references to deleted PR021 paths. | -+| `node ./dev/scripts/run-node-test-files.mjs dev/tests/tools/ToolManifestBoundary.test.mjs` | PASS | Tool manifest boundary test passed. | -+ -+## Exploratory Broader Test Notes -+ -+The following broader probes were run and failed for pre-existing stack issues outside PR021 deletion scope: -+ -+- `dev/tests/shared/GetStateVariantClassification.test.mjs`: existing classifier returns `runtime` for `src/shared/toolbox/test.js`; test expects `tool`. -+- `dev/tests/core/BackgroundImageAndFullscreenBezel.test.mjs`: existing fullscreen target assertion mismatch unrelated to deleted files. -+- Object Vector broader test lane: existing `www/toolbox/toolRegistry.js` Node import resolves `../../src/shared/toolbox/tool-metadata-inventory.js`, while the file exists under `www/src/shared/toolbox/tool-metadata-inventory.js`. -+ -+No deleted PR021 path appears in those failures. -+ -+## Active Reference Scan -+ -+Deleted path scan covered: -+ -+- `old_Parallax Scene Studio` -+- `old_object-vector-studio-v2` -+- `www/src/tools/common` -+- `GameManifestLoader` -+- `WorkspaceDirtyNotifier` -+- `notifyWorkspaceToolDirty` -+ -+Result: PASS outside allowed Project Instructions/report references. -diff --git a/dev/tools/toolbox-dev/checkSharedExtractionGuard.baseline.json b/dev/tools/toolbox-dev/checkSharedExtractionGuard.baseline.json -index 3572073e6..4170339f5 100644 ---- a/dev/tools/toolbox-dev/checkSharedExtractionGuard.baseline.json -+++ b/dev/tools/toolbox-dev/checkSharedExtractionGuard.baseline.json -@@ -3756,46 +3756,6 @@ - "type": "inline-helper-clone", - "match": "rule:number-is-finite-usage" - }, -- { -- "file": "www/src/tools/common/GameManifestLoader.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-1" -- }, -- { -- "file": "www/src/tools/common/GameManifestLoader.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-1" -- }, -- { -- "file": "www/src/tools/common/GameManifestLoader.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-2" -- }, -- { -- "file": "www/src/tools/common/GameManifestLoader.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-2" -- }, -- { -- "file": "www/src/tools/common/WorkspaceDirtyNotifier.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-1" -- }, -- { -- "file": "www/src/tools/common/WorkspaceDirtyNotifier.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-1" -- }, -- { -- "file": "www/src/tools/common/WorkspaceDirtyNotifier.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-2" -- }, -- { -- "file": "www/src/tools/common/WorkspaceDirtyNotifier.js", -- "type": "direct-shared-relative-import", -- "match": "rule:relative-shared-depth-2" -- }, - { - "file": "toolbox/tools-page-accordions.js", - "type": "inline-helper-clone", -diff --git a/www/src/tools/common/GameManifestLoader.js b/www/src/tools/common/GameManifestLoader.js -deleted file mode 100644 -index 335d1fc78..000000000 ---- a/www/src/tools/common/GameManifestLoader.js -+++ /dev/null -@@ -1,102 +0,0 @@ --import { asPositiveInteger } from "../../shared/number/numbers.js"; --import { readFileText } from '../../engine/persistence/FilePersistenceService.js'; --import SessionStorageService from '../../engine/persistence/SessionStorageService.js'; --import { isRecord } from "../../shared/object/objects.js"; -- --export { isRecord }; -- --function parseJson(rawValue, sourceLabel) { -- try { -- const parsed = JSON.parse(rawValue); -- return isRecord(parsed) -- ? { ok: true, manifest: parsed, sourceLabel } -- : { ok: false, message: `${sourceLabel} must contain a JSON object.` }; -- } catch (error) { -- return { ok: false, message: `${sourceLabel} contains invalid JSON: ${error.message}` }; -- } --} -- --export function resolveManifestScreenDimensions(manifest) { -- const screen = isRecord(manifest?.screen) ? manifest.screen : null; -- const width = asPositiveInteger(screen?.width, 0); -- const height = asPositiveInteger(screen?.height, 0); -- if (!width || !height) { -- return { -- ok: false, -- message: "Manifest screen dimensions are required at root.screen.width and root.screen.height." -- }; -- } -- return { ok: true, width, height }; --} -- --export class GameManifestLoader { -- constructor({ -- fetchRef = null, -- pathParams = ["manifestPath", "gameManifestPath"], -- sessionStorageRef = null, -- windowRef = window -- } = {}) { -- this.fetch = fetchRef || windowRef.fetch?.bind(windowRef) || null; -- this.pathParams = pathParams; -- this.sessionStorage = new SessionStorageService(sessionStorageRef || windowRef.sessionStorage || null); -- this.window = windowRef; -- } -- -- isWorkspaceLaunch(params = new URLSearchParams(this.window.location.search || "")) { -- return params.get("launch") === "workspace" -- && params.get("fromTool") === "workspace-manager-v2" -- && Boolean(params.get("hostContextId")); -- } -- -- async loadInitialManifest() { -- const params = new URLSearchParams(this.window.location.search || ""); -- if (this.isWorkspaceLaunch(params)) { -- return this.loadFromWorkspaceContext(params.get("hostContextId") || ""); -- } -- const manifestPath = this.pathParams.map((key) => params.get(key) || "").find(Boolean) || ""; -- if (manifestPath) { -- return this.loadFromPath(manifestPath, "URL manifest path"); -- } -- return { ok: false, skipped: true }; -- } -- -- loadFromWorkspaceContext(hostContextId) { -- if (!hostContextId) { -- return { ok: false, message: "Workspace launch did not include hostContextId." }; -- } -- const rawValue = this.sessionStorage.getItem(hostContextId, "") || ""; -- if (!rawValue) { -- return { ok: false, message: `Workspace manifest context was not found in sessionStorage: ${hostContextId}.` }; -- } -- return parseJson(rawValue, `workspace:${hostContextId}`); -- } -- -- async loadFromPath(manifestPath, sourceLabel = manifestPath) { -- if (typeof this.fetch !== "function") { -- return { ok: false, message: "Fetch API is unavailable for manifest loading." }; -- } -- try { -- const response = await this.fetch(manifestPath, { cache: "no-store" }); -- if (!response.ok) { -- return { ok: false, message: `Manifest load failed from ${manifestPath}: ${response.status} ${response.statusText}` }; -- } -- const manifest = await response.json(); -- return isRecord(manifest) -- ? { ok: true, manifest, sourceLabel: sourceLabel || manifestPath } -- : { ok: false, message: `${sourceLabel || manifestPath} must contain a JSON object.` }; -- } catch (error) { -- return { ok: false, message: `Manifest load failed from ${manifestPath}: ${error.message}` }; -- } -- } -- -- async loadFromFile(file) { -- if (!file) { -- return { ok: false, skipped: true }; -- } -- try { -- return parseJson(await readFileText(file), file.name || "selected manifest file"); -- } catch (error) { -- return { ok: false, message: `Manifest file could not be read: ${error.message}` }; -- } -- } --} -diff --git a/www/src/tools/common/WorkspaceDirtyNotifier.js b/www/src/tools/common/WorkspaceDirtyNotifier.js -deleted file mode 100644 -index da5e319a5..000000000 ---- a/www/src/tools/common/WorkspaceDirtyNotifier.js -+++ /dev/null -@@ -1,97 +0,0 @@ --import { deepClone } from "../../shared/json/clone.js"; --import { isPlainObject } from "../../shared/object/objects.js"; -- --const WORKSPACE_TOOL_SESSION_KEY_PREFIX = "workspace.tools."; -- --function workspaceToolSessionKey(toolId) { -- return `${WORKSPACE_TOOL_SESSION_KEY_PREFIX}${toolId}`; --} -- --function readJson(sessionStorageRef, key) { -- const rawValue = sessionStorageRef?.getItem(key); -- if (!rawValue) { -- return { ok: false, message: `${key} was not found in sessionStorage.` }; -- } -- try { -- const value = JSON.parse(rawValue); -- return isPlainObject(value) -- ? { ok: true, value } -- : { ok: false, message: `${key} must contain a JSON object.` }; -- } catch (error) { -- return { ok: false, message: `${key} contains invalid JSON: ${error.message}` }; -- } --} -- --function changedKeysForDirty(nextChangedKeys, previousDirty = {}) { -- return Array.from(new Set([ -- ...(Array.isArray(previousDirty.changedKeys) ? previousDirty.changedKeys : []), -- ...(Array.isArray(nextChangedKeys) ? nextChangedKeys : ["data"]) -- ] -- .map((key) => String(key || "").trim()) -- .filter(Boolean))); --} -- --export function notifyWorkspaceToolDirty({ -- changedKeys = ["data"], -- payload, -- reason = "tool-payload-updated", -- toolId, -- windowRef = window --} = {}) { -- const params = new URLSearchParams(windowRef.location?.search || ""); -- if (params.get("launch") !== "workspace" || params.get("fromTool") !== "workspace-manager-v2") { -- return { ok: true, skipped: true, message: "Not launched from Workspace Manager V2." }; -- } -- -- const normalizedToolId = String(toolId || "").trim(); -- const hostContextId = String(params.get("hostContextId") || "").trim(); -- if (!normalizedToolId) { -- return { ok: false, message: "Workspace dirty notification requires a tool id." }; -- } -- if (!hostContextId) { -- return { ok: false, message: "Workspace dirty notification requires hostContextId." }; -- } -- -- const sessionStorageRef = windowRef.sessionStorage; -- const sessionKey = workspaceToolSessionKey(normalizedToolId); -- const sessionResult = readJson(sessionStorageRef, sessionKey); -- if (!sessionResult.ok) { -- return sessionResult; -- } -- const session = sessionResult.value; -- if (!isPlainObject(session.schema) -- || !isPlainObject(session.workspace) -- || !Object.prototype.hasOwnProperty.call(session, "data") -- || !isPlainObject(session.dirty)) { -- return { ok: false, message: `${sessionKey} must use the normalized schema/workspace/data/dirty object shape.` }; -- } -- if (session.workspace.source !== "workspace-manager-v2" || session.workspace.toolId !== normalizedToolId) { -- return { ok: false, message: `${sessionKey}.workspace must be for ${normalizedToolId}.` }; -- } -- -- const nextData = deepClone(payload); -- if (JSON.stringify(session.data) === JSON.stringify(nextData)) { -- return { ok: true, changed: false, key: sessionKey, session: deepClone(session) }; -- } -- -- const nextSession = { -- ...session, -- data: nextData, -- dirty: { -- isDirty: true, -- reason, -- changedAt: new Date().toISOString(), -- changedKeys: changedKeysForDirty(changedKeys, session.dirty) -- } -- }; -- sessionStorageRef.setItem(sessionKey, JSON.stringify(nextSession)); -- -- const contextResult = readJson(sessionStorageRef, hostContextId); -- if (contextResult.ok && isPlainObject(contextResult.value.tools)) { -- const nextContext = deepClone(contextResult.value); -- nextContext.tools[normalizedToolId] = deepClone(nextData); -- sessionStorageRef.setItem(hostContextId, JSON.stringify(nextContext)); -- } -- -- return { ok: true, changed: true, key: sessionKey, session: deepClone(nextSession) }; --} ++++ b/dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md +@@ -0,0 +1,14 @@ ++# PR_26180_OWNER_022-delete-remaining-root-src-files Validation Report ++ ++## Commands ++ ++| Command | Result | Notes | ++|---|---:|---| ++| `git diff --check` and `git diff --cached --check` | PASS | Ran after report generation against unstaged and staged changes. | ++| `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed after report generation. | ++| Targeted root `src` reference scan | HARD STOP | Found 1284 active matches outside archive/report/workspace paths. | ++| `node ./dev/scripts/run-node-test-files.mjs dev/tests/shared/InMemoryProjectDataStore.test.mjs dev/tests/tools/ToolManifestBoundary.test.mjs dev/tests/shared/ContractIndexValidation.test.mjs` | FAIL | First two tests passed; `ContractIndexValidation` failed on missing legacy `replayContracts.js`. | ++ ++## Validation Decision ++ ++No source deletion/archive was performed because the hard-stop condition was met before implementation. +diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt +index 843a15fdf..769ed9684 100644 +--- a/dev/reports/codex_changed_files.txt ++++ b/dev/reports/codex_changed_files.txt +@@ -1,72 +1,8 @@ +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/README.md +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/how_to_use.html +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/index.html +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/main.js +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/parallaxEditor.css +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/cloud-bands.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/foreground-occluder.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/hero-proxy.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-short.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge-tall.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/mountain-ridge.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/images/sky-gradient.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_bushes_foreground.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_clouds.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_hills.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_leaf_overlay.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_learning_level_parallax.json +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_mountains.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/mario_style_sky.svg +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/parallax_sample.json +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/sample-manifest.json +-D dev/archive/v1-v2/tools/old_Parallax Scene Studio/samples/tilemap_parallax_sample.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/README.md +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/BATCH_GUARDRAIL_CONTRACT.md +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/CONTROL_SERVICE_CONTRACTS.md +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/docs/OBJECT_VECTOR_STUDIO_V2_REQUIREMENTS.md +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/how_to_use.html +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/index.html +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/ToolStarterApp.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/bootstrap.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/AccordionSection.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ActionNavControl.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/InspectorControl.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/PreviewPanelControl.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/SourceInputControl.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/StatusLogControl.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/controls/ToolStarterShellControl.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ObjectVectorStudioV2SchemaService.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/js/services/ToolStateSerializer.js +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/playwright.config.mjs +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/possible.future.adds.txt +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/styles/toolStarter.css +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/README.md +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/palettes/vector-native-primary.palette.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.parallax.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/parallax/template-backdrop.svg +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-arena.tilemap.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/tilemaps/template-ui.tileset.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-hud.vector.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-large.vector.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-obstacle-small.vector.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-player.vector.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/assets/data/vectors/template-title.vector.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/config/template.project.json +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/ROLLBACK_NOTES.md +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/docs/STARTER_GUIDE.md +-D dev/archive/v1-v2/tools/old_object-vector-studio-v2/vector-native-arcade/runtime/bootstrap.runtime.json +-M dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md +-M dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS_VERSION.md +-M dev/build/ProjectInstructions/PROJECT_STATE.md +-M dev/build/ProjectInstructions/backlog/BACKLOG_MASTER.md +-M dev/build/ProjectInstructions/repository/canonical_repository_structure.md +-A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_branch-validation.md +-A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_manual-validation-notes.md +-A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_report.md +-A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_requirement-checklist.md +-A dev/reports/PR_26180_OWNER_021-archive-legacy-games-samples-teardown_validation-report.md +-M dev/tools/toolbox-dev/checkSharedExtractionGuard.baseline.json +-D www/src/tools/common/GameManifestLoader.js +-D www/src/tools/common/WorkspaceDirtyNotifier.js +-M dev/reports/codex_review.diff +-M dev/reports/codex_changed_files.txt ++dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_branch-validation.md ++dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_manual-validation-notes.md ++dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_report.md ++dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_requirement-checklist.md ++dev/reports/PR_26180_OWNER_022-delete-remaining-root-src-files_validation-report.md ++dev/reports/codex_changed_files.txt ++dev/reports/codex_review.diff ++dev/workspace/zips/PR_26180_OWNER_022-delete-remaining-root-src-files_HARD_STOP.zip diff --git a/dev/workspace/zips/PR_26180_OWNER_022-delete-remaining-root-src-files_HARD_STOP.zip b/dev/workspace/zips/PR_26180_OWNER_022-delete-remaining-root-src-files_HARD_STOP.zip new file mode 100644 index 000000000..d89da8d11 Binary files /dev/null and b/dev/workspace/zips/PR_26180_OWNER_022-delete-remaining-root-src-files_HARD_STOP.zip differ