cozyclay 3.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/INSTALL_FOR_AGENTS.md +4 -4
- package/README.md +4 -2
- package/bin/agent/agent-failure-codes.mjs +34 -8
- package/bin/agent/agent-routes.mjs +6 -6
- package/bin/agent/agent-runner.mjs +16 -13
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -1
- package/bin/agent/studio-prompt.mjs +2 -2
- package/bin/agent/supervisor-followup.mjs +34 -0
- package/bin/agent/supervisor.mjs +6 -6
- package/bin/cozyclay.mjs +23 -60
- package/bin/star-prompt.mjs +15 -14
- package/bin/studio-routes.mjs +6 -3
- package/bin/telemetry-state.mjs +5 -7
- package/bin/update-check.mjs +61 -7
- package/bin/update-install.mjs +16 -3
- package/bin/update-restart.mjs +95 -0
- package/bin/update-routes.mjs +25 -3
- package/dist/app/index.html +13 -7
- package/dist/assets/TaskBoard-tgO0MNKL.js +1 -0
- package/dist/assets/analytics-CaTUuLPM.js +1 -0
- package/dist/assets/app-CnDkB5WO.css +1 -0
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/{compile-worker-DRWRa9pr.js → compile-worker-D1pl6G7x.js} +5 -5
- package/dist/assets/landing-2Fy7DU41.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BxaM0Xwl.js +1 -0
- package/dist/assets/scene-asset-sync-CLGNbEBq.js +13 -0
- package/dist/assets/task-board-UVGTZLRF.js +15 -0
- package/dist/assets/workflow-CSiBlX1s.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +4 -1
- package/dist/privacy/index.html +9 -6
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +4 -4
- package/mcp/tool-handlers.mjs +14 -16
- package/package.json +2 -1
- package/src/App.jsx +37 -70
- package/src/analytics-controls.js +25 -3
- package/src/analytics-strict.js +16 -4
- package/src/analytics.js +32 -26
- package/src/app-version.js +13 -0
- package/src/asset-drop.js +39 -0
- package/src/asset-pane.css +34 -0
- package/src/asset-pane.jsx +4 -35
- package/src/bus-tasks.js +1 -1
- package/src/character-kind.js +8 -6
- package/src/click-friction.js +5 -4
- package/src/command-bus-telemetry.js +8 -7
- package/src/command-bus.js +2 -16
- package/src/commands/cast.js +2 -1
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- package/src/commands/project.js +3 -3
- package/src/commands/sculpt-compare.js +53 -0
- package/src/content-tile.jsx +36 -0
- package/src/domains/cast.js +7 -6
- package/src/domains/motion.js +6 -5
- package/src/domains/scenes.js +41 -31
- package/src/execution-telemetry.js +7 -3
- package/src/export-menu-telemetry.js +26 -0
- package/src/finger-handles.jsx +128 -0
- package/src/friction-hint.js +82 -0
- package/src/live-control.js +2 -1
- package/src/locale-zh/panels.js +20 -0
- package/src/locale-zh/shell.js +4 -0
- package/src/locale-zh/workspace.js +6 -0
- package/src/motion/find-bone.js +13 -0
- package/src/motion/fingers.js +170 -0
- package/src/motion/fingertip-pick.js +21 -0
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/qa/hints-contact.js +12 -0
- package/src/motion3/qa/patch.js +4 -1
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- package/src/motion3/skills/catch.js +2 -1
- package/src/panels/FingerPanel.jsx +38 -0
- package/src/panels/RigControlPanel.jsx +9 -1
- package/src/panels/SculptPanel.jsx +12 -12
- package/src/panels/SculptPartTree.jsx +46 -0
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/panels/sculpt.css +11 -0
- package/src/pose-fade.js +7 -3
- package/src/posestudio.jsx +14 -27
- package/src/project-browser.css +6 -0
- package/src/project-browser.jsx +9 -18
- package/src/project-start-choices.jsx +45 -0
- package/src/project.js +16 -12
- package/src/proxy-figure-shape.js +11 -0
- package/src/proxy-figure.jsx +25 -18
- package/src/scene-assets.js +17 -13
- package/src/scenes.js +22 -11
- package/src/sculpt-blueprint.js +56 -0
- package/src/sculpt-mode.js +5 -0
- package/src/sculpt-part-edit.js +115 -0
- package/src/sculpt-part-gizmo.jsx +159 -0
- package/src/sculpt-part-outline.jsx +72 -0
- package/src/sculpt-pick.js +53 -0
- package/src/sculpt-run-text.js +51 -0
- package/src/sculpt-run.js +10 -46
- package/src/sculpt-shelf-file.js +92 -0
- package/src/sculpt-shelf-tiles.jsx +69 -0
- package/src/sculpt-shelf.js +108 -0
- package/src/sculpt-stage.jsx +39 -2
- package/src/sculpt-thumb.js +72 -0
- package/src/sculpt-views.js +75 -0
- package/src/semantic-edit.js +0 -87
- package/src/settings-menu.jsx +2 -2
- package/src/shell/DetailsSlot.jsx +5 -6
- package/src/shell/LibrarySlot.jsx +8 -10
- package/src/shell/MenuBar.jsx +6 -6
- package/src/shell/PreferencesDialog.jsx +2 -3
- package/src/shell/ViewportToolbar.jsx +15 -18
- package/src/shell/add-character-entries.js +13 -0
- package/src/shell/pose-controls.js +32 -0
- package/src/shell/preference-pages.jsx +2 -11
- package/src/shell/storyboard-caption.js +21 -0
- package/src/shell/studio-shell-context.js +1 -0
- package/src/shortcut-friction.js +2 -1
- package/src/site/landing-analytics.js +28 -0
- package/src/studio-actions.js +3 -2
- package/src/studio-agent-protocol.js +4 -4
- package/src/studio-app-binding.js +4 -3
- package/src/update/UpdateChip.jsx +21 -2
- package/src/update/UpdateDialog.jsx +26 -6
- package/src/update/update-client.js +123 -17
- package/src/update/update-telemetry.js +15 -3
- package/src/use-sculpt-mode.js +62 -8
- package/src/workflow/agent-failure-codes.js +28 -9
- package/src/workflow/agent-turn-telemetry.js +1 -1
- package/src/workflow/task-board.js +2 -2
- package/tools/dev/page-shell.py +1 -1
- package/tools/dev/pages/greybox-to-video.html +21 -2
- package/tools/dev/pages/previs-software.html +3 -3
- package/tools/dev/pages/privacy.html +9 -6
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +45 -69
- package/tools/select-tests.mjs +119 -0
- package/tools/test-durations.json +455 -0
- package/tools/test-graph.mjs +181 -0
- package/tools/test-shards.mjs +78 -0
- package/tools/tutorial/README.md +74 -0
- package/tools/tutorial/assemble.mjs +43 -0
- package/tools/tutorial/captions.mjs +43 -0
- package/tools/tutorial/cdp.mjs +55 -0
- package/tools/tutorial/cursor.mjs +159 -0
- package/tools/tutorial/episode.mjs +173 -0
- package/tools/tutorial/episodes/beginner-guide/00-install.mjs +42 -0
- package/tools/tutorial/episodes/beginner-guide/01-start.mjs +50 -0
- package/tools/tutorial/episodes/beginner-guide/02-layout.mjs +67 -0
- package/tools/tutorial/episodes/beginner-guide/03-stage.mjs +58 -0
- package/tools/tutorial/episodes/beginner-guide/04-motion.mjs +98 -0
- package/tools/tutorial/episodes/beginner-guide/05-camera.mjs +47 -0
- package/tools/tutorial/episodes/beginner-guide/06-export.mjs +59 -0
- package/tools/tutorial/episodes/beginner-guide/steps.mjs +32 -0
- package/tools/tutorial/episodes/beginner-guide.mjs +17 -0
- package/tools/tutorial/episodes/cast.mjs +12 -0
- package/tools/tutorial/episodes/first-video.mjs +121 -0
- package/tools/tutorial/episodes/stub-tour.mjs +97 -0
- package/tools/tutorial/launch.mjs +63 -0
- package/tools/tutorial/mux.mjs +61 -0
- package/tools/tutorial/overlay.mjs +142 -0
- package/tools/tutorial/record.mjs +156 -0
- package/tools/tutorial/scenes/terminal.html +63 -0
- package/tools/tutorial/steps.mjs +111 -0
- package/tools/tutorial/terminal.mjs +43 -0
- package/tools/tutorial/voice.mjs +49 -0
- package/tools/update-test-durations.mjs +42 -0
- package/tools/verify-package-tarball.mjs +18 -12
- package/dist/assets/TaskBoard-BUjjuvm5.js +0 -1
- package/dist/assets/app-Dige7q3X.css +0 -1
- package/dist/assets/app-DxoI941o.js +0 -5144
- package/dist/assets/motion3-compile-B4SX4TGx.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-LeI3eSnO.js +0 -13
- package/dist/assets/task-board-5PKn7ZaA.js +0 -15
- package/dist/assets/workflow-Dg9jCKOq.js +0 -179
|
@@ -1 +1 @@
|
|
|
1
|
-
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.2.
|
|
1
|
+
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.2.1\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"e718aecfd151dda276e5d63c11a57f1e47b52a54d35fc07dbea34a0b209de6c5\"}","signature":"57sTKSSWpbM4IS3uUaKX1UpUWwfo6aH0+GzQvkJP41AwzK9jGMCAwzatT6oDobQ86JFHBpXnpT2+kSSATwLpCg=="}
|
package/dist/index.html
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"applicationCategory": "DesignApplication",
|
|
47
47
|
"applicationSubCategory": "3D previsualization",
|
|
48
48
|
"operatingSystem": "Any (Chromium-based browser)",
|
|
49
|
-
"softwareVersion": "3.2.
|
|
49
|
+
"softwareVersion": "3.2.1",
|
|
50
50
|
"description": "CozyClay is an open-source, browser-based 3D staging studio. Block a scene, pose characters, sequence motion prompts on a timeline, and preview generated motion in one local workspace that handles like the Unity Editor.",
|
|
51
51
|
"image": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
|
|
52
52
|
"screenshot": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
|
|
@@ -380,6 +380,9 @@
|
|
|
380
380
|
.btn, a.card, .seg button, details summary::after { transition: none; }
|
|
381
381
|
}
|
|
382
382
|
</style>
|
|
383
|
+
<script type="module" crossorigin src="/assets/landing-2Fy7DU41.js"></script>
|
|
384
|
+
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
|
|
385
|
+
<link rel="modulepreload" crossorigin href="/assets/analytics-CaTUuLPM.js">
|
|
383
386
|
</head>
|
|
384
387
|
<body>
|
|
385
388
|
<header class="site">
|
package/dist/privacy/index.html
CHANGED
|
@@ -133,7 +133,7 @@ cclay telemetry on</code></pre>
|
|
|
133
133
|
<tbody>
|
|
134
134
|
<tr><td>install:first_launch, app:session_started, app:session_ended</td><td>Counting installs and sessions; duration and action counts are bucketed. install:first_launch may carry heard_from (x, hn, reddit, github, friend, other, site or playground), from the first-launch prompt or a copied <code>npx cozyclay --via</code> command. app:session_ended also carries scenes_touched (how many scenes were created or loaded in the session, an exact count capped at 20, not bucketed), session_id (the opaque random session id the PostHog SDK generates, used only to join a session's last events, such as an error just before it ended) and per-session edit gesture counts in five groups, pose_edit_bucket, camera_edit_bucket, object_edit_bucket, shot_edit_bucket and prompt_edit_bucket, each bucketed (0, 1-3, 4-10 or gte11); never what was edited</td></tr>
|
|
135
135
|
<tr><td>app:friction_summary</td><td>Once per session, next to app:session_ended: how much friction the session had, as closed buckets only. active_time (the session duration bucket); counts of refusals the Studio gave the user (refusals_ui) and the agent (refusals_agent), user undos (undos), errors (errors), browser long tasks (long_tasks) and motion compiles (compiles), each as 0, 1, 2, 3, 4-5, 6-10 or gte11; the longest long task (long_task_max: lt50, 50-150, 150-500, 500-2000 or gte2000 ms); the share of late playback ticks (late_tick_ratio: 0, lt5, 5-20, 20-50 or gte50 %). rage_clicks and dead_clicks count every ui:rage_click and ui:dead_click, shortcut_ignored every shortcut:ignored, search_empty every search:no_results, and nav_thrash and camera_thrash every nav:thrash and camera:thrash, including those past their event's cap. Never which command, control, key or text was involved.</td></tr>
|
|
136
|
-
<tr><td>studio:refused</td><td>A command the Studio refused, as closed values only: code (one of the Studio's fixed error codes, such as INVALID_ARGUMENT, STALE_SCENE or TARGET_BUSY, else other), origin (ui for the user's own control, agent for the agent or an MCP client), phase (admission, refused before any change, or commit, refused after it started changing)
|
|
136
|
+
<tr><td>studio:refused</td><td>A command the Studio refused, as closed values only: code (one of the Studio's fixed error codes, such as INVALID_ARGUMENT, STALE_SCENE or TARGET_BUSY, else other), origin (ui for the user's own control, agent for the agent or an MCP client), phase (admission, refused before any change, or commit, refused after it started changing) group (the command's family: object, shot, cast, scene, motion, edit, task, agent or other) and command (the refused action's id, such as object.set or shot.setCameraRail, only when it is one of the actions the Studio itself declares; a name the caller made up is left out). At most 3 per code, command (or group, without one) and origin per session; the rest are only counted in app:friction_summary. A cancel the caller asked for is not a refusal. Never the command's arguments, target or message.</td></tr>
|
|
137
137
|
<tr><td>edit:undone</td><td>A user undo (agent undos are not counted): age (lt2s, 2-10s, 10-60s or gte60s since the undone edit was made, only when the undo was done and its age is known), kind (pose, camera, object, shot, prompt, agent_turn for the agent's whole turn, ik_preview for an un-applied IK drag, or other), via (key, menu or button) and result (done, noop when there was nothing to undo, or refused). At most 20 per session. Never what was undone.</td></tr>
|
|
138
138
|
<tr><td>agent:turn_summary</td><td>One per agent turn: turn_id (the same random id as agent:turn_*); outcome (succeeded, failed or cancelled); wall (the turn's length) and first_tool (time to the first tool call, absent without tools), each lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m; counts of tool calls (tool_calls), failed tool calls (tool_failed), tool calls the Studio refused (refused), commands re-sent once after a stale revision or a busy gesture (stale_retry) and motion compiles (drafts), each as 0, 1, 2, 3, 4-5, 6-10 or gte11; refusal_top (the most frequent refusal code, or none); authored (true or false: the turn changed the scene); qa_first and qa_last (clean or problems, absent without drafts); qa_trend (improved, flat, worse, or na with fewer than 2 drafts); stuck (true or false: the motion compiler's stop rule fired); cancel_phase (before_tool, during_tool, after_tool or na). No prompt text, tool names, messages or responses.</td></tr>
|
|
139
139
|
<tr><td>agent:post_turn_edit</td><td>One per agent turn: turn_id (the same random id) and the user's own edit gestures in the 120 seconds after the turn ended, closed early when the next turn starts or the page closes. edited (true or false); counts per group (pose, camera, object, shot, and prompt_block, which counts edits to a take's prompt and take fields), each as 0, 1, 2, 3, 4-5, 6-10 or gte11, where one continuous drag is one gesture; first_delay (time to the first edit: lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m), only when edited is true. Never which object or what changed.</td></tr>
|
|
@@ -149,7 +149,7 @@ cclay telemetry on</code></pre>
|
|
|
149
149
|
<tr><td>agent:connect_finished</td><td>That attempt ended: provider, method, outcome (ok, failed, cancelled with the Cancel button, abandoned when closed or left unfinished), step it had reached (list, prompt, waiting or done) and duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m). Never the key, token, account, email or error text.</td></tr>
|
|
150
150
|
<tr><td>agent:model_changed</td><td>A model picked in the agent's model list: its provider (as above, or other) and after_failure (whether the last agent turn had failed). At most 10 per session.</td></tr>
|
|
151
151
|
<tr><td>agent:prompt_sent</td><td>One per prompt sent to the agent: turn_id (the same random id as agent:turn_*); provider (as for agent:connect_started, or other); via (typed, retry_button, regenerate, chip or supervisor); prompt_len (1-20, 21-80, 81-240 or gte241 characters); compose_ms (first keystroke to send, typed prompts only) and gap (since the previous turn ended, or first) as lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m; similarity (first, same, near or different, computed in the browser against the previous prompt's words); prev_outcome (succeeded, failed, cancelled or none); turn_index (1, 2, 3, 4-5, 6-10 or gte11); attached and task_mode (true or false). Never the prompt text or its words.</td></tr>
|
|
152
|
-
<tr><td>ui:rage_click</td><td>Three or more fast clicks (within 1 second and 24 pixels) on one primary control: control (a control id from a fixed list, such as tl_play or export_video, or other), clicks (3, 4-5 or gte6) and changed (whether the Studio responded within half a second). At most 2 per control per session. No coordinates, no text, no element or object ids.</td></tr>
|
|
152
|
+
<tr><td>ui:rage_click</td><td>Three or more fast clicks (within 1 second and 24 pixels) on one primary control: control (a control id from a fixed list, such as tl_play or export_video; for a control not on that list, the Studio area it sits in: other_guide, other_agent, other_menu, other_sculpt, other_assets, other_board, other_timeline, other_inspector or other_viewport; else other), clicks (3, 4-5 or gte6) and changed (whether the Studio responded within half a second). At most 2 per control per session. No coordinates, no text, no element or object ids.</td></tr>
|
|
153
153
|
<tr><td>ui:dead_click</td><td>A click on a primary control that did nothing: control (as above) and reason (aria_disabled when the control was disabled, refused when the Studio refused the command, no_effect when nothing changed within half a second). At most 2 per control per session. Clicks into text fields are never observed.</td></tr>
|
|
154
154
|
<tr><td>feature:used</td><td>One signal per feature per session: a fixed feature name such as pose_edit (a real bone or pose change), pose_save (saving a pose to the library), camera_fly, export_video, export_storyboard or export_render_pass</td></tr>
|
|
155
155
|
<tr><td>$pageview, scene:created, scene:loaded, craft:first_action, activation:completed</td><td>Funnel and drop-off analysis</td></tr>
|
|
@@ -161,14 +161,15 @@ cclay telemetry on</code></pre>
|
|
|
161
161
|
<tr><td>workflow:result_applied</td><td>Captured or generated image/video output was written to the Workflow canvas: run_id only, at most once per run, independently of a later failure. No image bytes or output contents.</td></tr>
|
|
162
162
|
<tr><td>agent:turn_requested</td><td>Explicit Agent turn requests from surface (studio or workflow), paired by ephemeral turn_id. No prompts, responses or model identifiers.</td></tr>
|
|
163
163
|
<tr><td>agent:tool_executed</td><td>Agent tool observations: turn_id, tool_category (workflow_read, workflow_write, workflow_run, frame_capture, image_generate, scene_write or other), outcome (succeeded, failed or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Tool names, arguments and results are excluded.</td></tr>
|
|
164
|
-
<tr><td>agent:turn_succeeded, agent:turn_failed, agent:turn_cancelled</td><td>Agent turn outcomes: turn_id and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s); failures and actual cancellations also include failure_code (aborted, auth, rate_limited, tool_failed, upstream, key_refused, stale_scene, capability_missing, busy, invalid_request or unknown). auth means CozyClay's local agent has no sign-in or refused the session; key_refused means the model provider rejected the key or credentials; stale_scene means the turn was refused because the scene changed underneath it; capability_missing means a required capability was unavailable, for example the live hub port was already in use; busy means another turn or target was busy; invalid_request means the request itself was rejected. Missing terminal delivery is not success.</td></tr>
|
|
164
|
+
<tr><td>agent:turn_succeeded, agent:turn_failed, agent:turn_cancelled</td><td>Agent turn outcomes: turn_id and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s); failures and actual cancellations also include failure_code (aborted, auth, rate_limited, tool_failed, upstream, network, key_refused, stale_scene, capability_missing, busy, invalid_request or unknown). upstream means the model provider failed (an HTTP 5xx, overload or a cut-off stream); network means the request never got an HTTP answer (offline, connection reset or DNS); unknown is any other failure. auth means CozyClay's local agent has no sign-in or refused the session; key_refused means the model provider rejected the key or credentials; stale_scene means the turn was refused because the scene changed underneath it; capability_missing means a required capability was unavailable, for example the live hub port was already in use; busy means another turn or target was busy; invalid_request means the request itself was rejected. Missing terminal delivery is not success.</td></tr>
|
|
165
165
|
<tr><td>agent:result_applied</td><td>An Agent turn produced an acknowledged editor change: turn_id only, at most once per turn. Successful reads and focus/navigation do not prove applied output.</td></tr>
|
|
166
|
-
<tr><td>mcp:tool_requested</td><td>Connected-editor MCP execution requests: ephemeral request_id
|
|
167
|
-
<tr><td>mcp:tool_executed</td><td>Connected-editor MCP outcomes: request_id, tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other), outcome (succeeded, failed, uncertain or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Control-plane success does not prove a mutation applied.</td></tr>
|
|
166
|
+
<tr><td>mcp:tool_requested</td><td>Connected-editor MCP execution requests: ephemeral request_id, tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other) and tool_name, the CozyClay tool that ran (describe_scene, describe_shot, render_prompt, mark_camera_move, describe_camera_move, save_project, set_camera, frame_shot, add_character, place_character, remove_character, focus_character, place_object, import_mesh, sculpt_object, compare_sculpt, group_objects, update_object, remove_object, apply_batch, add_scene, switch_scene, open_project, capture_frame, studio_commands, studio_run or other; never its arguments).</td></tr>
|
|
167
|
+
<tr><td>mcp:tool_executed</td><td>Connected-editor MCP outcomes: request_id, tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other), tool_name (describe_scene, describe_shot, render_prompt, mark_camera_move, describe_camera_move, save_project, set_camera, frame_shot, add_character, place_character, remove_character, focus_character, place_object, import_mesh, sculpt_object, compare_sculpt, group_objects, update_object, remove_object, apply_batch, add_scene, switch_scene, open_project, capture_frame, studio_commands, studio_run or other), outcome (succeeded, failed, uncertain or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Control-plane success does not prove a mutation applied.</td></tr>
|
|
168
168
|
<tr><td>mcp:result_applied</td><td>A connected editor acknowledged an MCP mutation, paired by request_id. Detached or disconnected MCP sessions remain unobserved.</td></tr>
|
|
169
169
|
<tr><td>export:blocking_frame_succeeded, export:video_succeeded</td><td>Export reliability</td></tr>
|
|
170
170
|
<tr><td>export:attempt_started, export:attempt_succeeded, export:attempt_failed, export:attempt_cancelled</td><td>Export attempts and outcomes, paired by an ephemeral random attempt_id; export_kind (video, depth_video, frame, keyframe_pack, contact_sheet, panel_pack, animatic, animation_project, workflow_send, storyboard, render_pass or otio), format (mp4, png, zip, cclayproject or otio), initiating surface (studio, workflow or embed), is_retry (whether the attempt retries an earlier one), retry_of (the retried attempt's attempt_id, only when is_retry is true) and objects (the scene's object count, bucketed 0, 1-3, 4-10 or gte11). Terminal events include a bucketed duration_bucket; failures and real cancellations include a normalized failure_code (unsupported_codec, encode_failed, render_failed, aborted or unknown), the stage where the export stopped (preparing, encoding, finalizing or handoff) and a progress bucket (0, 1-25, 26-50, 51-75 or 76-99 percent), never raw error messages, filenames, URLs, paths, prompts or scene content. Success means pipeline completion or download handoff, not confirmation that the OS saved a file.</td></tr>
|
|
171
171
|
<tr><td>export:saved</td><td>How each exported file was saved, one event per file: method (workspace, written into the export folder you chose, or download, a browser download) and reason (none; unsupported, the browser has no folder access; no_workspace, no export folder is chosen; permission, folder access was denied; or write_failed, the folder write failed and the file fell back to a download). Never the file name or path.</td></tr>
|
|
172
|
+
<tr><td>export:menu_opened</td><td>The Export menu opened, once per open: surface (studio or embed), mode (animation or storyboard), webcodecs (whether this browser has the video encoder an MP4 export needs; a yes/no check, not a codec test) and shots (shot count bucket, panels in a storyboard: 0, 1-3, 4-10, gte11). Never shot names or what you exported.</td></tr>
|
|
172
173
|
<tr><td>export:keyframe_pack</td><td>Legacy Workflow pack handoffs, with numeric entries count and source=workflow; not an additional export attempt</td></tr>
|
|
173
174
|
<tr><td>sculpt_run</td><td>One event per finished Sculpt mode run: outcome (done, cancelled or failed), rounds (0 to 10), final_iou (how closely the final sculpt matched the reference image, 0 to 1 rounded to two decimals; omitted when the run was never scored), duration_ms (elapsed run time in milliseconds) and had_target (whether the run reworked an existing sculpt instead of creating one). Numbers only: never the reference image, recipe, part names or sculpt id.</td></tr>
|
|
174
175
|
<tr><td>playground:opened, playground:first_action</td><td>Whether the home-page playground gets used</td></tr>
|
|
@@ -177,6 +178,7 @@ cclay telemetry on</code></pre>
|
|
|
177
178
|
<tr><td>app:gl_lost</td><td>The 3D view's graphics failed, at most once per kind per session: kind (unavailable when WebGL is off at startup, context_lost when the GPU context drops later). Never the GPU renderer string.</td></tr>
|
|
178
179
|
<tr><td>studio:ready</td><td>Once per session: how long the studio took to show its first frame, from page navigation to the first frame after the first render: boot (lt2s, 2-5s, 5-15s or gte15s). Never what was loaded.</td></tr>
|
|
179
180
|
<tr><td>playback:summary</td><td>One per playback run (play until stop, or until the frame rate or speed changes), at most 10 per session: duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m); late_ticks, an estimate of dropped frames, not a measured one: the share of playback timer ticks that had to catch up more than one frame (0, lt5, 5-20, 20-50 or gte50 %); max_catchup (the most frames one tick caught up: 1, 2, 3-5 or gte6); fps (12, 24, 30, 60 or other); speed (0.5, 1, 2 or other); measured_fps (the browser's frame rate during playback, the device:profile frame-rate buckets). Every run also feeds app:friction_summary.late_tick_ratio. Never the scene, the take or the frame range.</td></tr>
|
|
181
|
+
<tr><td>landing:cta_clicked</td><td>The landing page (cozyclay.org/) sends one $pageview and, when a call to action is pressed, which one: target (app, github, discord, npm, copy_npx, copy_clone, copy_mcp, copy_terminal or agent_install_doc). Never the URL or the copied text. The landing page sends no session, error, device or performance events.</td></tr>
|
|
180
182
|
<tr><td>guide:opened</td><td>The first-60-seconds guide opened, at most once per session: mode (animation or storyboard). Never step text or what you did.</td></tr>
|
|
181
183
|
<tr><td>guide:step</td><td>A guide step marked done, each step at most once per session: mode (animation or storyboard), step (1 to 4) and step_ms (time on that step: lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m).</td></tr>
|
|
182
184
|
<tr><td>guide:dismissed</td><td>The guide closed before its last step, at most once per session: mode (animation or storyboard) and reached (1 to 4, the step it was showing).</td></tr>
|
|
@@ -185,8 +187,9 @@ cclay telemetry on</code></pre>
|
|
|
185
187
|
<tr><td>object:deleted_soon</td><td>You deleted an object you added in this session, at most 10 per session: age (how long after adding it: lt2s, 2-10s, 10-60s or gte60s). Object ids are reused names, so this is an estimate. Never the object's id, name or kind.</td></tr>
|
|
186
188
|
<tr><td>object:delete_undone</td><td>You undid an object deletion within its 7-second undo window, at most 10 per session: age (time from the deletion to the undo: lt2s or 2-10s). Never the object's id, name or kind.</td></tr>
|
|
187
189
|
<tr><td>update:notice_shown</td><td>The update notice appeared in the top bar of an npm studio, once per session: kind (global or npx) and behind (patch, minor or major: how far the running version is behind the newest release). Never a path or a command</td></tr>
|
|
188
|
-
<tr><td>update:clicked</td><td>A button on the update notice, at most 10 per session: action (update_now, copy_command, later, dismiss or
|
|
190
|
+
<tr><td>update:clicked</td><td>A button on the update notice, at most 10 per session: action (update_now, copy_command, later, dismiss, release_notes or restart) and kind (global or npx)</td></tr>
|
|
189
191
|
<tr><td>update:finished</td><td>A one-click update ended, at most 3 per session: outcome (ok or failed), error_class (none, permission, network, node_version, npm_missing or other) and duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m). Never npm's output, a path or the command line</td></tr>
|
|
192
|
+
<tr><td>update:restarted</td><td>Restart now ended in the tab that pressed it, once per session: outcome (ok: the updated launcher answered; timeout: it did not within 30 seconds). Never a version string, a path or the command line</td></tr>
|
|
190
193
|
<tr><td>device:profile</td><td>Once per session, whether the studio can run smoothly: gpu_vendor (nvidia, amd, intel, apple, qualcomm, arm, software, other or unknown), gpu_class (discrete, integrated, software or unknown), webgl (webgl2, webgl1 or none), cpu_bucket (1-4, 5-8, 9-16, gte17 or unknown), memory_bucket (lt4, 4-7, gte8 or unknown) and frame_rate_bucket (lt20, 20-40, 40-55, gte55 or unknown) from one three-second sample. The GPU renderer string and exact hardware values never leave the browser.</td></tr>
|
|
191
194
|
<tr><td>survey:use_case</td><td>An optional question shown at most once after a successful export, only while telemetry is on: use_case (animation, film, game, ad_mv, personal, other or skip) and team (team, solo or skip). Closed choices only; no free text.</td></tr>
|
|
192
195
|
<tr><td>hosted:composer_viewed, hosted:login_started, hosted:ticket_created, hosted:result_opened, hosted:opened_in_studio</td><td>Hosted demo funnel</td></tr>
|
package/dist/sitemap.xml
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3
3
|
<url>
|
|
4
4
|
<loc>https://cozyclay.org/</loc>
|
|
5
|
-
<lastmod>2026-10-
|
|
5
|
+
<lastmod>2026-10-27</lastmod>
|
|
6
6
|
</url>
|
|
7
7
|
<url>
|
|
8
8
|
<loc>https://cozyclay.org/greybox-to-video/</loc>
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
</url>
|
|
23
23
|
<url>
|
|
24
24
|
<loc>https://cozyclay.org/privacy/</loc>
|
|
25
|
-
<lastmod>2026-10-
|
|
25
|
+
<lastmod>2026-10-27</lastmod>
|
|
26
26
|
</url>
|
|
27
27
|
</urlset>
|
package/dist/workflow/index.html
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
<meta name="theme-color" content="#101116" />
|
|
9
9
|
<meta name="description" content="CozyClay visual workflow builder" />
|
|
10
10
|
<title>CozyClay Workflow</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/workflow-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/task-board-
|
|
11
|
+
<script type="module" crossorigin src="/assets/workflow-CSiBlX1s.js"></script>
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/analytics-CaTUuLPM.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/task-board-UVGTZLRF.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
|
|
14
15
|
<link rel="modulepreload" crossorigin href="/assets/fflate.module-KbGj5-H6.js">
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-CLGNbEBq.js">
|
|
16
17
|
<link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-BEjjhNEo.css">
|
|
17
18
|
<link rel="stylesheet" crossorigin href="/assets/workflow-DRdwoDBT.css">
|
|
18
19
|
</head>
|
package/mcp/README.md
CHANGED
|
@@ -73,7 +73,7 @@ generated frame matches the blocking instead of drifting off into a generic shot
|
|
|
73
73
|
| `place_object` / `update_object` / `remove_object` | the set — `place_object` also accepts `name` and `parent`, so multi-part assets like "Building A" land as one named assembly |
|
|
74
74
|
| `import_mesh` | load a local GLB, OBJ or FBX from a filesystem path as a mesh prop (live editor required); optional floor position, yaw, standing height, clay |
|
|
75
75
|
| `sculpt_object` | build a clay prop from a recipe of soft parts (blob, box, cylinder, torus, frame), usually from a reference image; pass `id` with `recipe` to re-sculpt or with `edits` to patch parts; `capture: true` returns the frame too. Works without an editor. Contract: `docs/sculpt-from-image.md` |
|
|
76
|
-
| `compare_sculpt` | score a sculpt's silhouette against its reference image (PNG/JPEG path): IoU, proportion delta, missing area by height band, parts that stick out, and a reference \| overlay sheet; `candidates` ranks up to 12 edit sets without applying any. Works without an editor |
|
|
76
|
+
| `compare_sculpt` | score a sculpt's silhouette against its reference image (PNG/JPEG path): IoU, proportion delta, missing area by height band, parts that stick out, and a reference \| overlay sheet; `views` [{ yaw, reference, weight? }] scores several views (front 0.6, side 0.25, back 0.15 by default) in place of `reference`; `candidates` ranks up to 12 edit sets without applying any. Works without an editor |
|
|
77
77
|
| `group_objects` | attach children to a parent so they move as one; pass `parent: null` to detach |
|
|
78
78
|
| `apply_batch` | execute up to 100 object mutations as one user-visible undo transaction |
|
|
79
79
|
| `render_prompt` | the shot as an AI image or video prompt |
|
package/mcp/live-hub.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { bucketMs, sanitizeProps } from "../src/analytics.js";
|
|
5
|
-
import { mcpToolCategory } from "../src/execution-telemetry.js";
|
|
5
|
+
import { mcpToolCategory, mcpToolName } from "../src/execution-telemetry.js";
|
|
6
6
|
|
|
7
7
|
import { WebSocket, WebSocketServer } from "ws";
|
|
8
8
|
|
|
@@ -192,7 +192,7 @@ export class LiveHub {
|
|
|
192
192
|
const startedAt = safely(now);
|
|
193
193
|
if (typeof requestId !== "string" || !/^[a-f0-9]{32}$/.test(requestId) || !Number.isFinite(startedAt)) return work();
|
|
194
194
|
const workspaceId = this.workspaceId(handle);
|
|
195
|
-
const category = mcpToolCategory(name);
|
|
195
|
+
const category = mcpToolCategory(name), toolName = mcpToolName(name);
|
|
196
196
|
const emit = (event, props) => safely(() => {
|
|
197
197
|
// Do not reconstruct this lifecycle on a replacement connection.
|
|
198
198
|
if (this.editors.get(handle) !== socket || socket.readyState !== WebSocket.OPEN) return;
|
|
@@ -266,14 +266,14 @@ export class LiveHub {
|
|
|
266
266
|
if (value?.isError === true && !failure) failure = acknowledgedMutation ? "uncertain" : "failed";
|
|
267
267
|
terminal = true;
|
|
268
268
|
const endedAt = safely(now);
|
|
269
|
-
if (Number.isFinite(endedAt)) emit("mcp:tool_executed", { request_id: requestId, tool_category: category, outcome: failure ?? "succeeded", duration_bucket: bucketMs(endedAt - startedAt) });
|
|
269
|
+
if (Number.isFinite(endedAt)) emit("mcp:tool_executed", { request_id: requestId, tool_category: category, tool_name: toolName, outcome: failure ?? "succeeded", duration_bucket: bucketMs(endedAt - startedAt) });
|
|
270
270
|
emitApplied();
|
|
271
271
|
// Never retain scene data with an asynchronous motion job.
|
|
272
272
|
description = undefined;
|
|
273
273
|
studioContext = undefined;
|
|
274
274
|
mutation = null;
|
|
275
275
|
});
|
|
276
|
-
emit("mcp:tool_requested", { request_id: requestId, tool_category: category });
|
|
276
|
+
emit("mcp:tool_requested", { request_id: requestId, tool_category: category, tool_name: toolName });
|
|
277
277
|
return executionObserver.run(observer, async () => {
|
|
278
278
|
try {
|
|
279
279
|
const value = await work();
|
package/mcp/tool-handlers.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { MAX_COMMAND_TIMEOUT_MS } from "./live-hub.mjs";
|
|
|
32
32
|
import { readMeshFromPath } from "./mesh-file.mjs";
|
|
33
33
|
import { encodePng, readImageFromPath } from "./image-file.mjs";
|
|
34
34
|
import { rankingRow, scoreSculptAgainstReference } from "../src/sculpt-compare.js";
|
|
35
|
+
import { scoreSculptAgainstViews } from "../src/sculpt-views.js";
|
|
35
36
|
import { DEFAULT_POSE } from "../src/poses.js";
|
|
36
37
|
|
|
37
38
|
import {
|
|
@@ -75,7 +76,9 @@ import { documentWithCamera, splitSavedCamera } from "./project-camera.mjs";
|
|
|
75
76
|
|
|
76
77
|
/** The authoring state. One scene document, one camera, one project name. */
|
|
77
78
|
export const state = {
|
|
78
|
-
|
|
79
|
+
// Memory mode keeps the one-character stage it always opened with; the
|
|
80
|
+
// studio's new project starts empty, and a connected editor reports its own.
|
|
81
|
+
doc: createSceneDocument({ seedCharacter: true }),
|
|
79
82
|
name: "Untitled",
|
|
80
83
|
/** the project's previs mode ("storyboard" | "animation"); the live editor reports it with every describe */
|
|
81
84
|
previsMode: DEFAULT_PREVIS_MODE,
|
|
@@ -1009,7 +1012,6 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1009
1012
|
const st = stage();
|
|
1010
1013
|
const target = findCharacter(character);
|
|
1011
1014
|
if (!target) return text(`No character "${character}". ${castHint()}`);
|
|
1012
|
-
if (st.characters.length === 1) return text("The scene needs at least one character.");
|
|
1013
1015
|
const letter = letterFor(target);
|
|
1014
1016
|
st.characters = st.characters.filter((c) => c !== target);
|
|
1015
1017
|
if (state.focus && !findCharacter(state.focus)) {
|
|
@@ -1231,17 +1233,18 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1231
1233
|
"Unlike capture_frame and sculpt_object, compare_sculpt scores a sculpt's shape against a reference image instead of rendering the shot or changing the sculpt. " +
|
|
1232
1234
|
"Give the sculpt's id and the path of the reference (PNG or JPEG on this machine; a flat background or transparency). It reads the reference's subject as a mask, draws the sculpt's silhouette from a view (yaw: the camera's azimuth, 0 in front, positive toward the subject's own left; elevation: degrees above), " +
|
|
1233
1235
|
"fits it to the subject's height and position, and returns IoU, the width-to-height delta, which height band is missing area (top, middle, bottom) and which parts stick out, plus a sheet: reference | overlay (grey both, red missing, blue extra). " +
|
|
1234
|
-
"Omit yaw to try -45..45 and keep the best. Pass candidates (up to 12 labelled edit sets) to score several fixes in one call without applying any; the answer ranks them against the current sculpt. Shape only: colour is not scored. Works without an editor.",
|
|
1236
|
+
"Omit yaw to try -45..45 and keep the best. Instead of reference, views [{ yaw, reference, weight? }] scores several views of the subject (a blueprint's front 0, side 90, back 180) and blends their IoUs by weight (default front 0.6, side 0.25, back 0.15; a view whose height:width differs from the front's by more than 35% gets 0), with one row per view. Pass candidates (up to 12 labelled edit sets) to score several fixes in one call without applying any; the answer ranks them against the current sculpt. Shape only: colour is not scored. Works without an editor.",
|
|
1235
1237
|
inputSchema: {
|
|
1236
1238
|
id: z.string().min(1).describe("the sculpt's object id"),
|
|
1237
|
-
reference: z.string().min(1).describe("filesystem path to the reference PNG or JPEG"),
|
|
1239
|
+
reference: z.string().min(1).optional().describe("filesystem path to the reference PNG or JPEG; give this or views"),
|
|
1240
|
+
views: z.array(z.object({ yaw: z.number().min(-180).max(180), reference: z.string().min(1), weight: z.number().min(0).max(1).optional() })).min(1).max(4).optional().describe("several views of the subject (path + camera yaw each), scored together by weight; give this or reference"),
|
|
1238
1241
|
yaw: z.number().min(-180).max(180).optional().describe("reference camera azimuth in degrees; omitted tries -45..45"),
|
|
1239
1242
|
elevation: z.number().min(-60).max(60).optional().describe("reference camera elevation in degrees; default 0"),
|
|
1240
1243
|
candidates: z.array(z.object({ label: z.string().min(1).max(60), edits: z.array(z.object({}).passthrough()).min(1).max(48) })).min(1).max(12).optional()
|
|
1241
1244
|
.describe("alternative edit sets to score side by side without applying any; returns them ranked by IoU with the winner's sheet"),
|
|
1242
1245
|
},
|
|
1243
1246
|
},
|
|
1244
|
-
async ({ id, reference, yaw, elevation = 0, candidates }) => {
|
|
1247
|
+
async ({ id, reference, views, yaw, elevation = 0, candidates }) => {
|
|
1245
1248
|
const workspaceHandle = liveWorkspace.getStore();
|
|
1246
1249
|
if (liveHub?.connected) {
|
|
1247
1250
|
try { await refreshLiveDescription(workspaceHandle); } catch (error) { return liveError(error); }
|
|
@@ -1249,11 +1252,11 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1249
1252
|
const target = scene().objects.find((o) => o.id === id);
|
|
1250
1253
|
if (!target) return text(`No object "${id}". Call describe_scene for the current ids.`);
|
|
1251
1254
|
if (target.renderer !== SCULPT_KIND || !target.recipe) return { content: [{ type: "text", text: `${id} is a ${target.renderer}, not a sculpt.` }], isError: true };
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
+
if ((reference === undefined) === (views === undefined)) return { content: [{ type: "text", text: "Give exactly one of reference (one view) or views (several weighted views)." }], isError: true };
|
|
1256
|
+
let image, result;
|
|
1257
|
+
try { image = views ? await Promise.all(views.map(async (view) => ({ ...view, image: await readImageFromPath(view.reference) }))) : await readImageFromPath(reference); } catch (error) { return { content: [{ type: "text", text: error.message }], isError: true }; }
|
|
1255
1258
|
try {
|
|
1256
|
-
const scored = scoreSculptAgainstReference(target.recipe, image, { yaw, elevation, candidates });
|
|
1259
|
+
const scored = views ? scoreSculptAgainstViews(target.recipe, image, { elevation, candidates }) : scoreSculptAgainstReference(target.recipe, image, { yaw, elevation, candidates });
|
|
1257
1260
|
result = scored.result;
|
|
1258
1261
|
if (candidates) {
|
|
1259
1262
|
const { ranked, refused } = scored;
|
|
@@ -1267,12 +1270,7 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1267
1270
|
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
1268
1271
|
}
|
|
1269
1272
|
const png = encodePng(result.sheet.width, result.sheet.height, result.sheet.data);
|
|
1270
|
-
return {
|
|
1271
|
-
content: [
|
|
1272
|
-
{ type: "text", text: JSON.stringify({ id, ...result.report }) },
|
|
1273
|
-
{ type: "image", data: png.toString("base64"), mimeType: "image/png" },
|
|
1274
|
-
],
|
|
1275
|
-
};
|
|
1273
|
+
return { content: [{ type: "text", text: JSON.stringify({ id, ...result.report }) }, { type: "image", data: png.toString("base64"), mimeType: "image/png" }] };
|
|
1276
1274
|
},
|
|
1277
1275
|
),
|
|
1278
1276
|
|
|
@@ -1527,7 +1525,7 @@ export const createToolHandlers = ({ projectRootPromise } = {}) => {
|
|
|
1527
1525
|
mode,
|
|
1528
1526
|
model: modelById(model) ?? undefined,
|
|
1529
1527
|
shot: currentShot(),
|
|
1530
|
-
subject: framed
|
|
1528
|
+
subject: framed?.subject ?? "",
|
|
1531
1529
|
subject2: other?.subject ?? null,
|
|
1532
1530
|
posePhrase: pose_phrase,
|
|
1533
1531
|
pose2Phrase: other ? pose2_phrase : "",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozyclay",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Open source previs software in the browser: block a scene, pose characters, author camera moves and cuts, then take the same shots to an AI video model.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "node tools/dev-full.mjs --host 127.0.0.1 --port 5180",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"demo:api": "npm --prefix workers/api run dev",
|
|
42
42
|
"demo:api:install": "npm --prefix workers/api ci",
|
|
43
43
|
"qa:browser": "node tools/qa-browser.mjs --",
|
|
44
|
+
"tutorial:record": "node tools/tutorial/launch.mjs",
|
|
44
45
|
"test:shot-authoring": "node test/verify-shot-authoring.mjs",
|
|
45
46
|
"test:camera-follow": "node test/verify-camera-follow.mjs",
|
|
46
47
|
"test:sample-at": "node test/verify-sample-at.mjs",
|
package/src/App.jsx
CHANGED
|
@@ -36,10 +36,12 @@ import { noteNavSwitch } from "./thrash-friction.js";
|
|
|
36
36
|
|
|
37
37
|
import AgentPanel from "./workflow/AgentPanel.jsx";
|
|
38
38
|
import SculptPanel from "./panels/SculptPanel.jsx";
|
|
39
|
+
import { dropAssetAt } from "./asset-drop.js";
|
|
40
|
+
import { saveSculptToShelf } from "./sculpt-shelf.js";
|
|
39
41
|
import { createAgentPort, createSculptRun } from "./sculpt-run.js";
|
|
40
42
|
import { SculptTurntable } from "./sculpt-stage.jsx";
|
|
41
43
|
import { measureReferenceOutline } from "./sculpt-compare.js";
|
|
42
|
-
import { BLUEPRINT_LAYOUT, buildBlueprintPrompt } from "./sculpt-blueprint.js";
|
|
44
|
+
import { BLUEPRINT_LAYOUT, blueprintRunViews, buildBlueprintPrompt } from "./sculpt-blueprint.js";
|
|
43
45
|
import { browserSculptImages } from "./commands/sculpt-images.js";
|
|
44
46
|
import { sculptAgentClient, sculptModelLabel } from "./sculpt-mode.js";
|
|
45
47
|
import { useSculptMode } from "./use-sculpt-mode.js";
|
|
@@ -168,6 +170,8 @@ import {
|
|
|
168
170
|
RangePinMarker,
|
|
169
171
|
warmPoseThumbnails,
|
|
170
172
|
} from "./posestudio.jsx";
|
|
173
|
+
import { FingerHandles, useFingerEdit } from "./finger-handles.jsx";
|
|
174
|
+
import { STORYBOARD_FONT, storyboardCaption, storyboardLine } from "./shell/storyboard-caption.js";
|
|
171
175
|
|
|
172
176
|
import {
|
|
173
177
|
createIkState,
|
|
@@ -297,28 +301,6 @@ const CHARACTER_SCALE_BOUNDS = elementByPath("character.scale");
|
|
|
297
301
|
// fades the same highlight out over the same two seconds.
|
|
298
302
|
const AGENT_RECEIPT_HIGHLIGHT_MS = 2000;
|
|
299
303
|
|
|
300
|
-
// The storyboard contact sheet is drawn on a bare 2d canvas, which has no
|
|
301
|
-
// stylesheet to inherit from: it gets the studio's own type stack explicitly
|
|
302
|
-
// so the sheet reads like the app it came out of.
|
|
303
|
-
const STORYBOARD_FONT = '12px Inter, "Pretendard", "Noto Sans KR", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
304
|
-
// One unwrapped caption line at 12px inside a 480px cell holds about 64
|
|
305
|
-
// characters before it runs into the next column.
|
|
306
|
-
const STORYBOARD_CAPTION_CHARS = 64;
|
|
307
|
-
|
|
308
|
-
/** Cut a caption to what one unwrapped cell line holds. */
|
|
309
|
-
function storyboardLine(text) {
|
|
310
|
-
return text.length > STORYBOARD_CAPTION_CHARS ? `${text.slice(0, STORYBOARD_CAPTION_CHARS - 1)}\u2026` : text;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** Fold a labelled shot prompt into the one line a board cell can hold. */
|
|
314
|
-
function storyboardCaption(prompt) {
|
|
315
|
-
return storyboardLine(prompt
|
|
316
|
-
.split("\n")
|
|
317
|
-
.filter((line) => line.startsWith("SHOT:") || line.startsWith("LENS:"))
|
|
318
|
-
.map((line) => line.slice(line.indexOf(":") + 1).trim())
|
|
319
|
-
.join(" · "));
|
|
320
|
-
}
|
|
321
|
-
|
|
322
304
|
/**
|
|
323
305
|
* Pose ONE cast member's rig at an absolute timeline frame: its own clip first,
|
|
324
306
|
* then its own IK correction layer on top. This is the single description of
|
|
@@ -1012,21 +994,8 @@ export default function App() {
|
|
|
1012
994
|
raycaster.setFromCamera(pointer, cam);
|
|
1013
995
|
const hit = new THREE.Vector3();
|
|
1014
996
|
if (!raycaster.ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), hit)) return;
|
|
1015
|
-
// Dispatch on the payload kind: same ray,
|
|
1016
|
-
|
|
1017
|
-
// objects, cutouts and meshes take the same ROOM_LIMIT clamp their
|
|
1018
|
-
// creators already apply.
|
|
1019
|
-
if (payload.kind === "character") {
|
|
1020
|
-
spawnCharacter(payload.id,
|
|
1021
|
-
THREE.MathUtils.clamp(hit.x, CHARACTER_POSITION_BOUNDS.min.x, CHARACTER_POSITION_BOUNDS.max.x),
|
|
1022
|
-
THREE.MathUtils.clamp(hit.z, CHARACTER_POSITION_BOUNDS.min.z, CHARACTER_POSITION_BOUNDS.max.z));
|
|
1023
|
-
} else if (payload.kind === "object") {
|
|
1024
|
-
addSceneObject(payload.objectKind, { x: hit.x, z: hit.z });
|
|
1025
|
-
} else if (payload.kind === "image") {
|
|
1026
|
-
spawnCutoutAt(payload.assetId, { x: hit.x, z: hit.z });
|
|
1027
|
-
} else if (payload.kind === "mesh") {
|
|
1028
|
-
spawnMeshAt(payload.assetId, { x: hit.x, z: hit.z });
|
|
1029
|
-
}
|
|
997
|
+
// Dispatch on the payload kind (asset-drop.js): same ray, one spawner per kind.
|
|
998
|
+
dropAssetAt(payload, hit, { spawnCharacter, addSceneObject, spawnCutoutAt, spawnMeshAt, runStudioAction, select: setSelectedHierarchyId });
|
|
1030
999
|
};
|
|
1031
1000
|
window.addEventListener("pointermove", onMove);
|
|
1032
1001
|
window.addEventListener("pointerup", onUp);
|
|
@@ -1121,6 +1090,7 @@ export default function App() {
|
|
|
1121
1090
|
ikDragEnd, ikAddKeyframe,
|
|
1122
1091
|
ikDeleteKeyframe, ikApplyPoseAsKey,
|
|
1123
1092
|
} = motionDomain;
|
|
1093
|
+
const { handFingers, fingerEditing, setFingerEdit, exposureProbeRef: fingerExposureRef } = useFingerEdit(ikFocus, ikFkJoints);
|
|
1124
1094
|
const [partColoursEnabled, setPartColoursEnabled] = useState(false);
|
|
1125
1095
|
const [partColoursMode, setPartColoursMode] = useState("shaded");
|
|
1126
1096
|
|
|
@@ -1239,7 +1209,7 @@ export default function App() {
|
|
|
1239
1209
|
attachTargetForRow, attachTargetLabel, attachSceneObject,
|
|
1240
1210
|
} = objectsDomain;
|
|
1241
1211
|
|
|
1242
|
-
const { sculptMode, sculptCamRef, sculptView, sculptReference, setSculptReference, sculptRun, setSculptRun, sculptRunRef, sculptAgentStatus, sculptModels, sculptModelPick, chooseSculptModel, sculptTransport, sculptTap, sculptTarget, setSculptTargetId, requestSculptView, sculptBlueprint, setSculptBlueprint, generateSculptBlueprint, cancelSculptBlueprint } = useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectId });
|
|
1212
|
+
const { sculptMode, sculptCamRef, sculptView, sculptReference, setSculptReference, sculptRun, setSculptRun, sculptRunRef, sculptAgentStatus, sculptChatgpt, sculptModels, sculptModelPick, chooseSculptModel, sculptTransport, sculptTap, sculptTarget, setSculptTargetId, requestSculptView, sculptBlueprint, setSculptBlueprint, generateSculptBlueprint, cancelSculptBlueprint, sculptParts } = useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectId, appContext });
|
|
1243
1213
|
async function chooseSculptReference(file) {
|
|
1244
1214
|
try {
|
|
1245
1215
|
// A new reference makes the old sheet a drawing of something else.
|
|
@@ -1257,6 +1227,8 @@ export default function App() {
|
|
|
1257
1227
|
bus: { subscribe: (listener) => sculptTap.subscribe(listener, studioBindingRef.current?.bus) },
|
|
1258
1228
|
agent: createAgentPort(sculptAgentClient(sculptTransport, () => buildStudioAgentContext())),
|
|
1259
1229
|
reference: sculptReference,
|
|
1230
|
+
// The blueprint's side and back crops, when a sheet was drawn (#970).
|
|
1231
|
+
views: blueprintRunViews(sculptBlueprint.sheet),
|
|
1260
1232
|
sculptId: sculptTarget?.id ?? null,
|
|
1261
1233
|
score: (args) => appContext.bus.run("sculpt.compare", args, { origin: "ui" }),
|
|
1262
1234
|
measure: async (reference) => measureReferenceOutline(await browserSculptImages.decode(reference)),
|
|
@@ -1629,10 +1601,10 @@ export default function App() {
|
|
|
1629
1601
|
if (modeKey !== workflowMode && selectWorkflowMode(modeKey) === false) noteShortcutIgnored("mode", "workflow_mode");
|
|
1630
1602
|
return;
|
|
1631
1603
|
}
|
|
1632
|
-
// Sculpt mode's W/E/R swing the turntable to front, side and back.
|
|
1604
|
+
// Sculpt mode's W/E/R swing the turntable to front, side and back, or drive a selected part's gizmo.
|
|
1633
1605
|
if (workflowMode === "sculpt" && GIZMO_HOTKEYS[event.code]) {
|
|
1634
1606
|
event.preventDefault();
|
|
1635
|
-
|
|
1607
|
+
sculptParts.onKey(event.code);
|
|
1636
1608
|
return;
|
|
1637
1609
|
}
|
|
1638
1610
|
// Pose mode's W/R are the IK tools, gated like their Rig Control
|
|
@@ -1683,7 +1655,7 @@ export default function App() {
|
|
|
1683
1655
|
runStudioAction("object.duplicate");
|
|
1684
1656
|
return;
|
|
1685
1657
|
}
|
|
1686
|
-
if (event.key === "Escape" && selectedSceneObjectId) {
|
|
1658
|
+
if (event.key === "Escape" && selectedSceneObjectId && !event.defaultPrevented) {
|
|
1687
1659
|
setSelectedHierarchyId("props");
|
|
1688
1660
|
return;
|
|
1689
1661
|
}
|
|
@@ -2343,7 +2315,7 @@ export default function App() {
|
|
|
2343
2315
|
// patchable state, so every reader sees the body the save path writes.
|
|
2344
2316
|
stage: { shotAspect: shotAspectKey, cameraPresetId, sensorId, hasCharSheet, environmentImage, environment, style, hasEnvSheet, keyLight, lighting },
|
|
2345
2317
|
timeline: { currentFrame: tlFrame, frameCount: tlFrameCount, fps: tlFps },
|
|
2346
|
-
activeCharacterId,
|
|
2318
|
+
activeCharacterId: characters.length ? activeCharacterId : null,
|
|
2347
2319
|
partColours: partColoursEnabled ? PART_COLOURS : null,
|
|
2348
2320
|
waypoints,
|
|
2349
2321
|
characters,
|
|
@@ -3702,22 +3674,14 @@ export default function App() {
|
|
|
3702
3674
|
// instead of driving a file dialog. Same liveStateRef reasoning as
|
|
3703
3675
|
// captureMeta — the effect does not depend on the shot list.
|
|
3704
3676
|
exportKeyframePack: async (shotId) => {
|
|
3705
|
-
const
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
for (const byte of pack.bytes) binary += String.fromCharCode(byte);
|
|
3714
|
-
const result = { name: pack.name, entries: pack.entries.map((entry) => entry.name), byteLength: pack.bytes.byteLength, bytes: btoa(binary) };
|
|
3715
|
-
attempt.succeed();
|
|
3716
|
-
return result;
|
|
3717
|
-
} catch (error) {
|
|
3718
|
-
attempt.fail(error);
|
|
3719
|
-
throw error;
|
|
3720
|
-
}
|
|
3677
|
+
const live = appContext.live.state;
|
|
3678
|
+
const index = live.shotIndexForPack(shotId ?? null);
|
|
3679
|
+
const pack = await live.buildShotKeyframePack(live.shots[index], index);
|
|
3680
|
+
// base64: a pack holds an MP4, and a megabyte-scale JS number array
|
|
3681
|
+
// is not something to hand a CDP evaluate.
|
|
3682
|
+
let binary = "";
|
|
3683
|
+
for (const byte of pack.bytes) binary += String.fromCharCode(byte);
|
|
3684
|
+
return { name: pack.name, entries: pack.entries.map((entry) => entry.name), byteLength: pack.bytes.byteLength, bytes: btoa(binary) };
|
|
3721
3685
|
},
|
|
3722
3686
|
renderPass: (kind) => appContext.live.state.renderPassDataUrls([kind])[kind],
|
|
3723
3687
|
// QA-only video export (#193): the Export menu's Video item without the
|
|
@@ -4205,7 +4169,7 @@ export default function App() {
|
|
|
4205
4169
|
preserveAuthoredMotion: Boolean(c.layer?.waypoints?.length) }]));
|
|
4206
4170
|
return { host: { workspaceId: liveWorkspaceIdRef.current, documentEpoch: studioDocumentEpochRef.current,
|
|
4207
4171
|
sceneId: activeSceneIdRef.current, sceneEpoch: studioSceneEpochRef.current }, workspaceHandle: liveWorkspaceHandleRef.current,
|
|
4208
|
-
sceneName: live.scenes.find(s => s.id === activeSceneIdRef.current)?.name ?? "Untitled Scene", previsMode: scenesDomain.previsMode, aspect: live.stage.shotAspect, stage: live.stage,
|
|
4172
|
+
sceneName: live.scenes.find(s => s.id === activeSceneIdRef.current)?.name ?? "Untitled Scene", previsMode: scenesDomain.previsMode, castStyle: scenesDomain.castStyle, aspect: live.stage.shotAspect, stage: live.stage,
|
|
4209
4173
|
objects: storeRef.current.objects, characters: list, targets, shots: live.shots, frameCount: live.timeline.frameCount,
|
|
4210
4174
|
selection: live.studioSelection, activeCharacterId: live.activeCharacterId, selectedShotId: live.studioShotId,
|
|
4211
4175
|
view: live.studioView, camera: live.studioCamera ?? readStudioCamera(), filmback: live.filmback, manual: manualCameraOverrideRef.current,
|
|
@@ -4400,7 +4364,7 @@ export default function App() {
|
|
|
4400
4364
|
// an action sees its own edit before React renders it.
|
|
4401
4365
|
state: () => ({
|
|
4402
4366
|
shots: appContext.live.state.shots, objects: storeRef.current.objects, characters: appContext.live.characters, frame: tlFrame, frameCount: tlFrameCount,
|
|
4403
|
-
selectedObjectId: selectedSceneObjectId, activeCharacterId: activeChar
|
|
4367
|
+
selectedObjectId: selectedSceneObjectId, activeCharacterId: characters.length ? activeChar.id : null,
|
|
4404
4368
|
|
|
4405
4369
|
generating: false,
|
|
4406
4370
|
motionReady: false,
|
|
@@ -4476,19 +4440,20 @@ export default function App() {
|
|
|
4476
4440
|
// Sculpt mode's panel heads the Details column (DetailsSlot mounts it).
|
|
4477
4441
|
const sculptPanel = sculptMode && !embedMode ? (
|
|
4478
4442
|
<SculptPanel
|
|
4479
|
-
run={sculptRun}
|
|
4443
|
+
run={sculptRun} parts={sculptParts}
|
|
4480
4444
|
reference={sculptReference}
|
|
4481
4445
|
onReference={(file) => void chooseSculptReference(file)}
|
|
4482
4446
|
onStart={startSculptRun}
|
|
4483
4447
|
onCancel={() => sculptRunRef.current?.cancel()}
|
|
4484
4448
|
onPlaceInScene={placeSculptInScene}
|
|
4449
|
+
onSaveToShelf={() => void saveSculptToShelf(sculptTarget).then(setToast)}
|
|
4485
4450
|
canStart={Boolean(sculptReference) && sculptAgentStatus === "ready" && Boolean(liveWorkspaceHandle) && Boolean(sculptModelPick.model)}
|
|
4486
4451
|
blueprint={{
|
|
4487
4452
|
...sculptBlueprint,
|
|
4488
4453
|
prompt: buildBlueprintPrompt({ pose: sculptBlueprint.pose, notes: sculptBlueprint.notes }),
|
|
4489
4454
|
layout: BLUEPRINT_LAYOUT,
|
|
4490
|
-
// The image model is ChatGPT-only
|
|
4491
|
-
refusal:
|
|
4455
|
+
// The image model is ChatGPT-only: without a ChatGPT token the button is hidden and this says why (#970).
|
|
4456
|
+
refusal: !sculptChatgpt ? ko("Sign in with ChatGPT in the Agent panel to generate a blueprint.", "도면을 생성하려면 에이전트 패널에서 ChatGPT로 로그인하세요.") : null,
|
|
4492
4457
|
onPose: (pose) => setSculptBlueprint((current) => ({ ...current, pose })),
|
|
4493
4458
|
onNotes: (notes) => setSculptBlueprint((current) => ({ ...current, notes })),
|
|
4494
4459
|
onGenerate: () => void generateSculptBlueprint(),
|
|
@@ -4515,7 +4480,7 @@ export default function App() {
|
|
|
4515
4480
|
// The single shell snapshot keeps subsequent region work out of App.
|
|
4516
4481
|
const shellContext = {
|
|
4517
4482
|
sculptPanel,
|
|
4518
|
-
projectMenuOpen, setProjectMenuOpen, projectDirty, projectName, previsMode, projectStartupOpen,
|
|
4483
|
+
projectMenuOpen, setProjectMenuOpen, projectDirty, projectName, previsMode, castStyle: scenesDomain.castStyle, projectStartupOpen,
|
|
4519
4484
|
requestNewProject, setProjectStartupOpen, setProjectBrowserOpen, runStudioAction, saveProject,
|
|
4520
4485
|
projectManifest, projectSaveState, recState, exportMenuTriggerRef, exportMenuOpen,
|
|
4521
4486
|
exportShotIdRef, setExportMenuAnchor, setExportMenuOpen, exportPhaseLabel,
|
|
@@ -4529,7 +4494,7 @@ export default function App() {
|
|
|
4529
4494
|
activeSceneId, selectSceneDocument, createSceneDocumentFromUi, duplicateSceneDocumentFromUi, renameSceneDocumentFromUi,
|
|
4530
4495
|
deleteSceneDocumentFromUi, addSceneObject, renameSceneObject, deleteSceneObject, frameSelection, groupObjectUnderNewEmpty, renameRequest,
|
|
4531
4496
|
toggleHierarchyHidden, propsDrop, hierarchyReparent, agentTouchedRows, workflowMode,
|
|
4532
|
-
selectWorkflowMode, gizmoMode, setGizmoMode, snapEnabled, setSnapEnabled, sculptView, requestSculptView,
|
|
4497
|
+
selectWorkflowMode, gizmoMode, setGizmoMode, snapEnabled, setSnapEnabled, sculptView, requestSculptView, sculptParts,
|
|
4533
4498
|
preset, applyPreset, cameraPresetId, shotAspectKey,
|
|
4534
4499
|
fovDeg, shotsDomain, shot, setNonce, workspaceLayout,
|
|
4535
4500
|
viewMenuTriggerRef, viewMenuOpen, setViewMenuAnchor, setViewMenuOpen, viewLooksActive,
|
|
@@ -4546,7 +4511,7 @@ export default function App() {
|
|
|
4546
4511
|
setStudioPick, appContext, removePose, activeRig, saveCurrentPose,
|
|
4547
4512
|
setTlFrame, tlFrameCount,
|
|
4548
4513
|
tlFrame, isRigSelection, ikChains, ikFocus, footSnap, poseObjectOpacity, setPoseObjectOpacity, capturePoseImage,
|
|
4549
|
-
ikEditTool, setIkEditTool,
|
|
4514
|
+
ikEditTool, setIkEditTool, ikSolve, ikDragEnd, ikTick, handFingers, fingerEditing, setFingerEdit,
|
|
4550
4515
|
pendingIkEdit, applyPendingIkEdit, cancelPendingIkEdit,
|
|
4551
4516
|
rangePins, rangePinResiduals, rangePinSelection, rangePinPartPick,
|
|
4552
4517
|
rangePinPreview, setRangePinSelection, setRangePinPartPick, previewRangePinDraft, applyRangePinDraft,
|
|
@@ -4724,7 +4689,7 @@ export default function App() {
|
|
|
4724
4689
|
{sculptMode && (
|
|
4725
4690
|
<SculptTurntable
|
|
4726
4691
|
camRef={sculptCamRef}
|
|
4727
|
-
objects={sceneObjects}
|
|
4692
|
+
objects={sceneObjects} parts={sculptParts}
|
|
4728
4693
|
target={sculptTarget}
|
|
4729
4694
|
size={sculptTarget ? objectSize(sculptTarget) : null}
|
|
4730
4695
|
view={sculptView}
|
|
@@ -4882,7 +4847,9 @@ export default function App() {
|
|
|
4882
4847
|
onFocus={focusIkHandle}
|
|
4883
4848
|
onSolve={ikSolve}
|
|
4884
4849
|
onDragEnd={ikDragEnd}
|
|
4850
|
+
focusToolsHidden={fingerEditing} exposureProbeRef={fingerExposureRef}
|
|
4885
4851
|
/>
|
|
4852
|
+
<FingerHandles fingers={handFingers} enabled={fingerEditing && ikMode && ikEditTool === "ik" && !posing && !playMode} onSolve={ikSolve} onDragEnd={ikDragEnd} exposureProbeRef={fingerExposureRef} />
|
|
4886
4853
|
<RangePinMarker
|
|
4887
4854
|
chains={ikChains}
|
|
4888
4855
|
track={rangePinPreview?.track}
|
|
@@ -5268,9 +5235,9 @@ export default function App() {
|
|
|
5268
5235
|
openProject();
|
|
5269
5236
|
}}
|
|
5270
5237
|
starters={STARTER_SCENES}
|
|
5271
|
-
onStarter={(id, name) => {
|
|
5238
|
+
onStarter={(id, name, options) => {
|
|
5272
5239
|
setProjectBrowserOpen(false);
|
|
5273
|
-
void openStarterScene(id, "starter", name);
|
|
5240
|
+
void openStarterScene(id, "starter", name, options?.castStyle);
|
|
5274
5241
|
}}
|
|
5275
5242
|
onNew={(name, options) => {
|
|
5276
5243
|
setProjectBrowserOpen(false);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The closed list of primary Studio controls that carry `data-fid`, for
|
|
2
|
-
// ui:rage_click and ui:dead_click. A click on any other control reports "other".
|
|
2
|
+
// ui:rage_click and ui:dead_click. A click on any other control reports its area (AREA_IDS) or "other".
|
|
3
3
|
// Ids name a control, never a scene object, a name or any text.
|
|
4
4
|
export const CONTROL_IDS = Object.freeze([
|
|
5
5
|
"dock_tab_board", "dock_tab_animation", "dock_tab_assets",
|
|
@@ -13,5 +13,27 @@ export const CONTROL_IDS = Object.freeze([
|
|
|
13
13
|
"board_export", "board_stylize",
|
|
14
14
|
]);
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// A control with no listed `data-fid` reports the Studio area it sits in, by
|
|
17
|
+
// the area's own class; the first match wins, so nested areas come first.
|
|
18
|
+
const AREAS = [
|
|
19
|
+
["other_guide", ".first-success-guide"],
|
|
20
|
+
["other_agent", ".agent-panel"],
|
|
21
|
+
["other_menu", ".menubar, [role='menu']"],
|
|
22
|
+
["other_sculpt", ".sculpt-panel"],
|
|
23
|
+
["other_assets", ".content-browser"],
|
|
24
|
+
["other_board", ".dock-board"],
|
|
25
|
+
["other_timeline", ".bottom-timeline, .studio-dock-slot"],
|
|
26
|
+
["other_inspector", ".inspector-pane, .studio-right-column"],
|
|
27
|
+
["other_viewport", ".studio-left-column"],
|
|
28
|
+
];
|
|
29
|
+
export const AREA_IDS = Object.freeze(AREAS.map(([id]) => id));
|
|
30
|
+
|
|
31
|
+
/** The closed control id of a `data-fid` value; else the area of `element`, else "other". */
|
|
32
|
+
export function controlOf(fid, element = null) {
|
|
33
|
+
if (CONTROL_IDS.includes(fid)) return fid;
|
|
34
|
+
for (const [id, selector] of AREAS) if (element?.closest?.(selector)) return id;
|
|
35
|
+
return "other";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** An area id or "other": a control the Studio does not name. */
|
|
39
|
+
export const isUnnamedControl = (control) => control === "other" || AREA_IDS.includes(control);
|