cozyclay 2.0.0 → 2.1.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 +112 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +219 -38
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +6 -5
- package/bin/cozyclay.mjs +12 -3
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +12 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +5 -5
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +801 -259
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +196 -3
- package/src/ardy/timeline.jsx +117 -29
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +1 -4
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +43 -24
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +16 -3
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +89 -32
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion/generation.js +1 -1
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +52 -3
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +62 -17
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +111 -10
- package/src/theme.js +1 -1
- package/src/timeline-fps.js +13 -0
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +110 -20
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +96 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-C1sdVbg4.css +0 -1
- package/dist/assets/app-CA55tcI3.js +0 -4876
- package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
- package/dist/assets/workflow-dNPGY9xz.js +0 -179
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { ko } from "./locale.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const ANIMATION_STEPS = [
|
|
5
5
|
{
|
|
6
6
|
en: "Select a character in the Outliner.",
|
|
7
7
|
ko: "아웃라이너에서 캐릭터를 선택하세요.",
|
|
@@ -28,8 +28,39 @@ const STEPS = [
|
|
|
28
28
|
},
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
+
const STORYBOARD_STEPS = [
|
|
32
|
+
{
|
|
33
|
+
en: "Open the Agent panel (Cmd/Ctrl+B).",
|
|
34
|
+
ko: "에이전트 패널을 여세요 (Cmd/Ctrl+B).",
|
|
35
|
+
hintEn: "The Agent panel is where you describe each panel.",
|
|
36
|
+
hintKo: "에이전트 패널에서 각 패널을 설명합니다.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
en: "Describe one scene in a sentence.",
|
|
40
|
+
ko: "장면 하나를 한 문장으로 설명하세요.",
|
|
41
|
+
hintEn: "One sentence per turn is enough for one panel.",
|
|
42
|
+
hintKo: "패널 하나에는 한 문장이면 충분합니다.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
en: "Your panel appears on the Board.",
|
|
46
|
+
ko: "패널이 보드에 나타납니다.",
|
|
47
|
+
hintEn: "Open the Board tab to see the new panel card.",
|
|
48
|
+
hintKo: "보드 탭에서 새 패널 카드를 확인하세요.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
en: "Stylize it, or export the panel pack.",
|
|
52
|
+
ko: "스타일을 입히거나 패널 팩으로 내보내세요.",
|
|
53
|
+
hintEn: "Use Stylize on the card, or export the panel pack.",
|
|
54
|
+
hintKo: "카드에서 스타일을 입히거나 패널 팩으로 내보내세요.",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const STEPS_BY_MODE = { animation: ANIMATION_STEPS, storyboard: STORYBOARD_STEPS };
|
|
59
|
+
|
|
31
60
|
/** A compact, dismissible first-run checklist that follows the v2 shell grammar. */
|
|
32
|
-
export default function FirstSuccessGuide({ open, onDismiss }) {
|
|
61
|
+
export default function FirstSuccessGuide({ open, onDismiss, previsMode = "animation" }) {
|
|
62
|
+
const storyboard = previsMode === "storyboard";
|
|
63
|
+
const STEPS = STEPS_BY_MODE[storyboard ? "storyboard" : "animation"];
|
|
33
64
|
const [step, setStep] = useState(0);
|
|
34
65
|
useEffect(() => {
|
|
35
66
|
if (open) setStep(0);
|
|
@@ -49,8 +80,8 @@ export default function FirstSuccessGuide({ open, onDismiss }) {
|
|
|
49
80
|
{complete ? (
|
|
50
81
|
<div className="v2-first-success-guide-complete first-success-guide-complete" id="first-success-guide-description" role="status">
|
|
51
82
|
<div className="v2-first-success-guide-check first-success-guide-check">✓</div>
|
|
52
|
-
<strong>{ko("You made your first shot.", "첫 샷을 만들었어요.")}</strong>
|
|
53
|
-
<p>{ko("Select, move, key, and play are the core CozyClay loop. You can close this guide and keep blocking.", "선택하고, 움직이고, 키를 찍고, 재생하는 것이 CozyClay의 핵심 흐름입니다. 가이드를 닫고 계속 블로킹하세요.")}</p>
|
|
83
|
+
<strong>{storyboard ? ko("You made your first panel.", "첫 패널을 만들었어요.") : ko("You made your first shot.", "첫 샷을 만들었어요.")}</strong>
|
|
84
|
+
<p>{storyboard ? ko("Describe, review on the Board, then stylize or export. You can close this guide and keep adding panels.", "설명하고, 보드에서 확인한 뒤 스타일을 입히거나 내보내세요. 가이드를 닫고 패널을 계속 추가하세요.") : ko("Select, move, key, and play are the core CozyClay loop. You can close this guide and keep blocking.", "선택하고, 움직이고, 키를 찍고, 재생하는 것이 CozyClay의 핵심 흐름입니다. 가이드를 닫고 계속 블로킹하세요.")}</p>
|
|
54
85
|
<button type="button" className="v2-first-success-guide-next first-success-guide-next btn primary" onClick={onDismiss}>{ko("Continue editing", "편집 계속하기")}</button>
|
|
55
86
|
</div>
|
|
56
87
|
) : (
|
package/src/gizmo-claim.js
CHANGED
|
@@ -27,7 +27,9 @@ const CLAIM_KEYS = [
|
|
|
27
27
|
HANDLE_PROXY_FLAG,
|
|
28
28
|
"keyLightPick", // the key-light sun's grab surfaces (app-stage KeyLightPuck)
|
|
29
29
|
"pathAxis", // travel path: axis-gizmo pick proxies (app-stage ObjectPathHandles)
|
|
30
|
-
"pathIndex", // travel path:
|
|
30
|
+
"pathIndex", // travel path: mark dots — index 0 is a valid value
|
|
31
|
+
"pathRing", // travel path: the selected dot's bank / pitch rings
|
|
32
|
+
"pathLine", // travel path: the drawn route itself — a press near it is aimed at the route
|
|
31
33
|
"craneAxis", // crane marks: axis-gizmo pick proxies (app-stage CraneHandles)
|
|
32
34
|
"craneIndex", // crane marks: height dots
|
|
33
35
|
];
|
package/src/grid-floor.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import * as THREE from "three";
|
|
3
3
|
import { STAGE_SIZE } from "./room.jsx";
|
|
4
|
-
import {
|
|
4
|
+
import { GRID_COLORS } from "./grid-view.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Blender-style reference grid: one shader plane instead of a floor.
|
|
@@ -92,6 +92,3 @@ export function GridFloor({ layer = null }) {
|
|
|
92
92
|
</mesh>
|
|
93
93
|
);
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
/** the void color the mode pairs with; re-exported for the App's background swap */
|
|
97
|
-
export { GRID_BACKGROUND };
|
package/src/hierarchy-model.js
CHANGED
|
@@ -120,6 +120,8 @@ function objectRow(object, label) {
|
|
|
120
120
|
label,
|
|
121
121
|
kind: "object",
|
|
122
122
|
hidden: object.hidden === true,
|
|
123
|
+
// The record's kind, so a row can say it is a node with nothing to draw.
|
|
124
|
+
...(object.renderer === "empty" ? { renderer: "empty" } : {}),
|
|
123
125
|
};
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -147,6 +149,47 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
147
149
|
// stale record cannot hide a group's children behind an attached row.
|
|
148
150
|
const listed = sceneObjects.filter((object) => !attachedIds.has(object.id));
|
|
149
151
|
|
|
152
|
+
// One row per cast member, built once: a character grouped under a listed
|
|
153
|
+
// object reads under that object's row (it rides the object), every other
|
|
154
|
+
// one stays in the cast. A parent that is missing or carried leaves the
|
|
155
|
+
// character in the cast, as an orphaned prop stays at the top of Props.
|
|
156
|
+
const listedIds = new Set(listed.map((object) => object.id));
|
|
157
|
+
const castRows = [];
|
|
158
|
+
const groupedCharacters = new Map(); // object id → character rows
|
|
159
|
+
if (Array.isArray(characters)) {
|
|
160
|
+
characters.forEach((entry, listIndex) => {
|
|
161
|
+
if (!entry) return;
|
|
162
|
+
const id = characterRowId(entry, listIndex);
|
|
163
|
+
// Every cast member carries its own rig subtree (#78) — ids are
|
|
164
|
+
// namespaced per row (#76) and IK state is per character (#77), so
|
|
165
|
+
// the tree no longer needs the primary-only gate. Carried props
|
|
166
|
+
// follow the rig so the body reads first and the luggage after it.
|
|
167
|
+
const children = [
|
|
168
|
+
...(entry.model === "proxy-figure" ? [] : [rigSubtree(id)]),
|
|
169
|
+
...(attachedRows.get(id) ?? []),
|
|
170
|
+
];
|
|
171
|
+
const parent = typeof entry.parent === "string" && listedIds.has(entry.parent) ? entry.parent : null;
|
|
172
|
+
const row = {
|
|
173
|
+
id,
|
|
174
|
+
label: `Character ${listIndex + 1}`,
|
|
175
|
+
kind: "character",
|
|
176
|
+
hidden: entry.hidden === true,
|
|
177
|
+
...(parent ? { grouped: true } : {}),
|
|
178
|
+
...(children.length ? { children } : {}),
|
|
179
|
+
};
|
|
180
|
+
if (!parent) castRows.push(row);
|
|
181
|
+
else if (groupedCharacters.has(parent)) groupedCharacters.get(parent).push(row);
|
|
182
|
+
else groupedCharacters.set(parent, [row]);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// An object row's children: the parts grouped under it, then the cast
|
|
186
|
+
// members riding it.
|
|
187
|
+
const withRiders = (row, objectId, nested) => {
|
|
188
|
+
const children = [...nested, ...(groupedCharacters.get(objectId) ?? [])];
|
|
189
|
+
if (children.length) row.children = children;
|
|
190
|
+
return row;
|
|
191
|
+
};
|
|
192
|
+
|
|
150
193
|
const clone = (node) => {
|
|
151
194
|
const next = { ...node };
|
|
152
195
|
if (node.id === "props") {
|
|
@@ -155,12 +198,7 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
155
198
|
const childrenOf = (parentId) =>
|
|
156
199
|
listed
|
|
157
200
|
.filter((object) => (object.parent ?? null) === parentId)
|
|
158
|
-
.map((object) =>
|
|
159
|
-
const row = objectRow(object, object.name);
|
|
160
|
-
const nested = childrenOf(object.id);
|
|
161
|
-
if (nested.length) row.children = nested;
|
|
162
|
-
return row;
|
|
163
|
-
});
|
|
201
|
+
.map((object) => withRiders(objectRow(object, object.name), object.id, childrenOf(object.id)));
|
|
164
202
|
// An object whose parent is missing still has to appear somewhere, so
|
|
165
203
|
// anything unreachable from the top level is shown at the top level.
|
|
166
204
|
// An attached object is not a grouping parent, so its children are
|
|
@@ -169,12 +207,7 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
169
207
|
const rooted = listed.filter(
|
|
170
208
|
(object) => (object.parent ?? null) === null || !ids.has(object.parent),
|
|
171
209
|
);
|
|
172
|
-
next.children = rooted.map((object) =>
|
|
173
|
-
const row = objectRow(object, object.name);
|
|
174
|
-
const nested = childrenOf(object.id);
|
|
175
|
-
if (nested.length) row.children = nested;
|
|
176
|
-
return row;
|
|
177
|
-
});
|
|
210
|
+
next.children = rooted.map((object) => withRiders(objectRow(object, object.name), object.id, childrenOf(object.id)));
|
|
178
211
|
} else if (node.children) {
|
|
179
212
|
next.children = node.children.map(clone);
|
|
180
213
|
}
|
|
@@ -186,29 +219,9 @@ export function buildHierarchyNodes(sceneObjects = [], characters = null) {
|
|
|
186
219
|
// the inspector already route to; extras carry their character id.
|
|
187
220
|
if (Array.isArray(characters)) {
|
|
188
221
|
const group = nodes[0]?.children?.find((node) => node.id === "characters");
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
group.children = characters.flatMap((entry, listIndex) => {
|
|
193
|
-
if (!entry) return [];
|
|
194
|
-
const id = characterRowId(entry, listIndex);
|
|
195
|
-
// Every cast member carries its own rig subtree (#78) — ids are
|
|
196
|
-
// namespaced per row (#76) and IK state is per character (#77), so
|
|
197
|
-
// the tree no longer needs the primary-only gate. Carried props
|
|
198
|
-
// follow the rig so the body reads first and the luggage after it.
|
|
199
|
-
const children = [
|
|
200
|
-
rigSubtree(id),
|
|
201
|
-
...(attachedRows.get(id) ?? []),
|
|
202
|
-
];
|
|
203
|
-
return [{
|
|
204
|
-
id,
|
|
205
|
-
label: `Character ${listIndex + 1}`,
|
|
206
|
-
kind: "character",
|
|
207
|
-
hidden: entry.hidden === true,
|
|
208
|
-
...(children.length ? { children } : {}),
|
|
209
|
-
}];
|
|
210
|
-
});
|
|
211
|
-
}
|
|
222
|
+
// Row ids follow the entry's LIST index so they match the viewport
|
|
223
|
+
// pickId (A/B/charId) stamped by the Character renderer in App.jsx.
|
|
224
|
+
if (group) group.children = castRows;
|
|
212
225
|
}
|
|
213
226
|
return nodes;
|
|
214
227
|
}
|
package/src/hierarchy-panel.css
CHANGED
|
@@ -194,6 +194,7 @@
|
|
|
194
194
|
.v2-outliner .v2-outliner-chip.cast { background: var(--chip-cast, #5fb584); }
|
|
195
195
|
.v2-outliner .v2-outliner-chip.cam { background: var(--camera, #a78bfa); }
|
|
196
196
|
.v2-outliner .v2-outliner-chip.light { background: var(--chip-light, #d9b84a); }
|
|
197
|
+
.v2-outliner .v2-outliner-chip.empty { background: transparent; border: 1px dashed var(--chip-mesh, #7e8b99); }
|
|
197
198
|
|
|
198
199
|
.v2-outliner .hierarchy-label {
|
|
199
200
|
flex: 1 1 auto;
|
package/src/hierarchy-panel.jsx
CHANGED
|
@@ -4,6 +4,7 @@ import { buildHierarchyNodes } from "./hierarchy-model.js";
|
|
|
4
4
|
import { sceneObjectIdFromHierarchy } from "./scene-objects.js";
|
|
5
5
|
import { CatalogueEntries, displayObjectLabel } from "./object-catalog.jsx";
|
|
6
6
|
import { Dropdown } from "./ui.jsx";
|
|
7
|
+
import { isImeComposing } from "./ime.js";
|
|
7
8
|
import "./hierarchy-panel.css";
|
|
8
9
|
|
|
9
10
|
const HIERARCHY_LABELS_KO = {
|
|
@@ -58,96 +59,6 @@ export const HIERARCHY_DRAG_MIME = "application/x-cclay-hierarchy";
|
|
|
58
59
|
|
|
59
60
|
const carriesHierarchyRow = (event) => !!event.dataTransfer?.types?.includes?.(HIERARCHY_DRAG_MIME);
|
|
60
61
|
|
|
61
|
-
function HierarchyIcon({ kind, className = "" }) {
|
|
62
|
-
const common = {
|
|
63
|
-
className: `hierarchy-icon ${kind}${className ? ` ${className}` : ""}`,
|
|
64
|
-
viewBox: "0 0 24 24",
|
|
65
|
-
fill: "none",
|
|
66
|
-
stroke: "currentColor",
|
|
67
|
-
strokeWidth: 1.7,
|
|
68
|
-
strokeLinecap: "round",
|
|
69
|
-
strokeLinejoin: "round",
|
|
70
|
-
"aria-hidden": true,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
switch (kind) {
|
|
74
|
-
case "scene":
|
|
75
|
-
return (
|
|
76
|
-
<svg {...common}>
|
|
77
|
-
<path d="M12 3.25 21 8l-9 4.75L3 8l9-4.75Z" fill="currentColor" fillOpacity=".16" />
|
|
78
|
-
<path d="m4.25 12 7.75 4.1 7.75-4.1M4.25 16 12 20.1l7.75-4.1" />
|
|
79
|
-
</svg>
|
|
80
|
-
);
|
|
81
|
-
case "camera":
|
|
82
|
-
return (
|
|
83
|
-
<svg {...common}>
|
|
84
|
-
<rect x="3" y="6.5" width="13.5" height="11" rx="2" />
|
|
85
|
-
<path d="m16.5 10 4.5-2.25v8.5L16.5 14" />
|
|
86
|
-
<circle cx="9.5" cy="12" r="2.4" />
|
|
87
|
-
</svg>
|
|
88
|
-
);
|
|
89
|
-
case "light":
|
|
90
|
-
return (
|
|
91
|
-
<svg {...common}>
|
|
92
|
-
<circle cx="12" cy="12" r="4" fill="currentColor" fillOpacity=".16" />
|
|
93
|
-
<path d="M12 3v2.2M12 18.8V21M3 12h2.2M18.8 12H21M5.6 5.6l1.6 1.6M16.8 16.8l1.6 1.6M18.4 5.6l-1.6 1.6M7.2 16.8l-1.6 1.6" />
|
|
94
|
-
</svg>
|
|
95
|
-
);
|
|
96
|
-
case "group":
|
|
97
|
-
return (
|
|
98
|
-
<svg {...common}>
|
|
99
|
-
<circle cx="9" cy="8.25" r="2.75" />
|
|
100
|
-
<circle cx="16.5" cy="9.25" r="2.15" />
|
|
101
|
-
<path d="M3.75 18c.45-3.25 2.15-5 5.25-5s4.8 1.75 5.25 5M14 14.25c3.55-.55 5.65.75 6.25 3.75" />
|
|
102
|
-
</svg>
|
|
103
|
-
);
|
|
104
|
-
case "character":
|
|
105
|
-
return (
|
|
106
|
-
<svg {...common}>
|
|
107
|
-
<circle cx="12" cy="7.25" r="3" fill="currentColor" fillOpacity=".12" />
|
|
108
|
-
<path d="M5.25 20c.45-5 2.7-7.5 6.75-7.5S18.3 15 18.75 20M9.25 13.25 12 16l2.75-2.75" />
|
|
109
|
-
</svg>
|
|
110
|
-
);
|
|
111
|
-
case "rig":
|
|
112
|
-
return (
|
|
113
|
-
<svg {...common}>
|
|
114
|
-
<circle cx="12" cy="4.5" r="1.75" fill="currentColor" fillOpacity=".18" />
|
|
115
|
-
<circle cx="6" cy="11.5" r="1.75" />
|
|
116
|
-
<circle cx="18" cy="11.5" r="1.75" />
|
|
117
|
-
<circle cx="12" cy="19.5" r="1.75" />
|
|
118
|
-
<path d="m10.8 5.8-3.6 4.4m6-4.4 3.6 4.4M7.75 12.65l3 5.4m5.5-5.4-3 5.4" />
|
|
119
|
-
</svg>
|
|
120
|
-
);
|
|
121
|
-
case "bone":
|
|
122
|
-
return (
|
|
123
|
-
<svg {...common}>
|
|
124
|
-
<path d="M7.4 8.25a2.5 2.5 0 1 1-3.65-3.4A2.5 2.5 0 1 1 7.2 8.5l8.3 8.3a2.5 2.5 0 1 1 3.65 3.4 2.5 2.5 0 1 1-3.4-3.65L7.4 8.25Z" />
|
|
125
|
-
</svg>
|
|
126
|
-
);
|
|
127
|
-
case "environment":
|
|
128
|
-
return (
|
|
129
|
-
<svg {...common}>
|
|
130
|
-
<circle cx="12" cy="12" r="8.5" fill="currentColor" fillOpacity=".08" />
|
|
131
|
-
<path d="M3.8 13h16.4M12 3.5c2.25 2.35 3.4 5.2 3.4 8.5S14.25 18.15 12 20.5C9.75 18.15 8.6 15.3 8.6 12S9.75 5.85 12 3.5Z" />
|
|
132
|
-
</svg>
|
|
133
|
-
);
|
|
134
|
-
case "props":
|
|
135
|
-
case "object":
|
|
136
|
-
return (
|
|
137
|
-
<svg {...common}>
|
|
138
|
-
<path d="m12 3.5 8 4.25v8.5l-8 4.25-8-4.25v-8.5L12 3.5Z" fill="currentColor" fillOpacity=".1" />
|
|
139
|
-
<path d="m4.25 7.9 7.75 4.2 7.75-4.2M12 12.1v8.15" />
|
|
140
|
-
</svg>
|
|
141
|
-
);
|
|
142
|
-
default:
|
|
143
|
-
return (
|
|
144
|
-
<svg {...common}>
|
|
145
|
-
<circle cx="12" cy="12" r="7.5" />
|
|
146
|
-
</svg>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
62
|
function displaySceneName(name) {
|
|
152
63
|
if (!isKo) return name;
|
|
153
64
|
const generatedName = /^SCENE\s+(\d+)$/i.exec(name);
|
|
@@ -202,6 +113,7 @@ function displayHierarchyLabel(node) {
|
|
|
202
113
|
}
|
|
203
114
|
|
|
204
115
|
const OUTLINER_TYPES = {
|
|
116
|
+
empty: "Empty",
|
|
205
117
|
folder: "Folder",
|
|
206
118
|
mesh: "Mesh",
|
|
207
119
|
cast: "Cast",
|
|
@@ -211,7 +123,7 @@ const OUTLINER_TYPES = {
|
|
|
211
123
|
|
|
212
124
|
function outlinerTypeFor(node) {
|
|
213
125
|
const key = node.kind === "object"
|
|
214
|
-
? "mesh"
|
|
126
|
+
? node.renderer === "empty" ? "empty" : "mesh"
|
|
215
127
|
: node.kind === "character" || node.kind === "bone"
|
|
216
128
|
? "cast"
|
|
217
129
|
: node.kind === "camera"
|
|
@@ -219,7 +131,9 @@ function outlinerTypeFor(node) {
|
|
|
219
131
|
: node.kind === "light"
|
|
220
132
|
? "light"
|
|
221
133
|
: "folder";
|
|
222
|
-
const label = key === "
|
|
134
|
+
const label = key === "empty"
|
|
135
|
+
? ko(OUTLINER_TYPES[key], "빈 오브젝트")
|
|
136
|
+
: key === "folder"
|
|
223
137
|
? ko(OUTLINER_TYPES[key], "폴더")
|
|
224
138
|
: key === "mesh"
|
|
225
139
|
? ko(OUTLINER_TYPES[key], "메시")
|
|
@@ -339,9 +253,16 @@ function RowContextMenu({ menu, onClose, onAction, onAddObject }) {
|
|
|
339
253
|
</button>
|
|
340
254
|
</>
|
|
341
255
|
) : menu.kind === "character" ? (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
256
|
+
<>
|
|
257
|
+
<button type="button" role="menuitem" className="hierarchy-context-item" onClick={() => onAction("visibility", menu.id)}>
|
|
258
|
+
{menu.hidden ? ko("Show", "표시") : ko("Hide", "숨기기")}
|
|
259
|
+
</button>
|
|
260
|
+
{menu.grouped && (
|
|
261
|
+
<button type="button" role="menuitem" className="hierarchy-context-item" onClick={() => onAction("ungroup-character", menu.id)}>
|
|
262
|
+
{ko("Remove from group", "그룹에서 빼기")}
|
|
263
|
+
</button>
|
|
264
|
+
)}
|
|
265
|
+
</>
|
|
345
266
|
) : menu.kind === "object" ? (
|
|
346
267
|
<>
|
|
347
268
|
<button type="button" role="menuitem" className="hierarchy-context-item" onClick={() => onAction("visibility", menu.id)}>
|
|
@@ -359,6 +280,14 @@ function RowContextMenu({ menu, onClose, onAction, onAddObject }) {
|
|
|
359
280
|
<button type="button" role="menuitem" className="hierarchy-context-item" onClick={() => onAction("frame", menu.id)}>
|
|
360
281
|
{ko("Frame", "프레임 맞추기")}
|
|
361
282
|
</button>
|
|
283
|
+
<button type="button" role="menuitem" className="hierarchy-context-item" data-action="group-under-empty" onClick={() => onAction("group-under-empty", menu.id)}>
|
|
284
|
+
{ko("Group under new Empty", "빈 오브젝트로 묶기")}
|
|
285
|
+
</button>
|
|
286
|
+
{menu.grouped && (
|
|
287
|
+
<button type="button" role="menuitem" className="hierarchy-context-item" data-action="ungroup-object" onClick={() => onAction("ungroup-object", menu.id)}>
|
|
288
|
+
{ko("Move out to Props", "그룹에서 빼기 (Props로)")}
|
|
289
|
+
</button>
|
|
290
|
+
)}
|
|
362
291
|
</>
|
|
363
292
|
) : (
|
|
364
293
|
<CatalogueEntries onPick={onAddObject} />
|
|
@@ -423,10 +352,11 @@ function TreeRow({
|
|
|
423
352
|
dropHandlers.onDrop(event);
|
|
424
353
|
},
|
|
425
354
|
};
|
|
426
|
-
// Row drag: an object row is the handle, ANY row can be the
|
|
427
|
-
// the panel holds no policy, the caller's canDrop decides
|
|
355
|
+
// Row drag: an object or character row is the handle, ANY row can be the
|
|
356
|
+
// landing spot — the panel holds no policy, the caller's canDrop decides
|
|
357
|
+
// every rule.
|
|
428
358
|
const [rowDropOver, setRowDropOver] = useState(false);
|
|
429
|
-
const draggableRow = node.kind === "object" && !editing;
|
|
359
|
+
const draggableRow = (node.kind === "object" || node.kind === "character") && !editing;
|
|
430
360
|
const rowDropTarget = !!(reparent && dragSourceId && reparent.canDrop?.(dragSourceId, node.id));
|
|
431
361
|
const rowDrag = (reparent || draggableRow) && {
|
|
432
362
|
...(draggableRow
|
|
@@ -559,7 +489,7 @@ function TreeRow({
|
|
|
559
489
|
autoFocus
|
|
560
490
|
onFocus={(event) => event.currentTarget.select()}
|
|
561
491
|
onKeyDown={(event) => {
|
|
562
|
-
if (event.key === "Enter") {
|
|
492
|
+
if (event.key === "Enter" && !isImeComposing(event)) {
|
|
563
493
|
event.preventDefault();
|
|
564
494
|
event.stopPropagation(); // keep the tree hotkey quiet
|
|
565
495
|
finish();
|
|
@@ -627,6 +557,8 @@ export default function HierarchyPanel({
|
|
|
627
557
|
onDuplicateObject,
|
|
628
558
|
onDeleteObject,
|
|
629
559
|
onFrameObject,
|
|
560
|
+
onGroupObject,
|
|
561
|
+
renameRequest = null,
|
|
630
562
|
onToggleHidden,
|
|
631
563
|
propsDrop = null,
|
|
632
564
|
reparent = null,
|
|
@@ -647,6 +579,10 @@ export default function HierarchyPanel({
|
|
|
647
579
|
// Row currently in in-place rename. The panel owns it: F2/Return and the
|
|
648
580
|
// row context menu are the only ways in, so app state stays out of it.
|
|
649
581
|
const [editingId, setEditingId] = useState(null);
|
|
582
|
+
// A freshly made Empty asks to be named: open its row's rename field.
|
|
583
|
+
useEffect(() => {
|
|
584
|
+
if (renameRequest?.id) setEditingId(renameRequest.id);
|
|
585
|
+
}, [renameRequest?.id, renameRequest?.nonce]);
|
|
650
586
|
// The row being dragged. dataTransfer.getData() is deliberately blank during
|
|
651
587
|
// dragover in Chrome, so canDrop could never gate the highlight from the
|
|
652
588
|
// payload alone — the id lives here from dragstart until dragend/drop.
|
|
@@ -764,13 +700,18 @@ export default function HierarchyPanel({
|
|
|
764
700
|
event.stopPropagation(); // a row pick must not also open the create menu
|
|
765
701
|
const node = findHierarchyNode(hierarchyNodes, id);
|
|
766
702
|
if (node?.kind === "object" || node?.kind === "character") {
|
|
703
|
+
// A grouped object can leave its group from the menu too, not only by
|
|
704
|
+
// dragging its row onto Props.
|
|
705
|
+
const objectId = node.kind === "object" ? sceneObjectIdFromHierarchy(id) : null;
|
|
706
|
+
const inGroup = objectId ? Boolean(sceneObjects.find((entry) => entry.id === objectId)?.parent) : false;
|
|
767
707
|
setContextMenu({
|
|
768
708
|
x: event.clientX,
|
|
769
709
|
y: event.clientY,
|
|
770
|
-
height: node.kind === "object" ?
|
|
710
|
+
height: node.kind === "object" ? (inGroup ? 248 : 212) : node.grouped ? 80 : 44,
|
|
771
711
|
kind: node.kind,
|
|
772
712
|
id,
|
|
773
713
|
hidden: node.hidden === true,
|
|
714
|
+
grouped: node.grouped === true || inGroup,
|
|
774
715
|
});
|
|
775
716
|
} else if (id === SCENE_ROOT_ID) {
|
|
776
717
|
// The root row is the scene document: its own verbs, never the
|
|
@@ -784,13 +725,13 @@ export default function HierarchyPanel({
|
|
|
784
725
|
canDelete: availableScenes.length > 1,
|
|
785
726
|
});
|
|
786
727
|
} else {
|
|
787
|
-
setContextMenu({ x: event.clientX, y: event.clientY, height:
|
|
728
|
+
setContextMenu({ x: event.clientX, y: event.clientY, height: 376, kind: "create" });
|
|
788
729
|
}
|
|
789
730
|
};
|
|
790
731
|
|
|
791
732
|
const openCreateMenu = (event) => {
|
|
792
733
|
event.preventDefault(); // suppress the browser menu on the tree only
|
|
793
|
-
setContextMenu({ x: event.clientX, y: event.clientY, height:
|
|
734
|
+
setContextMenu({ x: event.clientX, y: event.clientY, height: 376, kind: "create" });
|
|
794
735
|
};
|
|
795
736
|
|
|
796
737
|
const deleteActiveScene = () => {
|
|
@@ -824,11 +765,22 @@ export default function HierarchyPanel({
|
|
|
824
765
|
onToggleHidden?.(hierarchyId);
|
|
825
766
|
return;
|
|
826
767
|
}
|
|
768
|
+
if (action === "ungroup-object") {
|
|
769
|
+
// The same door as dropping the row on Props: out of its group, kept where it is.
|
|
770
|
+
if (reparent?.canDrop?.(hierarchyId, "props")) reparent.onDrop?.(hierarchyId, "props");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (action === "ungroup-character") {
|
|
774
|
+
// Out of the group is a drop on the scene root: the same policy door.
|
|
775
|
+
if (reparent?.canDrop?.(hierarchyId, SCENE_ROOT_ID)) reparent.onDrop?.(hierarchyId, SCENE_ROOT_ID);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
827
778
|
const objectId = sceneObjectIdFromHierarchy(hierarchyId);
|
|
828
779
|
if (!objectId) return;
|
|
829
780
|
if (action === "duplicate") onDuplicateObject?.(objectId);
|
|
830
781
|
else if (action === "delete") onDeleteObject?.(objectId);
|
|
831
782
|
else if (action === "frame") onFrameObject?.(objectId);
|
|
783
|
+
else if (action === "group-under-empty") onGroupObject?.(objectId);
|
|
832
784
|
};
|
|
833
785
|
|
|
834
786
|
const commitRename = (hierarchyId, name) => {
|
package/src/history.js
CHANGED
|
@@ -46,8 +46,6 @@ export function canRedo(history) {
|
|
|
46
46
|
return history.future.length > 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// ---------------------------------------------------------------- tx ----
|
|
50
|
-
//
|
|
51
49
|
// `cancel` is teardown-only by contract: it must not call back into the
|
|
52
50
|
// coordinator (`endTransaction`/`settleTransaction`). The coordinator owns
|
|
53
51
|
// the close — it retires the token BEFORE running `cancel`, so a close
|
package/src/i2v-motion-client.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
export const I2V_MOTION_MODEL = "minimax/h3-max-turbo/image-to-video";
|
|
2
|
-
export const I2V_MOTION_RESOLUTION = "480P";
|
|
3
1
|
export const I2V_MOTION_MIN_DURATION = 5;
|
|
4
|
-
export const I2V_MOTION_MAX_DURATION = 15;
|
|
5
2
|
// The durations the AI motion card offers. 15 s (362 frames) is the measured upper
|
|
6
3
|
// end the H3 endpoint accepts and the extractor handles (#380 sit-to-bench).
|
|
7
4
|
export const I2V_MOTION_DURATIONS = Object.freeze([5, 10, 15]);
|
|
@@ -61,7 +58,7 @@ export function submitMotionJob(input, fetchImpl = fetch) {
|
|
|
61
58
|
return request(`/v1/motion/${kind}`, body, fetchImpl);
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
|
|
61
|
+
function getMotionJob(id, fetchImpl = fetch) {
|
|
65
62
|
return request(`/v1/motion/jobs/${encodeURIComponent(id)}`, undefined, fetchImpl);
|
|
66
63
|
}
|
|
67
64
|
|
package/src/ime.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Browsers report an Enter key from an active input method editor as a
|
|
2
|
+
// composing key event. Chromium and Safari also use the legacy 229 keyCode;
|
|
3
|
+
// keep both signals so text commits never trigger an action first.
|
|
4
|
+
export function isImeComposing(event) {
|
|
5
|
+
return Boolean(event?.isComposing || event?.nativeEvent?.isComposing || event?.keyCode === 229 || event?.which === 229);
|
|
6
|
+
}
|
package/src/keyframe-pack.js
CHANGED
|
@@ -19,7 +19,7 @@ export function keyframePackName(shot) {
|
|
|
19
19
|
return `cozyclay-shot-${shot.index}-${slugifyTitle(shot.title)}.zip`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function readmeText({ shot, fps, hasLast, hasClip, ext }) {
|
|
22
|
+
function readmeText({ shot, fps, hasLast, hasClip, ext, hasStylized }) {
|
|
23
23
|
const lines = [
|
|
24
24
|
`CozyClay keyframe pack - shot ${shot.index}: ${shot.title}`,
|
|
25
25
|
`Frame range ${shot.startFrame}-${shot.endFrame} at ${fps} fps.`,
|
|
@@ -58,30 +58,39 @@ function readmeText({ shot, fps, hasLast, hasClip, ext }) {
|
|
|
58
58
|
"- blocking-depth.mp4: shot-wide normalised depth video; lighter pixels are closer.",
|
|
59
59
|
"- prompt.txt: the text prompt describing the intended action.",
|
|
60
60
|
);
|
|
61
|
+
if (hasStylized) lines.push("- stylized.png: the stylized picture of this panel; first.png is its greybox.");
|
|
61
62
|
return lines.join("\n") + "\n";
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* Build the ZIP entries for one shot's keyframe pack.
|
|
66
67
|
* @param {object} args
|
|
67
|
-
* @param {{ title: string, index: number, startFrame: number, endFrame: number }} args.shot
|
|
68
|
+
* @param {{ title: string, index: number, startFrame: number, endFrame: number, kind?: "clip" | "still" }} args.shot
|
|
69
|
+
* a still (storyboard panel) is one picture held for its whole range:
|
|
70
|
+
* camera.json gets startFrame === endFrame === the panel start, `hold`
|
|
71
|
+
* frames and `kind: "still"`.
|
|
72
|
+
* @param {string} [args.folder] folder inside the zip, default `<index>-<slug>`
|
|
68
73
|
* @param {number} args.fps
|
|
69
74
|
* @param {Uint8Array} args.firstFramePng
|
|
70
75
|
* @param {Uint8Array | null} args.lastFramePng
|
|
71
76
|
* @param {{ data: Uint8Array, ext: "mp4" | "webm" } | null} args.clip
|
|
72
77
|
* @param {object} args.camera
|
|
73
78
|
* @param {string} args.prompt
|
|
79
|
+
* @param {Uint8Array | null} [args.stylizedPng] the panel's stylized picture
|
|
74
80
|
* @returns {Array<{ name: string, data: Uint8Array | string }>} entries for buildZip
|
|
75
81
|
*/
|
|
76
|
-
export function keyframePackEntries({ shot, fps, firstFramePng, lastFramePng, clip, camera, prompt }) {
|
|
77
|
-
const folder = `${shot.index}-${slugifyTitle(shot.title)}`;
|
|
82
|
+
export function keyframePackEntries({ shot, fps, firstFramePng, lastFramePng, clip, camera, prompt, stylizedPng = null, folder = `${shot.index}-${slugifyTitle(shot.title)}` }) {
|
|
78
83
|
const entries = [{ name: `${folder}/first.png`, data: firstFramePng }];
|
|
79
84
|
if (lastFramePng != null) entries.push({ name: `${folder}/last.png`, data: lastFramePng });
|
|
80
85
|
if (clip != null) entries.push({ name: `${folder}/clip.${clip.ext}`, data: clip.data });
|
|
86
|
+
if (stylizedPng != null) entries.push({ name: `${folder}/stylized.png`, data: stylizedPng });
|
|
87
|
+
const range = shot.kind === "still"
|
|
88
|
+
? { startFrame: shot.startFrame, endFrame: shot.startFrame, hold: shot.endFrame - shot.startFrame + 1, kind: "still" }
|
|
89
|
+
: { startFrame: shot.startFrame, endFrame: shot.endFrame };
|
|
81
90
|
entries.push(
|
|
82
|
-
{ name: `${folder}/camera.json`, data: JSON.stringify({ ...camera, fps,
|
|
91
|
+
{ name: `${folder}/camera.json`, data: JSON.stringify({ ...camera, fps, ...range }, null, 2) + "\n" },
|
|
83
92
|
{ name: `${folder}/prompt.txt`, data: `${prompt}\n` },
|
|
84
|
-
{ name: `${folder}/README.txt`, data: readmeText({ shot, fps, hasLast: lastFramePng != null, hasClip: clip != null, ext: clip ? clip.ext : "mp4" }) },
|
|
93
|
+
{ name: `${folder}/README.txt`, data: readmeText({ shot, fps, hasLast: lastFramePng != null, hasClip: clip != null, ext: clip ? clip.ext : "mp4", hasStylized: stylizedPng != null }) },
|
|
85
94
|
);
|
|
86
95
|
return entries;
|
|
87
96
|
}
|
package/src/line-edit.js
CHANGED
|
@@ -116,7 +116,7 @@ export const CURVE_GRAB_RADIUS_PX = 14;
|
|
|
116
116
|
* any amount of drift (App.jsx paints it detached and refuses new gestures
|
|
117
117
|
* until the view comes back). 1e-4 is tighter than any deliberate nudge and
|
|
118
118
|
* looser than float noise from re-deriving the same matrix. */
|
|
119
|
-
|
|
119
|
+
const CAMERA_DRIFT_EPSILON = 1e-4;
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* The rendering camera -> the C6 `camera` block: { fx, fy, cx, cy, R, t }.
|
|
@@ -577,7 +577,7 @@ export const DRAW_BUNCHED_WINDOW_FRAMES = 24;
|
|
|
577
577
|
* two distinguishable PLACES on the path, and that is a screen-distance
|
|
578
578
|
* question. Twice the grab radius is the app's own scale for "the same spot":
|
|
579
579
|
* inside it a user could not have aimed at one end rather than the other. */
|
|
580
|
-
|
|
580
|
+
const DRAW_MATCH_MIN_SEPARATION_PX = 2 * CURVE_GRAB_RADIUS_PX;
|
|
581
581
|
|
|
582
582
|
/** Seam easing, replacing the hard 2-frame pin on the DRAW path only.
|
|
583
583
|
*
|
|
@@ -608,7 +608,7 @@ export const DRAW_MATCH_MIN_SEPARATION_PX = 2 * CURVE_GRAB_RADIUS_PX;
|
|
|
608
608
|
* (Table 5), so a dozen frames is the scale at which the sampler already thinks
|
|
609
609
|
* about "near a constraint". The 2-frame floor keeps the shortest legal window
|
|
610
610
|
* behaving exactly like the pin it replaces. */
|
|
611
|
-
|
|
611
|
+
const SEAM_EASE_FRACTION = 0.15;
|
|
612
612
|
export const SEAM_EASE_MIN = 2;
|
|
613
613
|
export const SEAM_EASE_MAX = 12;
|
|
614
614
|
|
|
@@ -1133,7 +1133,7 @@ export function curveToPoints2d(curve) {
|
|
|
1133
1133
|
for (let i = 0; i < count; i += 1) {
|
|
1134
1134
|
// Rounded rather than floored so the sampling is symmetric about the
|
|
1135
1135
|
// middle; i = count-1 lands exactly on `last`, i = 0 exactly on 0.
|
|
1136
|
-
const source =
|
|
1136
|
+
const source = Math.round((i * last) / (count - 1));
|
|
1137
1137
|
const point = visible[source];
|
|
1138
1138
|
points2d.push([point.u, point.v]);
|
|
1139
1139
|
}
|
package/src/live-control.js
CHANGED
|
@@ -10,14 +10,14 @@ const MOTION_TELEMETRY_EVENTS = new Set([
|
|
|
10
10
|
]);
|
|
11
11
|
const LIVE_TELEMETRY_EVENTS = new Set([...MOTION_TELEMETRY_EVENTS, ...EXECUTION_TELEMETRY_EVENTS.filter((event) => event.startsWith("mcp:"))]);
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const LIVE_CONTROL_PORT = import.meta.env?.VITE_COZYCLAY_LIVE_PORT ?? "5184";
|
|
14
14
|
export const liveControlUrl = (port = LIVE_CONTROL_PORT) => `ws://127.0.0.1:${port}/live`;
|
|
15
|
-
|
|
15
|
+
const LIVE_CONTROL_URL = liveControlUrl();
|
|
16
16
|
// Reconnect schedule: 1 s doubling to a 15 s cap with +-25 % jitter, so a hub
|
|
17
17
|
// that comes back does not take every editor's retry in the same millisecond.
|
|
18
|
-
|
|
18
|
+
const LIVE_RECONNECT_BASE_MS = 1_000;
|
|
19
19
|
export const LIVE_RECONNECT_MAX_MS = 15_000;
|
|
20
|
-
|
|
20
|
+
const LIVE_RECONNECT_JITTER = 0.25;
|
|
21
21
|
// App-level liveness, sent only to a hub that advertises `heartbeatMs`: a TCP
|
|
22
22
|
// connection to a dead hub can stay open for minutes without a single frame.
|
|
23
23
|
export const LIVE_PING_INTERVAL_MS = 20_000;
|
|
@@ -25,11 +25,11 @@ export const LIVE_PONG_TIMEOUT_MS = 10_000;
|
|
|
25
25
|
// A blocked main thread (a 30 s capture_frame) delays every timer at once. A
|
|
26
26
|
// tick that late is evidence of that stall, not of a dead hub, so the round is
|
|
27
27
|
// restarted instead of being counted as a missed pong.
|
|
28
|
-
|
|
28
|
+
const LIVE_HEARTBEAT_SLACK_MS = 2_000;
|
|
29
29
|
// Per-tab identity. sessionStorage is per tab and survives a reload, which is
|
|
30
30
|
// exactly the lifetime the hub routes retained motion outcomes to.
|
|
31
31
|
export const LIVE_WORKSPACE_ID_KEY = "cozyclay:live-workspace-id";
|
|
32
|
-
|
|
32
|
+
const LIVE_DUPLICATE_CLOSE_CODE = 1008;
|
|
33
33
|
export const LIVE_DUPLICATE_CLOSE_REASON = "Workspace id is already connected";
|
|
34
34
|
|
|
35
35
|
// Storage can be absent (Node), disabled, or throw on access alone; the tab
|