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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Multi-view sculpt comparison (#970): one sculpt scored against several views
|
|
2
|
+
// of its subject (the blueprint's front, side and back crops), each by
|
|
3
|
+
// compareSculptToReference, then blended by weight into one IoU.
|
|
4
|
+
import { compareSculptToReference } from "./sculpt-compare.js";
|
|
5
|
+
import { patchSculptRecipe } from "./sculpt-recipe.js";
|
|
6
|
+
|
|
7
|
+
/** The default weight of each view: the front carries the likeness, the side the depth, the back the rest. */
|
|
8
|
+
export const VIEW_WEIGHTS = Object.freeze({ front: 0.6, side: 0.25, back: 0.15 });
|
|
9
|
+
/** A view whose subject's height:width differs from the front's by more than this is drawn at another scale or pose: weight 0. */
|
|
10
|
+
export const VIEW_RATIO_TOLERANCE = 0.35;
|
|
11
|
+
|
|
12
|
+
/** front (|yaw| <= 45), back (|yaw| >= 135) or side, from the camera yaw in degrees. */
|
|
13
|
+
export function viewOfYaw(yaw) {
|
|
14
|
+
const turn = Math.abs(((Number(yaw) % 360) + 540) % 360 - 180);
|
|
15
|
+
return turn <= 45 ? "front" : turn >= 135 ? "back" : "side";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The weight each view gets. `views` are [{ yaw, weight?, heightToWidth }]:
|
|
20
|
+
* an explicit weight replaces the table's; then any view whose height:width
|
|
21
|
+
* differs from the front's by more than VIEW_RATIO_TOLERANCE gets weight 0 with
|
|
22
|
+
* the reason. The front is the front view nearest yaw 0; without one the ratio
|
|
23
|
+
* rule is skipped. Returns [{ weight, excluded? }] in the views' order.
|
|
24
|
+
*/
|
|
25
|
+
export function viewWeights(views) {
|
|
26
|
+
const fronts = views.map((view, index) => ({ index, yaw: Math.abs(Number(view.yaw)) })).filter(({ index }) => viewOfYaw(views[index].yaw) === "front");
|
|
27
|
+
const front = fronts.sort((a, b) => a.yaw - b.yaw)[0];
|
|
28
|
+
const frontRatio = front ? views[front.index].heightToWidth : null;
|
|
29
|
+
return views.map((view, index) => {
|
|
30
|
+
const weight = view.weight ?? VIEW_WEIGHTS[viewOfYaw(view.yaw)];
|
|
31
|
+
if (!front || index === front.index || !(frontRatio > 0)) return { weight };
|
|
32
|
+
const difference = Math.abs(view.heightToWidth / frontRatio - 1);
|
|
33
|
+
if (difference <= VIEW_RATIO_TOLERANCE) return { weight };
|
|
34
|
+
return { weight: 0, excluded: `height:width ${view.heightToWidth.toFixed(2)} differs from the front's ${frontRatio.toFixed(2)} by ${Math.round(difference * 100)}% (over ${VIEW_RATIO_TOLERANCE * 100}%)` };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const round = value => Number(value.toFixed(3));
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Score `recipe` against every view ([{ yaw, image, weight? }], images RGBA) and
|
|
42
|
+
* blend the IoUs by weight. The report is the heaviest view's (its sheet too),
|
|
43
|
+
* with `iou` the weighted one and `views` one row per view.
|
|
44
|
+
*/
|
|
45
|
+
function scoreViews(recipe, views, elevation, weights) {
|
|
46
|
+
const results = views.map(view => compareSculptToReference(recipe, view.image, { yaw: view.yaw, elevation }));
|
|
47
|
+
const used = weights ?? viewWeights(views.map((view, index) => ({ yaw: view.yaw, weight: view.weight, heightToWidth: 1 / results[index].report.widthToHeight.reference })));
|
|
48
|
+
const total = used.reduce((sum, row) => sum + row.weight, 0);
|
|
49
|
+
if (!(total > 0)) throw new Error("No view carries any weight: every view was excluded or given weight 0.");
|
|
50
|
+
const primary = used.reduce((best, row, index) => (row.weight > used[best].weight ? index : best), 0);
|
|
51
|
+
const iou = results.reduce((sum, result, index) => sum + result.report.iou * used[index].weight, 0) / total;
|
|
52
|
+
const rows = results.map(({ report }, index) => ({ view: viewOfYaw(views[index].yaw), yaw: views[index].yaw, weight: round(used[index].weight / total), iou: report.iou,
|
|
53
|
+
widthToHeight: report.widthToHeight, missingByBand: report.missingByBand, extraByPart: report.extraByPart.slice(0, 3), ...(used[index].excluded ? { excluded: used[index].excluded } : {}) }));
|
|
54
|
+
return { result: { report: { ...results[primary].report, iou: round(iou), views: rows }, sheet: results[primary].sheet }, weights: used };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The multi-view twin of scoreSculptAgainstReference: same return shape
|
|
59
|
+
* ({ result, ranked?, refused? }), so sculpt.compare and compare_sculpt answer
|
|
60
|
+
* one way. Candidates are scored on the same views with the same weights.
|
|
61
|
+
*/
|
|
62
|
+
export function scoreSculptAgainstViews(recipe, views, { elevation = 0, candidates } = {}) {
|
|
63
|
+
if (!Array.isArray(views) || !views.length) throw new Error("views needs at least one { yaw, reference }.");
|
|
64
|
+
const { result, weights } = scoreViews(recipe, views, elevation);
|
|
65
|
+
if (!candidates) return { result };
|
|
66
|
+
const ranked = [{ label: "current", result }];
|
|
67
|
+
const refused = [];
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
const patched = patchSculptRecipe(recipe, candidate.edits);
|
|
70
|
+
if (!patched.ok) refused.push({ label: candidate.label, error: patched.error });
|
|
71
|
+
else ranked.push({ label: candidate.label, result: scoreViews(patched.recipe, views, elevation, weights).result });
|
|
72
|
+
}
|
|
73
|
+
ranked.sort((a, b) => b.result.report.iou - a.result.report.iou);
|
|
74
|
+
return { result, ranked, refused };
|
|
75
|
+
}
|
package/src/semantic-edit.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { createCameraBlock } from "./camera-block.js";
|
|
2
|
-
import { isBusRunActive } from "./command-bus.js";
|
|
3
|
-
|
|
4
1
|
// 2: counted from the user's own command-bus receipts (#922); 1 diffed domain values and
|
|
5
2
|
// missed shot, camera and object edits in the real Studio.
|
|
6
3
|
export const FIRST_EDIT_VERSION = 2;
|
|
@@ -10,62 +7,6 @@ export const FIRST_EDIT_KINDS = Object.freeze([
|
|
|
10
7
|
"prompt_block_add", "prompt_block_edit",
|
|
11
8
|
]);
|
|
12
9
|
|
|
13
|
-
// Compare authored values, not object identities. Solvers and pointer callbacks
|
|
14
|
-
// can reconstruct the same pose with floating-point roundoff. Values stay local;
|
|
15
|
-
// only the closed edit-kind vocabulary ever crosses the analytics boundary.
|
|
16
|
-
function equal(a, b) {
|
|
17
|
-
if (a === b) return true;
|
|
18
|
-
if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) < 1e-8;
|
|
19
|
-
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
20
|
-
if (a instanceof Map || b instanceof Map) {
|
|
21
|
-
return a instanceof Map && b instanceof Map && a.size === b.size
|
|
22
|
-
&& [...a].every(([key, value]) => b.has(key) && equal(value, b.get(key)));
|
|
23
|
-
}
|
|
24
|
-
const keys = Object.keys(a);
|
|
25
|
-
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && equal(a[key], b[key]));
|
|
26
|
-
}
|
|
27
|
-
const poseValue = (pose) => ({ bones: pose?.bones ?? {}, rootY: pose?.rootY ?? 0 });
|
|
28
|
-
|
|
29
|
-
const keyValues = (keys = []) => keys.map(({ frame, framing }) => ({ frame, framing }));
|
|
30
|
-
const shotValue = (shot) => ({ ...shot, camera: createCameraBlock(shot.camera), cameraKeys: keyValues(shot.cameraKeys) });
|
|
31
|
-
|
|
32
|
-
export function semanticEditKind(domain, before, after) {
|
|
33
|
-
if (before === after) return null;
|
|
34
|
-
if (domain === "pose") return equal(before, after) ? null : "pose_edit";
|
|
35
|
-
|
|
36
|
-
if (domain === "objects") {
|
|
37
|
-
const added = after.find((item) => !before.some((previous) => previous.id === item.id));
|
|
38
|
-
if (added) return added.renderer === "cutout" ? "cutout_insert" : "object_insert";
|
|
39
|
-
return equal(before, after) ? null : "object_transform";
|
|
40
|
-
}
|
|
41
|
-
if (domain === "characters") {
|
|
42
|
-
if (after.some((item) => !before.some((previous) => previous.id === item.id))) return "object_insert";
|
|
43
|
-
for (const item of after) {
|
|
44
|
-
const previous = before.find((entry) => entry.id === item.id);
|
|
45
|
-
if (previous && !equal(poseValue(previous.pose), poseValue(item.pose))) return "pose_edit";
|
|
46
|
-
}
|
|
47
|
-
// Motion buffers/layer synchronization are passive, not cast transforms.
|
|
48
|
-
const placement = (items) => items.map(({ id, x, y = 0, z, rot, scale = 1, hidden = false, model, tint, subject, identityImage }) => ({ id, x, y, z, rot, scale, hidden, model, tint, subject, identityImage }));
|
|
49
|
-
return equal(placement(before), placement(after)) ? null : "object_transform";
|
|
50
|
-
}
|
|
51
|
-
if (domain === "shots") {
|
|
52
|
-
if (after.some((item) => !before.some((previous) => previous.id === item.id))) return "shot_add";
|
|
53
|
-
for (const item of after) {
|
|
54
|
-
const previous = before.find((entry) => entry.id === item.id);
|
|
55
|
-
if (!previous) continue;
|
|
56
|
-
const camera = createCameraBlock(item.camera), oldCamera = createCameraBlock(previous.camera);
|
|
57
|
-
if (["cameraRail", "craneHeight", "railFollow", "dollyTiming"].some((key) => !equal(camera[key], oldCamera[key]))) return "rail_edit";
|
|
58
|
-
const keys = item.cameraKeys ?? [], oldKeys = previous.cameraKeys ?? [];
|
|
59
|
-
if (keys.length > oldKeys.length || keys.some((key) => {
|
|
60
|
-
const old = oldKeys.find((entry) => entry.frame === key.frame);
|
|
61
|
-
return old && !equal(old.framing, key.framing);
|
|
62
|
-
})) return "camera_key_record";
|
|
63
|
-
}
|
|
64
|
-
return equal(before.map(shotValue), after.map(shotValue)) ? null : "shot_edit";
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
10
|
/** (surface, kind) -> the surface's first edit, once: `${surface}:first_edit` with a closed kind. */
|
|
70
11
|
export function createFirstEditRecorder(emit) {
|
|
71
12
|
// Dedupe once per editor App mount, independently for Studio and Playground.
|
|
@@ -80,31 +21,3 @@ export function createFirstEditRecorder(emit) {
|
|
|
80
21
|
record.seen = surface => seen.has(surface);
|
|
81
22
|
return record;
|
|
82
23
|
}
|
|
83
|
-
|
|
84
|
-
/** The same dedupe over a domain diff (semanticEditKind), for callers that hold before/after values. */
|
|
85
|
-
export function createFirstEditTracker(emit) {
|
|
86
|
-
const record = createFirstEditRecorder(emit);
|
|
87
|
-
return (surface, domain, before, after) => {
|
|
88
|
-
if (record.seen(surface)) return false;
|
|
89
|
-
const kind = semanticEditKind(domain, before, after);
|
|
90
|
-
return kind ? record(surface, kind) : false;
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Synchronous mutation owner shared by UI and live commands. Keeping the before
|
|
95
|
-
// value here avoids stale React closures and analytics inside replayable updater
|
|
96
|
-
// functions. Passive writes (load, history, navigation) advance the same value
|
|
97
|
-
// without reporting; a rejected reducer never publishes or reports anything.
|
|
98
|
-
export function createSemanticState(initial, publish, observe, domain) {
|
|
99
|
-
let current = initial;
|
|
100
|
-
const apply = (update, authored) => {
|
|
101
|
-
const before = current;
|
|
102
|
-
const after = typeof update === "function" ? update(before) : update;
|
|
103
|
-
current = after;
|
|
104
|
-
if (authored && !isBusRunActive()) console.warn(`[bus] semantic edit for ${domain} called outside a bus run`);
|
|
105
|
-
publish(after);
|
|
106
|
-
if (authored) observe(domain, before, after);
|
|
107
|
-
return after;
|
|
108
|
-
};
|
|
109
|
-
return { set: (update) => apply(update, false), edit: (update) => apply(update, true) };
|
|
110
|
-
}
|
package/src/settings-menu.jsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from "react";
|
|
|
2
2
|
import { ko, localeChosen } from "./locale.js";
|
|
3
3
|
import PreferencesDialog from "./shell/PreferencesDialog.jsx";
|
|
4
4
|
|
|
5
|
-
// App settings — language
|
|
5
|
+
// App settings — language — behind one labelled topbar trigger
|
|
6
6
|
// (#193, docs/studio-ui-ia.md R4). Neither item edits the document, so they do
|
|
7
7
|
// not belong on the Save/Export row as bare toggles.
|
|
8
8
|
//
|
|
@@ -38,7 +38,7 @@ export default function SettingsMenu() {
|
|
|
38
38
|
data-testid="settings-menu-trigger"
|
|
39
39
|
aria-expanded={open}
|
|
40
40
|
aria-haspopup="dialog"
|
|
41
|
-
title={ko("Language
|
|
41
|
+
title={ko("Language settings", "언어 설정", "语言设置")}
|
|
42
42
|
ref={triggerRef}
|
|
43
43
|
onClick={() => (open ? close() : setOpen(true))}
|
|
44
44
|
>
|
|
@@ -15,7 +15,7 @@ import PropsPanel from "../panels/PropsPanel.jsx";
|
|
|
15
15
|
import ObjectTransformPanel from "../panels/ObjectTransformPanel.jsx";
|
|
16
16
|
import ObjectLookPanel from "../panels/ObjectLookPanel.jsx";
|
|
17
17
|
import { PoseStudioPanel } from "../posestudio.jsx";
|
|
18
|
-
import {
|
|
18
|
+
import { posePanelControls } from "./pose-controls.js";
|
|
19
19
|
import "../panels/details.css";
|
|
20
20
|
|
|
21
21
|
export default function DetailsSlot() {
|
|
@@ -50,6 +50,7 @@ export default function DetailsSlot() {
|
|
|
50
50
|
savePose, beginWorkspaceResize,
|
|
51
51
|
} = useStudioShell();
|
|
52
52
|
const selection = detailsSelection({ selectedHierarchyId, selectedSceneObject, rigSelection, characters, shot, inspectorHasContent });
|
|
53
|
+
const { resetPose, fingerPanel } = posePanelControls(useStudioShell());
|
|
53
54
|
return (
|
|
54
55
|
<aside className="panel hierarchy-sidebar inspector-sidebar" data-inspector={selectedHierarchyId}>
|
|
55
56
|
{/* Save failures live above the tab content, not inside the Props
|
|
@@ -194,6 +195,8 @@ export default function DetailsSlot() {
|
|
|
194
195
|
pendingIkEdit={pendingIkEdit}
|
|
195
196
|
applyPendingIkEdit={applyPendingIkEdit}
|
|
196
197
|
cancelPendingIkEdit={cancelPendingIkEdit}
|
|
198
|
+
onResetPose={() => resetPose(activeChar.id)}
|
|
199
|
+
fingerPanel={fingerPanel}
|
|
197
200
|
rangePins={rangePins}
|
|
198
201
|
rangePinResiduals={rangePinResiduals}
|
|
199
202
|
rangePinSelection={rangePinSelection}
|
|
@@ -334,11 +337,7 @@ export default function DetailsSlot() {
|
|
|
334
337
|
setToast(ko("Couldn't find the selected pose — pick again", "선택한 포즈를 찾지 못했어요. 다시 골라 주세요"));
|
|
335
338
|
}
|
|
336
339
|
}}
|
|
337
|
-
onReset={() => {
|
|
338
|
-
if (posingChar) castDomain.run('character.setPose', { characterId: posingChar.id, pose: DEFAULT_POSE.id, clearMotion: Boolean(motion) });
|
|
339
|
-
setStudioPick(DEFAULT_POSE.id);
|
|
340
|
-
setToast(ko("Back to the default pose", "기본 포즈로 돌아왔어요"));
|
|
341
|
-
}}
|
|
340
|
+
onReset={() => { if (posingChar) resetPose(posingChar.id); }}
|
|
342
341
|
onSave={savePose}
|
|
343
342
|
onDelete={removePose}
|
|
344
343
|
onClose={closeStudio}
|
|
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|
|
2
2
|
import { useStudioShell } from "./studio-shell-context.js";
|
|
3
3
|
import { ko } from "../locale.js";
|
|
4
4
|
import AssetPane from "../asset-pane.jsx";
|
|
5
|
+
import { dropAssetAt } from "../asset-drop.js";
|
|
5
6
|
|
|
6
7
|
const CONTENT_COLLAPSED_KEY = "cozyclay.dock.content-collapsed.v1";
|
|
7
8
|
|
|
@@ -29,7 +30,7 @@ export default function LibrarySlot({ active = true }) {
|
|
|
29
30
|
manageAssetStorage, setManageAssetStorage, unusedAssetIds, usedAssetIds, usageCounts,
|
|
30
31
|
projectAssetGraphSignature, assetTrash, deleteUnusedAsset, undoDeletedAsset, deletingAssetId,
|
|
31
32
|
projectManifest, shots, selectTimelineShot, allPoses,
|
|
32
|
-
spawnCharacter, addSceneObject, runStudioAction,
|
|
33
|
+
spawnCharacter, addSceneObject, runStudioAction, setSelectedHierarchyId,
|
|
33
34
|
} = useStudioShell();
|
|
34
35
|
const [collapsed, setCollapsed] = useState(readContentCollapsed);
|
|
35
36
|
const [shelfShown, setShelfShown] = useState(false);
|
|
@@ -41,15 +42,12 @@ export default function LibrarySlot({ active = true }) {
|
|
|
41
42
|
}, [active, shelfShown, setBottomTab]);
|
|
42
43
|
|
|
43
44
|
// Double-click in Content: the same payloads the drag carries, placed at
|
|
44
|
-
// the origin through the
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
runStudioAction("asset.import", { assetId: payload.assetId, placeAs: payload.kind === "mesh" ? "mesh" : "cutout", placement: origin });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
45
|
+
// the origin through the same drop table (asset-drop.js).
|
|
46
|
+
const placeAssetAtOrigin = (payload) => dropAssetAt(payload, { x: 0, z: 0 }, {
|
|
47
|
+
spawnCharacter, addSceneObject, runStudioAction, select: setSelectedHierarchyId,
|
|
48
|
+
spawnCutoutAt: (assetId, placement) => runStudioAction("asset.import", { assetId, placeAs: "cutout", placement }),
|
|
49
|
+
spawnMeshAt: (assetId, placement) => runStudioAction("asset.import", { assetId, placeAs: "mesh", placement }),
|
|
50
|
+
});
|
|
53
51
|
|
|
54
52
|
return (
|
|
55
53
|
<section className="assets-pane studio-library" aria-label={ko("Library", "라이브러리")} data-content-collapsed={collapsed || undefined} hidden={!active}>
|
package/src/shell/MenuBar.jsx
CHANGED
|
@@ -4,6 +4,8 @@ import { useStudioShell } from "./studio-shell-context.js";
|
|
|
4
4
|
import SourceOffer from "../source-offer.jsx";
|
|
5
5
|
import { ko } from "../locale.js";
|
|
6
6
|
import { noteShortcutsOpened } from "../shortcut-friction.js";
|
|
7
|
+
import { appVersion } from "../app-version.js";
|
|
8
|
+
import { useExportMenuTelemetry } from "../export-menu-telemetry.js";
|
|
7
9
|
|
|
8
10
|
const MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
|
|
9
11
|
const MOD = MAC ? "⌘" : "Ctrl+";
|
|
@@ -197,10 +199,9 @@ export default function MenuBar({ preferences }) {
|
|
|
197
199
|
? panelCount ? "" : noPanels
|
|
198
200
|
: shots.length ? "" : ko("Add a shot first — a pack describes one cut", "샷을 먼저 추가하세요 — 팩은 컷 하나를 설명합니다");
|
|
199
201
|
|
|
200
|
-
// Export's flyout lives in File, so an open Export menu opens File.
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
|
|
203
|
-
}, [exportMenuOpen]);
|
|
202
|
+
// Export's flyout lives in File, so an open Export menu opens File; each open is counted.
|
|
203
|
+
useEffect(() => { if (exportMenuOpen) setOpen("file"); }, [exportMenuOpen]);
|
|
204
|
+
useExportMenuTelemetry(exportMenuOpen, shell);
|
|
204
205
|
|
|
205
206
|
const closeMenus = (focusTrigger) => {
|
|
206
207
|
const was = open;
|
|
@@ -632,7 +633,6 @@ function MenuDialog({ kind, onClose }) {
|
|
|
632
633
|
document.addEventListener("keydown", onKey);
|
|
633
634
|
return () => document.removeEventListener("keydown", onKey);
|
|
634
635
|
}, [onClose]);
|
|
635
|
-
const version = import.meta.env.VITE_APP_VERSION;
|
|
636
636
|
return createPortal(
|
|
637
637
|
<div className="menubar-dialog-backdrop" onPointerDown={(event) => { if (event.target === event.currentTarget) onClose(); }}>
|
|
638
638
|
<div className="menubar-dialog" role="dialog" aria-modal="true" aria-labelledby={titleId} data-dialog={kind}>
|
|
@@ -645,7 +645,7 @@ function MenuDialog({ kind, onClose }) {
|
|
|
645
645
|
{kind === "about" ? (
|
|
646
646
|
<div className="menubar-dialog-body">
|
|
647
647
|
<p>{ko("Previs studio: stage, pose, camera and motion.", "프리비즈 스튜디오: 배치, 포즈, 카메라, 모션.")}</p>
|
|
648
|
-
|
|
648
|
+
<p className="menubar-dialog-meta">{ko("Version", "버전")} {appVersion()}</p>
|
|
649
649
|
<SourceOffer />
|
|
650
650
|
</div>
|
|
651
651
|
) : (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { ko } from "../locale.js";
|
|
4
|
-
import {
|
|
4
|
+
import { loadAutoColor, preferenceSection, readStoredGridView } from "./preference-pages.jsx";
|
|
5
5
|
import "./preferences.css";
|
|
6
6
|
|
|
7
7
|
const NAV_GROUPS = [
|
|
@@ -24,7 +24,6 @@ export default function PreferencesDialog({ open, onClose, initialSection = "App
|
|
|
24
24
|
closeRef.current = onClose;
|
|
25
25
|
const [activeSection, setActiveSection] = useState(initialSection);
|
|
26
26
|
const [query, setQuery] = useState("");
|
|
27
|
-
const [analyticsOptOut, setAnalyticsOptOutState] = useState(getAnalyticsOptOut);
|
|
28
27
|
const [autoColor, setAutoColor] = useState(() => loadAutoColor());
|
|
29
28
|
const [gridView, setGridView] = useState(() => readStoredGridView(globalThis.localStorage));
|
|
30
29
|
|
|
@@ -69,7 +68,7 @@ export default function PreferencesDialog({ open, onClose, initialSection = "App
|
|
|
69
68
|
}, [open, initialSection]);
|
|
70
69
|
|
|
71
70
|
if (!open || typeof document === "undefined") return null;
|
|
72
|
-
const currentProps = { autoColor, setAutoColor,
|
|
71
|
+
const currentProps = { autoColor, setAutoColor, gridView, setGridView };
|
|
73
72
|
|
|
74
73
|
return createPortal(
|
|
75
74
|
<div className="v2-preferences" data-testid="preferences-dialog-backdrop" onPointerDown={(event) => { if (event.target === event.currentTarget) closeRef.current?.(); }}>
|
|
@@ -6,12 +6,11 @@ import { PRESETS, SHOT_ASPECT_PRESETS } from "../app-stage.jsx";
|
|
|
6
6
|
import { CAMERA_PRESETS } from "../camera-move.js";
|
|
7
7
|
import { PRIME_SET } from "../shot.js";
|
|
8
8
|
import { CatalogueEntries } from "../object-catalog.jsx";
|
|
9
|
-
import { CHARACTER_MODEL_IDS } from "../scenes.js";
|
|
10
9
|
import { elementByPath } from "../studio-elements.js";
|
|
11
10
|
import { saveAutoColor } from "../auto-color.js";
|
|
12
11
|
import { trackFeature } from "../analytics.js";
|
|
13
12
|
import { GUIDE_LABELS, nextGuideMode } from "../shot-guides.js";
|
|
14
|
-
import GhostsMenuGroup from "./GhostsMenuGroup.jsx"; import { addCameraShot } from "./add-camera-shot.js";
|
|
13
|
+
import GhostsMenuGroup from "./GhostsMenuGroup.jsx"; import { addCameraShot } from "./add-camera-shot.js"; import { addCharacterEntries } from "./add-character-entries.js";
|
|
15
14
|
import "./viewport.css";
|
|
16
15
|
|
|
17
16
|
// G1: the single mode+tool pill. Keys 1-5 and W/E/R are bound in App's
|
|
@@ -122,7 +121,7 @@ export default function ViewportToolbar() {
|
|
|
122
121
|
autoColor, setAutoColor, isCharacterSelection, partColoursChoice, embedMode,
|
|
123
122
|
agentCollapsed, addSceneObject, spawnCharacter, characters, flySpeed,
|
|
124
123
|
ikEditTool, setIkEditTool, motion, setRangePinPartPick,
|
|
125
|
-
previsMode, sculptView, requestSculptView, appContext, setToast,
|
|
124
|
+
previsMode, castStyle, sculptView, requestSculptView, sculptParts, appContext, setToast,
|
|
126
125
|
} = useStudioShell();
|
|
127
126
|
const menu = useOverlayMenu();
|
|
128
127
|
|
|
@@ -154,11 +153,11 @@ export default function ViewportToolbar() {
|
|
|
154
153
|
],
|
|
155
154
|
// Motion is compiled by the agent (motion.compile); the department has no tools of its own.
|
|
156
155
|
motion: [],
|
|
157
|
-
// Sculpt mode's W/E/R swing the turntable camera to a reference view.
|
|
158
|
-
sculpt: [
|
|
159
|
-
{ key: "W", id: "front", name: ko("Front view", "정면 보기"), active: sculptView?.view === "front", pick: () => requestSculptView("front") },
|
|
160
|
-
{ key: "E", id: "side", name: ko("Side view", "측면 보기"), active: sculptView?.view === "side", pick: () => requestSculptView("side") },
|
|
161
|
-
{ key: "R", id: "back", name: ko("Back view", "뒷면 보기"), active: sculptView?.view === "back", pick: () => requestSculptView("back") },
|
|
156
|
+
// Sculpt mode's W/E/R swing the turntable camera to a reference view; with a part selected they drive its gizmo.
|
|
157
|
+
sculpt: sculptParts?.selected ? sculptParts.tools : [
|
|
158
|
+
{ key: "W", id: "front", name: ko("Front view (with a part selected, W moves it)", "정면 보기 (부품을 선택하면 W는 이동)"), active: sculptView?.view === "front", pick: () => requestSculptView("front") },
|
|
159
|
+
{ key: "E", id: "side", name: ko("Side view (with a part selected, E rotates it)", "측면 보기 (부품을 선택하면 E는 회전)"), active: sculptView?.view === "side", pick: () => requestSculptView("side") },
|
|
160
|
+
{ key: "R", id: "back", name: ko("Back view (with a part selected, R scales it)", "뒷면 보기 (부품을 선택하면 R은 크기)"), active: sculptView?.view === "back", pick: () => requestSculptView("back") },
|
|
162
161
|
],
|
|
163
162
|
}[workflowMode] ?? [];
|
|
164
163
|
|
|
@@ -277,16 +276,14 @@ export default function ViewportToolbar() {
|
|
|
277
276
|
<CatalogueEntries onPick={(kind) => { addSceneObject(kind); menu.close(); }} />
|
|
278
277
|
<div className="add-object-group">
|
|
279
278
|
<span className="add-object-heading">{ko("Scene", "장면")}</span>
|
|
280
|
-
|
|
281
|
-
type="button"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
<span>{ko("Character", "캐릭터")}</span>
|
|
289
|
-
</button>
|
|
279
|
+
{addCharacterEntries(castStyle).map((entry) => (
|
|
280
|
+
<button key={entry.add} type="button" role="menuitem" className="add-object-item" data-add={entry.add}
|
|
281
|
+
onClick={() => { spawnCharacter(entry.model, characterSpawnX(characters.length), 0); menu.close(); }}
|
|
282
|
+
>
|
|
283
|
+
<span className="add-object-swatch vp-add-swatch-cast" aria-hidden="true" />
|
|
284
|
+
<span>{entry.label()}</span>
|
|
285
|
+
</button>
|
|
286
|
+
))}
|
|
290
287
|
<button
|
|
291
288
|
type="button"
|
|
292
289
|
role="menuitem"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Add > Character offers both bodies in every project (ViewportToolbar). The
|
|
2
|
+
// project's cast style only decides which one comes first: never hide either.
|
|
3
|
+
import { ko } from "../locale.js";
|
|
4
|
+
import { CHARACTER_MODEL_IDS, PROXY_FIGURE_MODEL } from "../scenes.js";
|
|
5
|
+
|
|
6
|
+
const ENTRIES = Object.freeze([
|
|
7
|
+
Object.freeze({ add: "character", model: CHARACTER_MODEL_IDS[0], label: () => "Y Bot" }),
|
|
8
|
+
Object.freeze({ add: "capsule", model: PROXY_FIGURE_MODEL, label: () => ko("Capsule figure", "캡슐 인물") }),
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function addCharacterEntries(castStyle) {
|
|
12
|
+
return castStyle === "capsule" ? [ENTRIES[1], ENTRIES[0]] : ENTRIES;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ko } from "../locale.js";
|
|
2
|
+
import { DEFAULT_POSE } from "../poses.js";
|
|
3
|
+
import { FINGER_PRESETS } from "../motion/fingers.js";
|
|
4
|
+
|
|
5
|
+
// The Pose panel's reset and the focused hand's finger controls, read off the studio shell.
|
|
6
|
+
export function posePanelControls(shell) {
|
|
7
|
+
const { motion, characters, runStudioAction, castDomain, setStudioPick, setToast, cancelPendingIkEdit } = shell;
|
|
8
|
+
// Over a take the default is the take itself: drop the IK edits and keep the motion.
|
|
9
|
+
// Without one, back to the default pose; clearMotion also drops the IK keys (one undo step).
|
|
10
|
+
const resetPose = (characterId) => {
|
|
11
|
+
cancelPendingIkEdit?.();
|
|
12
|
+
const current = characters.find((entry) => entry.id === characterId)?.pose;
|
|
13
|
+
// Already on the default pose: only the IK edits go. A pose write would re-apply the
|
|
14
|
+
// bare pose on Undo over the restored IK keys.
|
|
15
|
+
if (motion || !current || current.id === DEFAULT_POSE.id) {
|
|
16
|
+
runStudioAction("character.clearIkKeys", { characterId });
|
|
17
|
+
if (motion) {
|
|
18
|
+
setToast(ko("Pose edits cleared — the motion stays", "포즈 수정을 지웠어요 — 모션은 그대로예요"));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
} else castDomain.run('character.setPose', { characterId, pose: DEFAULT_POSE.id, clearMotion: true });
|
|
22
|
+
setStudioPick(DEFAULT_POSE.id);
|
|
23
|
+
setToast(ko("Back to the default pose", "기본 포즈로 돌아왔어요"));
|
|
24
|
+
};
|
|
25
|
+
const { handFingers: fingers, ikTick, ikSolve, ikDragEnd, fingerEditing, setFingerEdit } = shell;
|
|
26
|
+
const fingerPanel = {
|
|
27
|
+
fingers, tick: ikTick, editing: fingerEditing, onToggleEdit: () => setFingerEdit(!fingerEditing),
|
|
28
|
+
onCurl: (id, curl) => ikSolve("finger", id, { curl }), onCommit: ikDragEnd,
|
|
29
|
+
onPreset: (name) => { for (const finger of fingers) ikSolve("finger", finger.id, { curl: FINGER_PRESETS[name][finger.name] }); ikDragEnd(); },
|
|
30
|
+
};
|
|
31
|
+
return { resetPose, fingerPanel };
|
|
32
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getAnalyticsOptOut, setAnalyticsOptOut } from "../analytics.js";
|
|
2
1
|
import { loadAutoColor, saveAutoColor } from "../auto-color.js";
|
|
3
2
|
import { readStoredGridView, writeStoredGridView } from "../grid-view.js";
|
|
4
3
|
import { ko } from "../locale.js";
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
} from "./preference-controls.jsx";
|
|
14
13
|
|
|
15
14
|
export function preferenceSection(section, props) {
|
|
16
|
-
const { autoColor, setAutoColor,
|
|
15
|
+
const { autoColor, setAutoColor, gridView, setGridView } = props;
|
|
17
16
|
|
|
18
17
|
switch (section) {
|
|
19
18
|
case "Appearance":
|
|
@@ -31,14 +30,6 @@ export function preferenceSection(section, props) {
|
|
|
31
30
|
<PreferenceRow title={ko("Auto colour", "자동 색")} description={ko("Give scene objects stable display colours.", "씬 오브젝트에 고정된 표시 색을 사용해요.")}>
|
|
32
31
|
<ToggleControl pressed={autoColor} onChange={(value) => { setAutoColor(value); saveAutoColor(value); }} label={ko("Auto colour", "자동 색")} testid="preferences-auto-color" />
|
|
33
32
|
</PreferenceRow>
|
|
34
|
-
<PreferenceRow title={ko("Anonymous analytics", "익명 사용 통계")} description={ko("Help improve CozyClay with anonymous product events.", "익명 제품 이벤트로 CozyClay 개선을 도와요.")}>
|
|
35
|
-
<ToggleControl
|
|
36
|
-
pressed={!analyticsOptOut}
|
|
37
|
-
onChange={async (value) => setAnalyticsOptOutState(await setAnalyticsOptOut(!value))}
|
|
38
|
-
label={ko("Anonymous analytics", "익명 사용 통계")}
|
|
39
|
-
testid="settings-analytics"
|
|
40
|
-
/>
|
|
41
|
-
</PreferenceRow>
|
|
42
33
|
</PreferenceSection>
|
|
43
34
|
</>
|
|
44
35
|
);
|
|
@@ -69,4 +60,4 @@ export function preferenceSection(section, props) {
|
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
// Kept here so the dialog can load a new setting's initial value lazily.
|
|
72
|
-
export {
|
|
63
|
+
export { loadAutoColor, readStoredGridView };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The storyboard contact sheet is drawn on a bare 2d canvas, which has no
|
|
2
|
+
// stylesheet to inherit from: it gets the studio's own type stack explicitly
|
|
3
|
+
// so the sheet reads like the app it came out of.
|
|
4
|
+
export const STORYBOARD_FONT = '12px Inter, "Pretendard", "Noto Sans KR", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
5
|
+
// One unwrapped caption line at 12px inside a 480px cell holds about 64
|
|
6
|
+
// characters before it runs into the next column.
|
|
7
|
+
const STORYBOARD_CAPTION_CHARS = 64;
|
|
8
|
+
|
|
9
|
+
/** Cut a caption to what one unwrapped cell line holds. */
|
|
10
|
+
export function storyboardLine(text) {
|
|
11
|
+
return text.length > STORYBOARD_CAPTION_CHARS ? `${text.slice(0, STORYBOARD_CAPTION_CHARS - 1)}\u2026` : text;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Fold a labelled shot prompt into the one line a board cell can hold. */
|
|
15
|
+
export function storyboardCaption(prompt) {
|
|
16
|
+
return storyboardLine(prompt
|
|
17
|
+
.split("\n")
|
|
18
|
+
.filter((line) => line.startsWith("SHOT:") || line.startsWith("LENS:"))
|
|
19
|
+
.map((line) => line.slice(line.indexOf(":") + 1).trim())
|
|
20
|
+
.join(" · "));
|
|
21
|
+
}
|
|
@@ -13,6 +13,7 @@ import { playbackFrame, usePlaybackSnapshot } from "../motion/playback-frame-sto
|
|
|
13
13
|
// - agentOpen/toggleAgent, preferencesOpen/setPreferencesOpen;
|
|
14
14
|
// - statusText/setStatusText, previewRanges/setPreviewRanges, liveHubStatus,
|
|
15
15
|
// flySpeed (the navigation multiplier, not metres per second);
|
|
16
|
+
// - castStyle ("rig" | "capsule" | "hybrid"), which orders Add > Character;
|
|
16
17
|
// - previsMode ("storyboard" | "animation"), sceneRevision (bumps on every
|
|
17
18
|
// authored edit) and capturePanelThumbnail(shot) (the shot's first frame as
|
|
18
19
|
// a PNG data URL, or null) for the Board dock tab.
|
package/src/shortcut-friction.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { track } from "./analytics.js";
|
|
7
7
|
import { admit, bump } from "./analytics-friction.js";
|
|
8
8
|
import { isImeComposing } from "./ime.js";
|
|
9
|
+
import { hintNoSelection } from "./friction-hint.js";
|
|
9
10
|
|
|
10
11
|
const IGNORED_PER_CLASS = 5, UNBOUND_PER_COMBO = 3, HELP_PER_SESSION = 3;
|
|
11
12
|
let send = track, defer = (run) => setTimeout(run, 0), listening = false;
|
|
@@ -52,7 +53,7 @@ export function noteUnselectedKey(event) {
|
|
|
52
53
|
: event.code === "KeyD" && mod && !event.altKey && !event.shiftKey ? "duplicate"
|
|
53
54
|
: (event.key === "Delete" || event.key === "Backspace") && !mod ? "delete" : null;
|
|
54
55
|
if (!keyClass) return;
|
|
55
|
-
defer(() => { if (!event.defaultPrevented) noteShortcutIgnored(keyClass, "no_selection"); });
|
|
56
|
+
defer(() => { if (!event.defaultPrevented) { noteShortcutIgnored(keyClass, "no_selection"); hintNoSelection(); } });
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const modOnly = (event) => (event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Landing page (index.html) analytics: one $pageview and landing:cta_clicked.
|
|
2
|
+
// Same hosted consent rules as /app/ (no cookies, DNT respected, no autocapture);
|
|
3
|
+
// the lite init skips session, error, long-task and device signals.
|
|
4
|
+
import { initAnalytics, track } from "../analytics.js";
|
|
5
|
+
import { LANDING_CTA_TARGETS } from "../analytics-strict.js";
|
|
6
|
+
|
|
7
|
+
const COPY_TARGETS = [["--via site", "copy_npx"], ["git clone", "copy_clone"], ["mcpServers", "copy_mcp"], ["cclay live", "copy_terminal"]];
|
|
8
|
+
|
|
9
|
+
/** The landing:cta_clicked target for a clicked element, or null when it is not a call to action. */
|
|
10
|
+
function ctaTarget(element) {
|
|
11
|
+
const copy = element.closest?.("button.copy[data-copy]");
|
|
12
|
+
if (copy) return COPY_TARGETS.find(([needle]) => copy.dataset.copy.includes(needle))?.[1] ?? null;
|
|
13
|
+
const href = element.closest?.("a[href]")?.getAttribute("href") ?? "";
|
|
14
|
+
if (href === "/app/" || href.startsWith("/app/?")) return "app";
|
|
15
|
+
if (href.includes("INSTALL_FOR_AGENTS")) return "agent_install_doc";
|
|
16
|
+
if (href.startsWith("https://github.com/NomaDamas/CozyClay")) return "github";
|
|
17
|
+
if (href.startsWith("https://discord.gg/")) return "discord";
|
|
18
|
+
if (href.startsWith("https://www.npmjs.com/package/cozyclay")) return "npm";
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof document !== "undefined") {
|
|
23
|
+
void initAnalytics({ lite: true });
|
|
24
|
+
document.addEventListener("click", (event) => {
|
|
25
|
+
const target = event.target instanceof Element ? ctaTarget(event.target) : null;
|
|
26
|
+
if (target && LANDING_CTA_TARGETS.includes(target)) track("landing:cta_clicked", { target });
|
|
27
|
+
}, { capture: true });
|
|
28
|
+
}
|
package/src/studio-actions.js
CHANGED
|
@@ -41,7 +41,8 @@ const waypointFrame = { ...frame, minimum: 1 };
|
|
|
41
41
|
/** The tracks an IK key stores (src/motion/ik.js IK_TRACKS and FK_TRACKS). A
|
|
42
42
|
* chain track keys its three bones (upper, lower, end); a joint track one. */
|
|
43
43
|
export const STUDIO_IK_CHAIN_TRACKS = freezeStudioData(["leftHand", "rightHand", "leftFoot", "rightFoot"]);
|
|
44
|
-
export const STUDIO_IK_JOINT_TRACKS = freezeStudioData(["hips", "spine", "chest", "neck", "head", "leftShoulder", "rightShoulder"
|
|
44
|
+
export const STUDIO_IK_JOINT_TRACKS = freezeStudioData(["hips", "spine", "chest", "neck", "head", "leftShoulder", "rightShoulder",
|
|
45
|
+
...["left", "right"].flatMap(side => ["Thumb", "Index", "Middle", "Ring", "Pinky"].flatMap(finger => [1, 2, 3].map(joint => `${side}${finger}${joint}`)))]);
|
|
45
46
|
const num = { type: "number" };
|
|
46
47
|
const perBone = items => ({ type: "array", items, minItems: 1, maxItems: 3 });
|
|
47
48
|
const quaternion = input({ x: num, y: num, z: num, w: num });
|
|
@@ -102,7 +103,7 @@ export const STUDIO_ACTIONS = freezeStudioData([
|
|
|
102
103
|
{ id: "character.clearWaypoints", label: "Clear root path", kind: "mutation", undoDomain: "cast", input: input(characterId),
|
|
103
104
|
description: "Remove every root waypoint of the character, leaving its motion unconstrained by a path." },
|
|
104
105
|
{ id: "character.setIkKey", label: "Set IK key", kind: "mutation", undoDomain: "motion", input: input({ ...characterId, frame, tracks: ikTracks }),
|
|
105
|
-
description: "Key a character's IK correction layer at frame, the same key a pose drag bakes. tracks maps a track id to its key: q is the bones' LOCAL rotations as unit quaternions {x,y,z,w}, three for a chain track (leftHand, rightHand, leftFoot, rightFoot: upper, lower and end bone) and one for a joint track (hips, spine, chest, neck, head, leftShoulder, rightShoulder); p is a joint's local position {x,y,z} (the hips' height and lean). Optional baseQ/basePos give the take's own pose the key was made over, so it applies as a delta on the take; chainP gives a chain's three local bone positions; keepTranslations keeps them over the take's; blend (frames, 1-240) is how far this key's correction eases into the take around it, 6 when omitted; pin (1-64 characters) tags the key as baked by that range pin. A key needs q or p. Each named track replaces its key at frame and starts evaluating; other tracks at that frame stay. Keyed frames are in inspect_studio { scope: \"motion\" } ikKeyFrames." },
|
|
106
|
+
description: "Key a character's IK correction layer at frame, the same key a pose drag bakes. tracks maps a track id to its key: q is the bones' LOCAL rotations as unit quaternions {x,y,z,w}, three for a chain track (leftHand, rightHand, leftFoot, rightFoot: upper, lower and end bone) and one for a joint track (hips, spine, chest, neck, head, leftShoulder, rightShoulder, and the finger bones <side><Thumb|Index|Middle|Ring|Pinky><1-3>, e.g. leftIndex2); p is a joint's local position {x,y,z} (the hips' height and lean). Optional baseQ/basePos give the take's own pose the key was made over, so it applies as a delta on the take; chainP gives a chain's three local bone positions; keepTranslations keeps them over the take's; blend (frames, 1-240) is how far this key's correction eases into the take around it, 6 when omitted; pin (1-64 characters) tags the key as baked by that range pin. A key needs q or p. Each named track replaces its key at frame and starts evaluating; other tracks at that frame stay. Keyed frames are in inspect_studio { scope: \"motion\" } ikKeyFrames." },
|
|
106
107
|
{ id: "character.removeIkKey", label: "Delete IK key", kind: "mutation", undoDomain: "motion", input: input({ ...characterId, frame }),
|
|
107
108
|
description: "Delete the character's whole IK key at frame (every track keyed there), like the Full-Body lane's delete." },
|
|
108
109
|
{ id: "character.clearIkKeys", label: "Clear IK keys", kind: "mutation", undoDomain: "motion", input: input(characterId),
|
|
@@ -4,9 +4,9 @@ import { STUDIO_ERROR_CODES, refusalCode } from "./studio-error-codes.js";
|
|
|
4
4
|
import { EFFECT_VALUE_SCHEMA } from "./effects/effect-record.js";
|
|
5
5
|
import { TEXT_VALUE_SCHEMA } from "./text/text-record.js";
|
|
6
6
|
import { wrongType, unknownField, missingField, arrayRefusal, duplicateItem, stringRefusal, enumRefusal, numberRefusal, unionRefusal } from "./studio-schema-errors.js";
|
|
7
|
-
// Mirrors PREVIS_MODES (project.js) and POSTURES (scenes.js). Neither is imported:
|
|
8
|
-
// both pull in three.js, which the agent sidecar must not load. The protocol
|
|
9
|
-
const PREVIS_MODES = ["storyboard", "animation"], POSTURES = ["stand", "sit", "lie"];
|
|
7
|
+
// Mirrors PREVIS_MODES and CAST_STYLES (project.js) and POSTURES (scenes.js). Neither is imported:
|
|
8
|
+
// both pull in three.js, which the agent sidecar must not load. The protocol tests pin the lists to their owners.
|
|
9
|
+
const PREVIS_MODES = ["storyboard", "animation"], CAST_STYLES = ["rig", "capsule", "hybrid"], POSTURES = ["stand", "sit", "lie"];
|
|
10
10
|
|
|
11
11
|
// Sized for the compact index of up to 400 entities (~100 bytes each) beside
|
|
12
12
|
// 24 detailed rows; a small scene stays far below it.
|
|
@@ -232,7 +232,7 @@ const jobSummary = object({ id, characterId: id, state: choices(STUDIO_VARIANTS.
|
|
|
232
232
|
const contextSchema = object({
|
|
233
233
|
schema: literal("studio-context-v1"), host, revision,
|
|
234
234
|
units: object({ distance: literal("m"), angle: literal("deg"), up: literal("+Y"), yawZero: literal("+Z"), yawPositiveToward: literal("+X"), pivot: literal("base"), fps: literal(24), rangeEnd: literal("exclusive") }),
|
|
235
|
-
scene: object({ name, aspect: text(40), floorY: number(), frameCount: integer(), objectCount: integer(), characterCount: integer() }, { previsMode: choices(PREVIS_MODES) }),
|
|
235
|
+
scene: object({ name, aspect: text(40), floorY: number(), frameCount: integer(), objectCount: integer(), characterCount: integer() }, { previsMode: choices(PREVIS_MODES), castStyle: choices(CAST_STYLES) }),
|
|
236
236
|
selection, activeCharacterId: nullable(id), view, shot: nullable(currentShot), camera: nullable(camera),
|
|
237
237
|
entities: array(entity, 24), entityPage: object({ returned: integer(0, 24), total: integer(), truncated: bool, nextCursor: nullable(text(512)) }),
|
|
238
238
|
shots: array(shotSummary, 8), shotsTruncated: bool, assets: array(assetSummary, STUDIO_CONTEXT_LIMITS.assets),
|