cozyclay 1.9.0 → 2.0.0
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 +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
package/src/dualview.jsx
CHANGED
|
@@ -28,6 +28,8 @@ export const SHOT_ASPECT = 16 / 9;
|
|
|
28
28
|
* styles.css) rather than the scene's sky. Kept in sync by eye: a mismatch
|
|
29
29
|
* here reads as a seam around the frame, not as a wrong colour. */
|
|
30
30
|
export const LETTERBOX = new THREE.Color("#1e1e1e");
|
|
31
|
+
// The light theme's bars take the light panel tone, not a black slab.
|
|
32
|
+
export const LETTERBOX_LIGHT = new THREE.Color("#dcdde1");
|
|
31
33
|
/** half-height of the plan frustum, in metres — covers the full camera throw */
|
|
32
34
|
export const PLAN_EXTENT = 12.4;
|
|
33
35
|
|
|
@@ -113,7 +115,7 @@ export function fitAspect(rect, aspect) {
|
|
|
113
115
|
return { x: rect.x + (rect.w - w) / 2, y: rect.y + (rect.h - h) / 2, w, h };
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCamRef, planCamRef, poserCamRef, editorCamRef, ikMode = false, planIsMain, playMode = false, lookThrough = false, insetCollapsed = false, planZoom = 1, shotAspect = SHOT_ASPECT }) {
|
|
118
|
+
export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCamRef, planCamRef, poserCamRef, editorCamRef, ikMode = false, planIsMain, playMode = false, lookThrough = false, insetCollapsed = false, planZoom = 1, shotAspect = SHOT_ASPECT, editorLook = false }) {
|
|
117
119
|
const invalidate = useThree((state) => state.invalidate);
|
|
118
120
|
const lastPoserPose = useRef({ position: new THREE.Vector3(), quaternion: new THREE.Quaternion(), valid: false });
|
|
119
121
|
const interactivePixelRatio = useRef({ base: null, reduced: false, needsFrozenRedraw: false });
|
|
@@ -129,6 +131,22 @@ export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCa
|
|
|
129
131
|
cancelAnimationFrame(f2);
|
|
130
132
|
};
|
|
131
133
|
}, [invalidate]);
|
|
134
|
+
// The theme is read per frame, but a demand-mode canvas draws nothing after
|
|
135
|
+
// a toggle unless something asks for a frame.
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
window.addEventListener("cozyclay:theme-change", invalidate);
|
|
138
|
+
return () => window.removeEventListener("cozyclay:theme-change", invalidate);
|
|
139
|
+
}, [invalidate]);
|
|
140
|
+
// Likewise when the preview card is resized (the dock handle changes
|
|
141
|
+
// --shell-dock): the dock's glass hole grows at once, and without a new
|
|
142
|
+
// frame it shows whatever the canvas last drew there.
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const card = shotPreviewRef?.current;
|
|
145
|
+
if (!card || typeof ResizeObserver === "undefined") return undefined;
|
|
146
|
+
const observer = new ResizeObserver(() => invalidate());
|
|
147
|
+
observer.observe(card);
|
|
148
|
+
return () => observer.disconnect();
|
|
149
|
+
}, [shotPreviewRef, invalidate]);
|
|
132
150
|
const { gl, scene, size } = useThree();
|
|
133
151
|
const edgePass = useMemo(() => {
|
|
134
152
|
const target = new THREE.WebGLRenderTarget(1, 1, {
|
|
@@ -230,7 +248,11 @@ export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCa
|
|
|
230
248
|
// A collapsed inset is just its tag pill: skip the whole inset pass
|
|
231
249
|
// instead of drawing a squished viewport into it.
|
|
232
250
|
const insetRect = insetCollapsed ? { x: 0, y: 0, w: 0, h: 0 } : rectOf(insetRef.current);
|
|
233
|
-
|
|
251
|
+
// The glass shell hides the plan/shot inset with CSS (display: none), which
|
|
252
|
+
// leaves it 0x0. That skips the inset pass like a collapsed one; it must not
|
|
253
|
+
// skip the main pane, or the stage stays blank.
|
|
254
|
+
const insetOff = insetCollapsed || (!playMode && insetRect.w < 2);
|
|
255
|
+
if (mainRect.w < 2) return;
|
|
234
256
|
|
|
235
257
|
const planPane = planIsMain ? mainRect : insetRect;
|
|
236
258
|
const shotPane = planIsMain ? insetRect : mainRect;
|
|
@@ -292,11 +314,13 @@ export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCa
|
|
|
292
314
|
// rather than information. They take the editor's own tone instead, and
|
|
293
315
|
// the scene draw is scissored to the image so the sky inside the frame is
|
|
294
316
|
// untouched.
|
|
317
|
+
const uiTheme = typeof document !== "undefined" ? document.documentElement.dataset.theme : undefined;
|
|
318
|
+
const letterbox = uiTheme === "light" ? LETTERBOX_LIGHT : LETTERBOX;
|
|
295
319
|
const draw = (camera, pane, imageRect, ink = true) => {
|
|
296
320
|
const glY = size.height - (pane.y + pane.h);
|
|
297
321
|
gl.setScissorTest(true);
|
|
298
322
|
gl.setScissor(pane.x, glY, pane.w, pane.h);
|
|
299
|
-
gl.setClearColor(
|
|
323
|
+
gl.setClearColor(letterbox, 1);
|
|
300
324
|
gl.clear(true, true, false);
|
|
301
325
|
const img = imageRect ?? pane;
|
|
302
326
|
const imgY = size.height - (img.y + img.h);
|
|
@@ -321,9 +345,14 @@ export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCa
|
|
|
321
345
|
// used to trace an empty frame around every pasted image — the
|
|
322
346
|
// "border that will not go away". Hiding the card from the prepass
|
|
323
347
|
// leaves the wall behind it continuous, so no edge is drawn.
|
|
348
|
+
// The deck takes no ink either (userData.noInk, room.jsx): seen at a
|
|
349
|
+
// grazing angle its depth runs away fast enough that the depth
|
|
350
|
+
// kernel inked the whole far floor as a dark band under the
|
|
351
|
+
// horizon. Without it in the prepass, figures still outline
|
|
352
|
+
// against the cleared depth behind them.
|
|
324
353
|
const cutouts = [];
|
|
325
354
|
scene.traverse((node) => {
|
|
326
|
-
if (node.userData?.cutoutTexture !== undefined && node.visible) {
|
|
355
|
+
if ((node.userData?.cutoutTexture !== undefined || node.userData?.noInk) && node.visible) {
|
|
327
356
|
cutouts.push(node);
|
|
328
357
|
node.visible = false;
|
|
329
358
|
}
|
|
@@ -350,9 +379,38 @@ export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCa
|
|
|
350
379
|
gl.autoClear = true;
|
|
351
380
|
}
|
|
352
381
|
};
|
|
382
|
+
// The stage is the grey workbench in every pane and in every capture
|
|
383
|
+
// (STAGE_BACKGROUND, room.jsx), so an editor pane is a plain draw.
|
|
384
|
+
const drawEditorPane = draw;
|
|
353
385
|
const drawVisibleInset = (...args) => {
|
|
354
|
-
if (!
|
|
386
|
+
if (!insetOff) draw(...args);
|
|
387
|
+
};
|
|
388
|
+
// The shot card. Every main-pane draw above covers the whole canvas,
|
|
389
|
+
// including the pixels under the card, so the card is repainted every
|
|
390
|
+
// frame — a camera drag used to leave the editor's pixels showing through
|
|
391
|
+
// it. Only the ink pass waits for the gesture to settle.
|
|
392
|
+
const drawPreviewCard = () => {
|
|
393
|
+
const previewEl = shotPreviewRef?.current;
|
|
394
|
+
if (!previewEl || previewEl.hidden) return;
|
|
395
|
+
const previewRect = rectOf(previewEl);
|
|
396
|
+
// The inset card's header strip (src/shell/viewport.css) is chrome:
|
|
397
|
+
// the frame is drawn into the card body below it.
|
|
398
|
+
const header = parseFloat(getComputedStyle(previewEl).getPropertyValue("--shot-preview-header")) || 0;
|
|
399
|
+
previewRect.y += header;
|
|
400
|
+
previewRect.h -= header;
|
|
401
|
+
if (previewRect.w < 2 || previewRect.h < 2) return;
|
|
402
|
+
const previewMask = shotCam.layers.mask;
|
|
403
|
+
shotCam.layers.disable(GIZMO_LAYER);
|
|
404
|
+
draw(shotCam, previewRect, fitAspect(previewRect, shotAspect), !navigatingCamera);
|
|
405
|
+
shotCam.layers.mask = previewMask;
|
|
355
406
|
};
|
|
407
|
+
// The heads' facing marks are the exported frame's heading cue, not
|
|
408
|
+
// something to look at: the studio hides them in every on-screen draw and
|
|
409
|
+
// puts them back before the frame ends, so captures (which render outside
|
|
410
|
+
// this loop) still carry them.
|
|
411
|
+
const marks = [];
|
|
412
|
+
if (editorLook) scene.traverse((node) => { if (node.userData.facingMark) marks.push(node); });
|
|
413
|
+
for (const mark of marks) mark.visible = false;
|
|
356
414
|
|
|
357
415
|
gl.autoClear = true;
|
|
358
416
|
shotCam.aspect = shotAspect;
|
|
@@ -378,60 +436,58 @@ export function DualRender({ stageRef, mainRef, insetRef, shotPreviewRef, shotCa
|
|
|
378
436
|
// tested first, so the Workflow embed and the playground rail land HERE —
|
|
379
437
|
// the framed player — and never in the editing draw below that keeps the
|
|
380
438
|
// gizmo layer, the plan inset, and fly controls on the shot camera.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
439
|
+
try {
|
|
440
|
+
if (playMode) {
|
|
441
|
+
// PlayView (Unity Game view): the shot camera owns the whole pane —
|
|
442
|
+
// no plan inset, no editing chrome, just the framed output. Editor
|
|
443
|
+
// furniture (selection cage, gizmo, grid, pins) lives on
|
|
444
|
+
// GIZMO_LAYER and is dropped for this draw, mask restored after.
|
|
445
|
+
const playMask = shotCam.layers.mask;
|
|
446
|
+
shotCam.layers.disable(GIZMO_LAYER);
|
|
447
|
+
draw(shotCam, mainRect, fitAspect(mainRect, shotAspect));
|
|
448
|
+
shotCam.layers.mask = playMask;
|
|
449
|
+
} else if (ikMode && poserCam) {
|
|
450
|
+
// IK mode: the main pane is the poser working view (free navigation,
|
|
451
|
+
// handle layer visible); the inset is the FROZEN shot camera — the
|
|
452
|
+
// separate camera-placement screen the framing lives on.
|
|
453
|
+
poserCam.aspect = mainRect.w / mainRect.h;
|
|
454
|
+
poserCam.updateProjectionMatrix();
|
|
455
|
+
drawEditorPane(poserCam, mainRect, null, !navigatingCamera);
|
|
456
|
+
if (!navigatingCamera || redrawFrozenPanes) drawVisibleInset(shotCam, insetRect, fitAspect(insetRect, shotAspect), !navigatingCamera);
|
|
457
|
+
drawPreviewCard();
|
|
458
|
+
} else if (planIsMain) {
|
|
459
|
+
draw(planCam, planPane, null, false);
|
|
460
|
+
if (!navigatingCamera || redrawFrozenPanes) drawVisibleInset(shotCam, shotPane, fitAspect(shotPane, shotAspect), !navigatingCamera);
|
|
461
|
+
drawPreviewCard();
|
|
462
|
+
} else if (editorCam && !lookThrough) {
|
|
463
|
+
// Split-camera editing: the main pane is the EDITOR camera — free
|
|
464
|
+
// navigation that never touches the recording. The shot camera only
|
|
465
|
+
// appears in its own preview pane, framed to the export aspect and
|
|
466
|
+
// stripped of editing chrome, exactly like an exported frame.
|
|
467
|
+
editorCam.aspect = mainRect.w / mainRect.h;
|
|
468
|
+
editorCam.updateProjectionMatrix();
|
|
469
|
+
// A right-drag is a camera gesture even with IK off. Keep the main
|
|
470
|
+
// image readable while the camera is moving by deferring the expensive
|
|
471
|
+
// edge/ink pass until the gesture ends; the plan inset stays frozen.
|
|
472
|
+
drawEditorPane(editorCam, mainRect, null, !navigatingCamera);
|
|
473
|
+
if (!navigatingCamera || redrawFrozenPanes) drawVisibleInset(planCam, planPane, null, false);
|
|
474
|
+
drawPreviewCard();
|
|
475
|
+
} else {
|
|
476
|
+
// Shot camera in the main pane WITH the editing chrome. Look-through
|
|
477
|
+
// (the PiP expand) lands here so the operator
|
|
478
|
+
// can fly the recording lens with the same bindings as the free camera.
|
|
479
|
+
// The QA hook (`window.__cozyclay.setLookThrough`) draws the same path.
|
|
480
|
+
// Look-through is the recording lens: it keeps the authored stage.
|
|
481
|
+
draw(shotCam, shotPane, fitAspect(shotPane, shotAspect), !navigatingCamera);
|
|
482
|
+
if (!navigatingCamera || redrawFrozenPanes) drawVisibleInset(planCam, planPane, null, false);
|
|
483
|
+
drawPreviewCard();
|
|
422
484
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
draw(shotCam, shotPane, fitAspect(shotPane, shotAspect), !navigatingCamera);
|
|
429
|
-
if (!navigatingCamera || redrawFrozenPanes) drawVisibleInset(planCam, planPane, null, false);
|
|
485
|
+
if (redrawFrozenPanes) pixelState.needsFrozenRedraw = false;
|
|
486
|
+
} finally {
|
|
487
|
+
for (const mark of marks) mark.visible = true;
|
|
488
|
+
gl.setScissorTest(false);
|
|
489
|
+
gl.setViewport(0, 0, size.width, size.height);
|
|
430
490
|
}
|
|
431
|
-
if (redrawFrozenPanes) pixelState.needsFrozenRedraw = false;
|
|
432
|
-
|
|
433
|
-
gl.setScissorTest(false);
|
|
434
|
-
gl.setViewport(0, 0, size.width, size.height);
|
|
435
491
|
}, 1);
|
|
436
492
|
|
|
437
493
|
return null;
|
package/src/facing-marks.js
CHANGED
|
@@ -66,6 +66,9 @@ export function addFacingMarks(clone) {
|
|
|
66
66
|
if (spec.rotation) mesh.rotation.set(...spec.rotation);
|
|
67
67
|
mesh.castShadow = true;
|
|
68
68
|
mesh.frustumCulled = false;
|
|
69
|
+
// The marks are for the exported frame (the model's heading cue); the
|
|
70
|
+
// studio's editor panes hide them per draw (dualview.jsx) by this tag.
|
|
71
|
+
mesh.userData.facingMark = true;
|
|
69
72
|
// The head bone's local +Z is the face direction on both rigs.
|
|
70
73
|
head.add(mesh);
|
|
71
74
|
}
|
|
@@ -3,8 +3,8 @@ import { ko } from "./locale.js";
|
|
|
3
3
|
|
|
4
4
|
const STEPS = [
|
|
5
5
|
{
|
|
6
|
-
en: "Select a character in the
|
|
7
|
-
ko: "
|
|
6
|
+
en: "Select a character in the Outliner.",
|
|
7
|
+
ko: "아웃라이너에서 캐릭터를 선택하세요.",
|
|
8
8
|
hintEn: "Click Character 1 (or its row) to make it active.",
|
|
9
9
|
hintKo: "캐릭터 1(또는 캐릭터 행)을 클릭하면 활성화됩니다.",
|
|
10
10
|
},
|
|
@@ -28,7 +28,7 @@ const STEPS = [
|
|
|
28
28
|
},
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
-
/** A
|
|
31
|
+
/** A compact, dismissible first-run checklist that follows the v2 shell grammar. */
|
|
32
32
|
export default function FirstSuccessGuide({ open, onDismiss }) {
|
|
33
33
|
const [step, setStep] = useState(0);
|
|
34
34
|
useEffect(() => {
|
|
@@ -38,38 +38,38 @@ export default function FirstSuccessGuide({ open, onDismiss }) {
|
|
|
38
38
|
const complete = step >= STEPS.length;
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
|
-
<aside className="first-success-guide" role="dialog" aria-labelledby="first-success-guide-title" aria-describedby="first-success-guide-description">
|
|
42
|
-
<div className="first-success-guide-head">
|
|
41
|
+
<aside className="v2-first-success-guide first-success-guide" role="dialog" aria-labelledby="first-success-guide-title" aria-describedby="first-success-guide-description">
|
|
42
|
+
<div className="v2-first-success-guide-head first-success-guide-head">
|
|
43
43
|
<div>
|
|
44
44
|
<strong id="first-success-guide-title">{ko("Your first 60 seconds", "첫 60초")}</strong>
|
|
45
45
|
<span>{complete ? ko("Core loop complete", "핵심 흐름 완료") : ko(`Step ${step + 1} of ${STEPS.length}`, `${step + 1} / ${STEPS.length} 단계`)}</span>
|
|
46
46
|
</div>
|
|
47
|
-
<button type="button" className="first-success-guide-close" onClick={onDismiss} aria-label={ko("Dismiss guide", "가이드 닫기")}>×</button>
|
|
47
|
+
<button type="button" className="v2-first-success-guide-close first-success-guide-close" onClick={onDismiss} aria-label={ko("Dismiss guide", "가이드 닫기")}>×</button>
|
|
48
48
|
</div>
|
|
49
49
|
{complete ? (
|
|
50
|
-
<div className="first-success-guide-complete" id="first-success-guide-description" role="status">
|
|
51
|
-
<div className="first-success-guide-check">✓</div>
|
|
50
|
+
<div className="v2-first-success-guide-complete first-success-guide-complete" id="first-success-guide-description" role="status">
|
|
51
|
+
<div className="v2-first-success-guide-check first-success-guide-check">✓</div>
|
|
52
52
|
<strong>{ko("You made your first shot.", "첫 샷을 만들었어요.")}</strong>
|
|
53
53
|
<p>{ko("Select, move, key, and play are the core CozyClay loop. You can close this guide and keep blocking.", "선택하고, 움직이고, 키를 찍고, 재생하는 것이 CozyClay의 핵심 흐름입니다. 가이드를 닫고 계속 블로킹하세요.")}</p>
|
|
54
|
-
<button type="button" className="btn primary" onClick={onDismiss}>{ko("Continue editing", "편집 계속하기")}</button>
|
|
54
|
+
<button type="button" className="v2-first-success-guide-next first-success-guide-next btn primary" onClick={onDismiss}>{ko("Continue editing", "편집 계속하기")}</button>
|
|
55
55
|
</div>
|
|
56
56
|
) : (
|
|
57
57
|
<>
|
|
58
|
-
<p id="first-success-guide-description" className="first-success-guide-intro">{ko("Follow these four actions to see a result quickly.", "네 가지 동작으로 빠르게 결과를 확인해 보세요.")}</p>
|
|
59
|
-
<ol className="first-success-guide-steps">
|
|
58
|
+
<p id="first-success-guide-description" className="v2-first-success-guide-intro first-success-guide-intro">{ko("Follow these four actions to see a result quickly.", "네 가지 동작으로 빠르게 결과를 확인해 보세요.")}</p>
|
|
59
|
+
<ol className="v2-first-success-guide-steps first-success-guide-steps">
|
|
60
60
|
{STEPS.map((item, index) => (
|
|
61
61
|
<li key={item.en} className={index < step ? "done" : index === step ? "current" : ""}>
|
|
62
|
-
<span className="first-success-guide-step-mark">{index < step ? "✓" : index + 1}</span>
|
|
62
|
+
<span className="v2-first-success-guide-step-mark first-success-guide-step-mark">{index < step ? "✓" : index + 1}</span>
|
|
63
63
|
<div><strong>{ko(item.en, item.ko)}</strong>{index === step && <span>{ko(item.hintEn, item.hintKo)}</span>}</div>
|
|
64
64
|
</li>
|
|
65
65
|
))}
|
|
66
66
|
</ol>
|
|
67
|
-
<button type="button" className="
|
|
67
|
+
<button type="button" className="v2-first-success-guide-next first-success-guide-next btn primary" onClick={() => setStep((value) => value + 1)}>
|
|
68
68
|
{ko(step === STEPS.length - 1 ? "Mark complete" : "I did this", step === STEPS.length - 1 ? "완료로 표시" : "했어요")}
|
|
69
69
|
</button>
|
|
70
70
|
</>
|
|
71
71
|
)}
|
|
72
|
-
<button type="button" className="first-success-guide-skip" onClick={onDismiss}>{ko("Skip guide", "가이드 건너뛰기")}</button>
|
|
72
|
+
<button type="button" className="v2-first-success-guide-skip first-success-guide-skip" onClick={onDismiss}>{ko("Skip guide", "가이드 건너뛰기")}</button>
|
|
73
73
|
</aside>
|
|
74
74
|
);
|
|
75
75
|
}
|
package/src/grid-view.js
CHANGED
|
@@ -6,11 +6,24 @@
|
|
|
6
6
|
|
|
7
7
|
export const GRID_VIEW_STORAGE_KEY = "cozyclay.grid-view.v1";
|
|
8
8
|
|
|
9
|
-
// Blender's solid-mode look: a dark grey void, not true black.
|
|
10
|
-
// the
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
9
|
+
// Blender's solid-mode look: a dark grey void, not true black. Under the dark
|
|
10
|
+
// UI theme the stage wears it — editor, shot preview and exports — so the set
|
|
11
|
+
// reads as one grey workbench; the grid view only trades the deck for the
|
|
12
|
+
// reference grid (and keeps the dark void under both themes: its line palette
|
|
13
|
+
// is tuned for it). Fog matches the background so distance still dissolves
|
|
14
|
+
// into the same neutral. The far plane stays below the capture rig's
|
|
15
|
+
// CAPTURE_FOG_NEAR (55) so pushing near past far still disables fog for
|
|
16
|
+
// exported frames.
|
|
17
|
+
export const STAGE_BACKGROUND = "#232428";
|
|
18
|
+
// The light UI theme's stage: a photo-studio cyclorama. A near-white sky the
|
|
19
|
+
// deck fades into, so the floor has no horizon line and the figures stand in
|
|
20
|
+
// a bright, soft room that sits in the light shell instead of punching a
|
|
21
|
+
// dark hole in it.
|
|
22
|
+
// Matched to the lit deck's far tone (room.jsx FLOOR_LIGHT after tone
|
|
23
|
+
// mapping), so fog blends floor into sky without a step.
|
|
24
|
+
export const STAGE_BACKGROUND_LIGHT = "#e7e8eb";
|
|
25
|
+
export const stageBackground = (theme) => (theme === "light" ? STAGE_BACKGROUND_LIGHT : STAGE_BACKGROUND);
|
|
26
|
+
export const GRID_BACKGROUND = STAGE_BACKGROUND;
|
|
14
27
|
export const GRID_FOG = Object.freeze({ color: GRID_BACKGROUND, near: 30, far: 54 });
|
|
15
28
|
|
|
16
29
|
// Line palette, tuned against the dark background the way Blender's theme
|
package/src/hierarchy-model.js
CHANGED
|
@@ -101,38 +101,45 @@ function characterRowId(entry, listIndex) {
|
|
|
101
101
|
return listIndex === 0 ? "characterA" : listIndex === 1 ? "characterB" : `character:${entry.id}`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
106
|
-
|
|
104
|
+
/** Every cast member as `characterId → row id`, including hidden ones.
|
|
105
|
+
* A prop attached to a hidden character stays under that row. An unknown
|
|
106
|
+
* characterId still has no row and falls back to Props. */
|
|
107
|
+
function characterRows(characters) {
|
|
107
108
|
const rows = new Map();
|
|
108
109
|
if (!Array.isArray(characters)) return rows;
|
|
109
110
|
characters.forEach((entry, listIndex) => {
|
|
110
|
-
if (!entry || entry.
|
|
111
|
+
if (!entry || typeof entry.id !== "string") return;
|
|
111
112
|
rows.set(entry.id, characterRowId(entry, listIndex));
|
|
112
113
|
});
|
|
113
114
|
return rows;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
function objectRow(object, label) {
|
|
118
|
+
return {
|
|
119
|
+
id: `object:${object.id}`,
|
|
120
|
+
label,
|
|
121
|
+
kind: "object",
|
|
122
|
+
hidden: object.hidden === true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
117
127
|
// An attached prop is carried BY a character, so it reads under that
|
|
118
128
|
// character rather than in the flat Props list it no longer follows.
|
|
119
|
-
const
|
|
129
|
+
const characterRowsById = characterRows(characters);
|
|
120
130
|
const attachedRows = new Map(); // character row id → attached object rows
|
|
121
131
|
const attachedIds = new Set();
|
|
122
132
|
for (const object of sceneObjects) {
|
|
123
133
|
const attach = object?.attach;
|
|
124
134
|
if (!attach || typeof attach !== "object") continue;
|
|
125
|
-
const rowId =
|
|
126
|
-
// A dangling
|
|
135
|
+
const rowId = characterRowsById.get(attach.characterId);
|
|
136
|
+
// A dangling characterId keeps the object in Props: a prop that
|
|
127
137
|
// vanishes from the tree is worse than one filed in the wrong place.
|
|
138
|
+
// A hidden character still has a row, so the prop stays under it.
|
|
128
139
|
if (!rowId) continue;
|
|
129
140
|
attachedIds.add(object.id);
|
|
130
141
|
const boneLabel = attachBoneLabel(attach.bone);
|
|
131
|
-
const row = {
|
|
132
|
-
id: `object:${object.id}`,
|
|
133
|
-
label: boneLabel ? `${object.name} · ${boneLabel}` : object.name,
|
|
134
|
-
kind: "object",
|
|
135
|
-
};
|
|
142
|
+
const row = objectRow(object, boneLabel ? `${object.name} · ${boneLabel}` : object.name);
|
|
136
143
|
if (attachedRows.has(rowId)) attachedRows.get(rowId).push(row);
|
|
137
144
|
else attachedRows.set(rowId, [row]);
|
|
138
145
|
}
|
|
@@ -149,7 +156,7 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
149
156
|
listed
|
|
150
157
|
.filter((object) => (object.parent ?? null) === parentId)
|
|
151
158
|
.map((object) => {
|
|
152
|
-
const row =
|
|
159
|
+
const row = objectRow(object, object.name);
|
|
153
160
|
const nested = childrenOf(object.id);
|
|
154
161
|
if (nested.length) row.children = nested;
|
|
155
162
|
return row;
|
|
@@ -163,7 +170,7 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
163
170
|
(object) => (object.parent ?? null) === null || !ids.has(object.parent),
|
|
164
171
|
);
|
|
165
172
|
next.children = rooted.map((object) => {
|
|
166
|
-
const row =
|
|
173
|
+
const row = objectRow(object, object.name);
|
|
167
174
|
const nested = childrenOf(object.id);
|
|
168
175
|
if (nested.length) row.children = nested;
|
|
169
176
|
return row;
|
|
@@ -174,7 +181,7 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
174
181
|
return next;
|
|
175
182
|
};
|
|
176
183
|
const nodes = HIERARCHY_NODES.map(clone);
|
|
177
|
-
// The cast is list-driven: one row per
|
|
184
|
+
// The cast is list-driven: one row per character, hidden ones included. The first two
|
|
178
185
|
// keep the legacy row ids (characterA/characterB) that selection, IK and
|
|
179
186
|
// the inspector already route to; extras carry their character id.
|
|
180
187
|
if (Array.isArray(characters)) {
|
|
@@ -183,7 +190,7 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
183
190
|
// Row ids follow the entry's LIST index so they match the viewport
|
|
184
191
|
// pickId (A/B/charId) stamped by the Character renderer in App.jsx.
|
|
185
192
|
group.children = characters.flatMap((entry, listIndex) => {
|
|
186
|
-
if (entry
|
|
193
|
+
if (!entry) return [];
|
|
187
194
|
const id = characterRowId(entry, listIndex);
|
|
188
195
|
// Every cast member carries its own rig subtree (#78) — ids are
|
|
189
196
|
// namespaced per row (#76) and IK state is per character (#77), so
|
|
@@ -197,6 +204,7 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
197
204
|
id,
|
|
198
205
|
label: `Character ${listIndex + 1}`,
|
|
199
206
|
kind: "character",
|
|
207
|
+
hidden: entry.hidden === true,
|
|
200
208
|
...(children.length ? { children } : {}),
|
|
201
209
|
}];
|
|
202
210
|
});
|