cozyclay 2.0.2 → 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 +56 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- 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 +4 -4
- 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 +2 -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 +1 -1
- 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 +789 -263
- 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 +184 -6
- package/src/ardy/timeline.jsx +113 -28
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- 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 +6 -7
- 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 +15 -2
- 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 +79 -28
- 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-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 +49 -4
- 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 +58 -9
- 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 +92 -9
- 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 +106 -18
- 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 +94 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { ko } from "./locale.js";
|
|
2
|
+
import { CUSTOM_MOVE } from "./shot.js";
|
|
3
|
+
import { CUTOUT_KIND, MESH_KIND } from "./scene-objects.js";
|
|
4
|
+
|
|
5
|
+
export const RIG_HIERARCHY_FOCUS = {
|
|
6
|
+
"rig.torso": "spine",
|
|
7
|
+
"rig.hips": "hips",
|
|
8
|
+
"rig.spine": "spine",
|
|
9
|
+
"rig.chest": "chest",
|
|
10
|
+
"rig.neck": "neck",
|
|
11
|
+
"rig.head": "head",
|
|
12
|
+
"rig.leftArm": "leftHand",
|
|
13
|
+
"rig.leftShoulder": "leftShoulder",
|
|
14
|
+
"rig.leftElbow": "leftElbow",
|
|
15
|
+
"rig.leftHand": "leftHand",
|
|
16
|
+
"rig.rightArm": "rightHand",
|
|
17
|
+
"rig.rightShoulder": "rightShoulder",
|
|
18
|
+
"rig.rightElbow": "rightElbow",
|
|
19
|
+
"rig.rightHand": "rightHand",
|
|
20
|
+
"rig.leftLeg": "leftFoot",
|
|
21
|
+
"rig.leftKnee": "leftKnee",
|
|
22
|
+
"rig.leftFoot": "leftFoot",
|
|
23
|
+
"rig.rightLeg": "rightFoot",
|
|
24
|
+
"rig.rightKnee": "rightKnee",
|
|
25
|
+
"rig.rightFoot": "rightFoot",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const HIERARCHY_INSPECTOR_TITLES = {
|
|
29
|
+
shot: ko("Shot settings", "샷 설정"),
|
|
30
|
+
camera: ko("Camera", "카메라"),
|
|
31
|
+
light: ko("Light", "조명"),
|
|
32
|
+
characters: ko("Characters", "인물"),
|
|
33
|
+
characterA: ko("Character 1", "인물 1"),
|
|
34
|
+
rig: ko("Rig", "리그"),
|
|
35
|
+
characterB: ko("Character 2", "인물 2"),
|
|
36
|
+
environment: ko("Environment", "환경"),
|
|
37
|
+
props: ko("Props", "소품"),
|
|
38
|
+
"rig.torso": ko("Torso", "몸통"),
|
|
39
|
+
"rig.hips": ko("Root / Hips", "루트 / 엉덩이"),
|
|
40
|
+
"rig.spine": ko("Spine", "척추"),
|
|
41
|
+
"rig.chest": ko("Chest", "가슴"),
|
|
42
|
+
"rig.neck": ko("Neck", "목"),
|
|
43
|
+
"rig.head": ko("Head", "머리"),
|
|
44
|
+
"rig.leftArm": ko("Left Arm", "왼팔"),
|
|
45
|
+
"rig.leftShoulder": ko("Left Shoulder", "왼쪽 어깨"),
|
|
46
|
+
"rig.leftElbow": ko("Left Elbow", "왼쪽 팔꿈치"),
|
|
47
|
+
"rig.leftHand": ko("Left Hand", "왼손"),
|
|
48
|
+
"rig.rightArm": ko("Right Arm", "오른팔"),
|
|
49
|
+
"rig.rightShoulder": ko("Right Shoulder", "오른쪽 어깨"),
|
|
50
|
+
"rig.rightElbow": ko("Right Elbow", "오른쪽 팔꿈치"),
|
|
51
|
+
"rig.rightHand": ko("Right Hand", "오른손"),
|
|
52
|
+
"rig.leftLeg": ko("Left Leg", "왼다리"),
|
|
53
|
+
"rig.leftKnee": ko("Left Knee", "왼쪽 무릎"),
|
|
54
|
+
"rig.leftFoot": ko("Left Foot", "왼발"),
|
|
55
|
+
"rig.rightLeg": ko("Right Leg", "오른다리"),
|
|
56
|
+
"rig.rightKnee": ko("Right Knee", "오른쪽 무릎"),
|
|
57
|
+
"rig.rightFoot": ko("Right Foot", "오른발"),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const CAMERA_MOVE_LABELS_KO = new Map([
|
|
61
|
+
["Static / locked-off", ko("Static / locked-off", "고정 샷")],
|
|
62
|
+
["Zoom in", ko("Zoom in", "줌 인")],
|
|
63
|
+
["Zoom out", ko("Zoom out", "줌 아웃")],
|
|
64
|
+
["Push-in (dolly in)", ko("Push-in (dolly in)", "푸시인(돌리 인)")],
|
|
65
|
+
["Pull-out (dolly out)", ko("Pull-out (dolly out)", "풀아웃(돌리 아웃)")],
|
|
66
|
+
["Pan left", ko("Pan left", "왼쪽 팬")],
|
|
67
|
+
["Pan right", ko("Pan right", "오른쪽 팬")],
|
|
68
|
+
["Tilt up", ko("Tilt up", "틸트 업")],
|
|
69
|
+
["Tilt down", ko("Tilt down", "틸트 다운")],
|
|
70
|
+
["Tracking / follow", ko("Tracking / follow", "트래킹 / 팔로우")],
|
|
71
|
+
["Orbit / arc", ko("Orbit / arc", "오빗 / 아크")],
|
|
72
|
+
["Crane up", ko("Crane up", "크레인 업")],
|
|
73
|
+
["Crane down", ko("Crane down", "크레인 다운")],
|
|
74
|
+
["Handheld", ko("Handheld", "핸드헬드")],
|
|
75
|
+
["Crash zoom in", ko("Crash zoom in", "크래시 줌 인")],
|
|
76
|
+
["Dolly-zoom (vertigo)", ko("Dolly-zoom (vertigo)", "돌리 줌(버티고)")],
|
|
77
|
+
["Whip pan", ko("Whip pan", "휩 팬")],
|
|
78
|
+
["Aerial / drone", ko("Aerial / drone", "공중 / 드론")],
|
|
79
|
+
[CUSTOM_MOVE, ko(CUSTOM_MOVE, "직접 입력…")],
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
export const POSE_LABELS_KO = new Map([
|
|
83
|
+
["T-pose", ko("T-pose", "T 포즈")],
|
|
84
|
+
["Relaxed", ko("Relaxed", "편안한 자세")],
|
|
85
|
+
["Contrapposto", ko("Contrapposto", "콘트라포스토")],
|
|
86
|
+
["Walking", ko("Walking", "걷는 자세")],
|
|
87
|
+
["Seated", ko("Seated", "앉은 자세")],
|
|
88
|
+
["Arms crossed", ko("Arms crossed", "팔짱")],
|
|
89
|
+
["Pointing", ko("Pointing", "가리키기")],
|
|
90
|
+
["Hands on hips", ko("Hands on hips", "허리에 손")],
|
|
91
|
+
["Looking back", ko("Looking back", "뒤돌아보기")],
|
|
92
|
+
["Hands up", ko("Hands up", "손 올리기")],
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
export const SHOT_SIZE_LABELS_KO = new Map([
|
|
96
|
+
["extreme close-up", ko("extreme close-up", "익스트림 클로즈업")],
|
|
97
|
+
["close-up", ko("close-up", "클로즈업")],
|
|
98
|
+
["medium close-up", ko("medium close-up", "미디엄 클로즈업")],
|
|
99
|
+
["medium shot", ko("medium shot", "미디엄 샷")],
|
|
100
|
+
["medium-wide shot", ko("medium-wide shot", "미디엄 와이드 샷")],
|
|
101
|
+
["wide shot", ko("wide shot", "와이드 샷")],
|
|
102
|
+
["extreme wide shot", ko("extreme wide shot", "익스트림 와이드 샷")],
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
export const SHOT_LEVEL_LABELS_KO = new Map([
|
|
106
|
+
["overhead", ko("overhead", "오버헤드")],
|
|
107
|
+
["high angle", ko("high angle", "하이 앵글")],
|
|
108
|
+
["eye level", ko("eye level", "아이 레벨")],
|
|
109
|
+
["chest level", ko("chest level", "가슴 높이")],
|
|
110
|
+
["hip level", ko("hip level", "엉덩이 높이")],
|
|
111
|
+
["knee level", ko("knee level", "무릎 높이")],
|
|
112
|
+
["ground level", ko("ground level", "바닥 높이")],
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
export const SCENE_RENDERER_LABELS_KO = new Map([
|
|
116
|
+
["cube", ko("cube", "큐브")],
|
|
117
|
+
["sphere", ko("sphere", "구")],
|
|
118
|
+
["capsule", ko("capsule", "캡슐")],
|
|
119
|
+
["cylinder", ko("cylinder", "원기둥")],
|
|
120
|
+
["cone", ko("cone", "원뿔")],
|
|
121
|
+
["plane", ko("plane", "평면")],
|
|
122
|
+
["chair", ko("chair", "의자")],
|
|
123
|
+
["car", ko("car", "자동차")],
|
|
124
|
+
["aircraft", ko("aircraft", "비행기")],
|
|
125
|
+
[CUTOUT_KIND, ko("cutout", "컷아웃")],
|
|
126
|
+
[MESH_KIND, ko("Mesh", "모델")],
|
|
127
|
+
["sculpt", ko("Sculpt", "조형")],
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
export const SCENE_OBJECT_NAME_LABELS_KO = new Map([
|
|
131
|
+
["Cube", ko("Cube", "큐브")],
|
|
132
|
+
["Sphere", ko("Sphere", "구")],
|
|
133
|
+
["Capsule", ko("Capsule", "캡슐")],
|
|
134
|
+
["Cylinder", ko("Cylinder", "원기둥")],
|
|
135
|
+
["Cone", ko("Cone", "원뿔")],
|
|
136
|
+
["Plane", ko("Plane", "평면")],
|
|
137
|
+
["Chair", ko("Chair", "의자")],
|
|
138
|
+
["Car", ko("Car", "자동차")],
|
|
139
|
+
["Plane (aircraft)", ko("Plane (aircraft)", "비행기")],
|
|
140
|
+
["Mesh", ko("Mesh", "모델")],
|
|
141
|
+
["Model", ko("Model", "모델")],
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
export function poseLabelKo(pose) {
|
|
145
|
+
return pose?.custom ? pose.label : POSE_LABELS_KO.get(pose?.label) ?? pose?.label ?? "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function cameraMoveLabelKo(move) {
|
|
149
|
+
return CAMERA_MOVE_LABELS_KO.get(move) ?? move;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function sceneRendererLabelKo(renderer) {
|
|
153
|
+
return SCENE_RENDERER_LABELS_KO.get(renderer) ?? renderer;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function sceneObjectNameDisplayKo(name) {
|
|
157
|
+
const match = /^(.+?)(?: ([2-9]\d*))?$/.exec(name ?? "");
|
|
158
|
+
if (!match) return name;
|
|
159
|
+
const [, base, suffix] = match;
|
|
160
|
+
const label = SCENE_OBJECT_NAME_LABELS_KO.get(base);
|
|
161
|
+
return label ? `${label}${suffix ? ` ${suffix}` : ""}` : name;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function viewShortKo(viewShort) {
|
|
165
|
+
if (viewShort === "front") return ko("front", "정면");
|
|
166
|
+
if (viewShort === "back") return ko("back", "후면");
|
|
167
|
+
if (viewShort?.includes("profile")) return viewShort.startsWith("left") ? ko("left profile", "왼쪽 측면") : ko("right profile", "오른쪽 측면");
|
|
168
|
+
if (viewShort?.startsWith("front ¾")) return / L$/.test(viewShort) ? ko("front ¾ L", "정면 ¾ 왼쪽") : ko("front ¾ R", "정면 ¾ 오른쪽");
|
|
169
|
+
if (viewShort?.startsWith("rear ¾")) return / L$/.test(viewShort) ? ko("rear ¾ L", "후면 ¾ 왼쪽") : ko("rear ¾ R", "후면 ¾ 오른쪽");
|
|
170
|
+
return viewShort;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function slateLineKo(shot) {
|
|
174
|
+
return [
|
|
175
|
+
SHOT_SIZE_LABELS_KO.get(shot.sizeLabel) ?? shot.sizeLabel,
|
|
176
|
+
viewShortKo(shot.viewShort),
|
|
177
|
+
SHOT_LEVEL_LABELS_KO.get(shot.levelLabel) ?? shot.levelLabel,
|
|
178
|
+
`${shot.focalMm}mm`,
|
|
179
|
+
].filter(Boolean).join(" · ");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function moveSequenceSlateKo(segments) {
|
|
183
|
+
if (!segments.length) return "";
|
|
184
|
+
if (segments.length === 1) {
|
|
185
|
+
const seg = segments[0];
|
|
186
|
+
return `${slateLineKo(seg.from)} → ${slateLineKo(seg.to)} · ${cameraMoveLabelKo(seg.label)}`;
|
|
187
|
+
}
|
|
188
|
+
const parts = [slateLineKo(segments[0].from)];
|
|
189
|
+
for (const seg of segments) parts.push(`${cameraMoveLabelKo(seg.label)} → ${slateLineKo(seg.to)}`);
|
|
190
|
+
return parts.join(" · ");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The hierarchy node id an IK focus lights up. Focus maps stay keyed by the
|
|
194
|
+
* BONE TOKEN; the row id namespaces the result so each cast member's tree
|
|
195
|
+
* highlights its own bone (#76). */
|
|
196
|
+
export function hierarchyIdForIkFocus(focus, rowId = "characterA") {
|
|
197
|
+
if (!focus) return null;
|
|
198
|
+
const exact = Object.entries(RIG_HIERARCHY_FOCUS)
|
|
199
|
+
.find(([id, mappedFocus]) =>
|
|
200
|
+
!["rig.torso", "rig.leftArm", "rig.rightArm", "rig.leftLeg", "rig.rightLeg"].includes(id) &&
|
|
201
|
+
mappedFocus === focus
|
|
202
|
+
);
|
|
203
|
+
return `${rowId}.${exact?.[0] ?? "rig"}`;
|
|
204
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { ko, isKo } from "./locale.js";
|
|
3
|
+
import { SHOT_ASPECT_RATIOS } from "./shot.js";
|
|
4
|
+
import {
|
|
5
|
+
SCENES_QUARANTINE_KEY,
|
|
6
|
+
createSceneDocument,
|
|
7
|
+
DEFAULT_SUBJECT_ONE,
|
|
8
|
+
DEFAULT_SUBJECT_TWO,
|
|
9
|
+
loadSceneDocumentFromStorage,
|
|
10
|
+
} from "./scenes.js";
|
|
11
|
+
import { POSE_BONES } from "./poses.js";
|
|
12
|
+
import { isPlaygroundEmbed, takePlaygroundProject } from "./playground.js";
|
|
13
|
+
import { DEFAULT_SCENE_OBJECTS } from "./scene-objects.js";
|
|
14
|
+
|
|
15
|
+
// Stated the way a crew states a setup: how far back, which side, how high the
|
|
16
|
+
// lens rides, and what glass is on it. Order matters — Medium is the setup a
|
|
17
|
+
// director reaches for first, so it leads.
|
|
18
|
+
export const PRESETS = {
|
|
19
|
+
medium: { label: ko("Medium", "미디엄"), distance: 2.6, azimuth: 22, elevation: 6, fov: 45, targetY: 1.35, two: false },
|
|
20
|
+
wide: { label: ko("Wide", "와이드"), distance: 7, azimuth: 25, elevation: 4, fov: 38, targetY: 1.2, two: false },
|
|
21
|
+
closeup: { label: ko("Close-Up", "클로즈업"), distance: 1.3, azimuth: 16, elevation: 2, fov: 45, targetY: 1.55, two: false },
|
|
22
|
+
low: { label: ko("Low Angle", "로우 앵글"), distance: 3.5, azimuth: 20, elevation: -14, fov: 50, targetY: 1.1, two: false },
|
|
23
|
+
high: { label: ko("High Angle", "하이 앵글"), distance: 4.5, azimuth: 20, elevation: 16, fov: 45, targetY: 1.1, two: false },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const SHOT_ASPECT_PRESETS = Object.freeze({
|
|
27
|
+
"16:9": Object.freeze({ label: "16:9", aspect: SHOT_ASPECT_RATIOS["16:9"], width: 1920, height: 1080 }),
|
|
28
|
+
"2.39:1": Object.freeze({ label: "2.39:1", aspect: SHOT_ASPECT_RATIOS["2.39:1"], width: 2390, height: 1000 }),
|
|
29
|
+
"9:16": Object.freeze({ label: "9:16", aspect: SHOT_ASPECT_RATIOS["9:16"], width: 1080, height: 1920 }),
|
|
30
|
+
"1:1": Object.freeze({ label: "1:1", aspect: SHOT_ASPECT_RATIOS["1:1"], width: 1080, height: 1080 }),
|
|
31
|
+
"4:3": Object.freeze({ label: "4:3", aspect: SHOT_ASPECT_RATIOS["4:3"], width: 1440, height: 1080 }),
|
|
32
|
+
"12:7": Object.freeze({ label: "12:7", aspect: SHOT_ASPECT_RATIOS["12:7"], width: 1536, height: 896, title: "12:7 video-inbetweener ratio" }),
|
|
33
|
+
"fal 480P": Object.freeze({ label: "fal 480P", aspect: SHOT_ASPECT_RATIOS["fal 480P"], width: 1664, height: 960, title: "Fal H3 480P canvas (832x480)" }),
|
|
34
|
+
});
|
|
35
|
+
// Pre-generated clip shipped with the build so a bridge-less session (a hosted
|
|
36
|
+
// static demo, or `npm run dev:ui`) still shows real generated motion.
|
|
37
|
+
// Root-absolute on purpose: the studio is served from "/app/" while these
|
|
38
|
+
// public files stay at the site root, so a page-relative path would resolve
|
|
39
|
+
// to "/app/models/..." and 404. Vite's base is "/" (own apex domain), so a
|
|
40
|
+
// leading slash is now the correct — and only — form that works from both
|
|
41
|
+
// the dev server and the built site.
|
|
42
|
+
// Per-character rig model: the stage entry's `model` is the FBX file stem in
|
|
43
|
+
// public/models AND the wire rig name sent to ARDY, so mesh and export can
|
|
44
|
+
// never drift apart.
|
|
45
|
+
export const characterModelUrl = (model) => `/models/${model}.fbx`;
|
|
46
|
+
|
|
47
|
+
/** Shipped rig names as the operator says them, not as the files spell them. */
|
|
48
|
+
export const CHARACTER_MODEL_LABELS = { "y-bot-tpose": "Y Bot", "x-bot-tpose": "X Bot" };
|
|
49
|
+
CHARACTER_MODEL_LABELS["proxy-figure"] = ko("Capsule figure", "캡슐 인물");
|
|
50
|
+
|
|
51
|
+
/** Sequential ids for spawned characters, collision-free against the cast. */
|
|
52
|
+
export function nextCharacterId(list) {
|
|
53
|
+
const ids = new Set(list.map((entry) => entry.id));
|
|
54
|
+
let n = list.length + 1;
|
|
55
|
+
let id = `char-${n}`;
|
|
56
|
+
while (ids.has(id)) {
|
|
57
|
+
n += 1;
|
|
58
|
+
id = `char-${n}`;
|
|
59
|
+
}
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
export const DEMO_MOTION_URL = "/demo/walk-then-stop.npz";
|
|
63
|
+
export const DEMO_MOTION_PROMPT = "A person walks forward.";
|
|
64
|
+
// How long a deletion keeps offering its one-press Undo. Both toasts use the
|
|
65
|
+
// same window so the two deletion paths feel like one rule.
|
|
66
|
+
export const OBJECT_DELETE_UNDO_MS = 7000;
|
|
67
|
+
export const ASSET_DELETE_UNDO_MS = 7000;
|
|
68
|
+
// Neutral grey clay: the set is the grey workbench, so the figures are grey
|
|
69
|
+
// too, light enough to stand off the deck and the sky.
|
|
70
|
+
export const CLAY = "#bdbec3";
|
|
71
|
+
export const CLAY_B = "#a4a5aa";
|
|
72
|
+
// X Bot's shell is smooth (no raised exoskeleton like Y Bot's), so it gets a
|
|
73
|
+
// brighter, whiter clay to keep it readable against the set.
|
|
74
|
+
export const CLAY_X = "#d0d0d4";
|
|
75
|
+
// Model/role default for a cast member; a user-picked entry.tint always wins
|
|
76
|
+
// over this at render time.
|
|
77
|
+
export const defaultCharacterTint = (entry, index) => (entry.model === "x-bot-tpose" ? CLAY_X : index === 0 ? CLAY : CLAY_B);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The first editor view should read like a blocking desk, not a security
|
|
81
|
+
* camera at the back of the stage. Keep the selected subject large enough to
|
|
82
|
+
* judge a pose while leaving a little floor around the feet. This is a pure
|
|
83
|
+
* framing helper so the seed and any future reset action share the same
|
|
84
|
+
* distance rule.
|
|
85
|
+
*/
|
|
86
|
+
export function editorFrameForSubject(subject) {
|
|
87
|
+
const scale = Number.isFinite(subject?.scale) && subject.scale > 0 ? subject.scale : 1;
|
|
88
|
+
const distance = THREE.MathUtils.clamp(3.5 / scale, 2.8, 4.8);
|
|
89
|
+
const x = subject?.x ?? 0;
|
|
90
|
+
const z = subject?.z ?? 0;
|
|
91
|
+
const target = { x, y: 1.05, z };
|
|
92
|
+
return {
|
|
93
|
+
target,
|
|
94
|
+
position: {
|
|
95
|
+
x: x + distance * 0.62,
|
|
96
|
+
y: target.y + distance * 0.34,
|
|
97
|
+
z: z + distance * 0.78,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const DEFAULT_SUBJECT = DEFAULT_SUBJECT_ONE;
|
|
103
|
+
export const DEFAULT_SUBJECT2 = DEFAULT_SUBJECT_TWO;
|
|
104
|
+
export const DEFAULT_ENVIRONMENT = "a sunlit modern living room";
|
|
105
|
+
export const DEFAULT_CAMERA_POSITION = { x: 0.97, y: 1.62, z: 2.39 };
|
|
106
|
+
export const REST_BONES = Object.fromEntries(POSE_BONES.map((b) => [b.id, [0, 0, 0]]));
|
|
107
|
+
|
|
108
|
+
// Unity's tool keys. They are free because camera movement now lives behind a
|
|
109
|
+
// held right button. (docs/unity-reference.md §9.2)
|
|
110
|
+
export const GIZMO_HOTKEYS = { KeyW: "move", KeyE: "rotate", KeyR: "scale" };
|
|
111
|
+
|
|
112
|
+
export const WORKSPACE_LAYOUT_KEY = "cozyclay.workspace-layout.v1";
|
|
113
|
+
export const DEFAULT_WORKSPACE_LAYOUT = Object.freeze({
|
|
114
|
+
hierarchyWidth: 300,
|
|
115
|
+
sidebarWidth: 380,
|
|
116
|
+
timelineHeight: 224,
|
|
117
|
+
insetWidth: 310,
|
|
118
|
+
insetHeight: 310,
|
|
119
|
+
insetCollapsed: false,
|
|
120
|
+
planZoom: 1,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export function loadWorkspaceLayout() {
|
|
124
|
+
// The landing-page playground is a fixed, chrome-less layout: the plan
|
|
125
|
+
// inset starts folded and nothing the visitor drags is written back.
|
|
126
|
+
if (isPlaygroundEmbed(globalThis.location?.search)) return { ...DEFAULT_WORKSPACE_LAYOUT, insetCollapsed: true };
|
|
127
|
+
try {
|
|
128
|
+
const saved = JSON.parse(localStorage.getItem(WORKSPACE_LAYOUT_KEY) || "null");
|
|
129
|
+
return saved ? { ...DEFAULT_WORKSPACE_LAYOUT, ...saved } : { ...DEFAULT_WORKSPACE_LAYOUT };
|
|
130
|
+
} catch {
|
|
131
|
+
return { ...DEFAULT_WORKSPACE_LAYOUT };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Load the Scene envelope once. Legacy single-scene objects are migrated by
|
|
136
|
+
* scenes.js; App only supplies the familiar starter set for a truly new room. */
|
|
137
|
+
export function loadSceneStartup() {
|
|
138
|
+
const defaults = () => DEFAULT_SCENE_OBJECTS.map((object) => ({ ...object, footprint: { ...object.footprint } }));
|
|
139
|
+
// The landing-page playground never reads or writes the visitor's saved
|
|
140
|
+
// scenes: it opens the preset the page fetched (or an empty room if that
|
|
141
|
+
// fetch failed) and saving stays off for the whole session.
|
|
142
|
+
if (isPlaygroundEmbed(globalThis.location?.search)) {
|
|
143
|
+
const preset = takePlaygroundProject();
|
|
144
|
+
const document = preset?.document ?? createSceneDocument();
|
|
145
|
+
if (!preset?.document) document.scenes[0].objects = defaults();
|
|
146
|
+
return { document, saveBlocked: true, error: null, startupCreatedScene: false, toast: null };
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const result = loadSceneDocumentFromStorage(localStorage);
|
|
150
|
+
if (result.status === "future") {
|
|
151
|
+
const document = createSceneDocument();
|
|
152
|
+
document.scenes[0].objects = defaults();
|
|
153
|
+
return {
|
|
154
|
+
document,
|
|
155
|
+
saveBlocked: true,
|
|
156
|
+
error: null,
|
|
157
|
+
toast: ko("Saved scenes were written by a newer CozyClay — they have been left untouched and this session will not save", "저장된 장면은 더 최신 CozyClay에서 만들어졌어요. 이 세션에서는 건드리지 않고 저장도 하지 않습니다."),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const document = result.document;
|
|
161
|
+
if ((result.status === "absent" || result.status === "corrupt") && document.scenes[0].objects.length === 0) {
|
|
162
|
+
document.scenes[0].objects = defaults();
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
document,
|
|
166
|
+
saveBlocked: false,
|
|
167
|
+
error: null,
|
|
168
|
+
// The startup scene is authored silently; without this flag the
|
|
169
|
+
// funnel's "scene created" step never fires for a brand-new room.
|
|
170
|
+
startupCreatedScene: result.status === "absent" || result.status === "corrupt",
|
|
171
|
+
toast: result.status === "corrupt"
|
|
172
|
+
? ko(`Saved scenes were unreadable — starting fresh; the old data is kept under ${SCENES_QUARANTINE_KEY}`, `저장된 장면을 읽을 수 없어 새로 시작합니다. 기존 데이터는 ${SCENES_QUARANTINE_KEY}에 보관했어요.`)
|
|
173
|
+
: result.dropped > 0
|
|
174
|
+
? (isKo ? `저장된 장면 ${result.dropped}개를 복원하지 못했어요` : `${result.dropped} saved scene(s) could not be restored`)
|
|
175
|
+
: null,
|
|
176
|
+
};
|
|
177
|
+
} catch {
|
|
178
|
+
const document = createSceneDocument();
|
|
179
|
+
document.scenes[0].objects = defaults();
|
|
180
|
+
return { document, saveBlocked: false, error: null, startupCreatedScene: true, toast: null };
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/starter-scenes.js
CHANGED
package/src/storyboard.js
CHANGED
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
// 2d canvas. The canvas factory is injected so both the app (real canvas)
|
|
3
3
|
// and tests (recording stub) can supply it.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const STORYBOARD_PROMPT_CHARS = 90;
|
|
6
|
+
/** The greybox inset beside a stylized panel: half the drawn picture's width
|
|
7
|
+
* and height (a quarter of its area), in its bottom-right corner. */
|
|
8
|
+
export const STORYBOARD_INSET_SCALE = 0.5;
|
|
9
|
+
|
|
10
|
+
/** The line under a cell: the panel's own caption when it has one, else the
|
|
11
|
+
* prompt line the cell was given. */
|
|
12
|
+
function storyboardCellText(shot) {
|
|
13
|
+
const caption = typeof shot.caption === "string" ? shot.caption.trim() : "";
|
|
14
|
+
return (caption || String(shot.prompt ?? "")).slice(0, STORYBOARD_PROMPT_CHARS);
|
|
15
|
+
}
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* Compose a storyboard grid canvas.
|
|
9
19
|
* @param {object} args
|
|
10
|
-
* @param {Array<{ title: string, durationSeconds: number, prompt: string, image: CanvasImageSource | null }>} args.shots
|
|
20
|
+
* @param {Array<{ title: string, durationSeconds: number, prompt: string, image: CanvasImageSource | null, stylized?: CanvasImageSource | null, caption?: string }>} args.shots
|
|
21
|
+
* `image` is the greybox frame; with `stylized` the cell shows the stylized
|
|
22
|
+
* picture and the greybox as an inset. `caption` wins over `prompt`.
|
|
11
23
|
* @param {number} [args.columns] grid columns, default 3
|
|
12
24
|
* @param {{ width: number, height: number }} [args.cell] per-cell size, default 320x180
|
|
13
25
|
* @param {(width: number, height: number) => { width: number, height: number, getContext: (kind: string) => CanvasRenderingContext2D }} args.createCanvas
|
|
@@ -30,7 +42,7 @@ export function composeStoryboard({ shots, columns = 3, cell = { width: 320, hei
|
|
|
30
42
|
shots.forEach((shot, i) => {
|
|
31
43
|
const x = (i % cols) * cell.width;
|
|
32
44
|
const y = Math.floor(i / cols) * cell.height;
|
|
33
|
-
const image = shot.image;
|
|
45
|
+
const image = shot.stylized || shot.image;
|
|
34
46
|
if (image) {
|
|
35
47
|
const iw = image.width || image.videoWidth || 1;
|
|
36
48
|
const ih = image.height || image.videoHeight || 1;
|
|
@@ -39,7 +51,14 @@ export function composeStoryboard({ shots, columns = 3, cell = { width: 320, hei
|
|
|
39
51
|
const scale = Math.min(availW / iw, availH / ih);
|
|
40
52
|
const dw = iw * scale;
|
|
41
53
|
const dh = ih * scale;
|
|
42
|
-
|
|
54
|
+
const dx = x + pad + (availW - dw) / 2;
|
|
55
|
+
const dy = y + pad + (availH - dh) / 2;
|
|
56
|
+
ctx.drawImage(image, dx, dy, dw, dh);
|
|
57
|
+
if (shot.stylized && shot.image) {
|
|
58
|
+
const iw2 = dw * STORYBOARD_INSET_SCALE;
|
|
59
|
+
const ih2 = dh * STORYBOARD_INSET_SCALE;
|
|
60
|
+
ctx.drawImage(shot.image, dx + dw - iw2, dy + dh - ih2, iw2, ih2);
|
|
61
|
+
}
|
|
43
62
|
} else {
|
|
44
63
|
ctx.fillStyle = "#2a2a32";
|
|
45
64
|
ctx.fillRect(x + pad, y + pad, cell.width - pad * 2, thumbHeight - pad);
|
|
@@ -53,7 +72,7 @@ export function composeStoryboard({ shots, columns = 3, cell = { width: 320, hei
|
|
|
53
72
|
ctx.fillStyle = "#9a9aa5";
|
|
54
73
|
ctx.fillText(`${shot.durationSeconds}s`, textX, textY);
|
|
55
74
|
textY += 14;
|
|
56
|
-
ctx.fillText(
|
|
75
|
+
ctx.fillText(storyboardCellText(shot), textX, textY);
|
|
57
76
|
});
|
|
58
77
|
|
|
59
78
|
return canvas;
|
package/src/studio-actions.js
CHANGED
|
@@ -23,6 +23,18 @@ const sceneId = { sceneId: idSchema };
|
|
|
23
23
|
const NOT_UNDOABLE = "Scenes are outside the undo history: undo_edit and Ctrl+Z cannot revert this.";
|
|
24
24
|
const SCENE_MOVES = "The open scene changes, so later commands in this message are admitted in it; the receipt's host names it.";
|
|
25
25
|
const characterId = { characterId: idSchema };
|
|
26
|
+
const castOverride = {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
x: { type: "number" },
|
|
30
|
+
z: { type: "number" },
|
|
31
|
+
rot: { type: "number" },
|
|
32
|
+
posture: { type: "string", enum: ["stand", "sit", "lie"] },
|
|
33
|
+
pose: { type: "object", properties: {}, required: [], additionalProperties: true },
|
|
34
|
+
},
|
|
35
|
+
required: [],
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
};
|
|
26
38
|
/** A world floor point in metres; y is the floor. */
|
|
27
39
|
const floorPoint = input({ x: { type: "number" }, z: { type: "number" } });
|
|
28
40
|
const waypointFrame = { ...frame, minimum: 1 };
|
|
@@ -34,12 +46,14 @@ const num = { type: "number" };
|
|
|
34
46
|
const perBone = items => ({ type: "array", items, minItems: 1, maxItems: 3 });
|
|
35
47
|
const quaternion = input({ x: num, y: num, z: num, w: num });
|
|
36
48
|
/** One track's key: the IK state's { q, p, baseQ, basePos, chainP,
|
|
37
|
-
* keepTranslations, blend, pin } with quaternions as {x,y,z,w} and positions
|
|
49
|
+
* keepTranslations, blend, pin, correctionRange } with quaternions as {x,y,z,w} and positions
|
|
38
50
|
* as {x,y,z}; blend is the key's own correction range in frames (src/ardy/ik.js
|
|
39
51
|
* correctionWeight), absent for the editor's default range; pin is the id of
|
|
40
|
-
* the range pin that baked the key (src/ardy/range-pin.js).
|
|
52
|
+
* the range pin that baked the key (src/ardy/range-pin.js). correctionRange
|
|
53
|
+
* pairs the boundary keys of an explicitly selected interval. */
|
|
41
54
|
const ikTrackKey = input({}, { q: perBone(quaternion), p: StudioSchemas.Vec3, baseQ: perBone(quaternion), basePos: StudioSchemas.Vec3,
|
|
42
55
|
chainP: perBone(StudioSchemas.Vec3), keepTranslations: { type: "boolean" }, blend: { type: "integer", minimum: 1, maximum: 240 },
|
|
56
|
+
correctionRange: input({ start: frame, end: frame }),
|
|
43
57
|
pin: { type: "string", minLength: 1, maxLength: 64 } });
|
|
44
58
|
const ikTracks = input({}, Object.fromEntries([...STUDIO_IK_CHAIN_TRACKS, ...STUDIO_IK_JOINT_TRACKS].map(track => [track, ikTrackKey])));
|
|
45
59
|
/** The bones an object can ride (src/scene-objects.js SCENE_ATTACH_BONES). */
|
|
@@ -49,25 +63,40 @@ export const STUDIO_ATTACH_BONES = freezeStudioData(["hips", "spine", "chest", "
|
|
|
49
63
|
const GUIDE_MODES = freezeStudioData(["off", "thirds", "golden", "center", "safe"]);
|
|
50
64
|
/** The image models the Studio writes Send-to-AI prompts for (src/shot.js IMAGE_MODELS). */
|
|
51
65
|
const AI_IMAGE_MODELS = freezeStudioData(["nano_banana_pro", "nano_banana_2", "gpt_image_2", "seedream_5", "flux_2"]);
|
|
66
|
+
const stillHold = { type: "integer", minimum: 1, maximum: 240 };
|
|
67
|
+
const stillIndex = { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER };
|
|
68
|
+
const STORYBOARD_CAP = "Refused when the stills would run past the 28800-frame timeline.";
|
|
52
69
|
const WAYPOINT_RULES = "Pins sit at least 8 frames apart, the walk between two pins must stay within 0.5-3 m/s, and x/z are clamped to +/-11 m; a pin that breaks a rule is refused with the frame or distance that would work.";
|
|
53
70
|
|
|
54
71
|
export const STUDIO_ACTIONS = freezeStudioData([
|
|
55
72
|
{ id: "shot.create", label: "Add shot", kind: "mutation", undoDomain: "shot", input: input(),
|
|
56
|
-
description: "Add a new shot at the playhead, keyed with the current camera framing (the timeline's + Add shot). Move the playhead first with operate_studio { frame }. When the playhead is inside a shot, the new one goes in the next free gap." },
|
|
73
|
+
description: "Add a new shot at the playhead, keyed with the current camera framing (the timeline's + Add shot). Move the playhead first with operate_studio { frame }. When the playhead is inside a shot, the new one goes in the next free gap. In a storyboard project it adds a still instead, exactly like shot.createStill with no arguments." },
|
|
74
|
+
{ id: "shot.createStill", label: "Add still", kind: "mutation", undoDomain: "shot", input: input({}, { caption: { type: "string", maxLength: 500 }, hold: stillHold }),
|
|
75
|
+
description: `Add a still (a storyboard panel: one picture held for hold frames at 24 fps, default 48, 1-240) after the last still, keyed with the current camera framing. Stills sit end to end from frame 0 in order. ${STORYBOARD_CAP}` },
|
|
76
|
+
{ id: "shot.setCaption", label: "Set shot caption", kind: "mutation", undoDomain: "shot", input: input({ ...shotId, caption: { type: "string", maxLength: 500 } }),
|
|
77
|
+
description: "Set a shot's caption (the panel's action or dialogue line), trimmed, at most 500 characters; an empty caption clears it." },
|
|
78
|
+
{ id: "shot.setCastOverride", label: "Set shot cast override", kind: "mutation", undoDomain: "shot",
|
|
79
|
+
input: input({ ...shotId, ...characterId, override: { oneOf: [castOverride, { type: "null" }] } }),
|
|
80
|
+
description: "Set or clear one character's placement and pose override for a shot. Overrides apply only when the character has no installed take; a root path still wins." },
|
|
81
|
+
{ id: "shot.setHold", label: "Set still hold", kind: "mutation", undoDomain: "shot", input: input({ ...shotId, hold: stillHold }),
|
|
82
|
+
description: `Set how many frames (1-240 at 24 fps) a still holds; the stills after it move so they stay end to end. Stills only. ${STORYBOARD_CAP}` },
|
|
57
83
|
{ id: "shot.split", label: "Split shot", kind: "mutation", undoDomain: "shot", input: input(shotId),
|
|
58
84
|
description: "Cut a shot in two at the playhead. The playhead must be inside that shot, after its first frame; the second half starts at the playhead." },
|
|
59
85
|
{ id: "shot.duplicate", label: "Duplicate shot", kind: "mutation", undoDomain: "shot", input: input(shotId),
|
|
60
|
-
description: "Copy a shot, its camera and
|
|
86
|
+
description: "Copy a shot, its camera, keys, caption, per-shot cast and stylized image. A still's copy goes right after it and the stills after it move along; a clip's copy goes into the next free gap on the timeline." },
|
|
61
87
|
{ id: "shot.remove", label: "Delete shot", kind: "mutation", undoDomain: "shot", input: input(shotId),
|
|
62
88
|
description: "Delete a shot and leave its frames as free-camera time." },
|
|
63
89
|
{ id: "shot.setRange", label: "Set shot range", kind: "mutation", undoDomain: "shot", input: input({ ...shotId, range: StudioSchemas.FrameRange }),
|
|
64
90
|
description: "Move a shot's start and end to a half-open frame range. Edges are clamped to the timeline and refused where they would overlap another shot; the receipt's delta shows the range that landed." },
|
|
65
|
-
{ id: "shot.reorder", label: "Move shot", kind: "mutation", undoDomain: "shot",
|
|
66
|
-
|
|
91
|
+
{ id: "shot.reorder", label: "Move shot", kind: "mutation", undoDomain: "shot",
|
|
92
|
+
input: input(shotId, { startFrame: frame, index: stillIndex }),
|
|
93
|
+
description: "Move a shot; give exactly one of startFrame or index. With startFrame, a clip moves in time to start there, keeping its length and camera keys (a noop where it would overlap another shot), and a still moves to the place in the still order that frame falls in. With index (stills only), the still moves to that position among the stills (0 is first; past the end means last). Stills stay end to end from frame 0." },
|
|
67
94
|
{ id: "motion.generateAllBlocks", label: "Generate all blocks", kind: "job", generation: "motion", domain: "motion", background: true, input: input(),
|
|
68
95
|
description: "Generate the active character's motion from all of its prompt blocks, like the top-bar Generate Motion button when prompt blocks have text. It starts a job and returns status \"started\"; the take lands in the editor when the job finishes. Counts as the one motion generation of this message." },
|
|
69
96
|
{ id: "character.addWaypoint", label: "Add root waypoint", kind: "mutation", undoDomain: "cast", input: input({ ...characterId, position: floorPoint }, { frame: waypointFrame }),
|
|
70
97
|
description: `Pin a character's root path: at frame, the character's root stands at position (world x/z metres). Frame 0 is the character's own spot, so pins start at frame 1 and each frame holds one pin. Omit frame to pace the pin at a walk (1.4 m/s) from the previous one. ${WAYPOINT_RULES} Read paths with inspect_studio { scope: "motion" }.` },
|
|
98
|
+
{ id: "character.move", label: "Move character", kind: "mutation", undoDomain: "cast", input: input(characterId, { x: num, z: num, rot: num }),
|
|
99
|
+
description: "Move a character's stage placement. Inside a still this writes that panel's cast override; outside a still it changes the stage record." },
|
|
71
100
|
{ id: "character.moveWaypoint", label: "Move root waypoint", kind: "mutation", undoDomain: "cast", input: input({ ...characterId, frame: waypointFrame, position: floorPoint }),
|
|
72
101
|
description: `Move the character's root waypoint at frame to a new floor position (world x/z metres), keeping its frame. ${WAYPOINT_RULES}` },
|
|
73
102
|
{ id: "character.removeWaypoint", label: "Remove root waypoint", kind: "mutation", undoDomain: "cast", input: input({ ...characterId, frame: waypointFrame }),
|
|
@@ -96,7 +125,7 @@ export const STUDIO_ACTIONS = freezeStudioData([
|
|
|
96
125
|
description: "Show a composition guide over the shot frame: rule of thirds, golden ratio, center cross, safe areas, or off. Overlay only, never in exported pixels; a viewer setting that is never undone." },
|
|
97
126
|
{ id: "view.setInset", label: "Top-View inset", kind: "transient", input: input({ collapsed: { type: "boolean" } }),
|
|
98
127
|
description: "Fold (collapsed: true) or unfold the Top-View inset pane over the viewport. A viewer setting that is never undone." },
|
|
99
|
-
{ id: "export.shotVideo", exposure: "confirm", confirmationReason: "Exporting writes a video file outside the Studio.", label: "Export video (mp4)", kind: "job", timeoutMs: 300_000, input: input({}, shotId),
|
|
128
|
+
{ id: "export.shotVideo", exposure: "confirm", confirmationReason: "Exporting writes a video file outside the Studio.", label: "Export video (mp4)", kind: "job", timeoutMs: 300_000, input: input({}, { ...shotId, scope: { type: "string", enum: ["timeline", "shot"], description: "timeline: the whole timeline, every shot back to back; shot: one shot (shotId, or the one under the playhead)." } }),
|
|
100
129
|
description: "Record a shot to an MP4 (camera move and character motion, no editor chrome), like the Export menu's Video (mp4), and ask the browser to download it. With shotId it records that shot's own range; without it, what the menu records: the whole take when there is motion, else the shot under the playhead (else the first shot), keyed from the current camera if it has no keys. Waits until the file is encoded (up to 5 minutes) and answers status \"completed\" with output.fileName and output.frameCount. One export at a time; not a motion generation." },
|
|
101
130
|
{ id: "scene.create", label: "New scene", kind: "document", input: input(),
|
|
102
131
|
description: `Add an empty scene (named SCENE 01, SCENE 02, ...) after the others and open it, like the scene menu's New scene. ${SCENE_MOVES} ${NOT_UNDOABLE}` },
|
|
@@ -110,6 +139,8 @@ export const STUDIO_ACTIONS = freezeStudioData([
|
|
|
110
139
|
description: `Open another scene of the project, like the scene pill's menu; the scene being left keeps its state. ${SCENE_MOVES}` },
|
|
111
140
|
{ id: "project.save", exposure: "confirm", confirmationReason: "Saving may overwrite a file or open a file picker.", label: "Save project", kind: "document", input: input(),
|
|
112
141
|
description: "Save the whole project to its current file, like the Project menu's Save Project, and answer status \"completed\" with output.fileName. A browser file picker opens only from the user's own click, so it is refused, with the reason, when the project has no file yet this session or the browser must re-grant access to it; ask the user to press Save Project once. An unnamed project opens the Save dialog for the user to name it. Browsers without file access download the project file instead." },
|
|
142
|
+
{ id: "project.exportAsAnimation", exposure: "confirm", confirmationReason: "Exporting writes an animation project file outside the Studio.", label: "Export as Animation project", kind: "document", input: input(),
|
|
143
|
+
description: "Export the current storyboard as a new Animation project file, leaving the storyboard open and unchanged. Still shots become clip shots with their ranges, camera keys, captions, cast overrides and stylized asset references preserved. The browser asks where to save the new .cclayproject or downloads it when file access is unavailable." },
|
|
113
144
|
{ id: "asset.import", label: "Import asset", kind: "mutation", undoDomain: "objects",
|
|
114
145
|
input: input({ source: { type: "string", minLength: 1, maxLength: 8_000_000, pattern: "^(data:|https?://)" }, name: { type: "string", minLength: 1, maxLength: 120 },
|
|
115
146
|
placeAs: { type: "string", enum: ["cutout", "backdrop", "mesh"] } }),
|
|
@@ -158,7 +189,9 @@ export function createStudioActionRegistry({ readState } = {}) {
|
|
|
158
189
|
if (!Array.isArray(result?.affectedIds) || result.affectedIds.some(value => typeof value !== "string") || typeof result.summary !== "string") {
|
|
159
190
|
throw new Error(`Studio action ${id} must return { affectedIds, summary }.`);
|
|
160
191
|
}
|
|
161
|
-
return { affectedIds: [...result.affectedIds], summary: result.summary, ...(result.output === undefined ? {} : { output: structuredClone(result.output) })
|
|
192
|
+
return { affectedIds: [...result.affectedIds], summary: result.summary, ...(result.output === undefined ? {} : { output: structuredClone(result.output) }),
|
|
193
|
+
// What the command measured and what it found wrong; the bus puts both on the receipt.
|
|
194
|
+
...(result.checks === undefined ? {} : { checks: structuredClone(result.checks) }), ...(result.warnings?.length ? { warnings: structuredClone(result.warnings) } : {}) };
|
|
162
195
|
};
|
|
163
196
|
const registry = {
|
|
164
197
|
register(entry) {
|
|
@@ -183,7 +216,7 @@ export function createStudioActionRegistry({ readState } = {}) {
|
|
|
183
216
|
list(state = readState?.()) {
|
|
184
217
|
return [...entries.values()].map(entry => {
|
|
185
218
|
const verdict = availability(entry, state);
|
|
186
|
-
const row = { id: entry.id, label: entry.label, kind: entry.kind, description: entry.description, exposure: entry.exposure,
|
|
219
|
+
const row = { id: entry.id, label: entry.label, kind: entry.kind, description: entry.description, exposure: entry.exposure, ...(entry.agentHidden ? { agentHidden: true } : {}),
|
|
187
220
|
...(entry.timeoutMs === undefined ? {} : { timeoutMs: entry.timeoutMs }), ...(entry.generation ? { generation: entry.generation } : {}) };
|
|
188
221
|
return verdict === true ? { ...row, available: true, input: entry.input } : { ...row, available: false, reason: verdict };
|
|
189
222
|
});
|