cozyclay 3.1.6 → 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 +145 -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 +25 -17
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -1
- package/bin/agent/openai-compatible-provider.mjs +24 -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-CEP7jzXB.css → app-CnDkB5WO.css} +1 -1
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/compile-worker-D1pl6G7x.js +6 -0
- 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 +3 -1
- package/src/App.jsx +42 -75
- 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/index.js +2 -1
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- package/src/commands/physics.js +21 -0
- 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 +13 -14
- 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/center-of-mass.js +5 -23
- package/src/motion/contact-project.js +143 -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/fix-collisions.js +1 -1
- package/src/motion/ik-drag.js +93 -1
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion/timeline.css +12 -0
- package/src/motion/timeline.jsx +2 -2
- package/src/motion3/compile-job.js +3 -2
- package/src/motion3/compiler/compile.js +3 -3
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/compiler/spring.js +95 -0
- package/src/motion3/compiler/validate-objects.js +18 -2
- package/src/motion3/compiler/validate-parts.js +1 -0
- package/src/motion3/compiler/vocab.js +1 -1
- package/src/motion3/fix/index.js +8 -4
- package/src/motion3/fix/person.js +110 -0
- package/src/motion3/free-motion.js +49 -11
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/object-pose.js +44 -2
- package/src/motion3/people.js +16 -1
- package/src/motion3/physics-frames.js +72 -0
- package/src/motion3/physics-trail.js +45 -0
- package/src/motion3/physics-view.js +79 -0
- package/src/motion3/pipeline.js +5 -1
- package/src/motion3/qa/ballistic.js +67 -0
- package/src/motion3/qa/com.js +140 -0
- package/src/motion3/qa/ground.js +10 -3
- package/src/motion3/qa/hints-contact.js +21 -7
- package/src/motion3/qa/hints-motion.js +38 -1
- package/src/motion3/qa/key-checks.js +5 -4
- package/src/motion3/qa/patch.js +32 -4
- package/src/motion3/qa/people-checks.js +1 -1
- package/src/motion3/qa/spring-pivot.js +23 -0
- package/src/motion3/qa/style.js +13 -30
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- package/src/motion3/schema/fields.js +4 -2
- package/src/motion3/schema/topics.js +15 -11
- package/src/motion3/schema/types.js +13 -9
- 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/physics-overlay-settings.js +41 -0
- package/src/physics-overlay.js +152 -0
- package/src/physics-overlay.jsx +54 -0
- package/src/physics-toggle.jsx +20 -0
- package/src/pose-fade.js +7 -3
- package/src/posestudio.jsx +15 -28
- 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 +53 -84
- 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-DwXhsJXz.js +0 -1
- package/dist/assets/app-BLS1Zaru.js +0 -5144
- package/dist/assets/compile-worker-or7djI2v.js +0 -6
- package/dist/assets/motion3-compile-Du7UXrWn.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-t45pWCyq.js +0 -13
- package/dist/assets/task-board-CkLZGPkv.js +0 -15
- package/dist/assets/workflow-DJwOUzqJ.js +0 -179
|
@@ -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);
|
package/src/analytics-strict.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { STUDIO_ERROR_CODES } from "./studio-error-codes.js";
|
|
5
5
|
import { ERROR_TYPE_VALUES, sessionEndedPropAllowed } from "./analytics-values.js";
|
|
6
6
|
import { FRICTION_SUMMARY_KEYS, frictionPropAllowed } from "./analytics-friction.js";
|
|
7
|
-
import { CONTROL_IDS } from "./analytics-controls.js";
|
|
7
|
+
import { AREA_IDS, CONTROL_IDS } from "./analytics-controls.js";
|
|
8
8
|
|
|
9
9
|
const TURN_ID_PATTERN = /^[a-f0-9]{32}$/;
|
|
10
10
|
// The friction count vocabulary (analytics-friction.js bucketFrictionCount).
|
|
@@ -18,6 +18,9 @@ const REFUSAL_CODES = new Set([...STUDIO_ERROR_CODES, "other"]);
|
|
|
18
18
|
export const CONNECT_PROVIDERS = Object.freeze(["openai-codex", "anthropic", "anthropic-subscription", "openai", "google",
|
|
19
19
|
"minimax", "minimax-cn", "openrouter", "cliproxy", "openai-compatible"]);
|
|
20
20
|
|
|
21
|
+
/** landing:cta_clicked targets: the landing page's links out and copy buttons. */
|
|
22
|
+
export const LANDING_CTA_TARGETS = Object.freeze(["app", "github", "discord", "npm", "copy_npx", "copy_clone", "copy_mcp", "copy_terminal", "agent_install_doc"]);
|
|
23
|
+
|
|
21
24
|
/** A Studio refusal code as sent: one of STUDIO_ERROR_CODES, else "other". */
|
|
22
25
|
export const refusalCodeOf = (code) => (STUDIO_ERROR_CODES.includes(code) ? code : "other");
|
|
23
26
|
|
|
@@ -51,6 +54,8 @@ const SCHEMAS = Object.freeze({
|
|
|
51
54
|
"studio:refused": {
|
|
52
55
|
code: inSet(REFUSAL_CODES), origin: oneOf("ui", "agent"), phase: oneOf("admission", "commit"),
|
|
53
56
|
group: oneOf("object", "shot", "cast", "scene", "motion", "edit", "task", "agent", "other"),
|
|
57
|
+
// A registered Studio action id (command-bus-telemetry.js sends only those): code-defined, never args (#975).
|
|
58
|
+
command: (value) => typeof value === "string" && value.length <= 48 && /^[a-z][a-zA-Z0-9]*(?:\.[a-zA-Z0-9]+){1,3}$/.test(value),
|
|
54
59
|
},
|
|
55
60
|
// error_type: the app:error label set (analytics.js describeError); never a message or stack.
|
|
56
61
|
"app:render_crash": { error_type: inSet(ERROR_TYPE_VALUES) },
|
|
@@ -69,6 +74,10 @@ const SCHEMAS = Object.freeze({
|
|
|
69
74
|
"guide:dismissed": { mode: oneOf("animation", "storyboard"), reached: oneOf("1", "2", "3", "4") },
|
|
70
75
|
"guide:completed": { mode: oneOf("animation", "storyboard") },
|
|
71
76
|
"agent:panel_toggled": { open: bool, via: oneOf("key", "button") },
|
|
77
|
+
// The Export flyout opening (export-menu-telemetry.js); shots is analytics.js bucketCount.
|
|
78
|
+
"export:menu_opened": {
|
|
79
|
+
surface: oneOf("studio", "embed"), mode: oneOf("animation", "storyboard"), webcodecs: bool, shots: oneOf("0", "1-3", "4-10", "gte11"),
|
|
80
|
+
},
|
|
72
81
|
"object:deleted_soon": { age: inSet(AGE_BUCKETS) },
|
|
73
82
|
"object:delete_undone": { age: inSet(AGE_BUCKETS) },
|
|
74
83
|
"edit:undone": {
|
|
@@ -94,6 +103,8 @@ const SCHEMAS = Object.freeze({
|
|
|
94
103
|
},
|
|
95
104
|
"shortcut:unbound": { combo: oneOf("copy", "paste", "cut", "select_all", "group", "find", "rename", "help") },
|
|
96
105
|
"help:shortcuts_opened": {},
|
|
106
|
+
// The landing page (src/site/landing-analytics.js): which call to action was pressed, never a URL or copied text.
|
|
107
|
+
"landing:cta_clicked": { target: oneOf(...LANDING_CTA_TARGETS) },
|
|
97
108
|
"search:no_results": { surface: oneOf("assets", "outliner"), query_len: oneOf("1", "2-3", "4-8", "gte9") },
|
|
98
109
|
"nav:thrash": { surface: oneOf("dock_tab", "mode", "agent_panel"), switches: oneOf("4-5", "6-10", "gte11"), window_s: oneOf(10) },
|
|
99
110
|
"camera:thrash": { gestures: oneOf("6-10", "gte11"), kind_mix: oneOf("orbit", "fly", "pan", "mixed"), window_s: oneOf(20) },
|
|
@@ -110,11 +121,12 @@ const SCHEMAS = Object.freeze({
|
|
|
110
121
|
gap: (value) => value === "first" || long(value), prev_outcome: oneOf("succeeded", "failed", "cancelled", "none"), turn_index: count,
|
|
111
122
|
attached: bool, task_mode: bool,
|
|
112
123
|
},
|
|
113
|
-
"ui:rage_click": { control: oneOf(...CONTROL_IDS, "other"), clicks: oneOf("3", "4-5", "gte6"), changed: bool },
|
|
114
|
-
"ui:dead_click": { control: oneOf(...CONTROL_IDS, "other"), reason: oneOf("aria_disabled", "refused", "no_effect") },
|
|
124
|
+
"ui:rage_click": { control: oneOf(...CONTROL_IDS, ...AREA_IDS, "other"), clicks: oneOf("3", "4-5", "gte6"), changed: bool },
|
|
125
|
+
"ui:dead_click": { control: oneOf(...CONTROL_IDS, ...AREA_IDS, "other"), reason: oneOf("aria_disabled", "refused", "no_effect") },
|
|
115
126
|
// The update notice (src/update/update-telemetry.js): an install kind and buckets, never a version string or npm output.
|
|
116
127
|
"update:notice_shown": { kind: oneOf("global", "npx"), behind: oneOf("patch", "minor", "major") },
|
|
117
|
-
"update:clicked": { action: oneOf("update_now", "copy_command", "later", "dismiss", "release_notes"), kind: oneOf("global", "npx") },
|
|
128
|
+
"update:clicked": { action: oneOf("update_now", "copy_command", "later", "dismiss", "release_notes", "restart"), kind: oneOf("global", "npx") },
|
|
129
|
+
"update:restarted": { outcome: oneOf("ok", "timeout") },
|
|
118
130
|
"update:finished": {
|
|
119
131
|
outcome: oneOf("ok", "failed"), error_class: oneOf("none", "permission", "network", "node_version", "npm_missing", "other"), duration: long,
|
|
120
132
|
},
|
package/src/analytics.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
+
import { appVersion } from "./app-version.js";
|
|
1
2
|
import { FIRST_EDIT_KINDS, FIRST_EDIT_VERSION } from "./semantic-edit.js";
|
|
2
3
|
import { ERROR_TYPE_VALUES, EXPORT_FAILURE_CODES, exportPropAllowed } from "./analytics-values.js";
|
|
3
4
|
import { bump as bumpFriction, observeLongTasks, snapshot as frictionSnapshot } from "./analytics-friction.js";
|
|
4
5
|
import { STRICT_EVENT_KEYS, strictPropValidator } from "./analytics-strict.js";
|
|
5
|
-
export const MCP_TOOL_CATEGORIES = Object.freeze([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"project_io",
|
|
14
|
-
"other",
|
|
6
|
+
export const MCP_TOOL_CATEGORIES = Object.freeze(["read", "camera", "scene_write", "prompt_authoring", "frame_capture", "motion_generate", "motion_apply", "project_io", "other"]);
|
|
7
|
+
// The live MCP tools (mcp/tool-handlers.mjs liveWorkspaceTools) plus "other".
|
|
8
|
+
// Any other name is sent as "other"; test/verify-analytics.mjs holds the two lists equal.
|
|
9
|
+
export const MCP_TOOL_NAMES = Object.freeze([
|
|
10
|
+
"describe_scene", "describe_shot", "render_prompt", "mark_camera_move", "describe_camera_move", "save_project", "set_camera",
|
|
11
|
+
"frame_shot", "add_character", "place_character", "remove_character", "focus_character", "place_object", "import_mesh",
|
|
12
|
+
"sculpt_object", "compare_sculpt", "group_objects", "update_object", "remove_object", "apply_batch", "add_scene",
|
|
13
|
+
"switch_scene", "open_project", "capture_frame", "studio_commands", "studio_run", "other",
|
|
15
14
|
]);
|
|
16
15
|
export const AGENT_TOOL_CATEGORIES = Object.freeze([
|
|
17
16
|
"workflow_read",
|
|
@@ -52,8 +51,8 @@ export const EXECUTION_TELEMETRY_PROPERTY_KEYS = Object.freeze({
|
|
|
52
51
|
"agent:turn_failed": ["turn_id", "duration_bucket", "failure_code"],
|
|
53
52
|
"agent:turn_cancelled": ["turn_id", "duration_bucket", "failure_code"],
|
|
54
53
|
"agent:result_applied": ["turn_id"],
|
|
55
|
-
"mcp:tool_requested": ["tool_category", "request_id"],
|
|
56
|
-
"mcp:tool_executed": ["tool_category", "outcome", "duration_bucket", "request_id"],
|
|
54
|
+
"mcp:tool_requested": ["tool_category", "tool_name", "request_id"],
|
|
55
|
+
"mcp:tool_executed": ["tool_category", "tool_name", "outcome", "duration_bucket", "request_id"],
|
|
57
56
|
"mcp:result_applied": ["request_id"],
|
|
58
57
|
});
|
|
59
58
|
|
|
@@ -62,6 +61,7 @@ export const EXECUTION_TELEMETRY_VALUES = Object.freeze({
|
|
|
62
61
|
node_count_bucket: new Set(["0", "1-3", "4-10", "gte11"]),
|
|
63
62
|
duration_bucket: new Set(["lt1s", "1-3s", "3-10s", "10-30s", "gte30s"]),
|
|
64
63
|
tool_category: new Set([...MCP_TOOL_CATEGORIES, ...AGENT_TOOL_CATEGORIES]),
|
|
64
|
+
tool_name: new Set(MCP_TOOL_NAMES),
|
|
65
65
|
outcome: new Set(["succeeded", "failed", "uncertain", "cancelled"]),
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -90,8 +90,8 @@ export const EDIT_GROUPS = Object.freeze({
|
|
|
90
90
|
export const EDIT_BUCKET_KEYS = Object.freeze(Object.keys(EDIT_GROUPS).map((group) => `${group}_edit_bucket`));
|
|
91
91
|
export const USE_CASE_VALUES = Object.freeze(["animation", "film", "game", "ad_mv", "personal", "other", "skip"]);
|
|
92
92
|
export const TEAM_VALUES = Object.freeze(["team", "solo", "skip"]);
|
|
93
|
-
//
|
|
94
|
-
export const PREVIS_MODE_VALUES = new Set(["storyboard", "animation"]);
|
|
93
|
+
// Mirror PREVIS_MODES and CAST_STYLES in project.js; kept local so analytics stays import-light.
|
|
94
|
+
export const PREVIS_MODE_VALUES = new Set(["storyboard", "animation"]), CAST_STYLE_VALUES = new Set(["rig", "capsule", "hybrid"]);
|
|
95
95
|
export const UPDATE_STATUS_VALUES = Object.freeze(["latest", "outdated", "unknown"]);
|
|
96
96
|
const DEFAULT_ALLOWED_ORIGINS = Object.freeze([
|
|
97
97
|
"https://cozyclay.org",
|
|
@@ -112,7 +112,7 @@ const EVENT_PROPERTIES = Object.freeze({
|
|
|
112
112
|
"hosted:ticket_created": [],
|
|
113
113
|
"hosted:result_opened": [],
|
|
114
114
|
"hosted:opened_in_studio": [],
|
|
115
|
-
"scene:created": ["scene_source", "previs_mode"],
|
|
115
|
+
"scene:created": ["scene_source", "previs_mode", "cast_style"],
|
|
116
116
|
"storyboard:panel_created": ["source"],
|
|
117
117
|
"storyboard:panel_stylized": ["outcome"],
|
|
118
118
|
"cast:proxy_added": ["surface"],
|
|
@@ -154,7 +154,7 @@ const SCULPT_RUN_MAX_ROUNDS = 10;
|
|
|
154
154
|
const SCULPT_RUN_MAX_DURATION_MS = 86_400_000;
|
|
155
155
|
const EXECUTION_FAILURE_CODES = Object.freeze({
|
|
156
156
|
workflow: new Set(["aborted", "capture_failed", "generation_failed", "unknown"]),
|
|
157
|
-
agent: new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "unknown", "key_refused", "stale_scene", "capability_missing", "busy", "invalid_request"]),
|
|
157
|
+
agent: new Set(["aborted", "auth", "rate_limited", "tool_failed", "upstream", "network", "unknown", "key_refused", "stale_scene", "capability_missing", "busy", "invalid_request"]),
|
|
158
158
|
});
|
|
159
159
|
// Only a type label and a same-origin script location cross the boundary; the
|
|
160
160
|
// error message, stack text, URLs and anything a user typed never do.
|
|
@@ -282,6 +282,8 @@ export function sanitizeProps(event, props) {
|
|
|
282
282
|
else if (key === "tool_category") {
|
|
283
283
|
const categories = event.startsWith("agent:") ? AGENT_TOOL_CATEGORIES : MCP_TOOL_CATEGORIES;
|
|
284
284
|
if (!categories.includes(props[key])) continue;
|
|
285
|
+
} else if (key === "tool_name") {
|
|
286
|
+
if (!event.startsWith("mcp:") || !EXECUTION_TELEMETRY_VALUES.tool_name.has(props[key])) continue;
|
|
285
287
|
} else if (key === "outcome") {
|
|
286
288
|
const outcomes = event === "mcp:tool_executed" ? ["succeeded", "failed", "uncertain", "cancelled"] : ["succeeded", "failed", "cancelled"];
|
|
287
289
|
if (!outcomes.includes(props[key])) continue;
|
|
@@ -311,7 +313,7 @@ export function sanitizeProps(event, props) {
|
|
|
311
313
|
else if (key === "had_target" && typeof value === "boolean") sanitized[key] = value;
|
|
312
314
|
continue;
|
|
313
315
|
}
|
|
314
|
-
if (key === "previs_mode" && !PREVIS_MODE_VALUES.has(props[key])) continue;
|
|
316
|
+
if ((key === "previs_mode" && !PREVIS_MODE_VALUES.has(props[key])) || (key === "cast_style" && !CAST_STYLE_VALUES.has(props[key]))) continue;
|
|
315
317
|
if ((event.startsWith("storyboard:") || event.startsWith("cast:")) && !PREVIS_PROPERTY_VALUES[key]?.has(props[key])) continue;
|
|
316
318
|
if (event === "survey:use_case") {
|
|
317
319
|
if (key === "use_case" && !USE_CASE_VALUES.includes(props[key])) continue;
|
|
@@ -668,7 +670,10 @@ function runtimeConfig() {
|
|
|
668
670
|
function isLoopbackOrigin(origin) {
|
|
669
671
|
try {
|
|
670
672
|
const parsed = new URL(origin);
|
|
671
|
-
|
|
673
|
+
if (parsed.protocol !== "http:") return false;
|
|
674
|
+
// Loopback and LAN hosts both mean the studio is self-served, not hosted.
|
|
675
|
+
if (["127.0.0.1", "localhost"].includes(parsed.hostname)) return true;
|
|
676
|
+
return /^(10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(parsed.hostname);
|
|
672
677
|
} catch {
|
|
673
678
|
return false;
|
|
674
679
|
}
|
|
@@ -715,7 +720,8 @@ export function resolveAnalyticsRuntime({
|
|
|
715
720
|
distribution: "hosted",
|
|
716
721
|
apiKey: env.VITE_POSTHOG_KEY,
|
|
717
722
|
apiHost: env.VITE_POSTHOG_HOST || "https://us.i.posthog.com",
|
|
718
|
-
|
|
723
|
+
// The deploy sets no VITE_APP_VERSION, so the bundled package version stands in.
|
|
724
|
+
appVersion: env.VITE_APP_VERSION || appVersion(),
|
|
719
725
|
installationId: null,
|
|
720
726
|
firstLaunch: false,
|
|
721
727
|
firstLaunchHeardFrom: null,
|
|
@@ -743,7 +749,8 @@ function disabledReason(env) {
|
|
|
743
749
|
return runtime.kind === "disabled" ? runtime.reason : getAnalyticsOptOut() ? "opted out" : null;
|
|
744
750
|
}
|
|
745
751
|
|
|
746
|
-
|
|
752
|
+
/** `lite` (the landing page): $pageview and explicit events only; no session, error, long-task or device signals. */
|
|
753
|
+
export async function initAnalytics({ lite = false } = {}) {
|
|
747
754
|
if (initialized || initPromise) return initPromise;
|
|
748
755
|
// Explicit hosted opt-in only. This flag never changes consent or build
|
|
749
756
|
// policy; npm installations use the CLI-owned state instead.
|
|
@@ -781,7 +788,9 @@ export async function initAnalytics() {
|
|
|
781
788
|
// Hosted visits persist in the browser. Official npm sessions
|
|
782
789
|
// bootstrap from the CLI-owned installation id instead.
|
|
783
790
|
persistence: resolved.distribution === "npm" ? "memory" : "localStorage",
|
|
784
|
-
|
|
791
|
+
// Browser DNT is set by extensions and OS defaults, not by a deliberate
|
|
792
|
+
// per-site choice; the preference toggle remains the explicit signal.
|
|
793
|
+
respect_dnt: false,
|
|
785
794
|
disable_session_recording: true,
|
|
786
795
|
capture_dead_clicks: false,
|
|
787
796
|
capture_performance: false,
|
|
@@ -809,11 +818,8 @@ export async function initAnalytics() {
|
|
|
809
818
|
globalThis.__cozyclayAnalytics = { instance: posthog };
|
|
810
819
|
posthog.capture("$pageview");
|
|
811
820
|
sessionStartedAt = Date.now();
|
|
812
|
-
installSessionEndListeners(resolved, globalProperties);
|
|
813
|
-
|
|
814
|
-
observeLongTasks();
|
|
815
|
-
void recordDeviceProfile();
|
|
816
|
-
if (resolved.distribution === "npm") {
|
|
821
|
+
if (!lite) { installSessionEndListeners(resolved, globalProperties); installErrorListeners(); observeLongTasks(); void recordDeviceProfile(); }
|
|
822
|
+
if (resolved.distribution === "npm" && !lite) {
|
|
817
823
|
track("app:session_started");
|
|
818
824
|
if (resolved.firstLaunch) {
|
|
819
825
|
const heardFrom = resolved.firstLaunchHeardFrom;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The version Help › About names. The npm launcher injects the running
|
|
2
|
+
// package's version as window.__COZYCLAY_RUNTIME__.appVersion; the hosted
|
|
3
|
+
// site and Vite dev have no runtime, so they fall back to package.json the
|
|
4
|
+
// way the start screen does (src/project-browser.jsx).
|
|
5
|
+
import packageInfo from "../package.json" with { type: "json" };
|
|
6
|
+
|
|
7
|
+
export function appVersion(win = globalThis.window, fallback = packageInfo.version) {
|
|
8
|
+
try {
|
|
9
|
+
const runtime = win?.__COZYCLAY_RUNTIME__?.appVersion;
|
|
10
|
+
if (typeof runtime === "string" && runtime.trim()) return runtime.trim();
|
|
11
|
+
} catch { /* a throwing getter reads as no runtime */ }
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Where a Content tile lands: one spawner per payload kind, the same table for
|
|
2
|
+
// the drag onto the stage (App's floor raycast) and the double-click placement
|
|
3
|
+
// at the origin (LibrarySlot). Characters keep their tighter stage clamp (the
|
|
4
|
+
// rig walks, a prop does not); objects, cutouts, meshes and sculpts take the
|
|
5
|
+
// ROOM_LIMIT clamp their creators already apply.
|
|
6
|
+
import { elementByPath } from "./studio-elements.js";
|
|
7
|
+
|
|
8
|
+
const CHARACTER_BOUNDS = elementByPath("character.position").gizmo;
|
|
9
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
10
|
+
|
|
11
|
+
const DROPS = {
|
|
12
|
+
character: (payload, at, spawn) => spawn.spawnCharacter(payload.id,
|
|
13
|
+
clamp(at.x, CHARACTER_BOUNDS.min.x, CHARACTER_BOUNDS.max.x),
|
|
14
|
+
clamp(at.z, CHARACTER_BOUNDS.min.z, CHARACTER_BOUNDS.max.z)),
|
|
15
|
+
object: (payload, at, spawn) => spawn.addSceneObject(payload.objectKind, at),
|
|
16
|
+
image: (payload, at, spawn) => spawn.spawnCutoutAt(payload.assetId, at),
|
|
17
|
+
mesh: (payload, at, spawn) => spawn.spawnMeshAt(payload.assetId, at),
|
|
18
|
+
// A shelf card carries its recipe: object.sculpt makes it through
|
|
19
|
+
// createSculptObject as one undo step, and the new sculpt is selected.
|
|
20
|
+
sculpt: (payload, at, spawn) => {
|
|
21
|
+
const receipt = spawn.runStudioAction("object.sculpt", { recipe: payload.recipe, ...(payload.name ? { name: payload.name } : {}), placement: at });
|
|
22
|
+
const id = receipt?.affectedIds?.[0];
|
|
23
|
+
if (id) spawn.select?.(`object:${id}`);
|
|
24
|
+
return receipt;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Spawn `payload` at the floor point `hit` ({ x, z }; y is ignored). `spawn`
|
|
30
|
+
* names the creators: spawnCharacter, addSceneObject, spawnCutoutAt,
|
|
31
|
+
* spawnMeshAt, runStudioAction and select. An unknown kind does nothing and
|
|
32
|
+
* answers false.
|
|
33
|
+
*/
|
|
34
|
+
export function dropAssetAt(payload, hit, spawn) {
|
|
35
|
+
const drop = DROPS[payload?.kind];
|
|
36
|
+
if (!drop) return false;
|
|
37
|
+
drop(payload, { x: hit.x, z: hit.z }, spawn);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
package/src/asset-pane.css
CHANGED
|
@@ -460,3 +460,37 @@
|
|
|
460
460
|
scrollbar-color: var(--surface-active, #26262a) transparent;
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
+
|
|
464
|
+
@layer regions {
|
|
465
|
+
/* #740: the Sculpts folder's import / export / delete row above its grid. */
|
|
466
|
+
.content-browser .sculpt-shelf-actions {
|
|
467
|
+
display: flex;
|
|
468
|
+
flex-wrap: wrap;
|
|
469
|
+
align-items: center;
|
|
470
|
+
gap: var(--sp-1, 8px);
|
|
471
|
+
margin-bottom: var(--sp-1, 8px);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.content-browser .sculpt-shelf-actions button {
|
|
475
|
+
padding: 3px 10px;
|
|
476
|
+
border: 1px solid var(--line2, #2c2c31);
|
|
477
|
+
border-radius: var(--r-1, 4px);
|
|
478
|
+
background: var(--surface-raised, #1e1e21);
|
|
479
|
+
color: var(--text-row, #c9c9ce);
|
|
480
|
+
font-size: var(--content-font-secondary);
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.content-browser .sculpt-shelf-actions button:hover {
|
|
485
|
+
border-color: var(--select, #e8a33d);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.content-browser .sculpt-shelf-note {
|
|
489
|
+
color: var(--text-3, #636369);
|
|
490
|
+
font-size: var(--content-font-secondary);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.content-browser .sculpt-shelf-delete {
|
|
494
|
+
color: var(--danger, #e5484d);
|
|
495
|
+
}
|
|
496
|
+
}
|
package/src/asset-pane.jsx
CHANGED
|
@@ -7,6 +7,8 @@ import { assetAspect, isImageAssetId, isMeshAssetId, isSupportedMeshType } from
|
|
|
7
7
|
import { assetKind, formatAssetBytes } from "./asset-shelf.js";
|
|
8
8
|
import { assetRecord } from "./scene-asset-cache.js";
|
|
9
9
|
import ResourceStatus from "./resource-status.jsx";
|
|
10
|
+
import { ContentTile, grabProps } from "./content-tile.jsx";
|
|
11
|
+
import SculptShelf from "./sculpt-shelf-tiles.jsx";
|
|
10
12
|
import { logStore } from "./shell/log-store.js";
|
|
11
13
|
import { SearchEmpty } from "./search-friction.js";
|
|
12
14
|
import "./asset-pane.css";
|
|
@@ -130,18 +132,6 @@ function loadThumb(id) {
|
|
|
130
132
|
return thumbCache.get(id);
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
/** The grab wire every card shares: left button only, App owns the rest. */
|
|
134
|
-
function grabProps(onAssetGrab, payload) {
|
|
135
|
-
return {
|
|
136
|
-
onPointerDown: (event) => {
|
|
137
|
-
if (event.button !== 0) return;
|
|
138
|
-
event.preventDefault();
|
|
139
|
-
onAssetGrab?.(payload, event);
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
135
|
function StorageAssetRow({ id, onDelete, deleting, usageCount = 0, graphSignature }) {
|
|
146
136
|
const [thumb, setThumb] = useState(null);
|
|
147
137
|
// Capture the graph observed when the explicit confirmation opens. A render
|
|
@@ -245,29 +235,6 @@ function StorageManager({ unusedAssetIds, usedAssetIds, usageCounts, graphSignat
|
|
|
245
235
|
}
|
|
246
236
|
|
|
247
237
|
|
|
248
|
-
/** The Content grid's one tile grammar: a square thumbnail, an 11px name and
|
|
249
|
-
* a 10px type line. Click selects (amber ring), a left-button press starts the
|
|
250
|
-
* App-owned drag, double-click places the asset at the origin. */
|
|
251
|
-
function ContentTile({ assetKey, label, type, title, preview, selected, onSelect, grab, onPlace, failed = false }) {
|
|
252
|
-
return (
|
|
253
|
-
<button
|
|
254
|
-
type="button"
|
|
255
|
-
className={"content-tile" + (failed ? " is-failed" : "")}
|
|
256
|
-
data-testid="content-asset"
|
|
257
|
-
data-asset-key={assetKey}
|
|
258
|
-
aria-pressed={selected}
|
|
259
|
-
title={title}
|
|
260
|
-
{...(grab ?? {})}
|
|
261
|
-
onClick={() => onSelect(assetKey)}
|
|
262
|
-
onDoubleClick={onPlace ? () => onPlace() : undefined}
|
|
263
|
-
>
|
|
264
|
-
<span className="content-tile-thumb">{preview}</span>
|
|
265
|
-
<span className="content-tile-name">{label}</span>
|
|
266
|
-
<span className="content-tile-type">{type}</span>
|
|
267
|
-
</button>
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
238
|
function useThumb(id) {
|
|
272
239
|
// null = decoding (skeleton), undefined = record gone, object = ready.
|
|
273
240
|
const [thumb, setThumb] = useState(null);
|
|
@@ -375,6 +342,7 @@ const FOLDERS = [
|
|
|
375
342
|
{ id: "cameras", label: () => ko("Cameras", "카메라") },
|
|
376
343
|
{ id: "motions", label: () => ko("Motions", "모션") },
|
|
377
344
|
{ id: "poses", label: () => ko("Poses", "포즈") },
|
|
345
|
+
{ id: "sculpts", label: () => ko("Sculpts", "조형물", "雕塑") },
|
|
378
346
|
];
|
|
379
347
|
|
|
380
348
|
function catalogueTile(entry, props) {
|
|
@@ -403,6 +371,7 @@ export function FolderGrid({ folder, query, ...props }) {
|
|
|
403
371
|
const { imageAssetIds, meshAssetIds, shots = [], takeVersions = [], poses = [], onShotOpen, onTakeOpen, selectedKey, onSelect } = props;
|
|
404
372
|
let tiles = [];
|
|
405
373
|
let empty = null;
|
|
374
|
+
if (folder === "sculpts") return <SculptShelf query={query} {...props} />;
|
|
406
375
|
if (folder === "basic") {
|
|
407
376
|
tiles = OBJECT_LIBRARY
|
|
408
377
|
.filter((entry) => entry.group === "Primitives" && matchesQuery(displayObjectLabel(entry.label), query))
|
package/src/bus-tasks.js
CHANGED
|
@@ -167,7 +167,7 @@ export function createBusTasks({ ports, emit }) {
|
|
|
167
167
|
task.edits.push({ receiptId: recorded.receiptId, values: point.values, keys: point.keys, mark: point.mark });
|
|
168
168
|
task.affectedIds = [...new Set([...task.affectedIds, ...recorded.affectedIds])];
|
|
169
169
|
if (recorded.undo?.historyEntryId) entryTask.set(recorded.undo.historyEntryId, task.taskId);
|
|
170
|
-
for (const other of open.values()) for (const key of point.keys) if (other !== task && other.reads.
|
|
170
|
+
for (const other of open.values()) for (const key of point.keys) if (other !== task && [...other.reads].some(read => keysOverlap(read, key))) {
|
|
171
171
|
other.notices.push({ code: 'DEPENDENCY_CHANGED', message: `${key}, which this task reads, was changed by task ${task.taskId}.` });
|
|
172
172
|
emit?.({ type: 'task.dependencyChanged', taskId: other.taskId, key, by: task.taskId });
|
|
173
173
|
}
|
package/src/character-kind.js
CHANGED
|
@@ -5,25 +5,27 @@
|
|
|
5
5
|
import { isProxyFigure } from "./scenes.js";
|
|
6
6
|
import { isKo, isZh } from "./locale.js";
|
|
7
7
|
import { StudioProtocolError } from "./studio-agent-protocol.js";
|
|
8
|
+
import {
|
|
9
|
+
PROXY_BODY_HEIGHT, PROXY_BODY_RADIUS, PROXY_HEAD_LIFT, PROXY_HEAD_RADIUS, PROXY_SIT_LEG_REACH, PROXY_SIT_TORSO_HEIGHT,
|
|
10
|
+
} from "./proxy-figure-shape.js";
|
|
8
11
|
|
|
9
12
|
export const CHARACTER_CAPABILITIES = Object.freeze(["rig", "ik", "pose", "motion"]);
|
|
10
|
-
const PROXY_BODY_RADIUS = 0.22;
|
|
11
|
-
const PROXY_BODY_HEIGHT = 1.45;
|
|
12
|
-
const PROXY_HEAD_RADIUS = 0.12;
|
|
13
13
|
|
|
14
14
|
/** Return a capsule figure's rendered world-space bounds without a rig. */
|
|
15
15
|
export function proxyFigureBounds(character, placement = character) {
|
|
16
16
|
const posture = character?.posture === "sit" || character?.posture === "lie" ? character.posture : "stand";
|
|
17
|
-
const bodyHeight = posture === "sit" ?
|
|
17
|
+
const bodyHeight = posture === "sit" ? PROXY_SIT_TORSO_HEIGHT : PROXY_BODY_HEIGHT;
|
|
18
|
+
const top = bodyHeight + (PROXY_HEAD_LIFT + PROXY_HEAD_RADIUS);
|
|
18
19
|
const stature = Number.isFinite(placement?.scale) ? placement.scale : Number.isFinite(character?.scale) ? character.scale : 1;
|
|
19
20
|
const x = Number.isFinite(placement?.x) ? placement.x : character?.x ?? 0;
|
|
20
21
|
const y = Number.isFinite(placement?.y) ? placement.y : character?.y ?? 0;
|
|
21
22
|
const z = Number.isFinite(placement?.z) ? placement.z : character?.z ?? 0;
|
|
22
23
|
const yaw = ((Number.isFinite(placement?.rot) ? placement.rot : character?.rot ?? 0) + (posture === "lie" ? 180 : 0)) * Math.PI / 180;
|
|
23
24
|
const [minCorner, maxCorner] = posture === "lie"
|
|
24
|
-
? [[-PROXY_BODY_RADIUS, PROXY_BODY_RADIUS - PROXY_HEAD_RADIUS, -
|
|
25
|
+
? [[-PROXY_BODY_RADIUS, PROXY_BODY_RADIUS - PROXY_HEAD_RADIUS, -top],
|
|
25
26
|
[PROXY_BODY_RADIUS, PROXY_BODY_RADIUS + PROXY_HEAD_RADIUS, 0]]
|
|
26
|
-
|
|
27
|
+
// Sit reaches forward (local +z, the facing direction) with its legs.
|
|
28
|
+
: [[-PROXY_BODY_RADIUS, 0, -PROXY_BODY_RADIUS], [PROXY_BODY_RADIUS, top, posture === "sit" ? PROXY_SIT_LEG_REACH : PROXY_BODY_RADIUS]];
|
|
27
29
|
const min = { x: Infinity, y: Infinity, z: Infinity }, max = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
28
30
|
for (const lx of [minCorner[0], maxCorner[0]]) for (const ly of [minCorner[1], maxCorner[1]]) for (const lz of [minCorner[2], maxCorner[2]]) {
|
|
29
31
|
const sx = lx * stature, sy = ly * stature, sz = lz * stature;
|
package/src/click-friction.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// or element ids. Typing targets and IME composition are ignored outright.
|
|
9
9
|
import { track } from "./analytics.js";
|
|
10
10
|
import { admit, bump } from "./analytics-friction.js";
|
|
11
|
-
import { controlOf } from "./analytics-controls.js";
|
|
11
|
+
import { controlOf, isUnnamedControl } from "./analytics-controls.js";
|
|
12
|
+
import { hintDisabledClick } from "./friction-hint.js";
|
|
12
13
|
|
|
13
14
|
const RAGE_WINDOW_MS = 1000, RAGE_RADIUS_PX = 24, RAGE_MIN = 3, RAGE_COOLDOWN_MS = 10_000, EFFECT_MS = 500;
|
|
14
15
|
const RAGE_PER_CONTROL = 2, DEAD_PER_CONTROL = 2;
|
|
@@ -55,7 +56,7 @@ export function notePointerDown({ target, x, y, event, observe = () => () => {}
|
|
|
55
56
|
const element = target?.closest?.(CONTROL);
|
|
56
57
|
if (!element) return;
|
|
57
58
|
const fid = element.closest("[data-fid]")?.getAttribute("data-fid");
|
|
58
|
-
const control = controlOf(fid);
|
|
59
|
+
const control = controlOf(fid, element);
|
|
59
60
|
const at = now();
|
|
60
61
|
// A rage burst: the same control, within 1 s of its first click and 24 px of where it began.
|
|
61
62
|
if (burst && burst.control === control && burst.element === element && at - burst.at <= RAGE_WINDOW_MS && Math.hypot(x - burst.x, y - burst.y) <= RAGE_RADIUS_PX) {
|
|
@@ -66,9 +67,9 @@ export function notePointerDown({ target, x, y, event, observe = () => () => {}
|
|
|
66
67
|
burst = { control, element, at, x, y, count: 1, changed: () => lastReceiptAt >= since || lastMutationAt >= since };
|
|
67
68
|
burstTimer = setTimeout(finishBurst, RAGE_WINDOW_MS);
|
|
68
69
|
}
|
|
69
|
-
if (disabledOf(element)) { dead(control, "aria_disabled"); return; }
|
|
70
|
+
if (disabledOf(element)) { dead(control, "aria_disabled"); hintDisabledClick(element, control); return; }
|
|
70
71
|
// refused / no_effect: watched only on the closed controls, whose clicks are meant to do something.
|
|
71
|
-
if (control
|
|
72
|
+
if (isUnnamedControl(control)) return;
|
|
72
73
|
const stop = observe(() => { lastMutationAt = now(); });
|
|
73
74
|
setTimeout(() => {
|
|
74
75
|
try { stop(); } catch { /* the observer is advisory */ }
|
|
@@ -39,10 +39,8 @@ const EDIT_KINDS = [
|
|
|
39
39
|
// The take's fields carry its prompt (motion.take.prompt).
|
|
40
40
|
[/^motion\.set$/, 'prompt_block_edit'],
|
|
41
41
|
];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let groups = null;
|
|
45
|
-
const groupOf = kind => (groups ??= new Map(Object.entries(EDIT_GROUPS).flatMap(([group, kinds]) => kinds.map(entry => [entry, group])))).get(kind);
|
|
42
|
+
const GROUP_BY_KIND = new Map(Object.entries(EDIT_GROUPS).flatMap(([group, kinds]) => kinds.map(entry => [entry, group])));
|
|
43
|
+
const groupOf = kind => GROUP_BY_KIND.get(kind);
|
|
46
44
|
/** A command's closed edit kind, or null. `hint`: asset.import's closed `placeAs`, and `rail` when a
|
|
47
45
|
* shot.setCamera patch names a rail field (key names only; values are never read). */
|
|
48
46
|
export function firstEditKindOf(action, { placeAs, rail = false } = {}) {
|
|
@@ -83,15 +81,18 @@ const emit = (event, props) => {
|
|
|
83
81
|
try { send(event, props); } catch { /* telemetry is advisory */ }
|
|
84
82
|
};
|
|
85
83
|
|
|
86
|
-
/** A refusal receipt: the session counter always moves; the event, at most 3 times per (code,
|
|
87
|
-
|
|
84
|
+
/** A refusal receipt: the session counter always moves; the event, at most 3 times per (code, command, origin).
|
|
85
|
+
* `registered`: `id` is an action the registry declares, so it may ride along as `command`; an id the caller
|
|
86
|
+
* made up (an unknown action) is reported by its group alone. */
|
|
87
|
+
export function noteRefusal(value, origin, id, registered = false) {
|
|
88
88
|
// A cancel the caller asked for is not a refusal. A UI one tells a click it was refused (click-friction.js);
|
|
89
89
|
// an agent's or MCP client's refusal at the same moment is not the user's click.
|
|
90
90
|
if (value.code === 'CANCELLED') return;
|
|
91
91
|
if (origin === 'ui') noteBusReceipt(false);
|
|
92
92
|
const by = origin === 'ui' ? 'ui' : 'agent', code = refusalCodeOf(value.code), group = commandGroup(id);
|
|
93
93
|
bump(by === 'ui' ? 'refusals_ui' : 'refusals_agent');
|
|
94
|
-
|
|
94
|
+
const command = registered ? String(id) : undefined;
|
|
95
|
+
if (admit(`studio:refused:${code}:${command ?? group}:${by}`, REFUSALS_PER_KEY)) emit('studio:refused', { code, origin: by, phase: value.phase, group, ...(command ? { command } : {}) });
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
// receiptId -> { at, kind }: when a history receipt was recorded and what it did.
|
package/src/command-bus.js
CHANGED
|
@@ -12,8 +12,6 @@ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
|
12
12
|
const fail = (code, message) => { throw new StudioProtocolError(code, message); };
|
|
13
13
|
const mapResult = (value, success, failure) => value?.then ? value.then(success, failure) : success(value);
|
|
14
14
|
const toastWarnings = toasts => toasts.slice(-12).map(toast => ({ code: 'STUDIO_TOAST', message: [...toast.message].slice(0, 120).join('') }));
|
|
15
|
-
let activeRunDepth = 0;
|
|
16
|
-
export const isBusRunActive = () => activeRunDepth > 0;
|
|
17
15
|
|
|
18
16
|
export function createCommandBus({ registry, ports }) {
|
|
19
17
|
const pending = new Map(), transactions = new Map(), jobs = new Map(), listeners = new Set();
|
|
@@ -211,7 +209,7 @@ export function createCommandBus({ registry, ports }) {
|
|
|
211
209
|
catch (error) { if (level >= 2 || error?.code !== 'INVALID_RECEIPT' || !/8 KiB/.test(error.message)) throw error; }
|
|
212
210
|
}
|
|
213
211
|
}
|
|
214
|
-
function
|
|
212
|
+
function run(id, args = {}, options = {}) {
|
|
215
213
|
const request = { origin: 'ui', commandId: crypto.randomUUID(), ...options };
|
|
216
214
|
if (request.origin === 'ui' && (id === 'edit.undo' || id === 'edit.redo')) ports.finishHistoryGesture?.();
|
|
217
215
|
const before = ports.read(), journal = ports.journal();
|
|
@@ -251,7 +249,7 @@ export function createCommandBus({ registry, ports }) {
|
|
|
251
249
|
if (request.origin === 'ui' && error.uiMessage) ports.showRefusal?.(error.uiMessage);
|
|
252
250
|
const value = refusal(request, before, error, job ? applied : undefined);
|
|
253
251
|
// Session friction and studio:refused, by who asked and what (command-bus-telemetry.js).
|
|
254
|
-
noteRefusal(value, request.origin, action ?? id);
|
|
252
|
+
noteRefusal(value, request.origin, action ?? id, registry.ids?.().includes(action ?? id) === true);
|
|
255
253
|
const recorded = begun ? remember(value) : value;
|
|
256
254
|
if (job && !job.completion) { job.outcome = recorded; job.completion = Promise.resolve(recorded); emit({ type: 'job.completed', jobId: job.id, receipt: recorded }); }
|
|
257
255
|
return recorded;
|
|
@@ -368,18 +366,6 @@ export function createCommandBus({ registry, ports }) {
|
|
|
368
366
|
return answer;
|
|
369
367
|
} catch (error) { return rejected(error); }
|
|
370
368
|
}
|
|
371
|
-
function run(...args) {
|
|
372
|
-
activeRunDepth++;
|
|
373
|
-
try {
|
|
374
|
-
const value = executeRun(...args);
|
|
375
|
-
if (value?.then) return value.finally(() => { activeRunDepth--; });
|
|
376
|
-
activeRunDepth--;
|
|
377
|
-
return value;
|
|
378
|
-
} catch (error) {
|
|
379
|
-
activeRunDepth--;
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
369
|
// noteUndone: edit:undone for the user's own undo (domains/scene-undo.js), which reaches it through the bus.
|
|
384
370
|
return { run, noteUndone,
|
|
385
371
|
// Trusted UI adapter only: wire callers can consume, never mint a token.
|
package/src/commands/cast.js
CHANGED
|
@@ -64,7 +64,8 @@ export function register(registry, ports) {
|
|
|
64
64
|
if (owner().read().some(entry => entry.id === row.id)) fail('INVALID_ARGUMENT', 'Character id already exists.');
|
|
65
65
|
owner().write(rows => [...rows, row]);
|
|
66
66
|
},
|
|
67
|
-
|
|
67
|
+
// An empty stage is legal: a new project starts on one.
|
|
68
|
+
'character.remove': ({ characterId }) => { character(characterId); owner().write(rows => rows.filter(row => row.id !== characterId)); },
|
|
68
69
|
'character.update': ({ characterId, patch: value }) => {
|
|
69
70
|
// Rig <-> capsule figure is a different kind of cast member; rig-to-rig model swaps stay.
|
|
70
71
|
if (Object.hasOwn(value, 'model') && (value.model === PROXY_FIGURE_MODEL) !== isProxyFigure(character(characterId))) fail('INVALID_ARGUMENT', KIND_FIXED_REFUSAL.en);
|
package/src/commands/index.js
CHANGED
|
@@ -17,8 +17,9 @@ import * as exporting from "./export.js";
|
|
|
17
17
|
import * as ai from "./ai.js";
|
|
18
18
|
import * as stage from "./stage.js";
|
|
19
19
|
import * as kits from "./kits.js";
|
|
20
|
+
import * as physics from "./physics.js";
|
|
20
21
|
|
|
21
|
-
export const COMMAND_MODULES = Object.freeze({ shot, cast, motion, motion3, objects, kits, view, scene, project, export: exporting, ai, stage });
|
|
22
|
+
export const COMMAND_MODULES = Object.freeze({ shot, cast, motion, motion3, physics, objects, kits, view, scene, project, export: exporting, ai, stage });
|
|
22
23
|
|
|
23
24
|
export function commandDeclarations(modules = COMMAND_MODULES) {
|
|
24
25
|
return Object.values(modules).flatMap(module => module.declarations.map(declaration => ({
|
package/src/commands/motion3.js
CHANGED
|
@@ -26,6 +26,7 @@ import { normalizeCompileArgs } from "../motion3/lenient.js";
|
|
|
26
26
|
import { createCompileRunner } from "./motion3-compile.js";
|
|
27
27
|
import { createRepairWatch } from "../motion3/repair-watch.js";
|
|
28
28
|
import { peopleOf } from "../motion3/people.js";
|
|
29
|
+
import { hybridCapsuleHint } from "../motion3/hybrid-hint.js";
|
|
29
30
|
|
|
30
31
|
const TIMELINE_FPS = 24;
|
|
31
32
|
const id = { type: "string", minLength: 1 };
|
|
@@ -181,7 +182,8 @@ export function register(registry, ports) {
|
|
|
181
182
|
if (best) timing = put(character, best.take, context) ?? timing;
|
|
182
183
|
const self = best?.specId === own, how = kept ? "; already installed, unchanged" : self ? "; installed as the best take so far"
|
|
183
184
|
: best ? `; not installed; the best take so far, ${best.specId}, installed` : install === "ifClean" ? (installs ? "; installed" : "; not installed") : "";
|
|
184
|
-
const
|
|
185
|
+
const hint = hybridCapsuleHint(ports.storeDomain?.("scenes")?.metadata?.().castStyle, { stuck, qa });
|
|
186
|
+
const output = report(r, { frames: r.frameCount, fps: TIMELINE_FPS, installed: Boolean(wanted) || self, ...(stuck ? { stuck } : {}), ...(hint ? { hint } : {}), ...adjustedOf(notes) }, { ...delta, ...(r.repair ? { repair: r.repair } : {}), ...(timing ? { timing } : {}) });
|
|
185
187
|
// agent-progress.js: viewport ghost, timeline beats, agent panel
|
|
186
188
|
const draft = publishDraft(characterId, { take, installed: installs || kept || self, specId: output.specId, label: r.label, qa, timeline: r.timeline });
|
|
187
189
|
noteDraftCompiled(draft, r, { installed: installs || kept || self, stuck, repaired: Boolean(r.repair), adjusted: notes.length > 0 });
|