cozyclay 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
package/src/project-browser.jsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import packageInfo from "../package.json";
|
|
2
3
|
import { ko } from "./locale.js";
|
|
3
4
|
import {
|
|
4
5
|
hasDirectoryPicker,
|
|
@@ -12,11 +13,54 @@ import {
|
|
|
12
13
|
requestHandlePermission,
|
|
13
14
|
storeProjectsDirectory,
|
|
14
15
|
} from "./project.js";
|
|
16
|
+
import "./project-browser.css";
|
|
17
|
+
|
|
18
|
+
const APP_VERSION = packageInfo.version;
|
|
19
|
+
const CATEGORIES = ["All", "Film", "Dialogue", "Action", "Vehicle"];
|
|
20
|
+
const BLANK_TEMPLATE = Object.freeze({
|
|
21
|
+
id: "blank-stage",
|
|
22
|
+
name: "Blank Stage",
|
|
23
|
+
category: "All",
|
|
24
|
+
description: "An empty stage for blocking a new shot.",
|
|
25
|
+
kind: "blank",
|
|
26
|
+
code: "NEW",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// App.jsx keeps the project mutation behind its named requestNewProject/newProject
|
|
30
|
+
// path. The v2 chooser already has the name, so this short-lived handoff seeds
|
|
31
|
+
// the existing ProjectNameDialog and lets its normal submit path commit it.
|
|
32
|
+
let pendingProjectName = null;
|
|
33
|
+
|
|
34
|
+
function formatDate(value) {
|
|
35
|
+
const date = new Date(value);
|
|
36
|
+
if (!Number.isFinite(date.getTime())) return ko("Recently", "최근");
|
|
37
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sampleTemplate(scene) {
|
|
41
|
+
return {
|
|
42
|
+
id: `sample-${scene.id}`,
|
|
43
|
+
starterId: scene.id,
|
|
44
|
+
name: scene.name,
|
|
45
|
+
category: "Action",
|
|
46
|
+
description: scene.blurb,
|
|
47
|
+
kind: "sample",
|
|
48
|
+
code: "SAMPLE",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
15
51
|
|
|
16
52
|
export function ProjectNameDialog({ open, initialName = "My Project", onCancel, onSubmit }) {
|
|
17
53
|
const [value, setValue] = useState(initialName);
|
|
18
54
|
useEffect(() => {
|
|
19
|
-
if (open)
|
|
55
|
+
if (!open) return;
|
|
56
|
+
if (pendingProjectName !== null) {
|
|
57
|
+
const name = pendingProjectName;
|
|
58
|
+
pendingProjectName = null;
|
|
59
|
+
setValue(name);
|
|
60
|
+
onSubmit(name);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setValue(initialName);
|
|
20
64
|
}, [open, initialName]);
|
|
21
65
|
if (!open) return null;
|
|
22
66
|
const submit = (event) => {
|
|
@@ -25,16 +69,16 @@ export function ProjectNameDialog({ open, initialName = "My Project", onCancel,
|
|
|
25
69
|
onSubmit(name);
|
|
26
70
|
};
|
|
27
71
|
return (
|
|
28
|
-
<div className="project-name-dialog-backdrop" role="presentation">
|
|
29
|
-
<form className="project-name-dialog" role="dialog" aria-modal="true" aria-labelledby="project-name-dialog-title" onSubmit={submit}>
|
|
72
|
+
<div className="v2-project-name-dialog-backdrop project-name-dialog-backdrop" role="presentation">
|
|
73
|
+
<form className="v2-project-name-dialog project-name-dialog" role="dialog" aria-modal="true" aria-labelledby="project-name-dialog-title" onSubmit={submit}>
|
|
30
74
|
<strong id="project-name-dialog-title">{ko("Project name", "프로젝트 이름")}</strong>
|
|
31
75
|
<label>
|
|
32
76
|
<span>{ko("Name", "이름")}</span>
|
|
33
77
|
<input autoFocus value={value} onChange={(event) => setValue(event.target.value)} aria-label={ko("Project name", "프로젝트 이름")} />
|
|
34
78
|
</label>
|
|
35
|
-
<div className="project-name-dialog-actions">
|
|
36
|
-
<button type="button" className="btn ghost" onClick={onCancel}>{ko("Cancel", "취소")}</button>
|
|
37
|
-
<button type="submit" className="btn primary">{ko("Create", "생성")}</button>
|
|
79
|
+
<div className="v2-project-name-dialog-actions project-name-dialog-actions">
|
|
80
|
+
<button type="button" className="v2-dialog-secondary btn ghost" onClick={onCancel}>{ko("Cancel", "취소")}</button>
|
|
81
|
+
<button type="submit" className="v2-dialog-primary btn primary">{ko("Create", "생성")}</button>
|
|
38
82
|
</div>
|
|
39
83
|
</form>
|
|
40
84
|
</div>
|
|
@@ -42,27 +86,38 @@ export function ProjectNameDialog({ open, initialName = "My Project", onCancel,
|
|
|
42
86
|
}
|
|
43
87
|
|
|
44
88
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* - A projects folder: when granted, the folder is enumerated so files the
|
|
48
|
-
* operator never opened in THIS browser show up too.
|
|
49
|
-
* Fallback for browsers without the File System Access API: recents plus a
|
|
50
|
-
* plain "open file" button (the folder section is hidden).
|
|
89
|
+
* v2 start / project screen. Recent projects still come from the browser-local
|
|
90
|
+
* project store, while today's starter scenes become the template cards.
|
|
51
91
|
*/
|
|
52
92
|
export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew, onClose, startup = false, starters = [], onStarter }) {
|
|
93
|
+
const starterTemplates = useMemo(() => starters.map(sampleTemplate), [starters]);
|
|
94
|
+
const templates = useMemo(
|
|
95
|
+
() => [BLANK_TEMPLATE, ...starterTemplates],
|
|
96
|
+
[starterTemplates],
|
|
97
|
+
);
|
|
98
|
+
const defaultTemplate = starterTemplates[0] ?? BLANK_TEMPLATE;
|
|
99
|
+
const [activeNav, setActiveNav] = useState(startup ? "new" : "recent");
|
|
100
|
+
const [category, setCategory] = useState("All");
|
|
101
|
+
const [selectedTemplateId, setSelectedTemplateId] = useState(defaultTemplate.id);
|
|
102
|
+
const [name, setName] = useState(currentName || defaultTemplate.name || "My Project");
|
|
103
|
+
const [frameRate, setFrameRate] = useState("24");
|
|
104
|
+
const [units, setUnits] = useState("Meters");
|
|
53
105
|
const [recents, setRecents] = useState([]);
|
|
54
106
|
const [folder, setFolder] = useState(null);
|
|
55
107
|
const [folderProjects, setFolderProjects] = useState([]);
|
|
56
108
|
const [folderDenied, setFolderDenied] = useState(false);
|
|
57
|
-
// A stored folder handle Chromium demoted to "prompt": one click on the
|
|
58
|
-
// re-allow button re-grants it without re-picking the folder (#51).
|
|
59
109
|
const [lostFolder, setLostFolder] = useState(null);
|
|
110
|
+
const nameInputRef = useRef(null);
|
|
60
111
|
|
|
61
112
|
useEffect(() => {
|
|
62
|
-
|
|
113
|
+
let active = true;
|
|
114
|
+
loadRecentProjects().then((entries) => {
|
|
115
|
+
if (active) setRecents(Array.isArray(entries) ? entries : []);
|
|
116
|
+
});
|
|
63
117
|
loadProjectsDirectory().then(async (handle) => {
|
|
64
|
-
if (!handle) return;
|
|
118
|
+
if (!active || !handle) return;
|
|
65
119
|
const permission = await queryHandlePermission(handle);
|
|
120
|
+
if (!active) return;
|
|
66
121
|
if (permission !== "granted") {
|
|
67
122
|
setFolderDenied(true);
|
|
68
123
|
if (permission === "prompt") setLostFolder(handle);
|
|
@@ -70,9 +125,34 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
70
125
|
}
|
|
71
126
|
setFolder(handle);
|
|
72
127
|
setFolderProjects(await listProjectsInDirectory(handle));
|
|
128
|
+
}).catch(() => {
|
|
129
|
+
if (active) setFolderDenied(true);
|
|
73
130
|
});
|
|
131
|
+
return () => {
|
|
132
|
+
active = false;
|
|
133
|
+
};
|
|
74
134
|
}, []);
|
|
75
135
|
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (startup) nameInputRef.current?.focus();
|
|
138
|
+
}, [startup]);
|
|
139
|
+
|
|
140
|
+
// File › New reaches this screen with unsaved work, so Create can be refused
|
|
141
|
+
// by the discard confirmation. A name left behind then must not seed a later
|
|
142
|
+
// Save's name dialog once the screen closes.
|
|
143
|
+
useEffect(() => () => {
|
|
144
|
+
pendingProjectName = null;
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const selectedTemplate = templates.find((template) => template.id === selectedTemplateId) ?? templates[0];
|
|
148
|
+
const visibleTemplates = category === "All"
|
|
149
|
+
? templates
|
|
150
|
+
: templates.filter((template) => template.category === "All" || template.category === category);
|
|
151
|
+
const canCreate = name.trim().length > 0;
|
|
152
|
+
const createReason = ko("Enter a project name to create a project", "프로젝트를 만들려면 이름을 입력하세요");
|
|
153
|
+
const locationLabel = folder?.name || ko("Browser project store", "브라우저 프로젝트 저장소");
|
|
154
|
+
const canChooseFolder = hasFileSystemAccess() && hasDirectoryPicker();
|
|
155
|
+
|
|
76
156
|
const reauthorizeFolder = async () => {
|
|
77
157
|
if (!lostFolder) return;
|
|
78
158
|
if ((await requestHandlePermission(lostFolder)) !== "granted") return;
|
|
@@ -84,6 +164,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
84
164
|
};
|
|
85
165
|
|
|
86
166
|
const chooseFolder = async () => {
|
|
167
|
+
if (!canChooseFolder) return;
|
|
87
168
|
try {
|
|
88
169
|
const handle = await pickProjectsDirectory();
|
|
89
170
|
await storeProjectsDirectory(handle);
|
|
@@ -91,8 +172,8 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
91
172
|
setFolderDenied(false);
|
|
92
173
|
setLostFolder(null);
|
|
93
174
|
setFolderProjects(await listProjectsInDirectory(handle));
|
|
94
|
-
} catch (
|
|
95
|
-
if (
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error?.name !== "AbortError") setFolderDenied(true);
|
|
96
177
|
}
|
|
97
178
|
};
|
|
98
179
|
|
|
@@ -100,128 +181,208 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
100
181
|
if (folder) setFolderProjects(await listProjectsInDirectory(folder));
|
|
101
182
|
};
|
|
102
183
|
|
|
184
|
+
const selectTemplate = (template) => {
|
|
185
|
+
setSelectedTemplateId(template.id);
|
|
186
|
+
setName((current) => current.trim() ? current : template.name);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const createProject = (event) => {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
const projectName = name.trim();
|
|
192
|
+
if (!projectName) return;
|
|
193
|
+
pendingProjectName = projectName;
|
|
194
|
+
onNew(projectName);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const recentEntries = [
|
|
198
|
+
...recents.map((entry) => ({ ...entry, key: `recent-${entry.name}`, removable: true, date: entry.openedAt })),
|
|
199
|
+
...folderProjects.map((entry) => ({ ...entry, key: `folder-${entry.name}`, removable: false, date: entry.lastModified })),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const navItems = [
|
|
203
|
+
{ id: "recent", label: ko("Recent", "최근") },
|
|
204
|
+
{ id: "new", label: ko("New Project", "새 프로젝트") },
|
|
205
|
+
{ id: "samples", label: ko("Samples", "샘플") },
|
|
206
|
+
{ id: "learn", label: ko("Learn", "배우기") },
|
|
207
|
+
];
|
|
208
|
+
|
|
103
209
|
return (
|
|
104
|
-
<div className={`project-browser-backdrop${startup ? " startup" : ""}`} onPointerDown={(
|
|
105
|
-
<div className={`project-browser${startup ? " startup" : ""}`} role="dialog" aria-label={ko("Choose project", "프로젝트 선택")}>
|
|
106
|
-
<
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{ko("Open a project", "프로젝트 열기")}
|
|
210
|
+
<div className={`v2-start-screen project-browser-backdrop${startup ? " startup" : ""}`} onPointerDown={(event) => { if (!startup && event.target === event.currentTarget) onClose?.(); }}>
|
|
211
|
+
<div className={`v2-start-window project-browser${startup ? " startup" : ""}`} role="dialog" aria-modal="true" aria-label={ko("Choose project", "프로젝트 선택")}>
|
|
212
|
+
<nav className="v2-start-nav" aria-label={ko("Project navigation", "프로젝트 탐색")}>
|
|
213
|
+
<div className="v2-start-brand">
|
|
214
|
+
<span className="v2-start-brand-mark" aria-hidden="true" />
|
|
215
|
+
<strong>CozyClay</strong>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="v2-start-nav-list">
|
|
218
|
+
{navItems.map((item) => (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
key={item.id}
|
|
222
|
+
className={`v2-start-nav-item${activeNav === item.id ? " active" : ""}`}
|
|
223
|
+
aria-current={activeNav === item.id ? "page" : undefined}
|
|
224
|
+
onClick={() => setActiveNav(item.id)}
|
|
225
|
+
>
|
|
226
|
+
{item.label}
|
|
122
227
|
</button>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<div className="beginner-step"><b>3</b><strong>{ko("Play", "재생")}</strong><span>{ko("Press play to see the scene come alive.", "재생을 눌러 장면을 확인해요.")}</span></div>
|
|
128
|
-
</div>
|
|
129
|
-
</section>
|
|
130
|
-
)}
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
<div className="v2-start-version">v{APP_VERSION}</div>
|
|
231
|
+
</nav>
|
|
131
232
|
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
<div className="
|
|
135
|
-
<
|
|
233
|
+
<main className="v2-start-main">
|
|
234
|
+
<div className="v2-start-main-head">
|
|
235
|
+
<div className="v2-start-title-row">
|
|
236
|
+
<h1>{activeNav === "recent" ? ko("Recent", "최근") : activeNav === "samples" ? ko("Samples", "샘플") : activeNav === "learn" ? ko("Learn", "배우기") : ko("New Project", "새 프로젝트")}</h1>
|
|
237
|
+
{activeNav !== "new" && <button type="button" className="v2-start-close x" onClick={onClose} aria-label={ko("Close", "닫기")}>×</button>}
|
|
136
238
|
</div>
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
|
|
140
|
-
<button
|
|
141
|
-
|
|
142
|
-
|
|
239
|
+
{(activeNav === "new" || activeNav === "samples") && (
|
|
240
|
+
<div className="v2-start-tabs" role="tablist" aria-label={ko("Template categories", "템플릿 카테고리")}>
|
|
241
|
+
{CATEGORIES.map((entry) => (
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
key={entry}
|
|
245
|
+
role="tab"
|
|
246
|
+
aria-selected={category === entry}
|
|
247
|
+
className={`v2-start-tab${category === entry ? " active" : ""}`}
|
|
248
|
+
onClick={() => setCategory(entry)}
|
|
249
|
+
>
|
|
250
|
+
{ko(entry, entry === "All" ? "전체" : entry === "Film" ? "영화" : entry === "Dialogue" ? "대화" : entry === "Action" ? "액션" : "차량")}
|
|
143
251
|
</button>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
</section>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
<section>
|
|
151
|
-
<div className="project-browser-section-head">
|
|
152
|
-
<span>{ko("Recent projects", "최근 프로젝트")}</span>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
153
255
|
</div>
|
|
154
|
-
|
|
155
|
-
|
|
256
|
+
|
|
257
|
+
{activeNav === "learn" ? (
|
|
258
|
+
<div className="v2-start-learn">
|
|
259
|
+
<strong>{ko("Block a shot in three moves", "세 단계로 샷을 블로킹하세요")}</strong>
|
|
260
|
+
<p>{ko("Choose a template, name the project, then place a character and frame the camera in the editor.", "템플릿을 고르고 프로젝트 이름을 정한 다음 편집기에서 인물을 배치하고 카메라를 잡아 보세요.")}</p>
|
|
261
|
+
<button type="button" className="v2-start-learn-action" onClick={() => setActiveNav("new")}>{ko("Start a new project", "새 프로젝트 시작")}</button>
|
|
262
|
+
</div>
|
|
263
|
+
) : activeNav === "recent" ? (
|
|
264
|
+
<div className="v2-start-recent-page">
|
|
265
|
+
<div className="v2-start-recent-heading">
|
|
266
|
+
<span>{ko("Recent projects", "최근 프로젝트")}</span>
|
|
267
|
+
<div className="v2-start-recent-actions">
|
|
268
|
+
{folder && <button type="button" onClick={refreshFolder}>{ko("Refresh", "새로고침")}</button>}
|
|
269
|
+
<button type="button" onClick={onOpenFile}>{ko("Open project file…", "프로젝트 파일 열기…")}</button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<RecentList entries={recentEntries} currentName={currentName} onOpen={onOpen} onRemove={async (entry) => {
|
|
273
|
+
await removeRecentProject(entry.name);
|
|
274
|
+
setRecents(await loadRecentProjects());
|
|
275
|
+
}} />
|
|
276
|
+
</div>
|
|
156
277
|
) : (
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
<button type="button" onClick={() => onOpen(entry)}>
|
|
161
|
-
<strong>{entry.name}</strong>
|
|
162
|
-
<span>{new Date(entry.openedAt).toLocaleString()}</span>
|
|
163
|
-
</button>
|
|
278
|
+
<>
|
|
279
|
+
<div className="v2-start-template-grid" role="list" aria-label={ko("Project templates", "프로젝트 템플릿")}>
|
|
280
|
+
{visibleTemplates.map((template) => (
|
|
164
281
|
<button
|
|
165
282
|
type="button"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
283
|
+
role="listitem"
|
|
284
|
+
key={template.id}
|
|
285
|
+
className={`v2-start-template${selectedTemplate?.id === template.id ? " selected" : ""}`}
|
|
286
|
+
data-template-id={template.id}
|
|
287
|
+
onClick={() => selectTemplate(template)}
|
|
288
|
+
onDoubleClick={() => template.starterId && onStarter?.(template.starterId)}
|
|
172
289
|
>
|
|
173
|
-
|
|
290
|
+
<span className={`v2-start-template-art ${template.kind}`} aria-hidden="true">
|
|
291
|
+
<span className="v2-start-template-code">{template.code}</span>
|
|
292
|
+
</span>
|
|
293
|
+
<span className="v2-start-template-copy">
|
|
294
|
+
<strong>{template.name}</strong>
|
|
295
|
+
<span>{template.description}</span>
|
|
296
|
+
</span>
|
|
174
297
|
</button>
|
|
175
|
-
|
|
176
|
-
))}
|
|
177
|
-
</ul>
|
|
178
|
-
)}
|
|
179
|
-
</section>
|
|
180
|
-
|
|
181
|
-
{hasFileSystemAccess() && hasDirectoryPicker() && (
|
|
182
|
-
<section>
|
|
183
|
-
<div className="project-browser-section-head">
|
|
184
|
-
<span>{ko("Projects folder", "프로젝트 폴더")}</span>
|
|
185
|
-
<div className="row-actions">
|
|
186
|
-
{folder && <button type="button" onClick={refreshFolder}>{ko("Refresh", "새로고침")}</button>}
|
|
187
|
-
<button type="button" onClick={chooseFolder}>
|
|
188
|
-
{folder ? ko("Change folder…", "폴더 변경…") : ko("Choose folder…", "폴더 지정…")}
|
|
189
|
-
</button>
|
|
298
|
+
))}
|
|
190
299
|
</div>
|
|
300
|
+
|
|
301
|
+
<div className="v2-start-recent-block">
|
|
302
|
+
<div className="v2-start-recent-heading"><span>{ko("Recent", "최근")}</span></div>
|
|
303
|
+
<RecentList entries={recents.map((entry) => ({ ...entry, key: `recent-${entry.name}`, removable: true, date: entry.openedAt }))} currentName={currentName} onOpen={onOpen} onRemove={async (entry) => {
|
|
304
|
+
await removeRecentProject(entry.name);
|
|
305
|
+
setRecents(await loadRecentProjects());
|
|
306
|
+
}} compact />
|
|
307
|
+
</div>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
{activeNav !== "recent" && activeNav !== "learn" && (
|
|
311
|
+
<div className="v2-start-file-actions">
|
|
312
|
+
<button type="button" className="v2-start-file-button" onClick={onOpenFile}>{ko("Open project file…", "프로젝트 파일 열기…")}</button>
|
|
191
313
|
</div>
|
|
314
|
+
)}
|
|
315
|
+
</main>
|
|
316
|
+
|
|
317
|
+
<aside className="v2-start-preview" data-testid="start-project-preview" aria-label={ko("Project preview", "프로젝트 미리보기")}>
|
|
318
|
+
<div className={`v2-start-preview-art ${selectedTemplate?.kind || "blank"}`} aria-hidden="true">
|
|
319
|
+
<span>{ko("template preview", "템플릿 미리보기")}</span>
|
|
320
|
+
</div>
|
|
321
|
+
<div className="v2-start-preview-copy">
|
|
322
|
+
<strong>{selectedTemplate?.name}</strong>
|
|
323
|
+
<span>{selectedTemplate?.description}</span>
|
|
324
|
+
</div>
|
|
325
|
+
<form className="v2-start-project-form" onSubmit={createProject}>
|
|
326
|
+
<label className="v2-start-field">
|
|
327
|
+
<span>{ko("Name", "이름")}</span>
|
|
328
|
+
<input ref={nameInputRef} data-testid="start-project-name" aria-label={ko("Name", "이름")} value={name} onChange={(event) => setName(event.target.value)} />
|
|
329
|
+
</label>
|
|
330
|
+
<label className="v2-start-field">
|
|
331
|
+
<span>{ko("Location", "위치")}</span>
|
|
332
|
+
<button type="button" className="v2-start-location" onClick={chooseFolder} disabled={!canChooseFolder} title={canChooseFolder ? ko("Choose the project storage folder", "프로젝트 저장 폴더 선택") : ko("Projects are stored in this browser", "프로젝트는 이 브라우저에 저장됩니다")}>
|
|
333
|
+
<span>{locationLabel}</span><span aria-hidden="true">⌄</span>
|
|
334
|
+
</button>
|
|
335
|
+
</label>
|
|
336
|
+
<label className="v2-start-field">
|
|
337
|
+
<span>{ko("Frame rate", "프레임 레이트")}</span>
|
|
338
|
+
<span className="v2-start-select-wrap">
|
|
339
|
+
<select aria-label={ko("Frame rate", "프레임 레이트")} value={frameRate} onChange={(event) => setFrameRate(event.target.value)}>
|
|
340
|
+
<option value="24">24 fps</option>
|
|
341
|
+
<option value="30">30 fps</option>
|
|
342
|
+
</select>
|
|
343
|
+
</span>
|
|
344
|
+
</label>
|
|
345
|
+
<label className="v2-start-field">
|
|
346
|
+
<span>{ko("Units", "단위")}</span>
|
|
347
|
+
<span className="v2-start-select-wrap">
|
|
348
|
+
<select aria-label={ko("Units", "단위")} value={units} onChange={(event) => setUnits(event.target.value)}>
|
|
349
|
+
<option value="Meters">Meters</option>
|
|
350
|
+
<option value="Feet">Feet</option>
|
|
351
|
+
</select>
|
|
352
|
+
</span>
|
|
353
|
+
</label>
|
|
192
354
|
{folderDenied && (
|
|
193
|
-
<p className="
|
|
194
|
-
{lostFolder
|
|
195
|
-
|
|
196
|
-
: ko("Folder access is not granted — choose it again.", "폴더 접근 권한이 없어요. 다시 지정해 주세요.")}
|
|
197
|
-
{lostFolder && (
|
|
198
|
-
<button type="button" onClick={reauthorizeFolder}>{ko("Re-allow", "다시 허용")}</button>
|
|
199
|
-
)}
|
|
355
|
+
<p className="v2-start-location-status" role="status">
|
|
356
|
+
{lostFolder ? ko("Folder access needs to be re-allowed.", "폴더 접근을 다시 허용해야 해요.") : ko("Using the browser project store.", "브라우저 프로젝트 저장소를 사용합니다.")}
|
|
357
|
+
{lostFolder && <button type="button" onClick={reauthorizeFolder}>{ko("Re-allow", "다시 허용")}</button>}
|
|
200
358
|
</p>
|
|
201
359
|
)}
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</button>
|
|
213
|
-
</li>
|
|
214
|
-
))}
|
|
215
|
-
</ul>
|
|
216
|
-
)}
|
|
217
|
-
</section>
|
|
218
|
-
)}
|
|
360
|
+
<div className="v2-start-preview-actions">
|
|
361
|
+
<button type="button" className="v2-start-cancel" onClick={onClose}>{ko("Cancel", "취소")}</button>
|
|
362
|
+
<button type="submit" className="v2-start-create btn primary" data-testid="start-create" disabled={!canCreate} title={canCreate ? ko("Create project", "프로젝트 만들기") : createReason}>{ko("Create", "생성")}</button>
|
|
363
|
+
</div>
|
|
364
|
+
</form>
|
|
365
|
+
</aside>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
219
370
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
371
|
+
function RecentList({ entries, currentName, onOpen, onRemove, compact = false }) {
|
|
372
|
+
return entries.length === 0 ? (
|
|
373
|
+
<p className={`v2-start-empty${compact ? " compact" : ""}`}>{ko("No recent projects yet.", "아직 최근 프로젝트가 없습니다.")}</p>
|
|
374
|
+
) : (
|
|
375
|
+
<div className={`v2-start-recent-list${compact ? " compact" : ""}`} role="list">
|
|
376
|
+
{entries.map((entry) => (
|
|
377
|
+
<div className={`v2-start-recent-row${entry.name === currentName ? " active" : ""}`} role="listitem" key={entry.key}>
|
|
378
|
+
<button type="button" className="v2-start-recent-open" onClick={() => onOpen(entry)}>
|
|
379
|
+
<strong>{entry.name}</strong>
|
|
380
|
+
<span>{formatDate(entry.date)}</span>
|
|
381
|
+
<small>{entry.shots ?? "—"}{ko(" shots", " 샷")}</small>
|
|
382
|
+
</button>
|
|
383
|
+
{entry.removable && <button type="button" className="v2-start-recent-remove" aria-label={ko(`Remove ${entry.name} from recent projects`, `최근 프로젝트에서 ${entry.name} 제거`)} title={ko("Remove from list", "목록에서 제거")} onClick={() => onRemove(entry)}>×</button>}
|
|
223
384
|
</div>
|
|
224
|
-
|
|
385
|
+
))}
|
|
225
386
|
</div>
|
|
226
387
|
);
|
|
227
388
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { objectTransformAt } from "./object-path.js";
|
|
3
|
+
|
|
4
|
+
/** The local matrix SetProps composes before an optional attachment frame.
|
|
5
|
+
* Travel-path samples replace x/y/z/rot for the requested frame while the
|
|
6
|
+
* authored pitch/roll and per-axis scale remain part of the prop transform. */
|
|
7
|
+
export function rangePinObjectMatrixAt(object, frame, take = {}, out = new THREE.Matrix4()) {
|
|
8
|
+
const at = objectTransformAt(object, frame, take);
|
|
9
|
+
const source = at ? { ...object, ...at } : object;
|
|
10
|
+
return out.compose(
|
|
11
|
+
new THREE.Vector3(source.x ?? 0, source.y ?? 0, source.z ?? 0),
|
|
12
|
+
new THREE.Quaternion().setFromEuler(new THREE.Euler(
|
|
13
|
+
(source.rotX ?? 0) * THREE.MathUtils.DEG2RAD,
|
|
14
|
+
(source.rot ?? 0) * THREE.MathUtils.DEG2RAD,
|
|
15
|
+
(source.rotZ ?? 0) * THREE.MathUtils.DEG2RAD,
|
|
16
|
+
)),
|
|
17
|
+
new THREE.Vector3(source.scaleX ?? 1, source.scaleY ?? 1, source.scaleZ ?? 1),
|
|
18
|
+
);
|
|
19
|
+
}
|