cozyclay 2.0.0 → 2.0.2
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 +56 -0
- package/bin/agent/agent-routes.mjs +8 -2
- package/bin/codex-auth.mjs +2 -1
- package/bin/cozyclay.mjs +12 -3
- package/bin/mcp-runtime.mjs +10 -0
- package/dist/app/index.html +3 -3
- package/dist/assets/app-BxBLC6GT.js +4876 -0
- package/dist/assets/app-CJdxvnrl.css +1 -0
- package/dist/assets/{shot-prompt-Yrt99wZx.js → shot-prompt-hN0j_O-p.js} +3 -3
- package/dist/assets/{workflow-dNPGY9xz.js → workflow-B5XZwVfe.js} +1 -1
- package/dist/cozyclay-package.json +1 -1
- package/dist/sitemap.xml +4 -4
- package/dist/workflow/index.html +2 -2
- package/package.json +1 -1
- package/src/App.jsx +27 -11
- package/src/ardy/timeline.css +15 -0
- package/src/ardy/timeline.jsx +4 -1
- package/src/ardy/waypoints.js +1 -1
- package/src/camera-tutorial.jsx +38 -18
- package/src/commands/project.js +1 -1
- package/src/domains/scenes.js +10 -4
- package/src/motion/generation.js +1 -1
- package/src/project-browser.jsx +4 -0
- package/src/scenes.js +4 -8
- package/src/styles.css +19 -1
- package/src/theme.js +1 -1
- package/src/timeline-fps.js +13 -0
- package/src/workflow/AgentPanel.jsx +4 -2
- package/tools/run-tests.mjs +2 -0
- package/dist/assets/app-C1sdVbg4.css +0 -1
- package/dist/assets/app-CA55tcI3.js +0 -4876
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"2.0.
|
|
1
|
+
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"2.0.2\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"6b21f4b8112041967b14ed57d28da217b04fae78e0adeb9dd0da143622814822\"}","signature":"72GMEbX6c47ZRDJrLMi7FipjXz9cKt/piGTYyzGUXpUtNNPx3WCkP6GWJp4PoXZdVoaB9mOrEA71YGN9TEktAA=="}
|
package/dist/sitemap.xml
CHANGED
|
@@ -6,19 +6,19 @@
|
|
|
6
6
|
</url>
|
|
7
7
|
<url>
|
|
8
8
|
<loc>https://cozyclay.org/greybox-to-video/</loc>
|
|
9
|
-
<lastmod>2026-
|
|
9
|
+
<lastmod>2026-09-14</lastmod>
|
|
10
10
|
</url>
|
|
11
11
|
<url>
|
|
12
12
|
<loc>https://cozyclay.org/previs-software/</loc>
|
|
13
|
-
<lastmod>2026-10
|
|
13
|
+
<lastmod>2026-09-10</lastmod>
|
|
14
14
|
</url>
|
|
15
15
|
<url>
|
|
16
16
|
<loc>https://cozyclay.org/seedance-camera-control/</loc>
|
|
17
|
-
<lastmod>2026-10
|
|
17
|
+
<lastmod>2026-09-10</lastmod>
|
|
18
18
|
</url>
|
|
19
19
|
<url>
|
|
20
20
|
<loc>https://cozyclay.org/ai-camera-control/</loc>
|
|
21
|
-
<lastmod>2026-10
|
|
21
|
+
<lastmod>2026-09-10</lastmod>
|
|
22
22
|
</url>
|
|
23
23
|
<url>
|
|
24
24
|
<loc>https://cozyclay.org/privacy/</loc>
|
package/dist/workflow/index.html
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
<meta name="theme-color" content="#101116" />
|
|
9
9
|
<meta name="description" content="CozyClay visual workflow builder" />
|
|
10
10
|
<title>CozyClay Workflow</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/workflow-
|
|
11
|
+
<script type="module" crossorigin src="/assets/workflow-B5XZwVfe.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/analytics-MVZOEW8O.js">
|
|
14
|
-
<link rel="modulepreload" crossorigin href="/assets/shot-prompt-
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/assets/shot-prompt-hN0j_O-p.js">
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/shot-prompt-CbqWcWYi.css">
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/assets/workflow-DRdwoDBT.css">
|
|
17
17
|
</head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozyclay",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Open source previs software in the browser: block a scene, pose characters, author camera moves and cuts, then take the same shots to an AI video model.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "node tools/dev-full.mjs --host 127.0.0.1 --port 5180",
|
package/src/App.jsx
CHANGED
|
@@ -147,6 +147,7 @@ import {
|
|
|
147
147
|
} from "./matte.js";
|
|
148
148
|
import { createMatteEditor } from "./matte-editor.js";
|
|
149
149
|
import {
|
|
150
|
+
SCENES_STORAGE_KEY,
|
|
150
151
|
SCENES_VERSION,
|
|
151
152
|
activeSceneIndex,
|
|
152
153
|
createSceneStage,
|
|
@@ -154,6 +155,7 @@ import {
|
|
|
154
155
|
} from "./scenes.js";
|
|
155
156
|
import {
|
|
156
157
|
hasFileSystemAccess,
|
|
158
|
+
loadProjectSession,
|
|
157
159
|
loadStoredProjectHandle,
|
|
158
160
|
queryHandlePermission,
|
|
159
161
|
requestHandlePermission,
|
|
@@ -498,9 +500,17 @@ export default function App() {
|
|
|
498
500
|
// have something to frame. That function is declared with the project
|
|
499
501
|
// actions further down; the listeners here reach it through a ref so they
|
|
500
502
|
// always call the current render's closure (the confirm reads projectDirty).
|
|
501
|
-
const [
|
|
502
|
-
|
|
503
|
-
|
|
503
|
+
const [cameraTutorialEntry] = useState(() => {
|
|
504
|
+
const params = new URLSearchParams(globalThis.location?.search || "");
|
|
505
|
+
const query = params.get("tutorial") === "camera";
|
|
506
|
+
let firstRun = false;
|
|
507
|
+
try {
|
|
508
|
+
firstRun = !query && !params.get("scene") && !cameraTutorialSuppressed()
|
|
509
|
+
&& !loadProjectSession()?.name && !globalThis.localStorage?.getItem(SCENES_STORAGE_KEY);
|
|
510
|
+
} catch { firstRun = false; }
|
|
511
|
+
return !embedMode && !playgroundMode && !cameraTutorialSuppressed() && (query ? "query" : firstRun ? "first-run" : null);
|
|
512
|
+
});
|
|
513
|
+
const cameraTutorialQuery = Boolean(cameraTutorialEntry);
|
|
504
514
|
const [cameraTutorial, setCameraTutorial] = useState(false);
|
|
505
515
|
const cameraTutorialAnalytics = useRef(null);
|
|
506
516
|
const [cameraTutorialAttempt, setCameraTutorialAttempt] = useState(0);
|
|
@@ -524,8 +534,8 @@ export default function App() {
|
|
|
524
534
|
useEffect(() => {
|
|
525
535
|
if (!cameraTutorialQuery || cameraTutorialStarted.current) return;
|
|
526
536
|
cameraTutorialStarted.current = true;
|
|
527
|
-
void startCameraTutorialRef.current?.({ source:
|
|
528
|
-
}, [cameraTutorialQuery]);
|
|
537
|
+
void startCameraTutorialRef.current?.({ source: cameraTutorialEntry });
|
|
538
|
+
}, [cameraTutorialQuery, cameraTutorialEntry]);
|
|
529
539
|
useEffect(() => {
|
|
530
540
|
const onTutorial = (event) => {
|
|
531
541
|
if (event.detail?.open === false) {
|
|
@@ -3024,8 +3034,8 @@ export default function App() {
|
|
|
3024
3034
|
return [{
|
|
3025
3035
|
id: entry.id,
|
|
3026
3036
|
url: characterModelUrl(entry.model),
|
|
3027
|
-
position:
|
|
3028
|
-
rot:
|
|
3037
|
+
position: [entry.x, entry.y ?? 0, entry.z],
|
|
3038
|
+
rot: entry.rot,
|
|
3029
3039
|
tint: entry.tint ?? defaultCharacterTint(entry, index),
|
|
3030
3040
|
partColoursEnabled,
|
|
3031
3041
|
partColoursMode,
|
|
@@ -3047,8 +3057,7 @@ export default function App() {
|
|
|
3047
3057
|
if (!charIdFromHierarchyId(selectedHierarchyId)) return null;
|
|
3048
3058
|
const entry = characters.find((item) => item.id === activeChar.id);
|
|
3049
3059
|
if (!entry || entry.hidden) return null;
|
|
3050
|
-
|
|
3051
|
-
return { position: clip ? [clip.anchorX, entry.y ?? 0, clip.anchorZ] : [entry.x, entry.y ?? 0, entry.z] };
|
|
3060
|
+
return { position: [entry.x, entry.y ?? 0, entry.z] };
|
|
3052
3061
|
}, [characters, activeChar.id, motion, selectedHierarchyId]);
|
|
3053
3062
|
// The cast rides the SAME gizmo as scene objects — one movement grammar
|
|
3054
3063
|
// for everything on stage. The proxy hands ObjectGizmo the object shape
|
|
@@ -3950,6 +3959,11 @@ export default function App() {
|
|
|
3950
3959
|
});
|
|
3951
3960
|
return;
|
|
3952
3961
|
}
|
|
3962
|
+
// The seed dresses a visitor's first, unnamed stage and the starter set a
|
|
3963
|
+
// `?scene=` launch always re-opens ("one character mid-walk"). A named
|
|
3964
|
+
// project is the author's document: reloading it must never put the demo
|
|
3965
|
+
// walk back on a character they deliberately left without a take.
|
|
3966
|
+
if (loadProjectSession()?.name && !new URLSearchParams(window.location.search).get("scene")) { demoSeeded.current = true; return; }
|
|
3953
3967
|
if (!demoSeed.seed) return;
|
|
3954
3968
|
demoSeeded.current = true;
|
|
3955
3969
|
// Loaded, not played: the clip walks the subject out of the default
|
|
@@ -6770,6 +6784,8 @@ export default function App() {
|
|
|
6770
6784
|
key={cameraTutorialAttempt}
|
|
6771
6785
|
analytics={cameraTutorialAnalytics.current}
|
|
6772
6786
|
previewing={lookThroughShot}
|
|
6787
|
+
shotCount={shots.length}
|
|
6788
|
+
railReady={shots.some((shot) => Array.isArray(shot.camera?.cameraRail) && shot.camera.cameraRail.length >= 2)}
|
|
6773
6789
|
onStepChange={setCameraTutorialStep}
|
|
6774
6790
|
onComplete={() => { cameraTutorialCompletedRef.current = true; cameraTutorialHandoff?.complete(); }}
|
|
6775
6791
|
onClose={() => closeCameraTutorial()}
|
|
@@ -7455,9 +7471,9 @@ export default function App() {
|
|
|
7455
7471
|
openProject();
|
|
7456
7472
|
}}
|
|
7457
7473
|
starters={STARTER_SCENES}
|
|
7458
|
-
onStarter={(id) => {
|
|
7474
|
+
onStarter={(id, name) => {
|
|
7459
7475
|
setProjectBrowserOpen(false);
|
|
7460
|
-
void openStarterScene(id);
|
|
7476
|
+
void openStarterScene(id, "starter", name);
|
|
7461
7477
|
}}
|
|
7462
7478
|
onNew={() => {
|
|
7463
7479
|
setProjectBrowserOpen(false);
|
package/src/ardy/timeline.css
CHANGED
|
@@ -855,10 +855,25 @@
|
|
|
855
855
|
background: var(--select-tint, rgba(232, 163, 61, .14));
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
/* The bar hangs above the block's top-left corner instead of inside it: a
|
|
859
|
+
short cut (40 frames ~ 38px) is narrower than Split + Duplicate + Delete,
|
|
860
|
+
and an in-block bar would sit on the shot's name, turning a click on the
|
|
861
|
+
name into Delete. Hover is kept because the bar is still a child. */
|
|
858
862
|
.v2-sequencer .tl-shot-actions {
|
|
863
|
+
top: auto;
|
|
864
|
+
bottom: calc(100% + 2px);
|
|
865
|
+
left: 0;
|
|
866
|
+
right: auto;
|
|
859
867
|
background: var(--overlay, rgba(12, 12, 13, .78));
|
|
860
868
|
}
|
|
861
869
|
|
|
870
|
+
/* The block clips its own box (overflow hidden) so the hanging bar only exists
|
|
871
|
+
while it is wanted; the label clips its own text, so nothing else escapes. */
|
|
872
|
+
.v2-sequencer .tl-shot-block:hover,
|
|
873
|
+
.v2-sequencer .tl-shot-block:focus-within {
|
|
874
|
+
overflow: visible;
|
|
875
|
+
}
|
|
876
|
+
|
|
862
877
|
.v2-sequencer .tl-shot-actions button {
|
|
863
878
|
height: var(--h-row, 24px);
|
|
864
879
|
border-radius: var(--r-1, 4px);
|
package/src/ardy/timeline.jsx
CHANGED
|
@@ -1809,7 +1809,10 @@ export default function Timeline({
|
|
|
1809
1809
|
type="button"
|
|
1810
1810
|
className="tl-seq-action rail"
|
|
1811
1811
|
data-testid="sequencer-draw-rail"
|
|
1812
|
-
|
|
1812
|
+
// The rail belongs to a shot: with none selected the toggle used to
|
|
1813
|
+
// do nothing at all, so say why instead.
|
|
1814
|
+
disabled={cameraBlockIdx == null}
|
|
1815
|
+
title={cameraBlockIdx == null ? ko("Add or select a shot first — the rail belongs to a shot", "먼저 샷을 추가하거나 선택하세요 — 레일은 샷에 속합니다") : ko("Draw or redraw the selected camera rail", "선택한 카메라 레일을 그리거나 다시 그립니다")}
|
|
1813
1816
|
onClick={() => handlers.current.onCameraRailDrawToggle?.()}
|
|
1814
1817
|
>
|
|
1815
1818
|
{ko("Draw rail", "레일 그리기")}
|
package/src/ardy/waypoints.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const cleanZero = (value) => (Object.is(value, -0) || Math.abs(value) < 1e-9 ? 0 : value);
|
|
2
2
|
import { isKo } from "../locale.js";
|
|
3
|
-
import { TIMELINE_FRAME_FPS } from "../
|
|
3
|
+
import { TIMELINE_FRAME_FPS } from "../timeline-fps.js";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_WAYPOINT_SPACING_FRAMES = Math.round(0.4 * TIMELINE_FRAME_FPS);
|
|
6
6
|
|
package/src/camera-tutorial.jsx
CHANGED
|
@@ -11,7 +11,7 @@ import { ko } from "./locale.js";
|
|
|
11
11
|
// to them directly.
|
|
12
12
|
//
|
|
13
13
|
// cozyclay:nav fly / walk / dolly / orbit (src/controls.jsx)
|
|
14
|
-
//
|
|
14
|
+
// shotCount / railReady shot / rail (authored state)
|
|
15
15
|
// previewing prop play (lookThroughShot)
|
|
16
16
|
//
|
|
17
17
|
// Nothing here drives the studio. A step is done when the operator has done
|
|
@@ -69,16 +69,16 @@ export const CAMERA_TUTORIAL_STEPS = [
|
|
|
69
69
|
label: ko("Shot", "샷"),
|
|
70
70
|
// The control lives in another region than the card, so the card says
|
|
71
71
|
// which way to look before it says what to do.
|
|
72
|
-
where: ko("↓ Timeline,
|
|
72
|
+
where: ko("↓ Timeline, Camera Cuts", "↓ 타임라인 Camera Cuts"),
|
|
73
73
|
how: () => ko(
|
|
74
|
-
<>In the timeline's
|
|
75
|
-
<>타임라인의
|
|
74
|
+
<>In the timeline's Camera Cuts lane click <b>+ Add shot</b>. That is your cut.</>,
|
|
75
|
+
<>타임라인의 Camera Cuts 레인에서 <b>+ 샷 추가</b>를 누르세요. 그게 컷입니다.</>,
|
|
76
76
|
),
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
kind: "rail",
|
|
80
80
|
label: ko("Rail", "레일"),
|
|
81
|
-
where: ko("↓ Timeline,
|
|
81
|
+
where: ko("↓ Timeline, Camera Cuts", "↓ 타임라인 Camera Cuts"),
|
|
82
82
|
how: () => ko(
|
|
83
83
|
<>Select the shot, click <b>Draw rail</b> in the camera bar, and drag a line across the top view. That line is the dolly move.</>,
|
|
84
84
|
<>샷을 선택하고 카메라 바의 <b>레일 그리기</b>를 누른 뒤, 탑뷰에 선을 그으세요. 그 선이 돌리 이동입니다.</>,
|
|
@@ -87,10 +87,10 @@ export const CAMERA_TUTORIAL_STEPS = [
|
|
|
87
87
|
{
|
|
88
88
|
kind: "play",
|
|
89
89
|
label: ko("Play", "재생"),
|
|
90
|
-
where: ko("
|
|
90
|
+
where: ko("↘ Shot Camera monitor", "↘ Shot Camera 모니터"),
|
|
91
91
|
how: () => ko(
|
|
92
92
|
<>Click <b>Look through</b> in the Shot monitor to fly the shot camera; <b>▶</b> rides the rail, <kbd>Esc</kbd> returns to the free camera.</>,
|
|
93
|
-
|
|
93
|
+
<>Shot Camera 모니터의 <b>샷 시점</b>을 눌러 샷 카메라를 조종하세요. <b>▶</b>는 레일을 타고, <kbd>Esc</kbd>로 자유 카메라로 돌아옵니다.</>,
|
|
94
94
|
),
|
|
95
95
|
},
|
|
96
96
|
];
|
|
@@ -119,10 +119,13 @@ export const TUTORIAL_BEACONS = {
|
|
|
119
119
|
label: () => ko("1. Select the shot", "1. 샷을 선택"),
|
|
120
120
|
},
|
|
121
121
|
{ role: "draw-rail", selector: ".tl-rail-draw", label: () => ko("2. Draw rail, then drag across the top view", "2. 레일 그리기 후 탑뷰에 선 긋기") },
|
|
122
|
-
|
|
122
|
+
// The Top-View becomes an active drawing surface after the tool is armed.
|
|
123
|
+
{ role: "top-view", selector: ".vp-inset", requires: '[data-rail-draw="1"]', label: () => ko("Drag a line here", "여기에 선을 그으세요") },
|
|
123
124
|
],
|
|
124
125
|
play: [
|
|
125
|
-
|
|
126
|
+
// The glass shell keeps the same action in the bottom dock while the
|
|
127
|
+
// compact viewport card owns it in the classic shell.
|
|
128
|
+
{ role: "look-through", selector: ".dock-shot-look, .vp-look-through", label: () => ko("Click to fly the shot camera", "클릭해 샷 카메라를 조종") },
|
|
126
129
|
],
|
|
127
130
|
};
|
|
128
131
|
|
|
@@ -217,7 +220,7 @@ export function TutorialBeacon({ kind, step, role, selector, label, requires = n
|
|
|
217
220
|
if (pane) resize.observe(pane);
|
|
218
221
|
}
|
|
219
222
|
const mutations = new MutationObserver(schedule);
|
|
220
|
-
mutations.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style"] });
|
|
223
|
+
mutations.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "data-rail-draw"] });
|
|
221
224
|
return () => {
|
|
222
225
|
if (frame) cancelAnimationFrame(frame);
|
|
223
226
|
window.removeEventListener("resize", schedule);
|
|
@@ -327,11 +330,18 @@ export function GestureCue({ kind, walked, leaving = false, onLeft }) {
|
|
|
327
330
|
* Top-View inset, and never takes the pointer except on its own close button
|
|
328
331
|
* — every step is completed by working the studio underneath it.
|
|
329
332
|
*/
|
|
330
|
-
export function CameraTutorial({ previewing = false, analytics, onStepChange, onClose, onComplete, handoff = null, shotId = null, onOpenExport, onContinue }) {
|
|
333
|
+
export function CameraTutorial({ previewing = false, shotCount = 0, railReady = false, analytics, onStepChange, onClose, onComplete, handoff = null, shotId = null, onOpenExport, onContinue }) {
|
|
331
334
|
const [done, setDone] = useState(() => new Set());
|
|
332
335
|
const [walked, setWalked] = useState(() => new Set());
|
|
333
336
|
const [cue, setCue] = useState(null);
|
|
334
337
|
const completionReported = useRef(false);
|
|
338
|
+
const effectiveDone = useMemo(() => {
|
|
339
|
+
const next = new Set(done);
|
|
340
|
+
if (shotCount > 0) next.add("shot");
|
|
341
|
+
if (railReady) next.add("rail");
|
|
342
|
+
if (previewing && next.has("rail")) next.add("play");
|
|
343
|
+
return next;
|
|
344
|
+
}, [done, previewing, railReady, shotCount]);
|
|
335
345
|
|
|
336
346
|
useEffect(() => {
|
|
337
347
|
const complete = (kind) => setDone((current) => (current.has(kind) ? current : new Set(current).add(kind)));
|
|
@@ -362,6 +372,16 @@ export function CameraTutorial({ previewing = false, analytics, onStepChange, on
|
|
|
362
372
|
};
|
|
363
373
|
}, []);
|
|
364
374
|
|
|
375
|
+
// Studio-native completion comes from the authored state. The old playground
|
|
376
|
+
// signal was never emitted by the Studio shell, so a real Shot or rail could
|
|
377
|
+
// leave the tutorial stuck on its previous step.
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (shotCount > 0) setDone((current) => (current.has("shot") ? current : new Set(current).add("shot")));
|
|
380
|
+
}, [shotCount]);
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
if (railReady) setDone((current) => (current.has("rail") ? current : new Set(current).add("rail")));
|
|
383
|
+
}, [railReady]);
|
|
384
|
+
|
|
365
385
|
// The player is the last step, and only once there is a rail to ride:
|
|
366
386
|
// look-through before the dolly exists shows a still frame, which teaches
|
|
367
387
|
// nothing about the move. Analytics v1 observes this existing look-through
|
|
@@ -369,12 +389,12 @@ export function CameraTutorial({ previewing = false, analytics, onStepChange, on
|
|
|
369
389
|
useEffect(() => {
|
|
370
390
|
if (!previewing) return;
|
|
371
391
|
setDone((current) => (!current.has("rail") || current.has("play") ? current : new Set(current).add("play")));
|
|
372
|
-
}, [previewing]);
|
|
392
|
+
}, [previewing, railReady]);
|
|
373
393
|
|
|
374
|
-
const current = useMemo(() => CAMERA_TUTORIAL_STEPS.find((step) => !
|
|
394
|
+
const current = useMemo(() => CAMERA_TUTORIAL_STEPS.find((step) => !effectiveDone.has(step.kind)) ?? null, [effectiveDone]);
|
|
375
395
|
const complete = current === null;
|
|
376
396
|
const currentKind = current?.kind ?? null;
|
|
377
|
-
const showHandoff = !!shotId && (handoff?.canShow(
|
|
397
|
+
const showHandoff = !!shotId && (handoff?.canShow(effectiveDone) ?? false);
|
|
378
398
|
useEffect(() => {
|
|
379
399
|
if (!complete || completionReported.current) return;
|
|
380
400
|
completionReported.current = true;
|
|
@@ -384,8 +404,8 @@ export function CameraTutorial({ previewing = false, analytics, onStepChange, on
|
|
|
384
404
|
// Observe committed progression, outside state updaters (which StrictMode
|
|
385
405
|
// may replay). The attempt belongs to App and survives effect cleanup/resume.
|
|
386
406
|
useEffect(() => {
|
|
387
|
-
analytics?.observe(
|
|
388
|
-
}, [analytics,
|
|
407
|
+
analytics?.observe(effectiveDone, currentKind);
|
|
408
|
+
}, [analytics, effectiveDone, currentKind]);
|
|
389
409
|
|
|
390
410
|
// The step the operator is on is the studio's business too: App puts it on
|
|
391
411
|
// the .app root as data-tutorial-step, and styles.css spotlights whichever
|
|
@@ -425,10 +445,10 @@ export function CameraTutorial({ previewing = false, analytics, onStepChange, on
|
|
|
425
445
|
key={step.kind}
|
|
426
446
|
data-testid="camera-tutorial-step"
|
|
427
447
|
data-kind={step.kind}
|
|
428
|
-
data-done={
|
|
448
|
+
data-done={effectiveDone.has(step.kind) ? 1 : 0}
|
|
429
449
|
data-current={step === current ? 1 : 0}
|
|
430
450
|
>
|
|
431
|
-
<i aria-hidden="true">{
|
|
451
|
+
<i aria-hidden="true">{effectiveDone.has(step.kind) ? "✓" : index + 1}</i>
|
|
432
452
|
{step.label}
|
|
433
453
|
</li>
|
|
434
454
|
))}
|
package/src/commands/project.js
CHANGED
|
@@ -10,7 +10,7 @@ const save = { ...studioActionDeclaration("project.save"), input: input({ name }
|
|
|
10
10
|
const saveAs = { ...save, id: "project.saveAs", label: "Save project as" };
|
|
11
11
|
const opening = document("project.open", "Open a project file", { serialized: { type: "string", maxLength: 400_000_000 }, handleToken: token, projectToken: token });
|
|
12
12
|
const fresh = document("project.new", "Start a new project", { name });
|
|
13
|
-
const starter = document("project.openStarter", "Open a starter project", { id: token, source: { type: "string", enum: ["starter", "tutorial", "launch"] } });
|
|
13
|
+
const starter = document("project.openStarter", "Open a starter project", { id: token, source: { type: "string", enum: ["starter", "tutorial", "launch"] }, name });
|
|
14
14
|
const restore = { ...document("project.restore", "Restore the remembered project", { handleToken: token }), exposure: "ui-only" };
|
|
15
15
|
const browse = { id: "project.browse", label: "Browse projects", description: "Show the project browser.", kind: "transient", exposure: "ui-only", input: input() };
|
|
16
16
|
export const declarations = Object.freeze([rename, save, saveAs, opening, fresh, starter, restore, browse]);
|
package/src/domains/scenes.js
CHANGED
|
@@ -435,7 +435,8 @@ export function useScenes(appContext) {
|
|
|
435
435
|
/** Open a bundled starter scene as a fresh, saveable project. Used by the
|
|
436
436
|
* first-run dialog and by `npx cozyclay --scene <id>` (`?scene=`), which is
|
|
437
437
|
* how the landing-page tutorial hands people into the local studio. */
|
|
438
|
-
async function openStarterScene(id, source = "starter", context = null) {
|
|
438
|
+
async function openStarterScene(id, source = "starter", context = null, name = null) {
|
|
439
|
+
if (source === "starter" && domain.dirtyStore.read("projectDirty") && !window.confirm(ko("Discard unsaved changes and start a new project?", "저장되지 않은 변경사항을 버리고 새 프로젝트를 시작할까요?"))) return false;
|
|
439
440
|
const before = source === "tutorial" ? collectProjectSnapshot("Tutorial") : null;
|
|
440
441
|
const epoch = appContext.shared.tutorialProjectEpochRef.current;
|
|
441
442
|
const url = playgroundSceneUrl(`?scene=${encodeURIComponent(id)}`);
|
|
@@ -448,7 +449,7 @@ export function useScenes(appContext) {
|
|
|
448
449
|
return false;
|
|
449
450
|
}
|
|
450
451
|
context?.check();
|
|
451
|
-
applyProject({ ...project, savedAt: null }, true);
|
|
452
|
+
applyProject({ ...project, name: name?.trim() || project.name, savedAt: null }, true);
|
|
452
453
|
appContext.shared.projectHandleRef.current = null;
|
|
453
454
|
track("scene:loaded", { scene_source: source });
|
|
454
455
|
return true;
|
|
@@ -537,6 +538,8 @@ export function useScenes(appContext) {
|
|
|
537
538
|
setProjectNameDialog(null);
|
|
538
539
|
const fresh = createSceneDocument(ko("SCENE 01", "씬 01"));
|
|
539
540
|
storeWorkflowGraph(createWorkflowGraph());
|
|
541
|
+
appContext.shared.projectMotionsRef.current = new Map();
|
|
542
|
+
appContext.shared.motionFullRef.current.clear();
|
|
540
543
|
domain.replaceDocument(fresh.scenes, fresh.activeSceneId, name);
|
|
541
544
|
persistScenes(fresh.scenes, fresh.activeSceneId);
|
|
542
545
|
openScene(fresh.scenes[0], fresh.scenes);
|
|
@@ -763,6 +766,9 @@ export function useScenes(appContext) {
|
|
|
763
766
|
domain.pendingCheckpoint = null;
|
|
764
767
|
if (checkpoint.clock === appContext.undoClock && checkpoint.name === name) appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(name);
|
|
765
768
|
}
|
|
769
|
+
if (name !== null && !appContext.shared.projectSnapshotRef.current) {
|
|
770
|
+
appContext.shared.projectSnapshotRef.current = collectProjectSnapshot(name);
|
|
771
|
+
}
|
|
766
772
|
const dirty = name !== null && collectProjectSnapshot(name) !== appContext.shared.projectSnapshotRef.current;
|
|
767
773
|
domain.setDirty(dirty);
|
|
768
774
|
setProjectSaveState((current) => current === "saving" ? current : dirty ? "dirty" : "saved");
|
|
@@ -782,7 +788,7 @@ export function useScenes(appContext) {
|
|
|
782
788
|
};
|
|
783
789
|
if (id === "project.browse") { setProjectStartupOpen(false); setProjectBrowserOpen(true); return; }
|
|
784
790
|
if (id === "project.new") return newProject(args.name, true);
|
|
785
|
-
if (id === "project.openStarter") return openStarterScene(args.id, args.source, context);
|
|
791
|
+
if (id === "project.openStarter") return openStarterScene(args.id, args.source, context, args.name);
|
|
786
792
|
if (id === "project.restore") return restoreStoredProject({ handle: runtime("handleToken") }, context);
|
|
787
793
|
if (args.handleToken) return openProjectByHandle(runtime("handleToken"), context);
|
|
788
794
|
if (args.projectToken) { applyProject(runtime("projectToken"), true); return true; }
|
|
@@ -807,7 +813,7 @@ export function useScenes(appContext) {
|
|
|
807
813
|
projectStartupOpen, setProjectStartupOpen, projectManifest, setProjectManifest, saveBlockedReasons,
|
|
808
814
|
setSaveBlockedReasons, workflowRevision, setWorkflowRevision, collectProjectSnapshot,
|
|
809
815
|
collectProjectSerialized, projectProblemsNotice, rehydrateProjectAssets, saveProject, applyProject,
|
|
810
|
-
openStarterScene: async (id, source = "starter") => (await runProject("project.openStarter", { id, source })).output?.opened === true,
|
|
816
|
+
openStarterScene: async (id, source = "starter", name = null) => (await runProject("project.openStarter", { id, source, ...(name ? { name } : {}) })).output?.opened === true,
|
|
811
817
|
openProject, openProjectByHandle, requestNewProject, newProject, restoreOffer,
|
|
812
818
|
setRestoreOffer, restoreStoredProject, flushScenes, openScene, selectSceneDocument,
|
|
813
819
|
createSceneDocumentFromUi, duplicateSceneDocumentFromUi, renameSceneDocumentFromUi,
|
package/src/motion/generation.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure request construction. The caller supplies one rolled seed and sampled
|
|
2
2
|
// poses; no rig, store, random source, notification or network is touched here.
|
|
3
3
|
import { StudioProtocolError, compileStudioBeats } from '../studio-agent-protocol.js';
|
|
4
|
-
import { TIMELINE_FRAME_FPS as FPS } from '../
|
|
4
|
+
import { TIMELINE_FRAME_FPS as FPS } from '../timeline-fps.js';
|
|
5
5
|
import { judgeAuthoredPath, alignArdyPath } from '../ardy/waypoints.js';
|
|
6
6
|
import { planPosePin, PIN_BLOCKED } from '../ardy/pose-pin.js';
|
|
7
7
|
import { blocksFromRequest, replayPayload, replayTruncated } from '../take-recipe.js';
|
package/src/project-browser.jsx
CHANGED
|
@@ -190,6 +190,10 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
190
190
|
event.preventDefault();
|
|
191
191
|
const projectName = name.trim();
|
|
192
192
|
if (!projectName) return;
|
|
193
|
+
if (selectedTemplate?.starterId) {
|
|
194
|
+
onStarter?.(selectedTemplate.starterId, projectName);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
193
197
|
pendingProjectName = projectName;
|
|
194
198
|
onNew(projectName);
|
|
195
199
|
};
|
package/src/scenes.js
CHANGED
|
@@ -8,6 +8,9 @@ import { normalizeMotionCalibration } from "./ardy/motion-calibration.js";
|
|
|
8
8
|
import { normalizeTrailEdits } from "./motion-trail.js";
|
|
9
9
|
import { elementByPath } from "./studio-elements.js";
|
|
10
10
|
import { wrapAngle } from "./scene-objects.js";
|
|
11
|
+
// The timeline clock lives in timeline-fps.js (three-free, so the agent
|
|
12
|
+
// sidecar can import it); re-exported here for every scene-side caller.
|
|
13
|
+
import { LEGACY_FRAME_FPS, TIMELINE_FRAME_FPS, toTimelineFrame } from "./timeline-fps.js";
|
|
11
14
|
|
|
12
15
|
const CHARACTER_POSITION_LIMITS = elementByPath("character.position");
|
|
13
16
|
const CHARACTER_ROTATION_LIMITS = elementByPath("character.rot");
|
|
@@ -29,14 +32,7 @@ export const LEGACY_SCENES_STORAGE_KEYS = Object.freeze([
|
|
|
29
32
|
V1_SCENES_STORAGE_KEY,
|
|
30
33
|
]);
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
* them on the 24 fps production clock. The numbers are MULTIPLIED, never
|
|
34
|
-
* reinterpreted: a waypoint at frame 40 meant 2.0 s and must still mean 2.0 s,
|
|
35
|
-
* which is frame 48 — reinterpreting it would silently speed the scene up. */
|
|
36
|
-
export const LEGACY_FRAME_FPS = 20;
|
|
37
|
-
export const TIMELINE_FRAME_FPS = 24;
|
|
38
|
-
export const toTimelineFrame = (frame) =>
|
|
39
|
-
Math.round((frame * TIMELINE_FRAME_FPS) / LEGACY_FRAME_FPS);
|
|
35
|
+
export { LEGACY_FRAME_FPS, TIMELINE_FRAME_FPS, toTimelineFrame };
|
|
40
36
|
|
|
41
37
|
/** Rigged character models shipped in public/models. The id is both the FBX
|
|
42
38
|
* file stem and the wire `source.rig` value sent to ARDY. */
|
package/src/styles.css
CHANGED
|
@@ -899,7 +899,8 @@ select {
|
|
|
899
899
|
.app[data-tutorial-step="shot"] .tl-track.shots .tl-track-add,
|
|
900
900
|
.app[data-tutorial-step="rail"] .tl-track.shots .tl-shot-block:not(.selected),
|
|
901
901
|
.app[data-tutorial-step="rail"] .tl-rail-draw,
|
|
902
|
-
.app[data-tutorial-step="play"] .vp-look-through
|
|
902
|
+
.app[data-tutorial-step="play"] .vp-look-through,
|
|
903
|
+
.app[data-tutorial-step="play"] .dock-shot-look {
|
|
903
904
|
animation: tutorial-spotlight 1.5s var(--ease) infinite;
|
|
904
905
|
box-shadow: 0 0 0 3px var(--accent);
|
|
905
906
|
}
|
|
@@ -9427,6 +9428,23 @@ input[type=range] {
|
|
|
9427
9428
|
width: clamp(220px, calc(50% - 92px), 360px);
|
|
9428
9429
|
}
|
|
9429
9430
|
|
|
9431
|
+
/* The floating shell covers the full-bleed viewport with panels. Keep the
|
|
9432
|
+
instructions inside its open stage, clear of the tool rail and dock. */
|
|
9433
|
+
.app[data-shell="glass"]:not([data-embed-mode]) .camera-tutorial {
|
|
9434
|
+
top: calc(var(--shell-view-top, 52px) + 12px);
|
|
9435
|
+
left: calc(var(--shell-center-left) + 64px);
|
|
9436
|
+
width: min(430px, calc(100% - var(--shell-center-left) - var(--shell-center-right) - 128px));
|
|
9437
|
+
}
|
|
9438
|
+
.app[data-shell="glass"][data-tutorial-step="rail"] .camera-tutorial,
|
|
9439
|
+
.app[data-shell="glass"] .camera-tutorial[data-previewing="1"] {
|
|
9440
|
+
top: auto;
|
|
9441
|
+
bottom: calc(var(--shell-center-bottom) + 12px);
|
|
9442
|
+
}
|
|
9443
|
+
.app[data-shell="glass"]:not([data-embed-mode]) .tutorial-cue {
|
|
9444
|
+
left: calc((var(--shell-center-left) + 100% - var(--shell-center-right)) / 2);
|
|
9445
|
+
bottom: calc(var(--shell-center-bottom) + 18px);
|
|
9446
|
+
}
|
|
9447
|
+
|
|
9430
9448
|
.camera-tutorial-steps {
|
|
9431
9449
|
display: flex;
|
|
9432
9450
|
flex-wrap: wrap;
|
package/src/theme.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// shell, its regions and body-level popovers all read the same token set.
|
|
3
3
|
export const THEMES = ["dark", "light"];
|
|
4
4
|
export const THEME_KEY = "cozyclay.theme.v1";
|
|
5
|
-
export const DEFAULT_THEME = "
|
|
5
|
+
export const DEFAULT_THEME = "light";
|
|
6
6
|
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The timeline clock, on its own so the agent sidecar (bin/agent) can read it
|
|
2
|
+
// without importing scenes.js, whose scene-object and motion-trail imports pull
|
|
3
|
+
// in `three` — a devDependency the npm package does not ship
|
|
4
|
+
// (test/verify-package-agent-imports.mjs).
|
|
5
|
+
|
|
6
|
+
/** v3 and older authored every frame number on ARDY's 20 fps clock; v4 reads
|
|
7
|
+
* them on the 24 fps production clock. The numbers are MULTIPLIED, never
|
|
8
|
+
* reinterpreted: a waypoint at frame 40 meant 2.0 s and must still mean 2.0 s,
|
|
9
|
+
* which is frame 48 — reinterpreting it would silently speed the scene up. */
|
|
10
|
+
export const LEGACY_FRAME_FPS = 20;
|
|
11
|
+
export const TIMELINE_FRAME_FPS = 24;
|
|
12
|
+
export const toTimelineFrame = (frame) =>
|
|
13
|
+
Math.round((frame * TIMELINE_FRAME_FPS) / LEGACY_FRAME_FPS);
|
|
@@ -382,10 +382,12 @@ export default function AgentPanel({
|
|
|
382
382
|
const payload = await transport.loadSession(stored);
|
|
383
383
|
if (!Array.isArray(payload?.transcript)) throw new Error("missing transcript");
|
|
384
384
|
if (!cancelled) store.restore(payload.transcript, payload.sessionId || stored);
|
|
385
|
-
} catch {
|
|
385
|
+
} catch (error) {
|
|
386
386
|
if (!cancelled) {
|
|
387
387
|
store.newSession();
|
|
388
|
-
|
|
388
|
+
// A session id with no record behind it (the pane opened, nothing
|
|
389
|
+
// was ever sent) is nothing to restore, not a failure.
|
|
390
|
+
if (error?.status !== 404) setRestoreNotice("Previous conversation could not be restored");
|
|
389
391
|
}
|
|
390
392
|
} finally {
|
|
391
393
|
if (!cancelled) setRestoreReady(true);
|
package/tools/run-tests.mjs
CHANGED
|
@@ -108,6 +108,7 @@ const NODE_FILES = [
|
|
|
108
108
|
"test/verify-agent-host.mjs",
|
|
109
109
|
"test/verify-agent-routes.mjs",
|
|
110
110
|
"test/verify-agent-live-runtime.mjs",
|
|
111
|
+
"test/verify-package-agent-imports.mjs",
|
|
111
112
|
"test/verify-agent-runner-errors.mjs",
|
|
112
113
|
"test/verify-agent-steer.mjs",
|
|
113
114
|
"test/verify-pi-tools.mjs",
|
|
@@ -292,6 +293,7 @@ const BROWSER_FILES = [
|
|
|
292
293
|
"test/verify-object-gizmo.mjs",
|
|
293
294
|
"test/verify-offscreen-export-browser.mjs",
|
|
294
295
|
"test/verify-project-menu-browser.mjs",
|
|
296
|
+
"test/verify-project-session-reset.mjs",
|
|
295
297
|
"test/verify-settings-menu-browser.mjs",
|
|
296
298
|
"test/verify-static-shot-export-browser.mjs",
|
|
297
299
|
"test/qa-agent-view-toggle-browser.mjs",
|