cozyclay 3.0.3 → 3.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 +67 -0
- package/INSTALL_FOR_AGENTS.md +146 -0
- package/README.md +74 -192
- package/THIRD_PARTY_NOTICES.md +0 -21
- package/bin/agent/agent-routes.mjs +22 -56
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/agent/main-agent.mjs +70 -0
- package/bin/agent/main-prompt.mjs +17 -0
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-task-routes.mjs +185 -0
- package/bin/agent/studio-tasks.mjs +156 -0
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/agent/studio-turn-transaction.mjs +76 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-DdDXMgZm.js +1 -0
- package/dist/assets/app-B2BpfCl2.js +5088 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/compile-worker-D0pKioa0.js +5 -0
- package/dist/assets/landing-y3EqDgSZ.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-B887n77J.js +1 -0
- package/dist/assets/scene-asset-sync-COGOlF5T.css +1 -0
- package/dist/assets/scene-asset-sync-CQzEdsqu.js +13 -0
- package/dist/assets/task-board-BKaM7d-9.js +14 -0
- package/dist/assets/workflow-B8Dhzexj.js +179 -0
- package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
- package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +2 -3
- package/dist/index.html +7 -57
- package/dist/llms.txt +2 -1
- package/dist/previs-software/index.html +2 -2
- package/dist/privacy/index.html +3 -7
- package/dist/seedance-camera-control/index.html +2 -2
- package/dist/sitemap.xml +5 -5
- package/dist/sw.js +5 -1
- package/dist/thumbs/blank-stage-dark.webp +0 -0
- package/dist/thumbs/blank-stage-light.webp +0 -0
- package/dist/workflow/index.html +5 -4
- package/mcp/mesh-file.mjs +3 -1
- package/mcp/server.mjs +3 -0
- package/package.json +3 -3
- package/src/App.jsx +50 -281
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-tasks.js +11 -5
- package/src/command-bus.js +1 -1
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +3 -3
- package/src/commands/project.js +1 -1
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/compile.js +7 -1
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/fix/index.js +5 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +3 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +10 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +9 -8
- package/src/motion3/schema/types.js +4 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +10 -3
- package/src/motion3/skills/info.js +13 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-path.js +1 -1
- package/src/panels/ProjectPanel.jsx +42 -21
- package/src/panels/details.css +15 -4
- package/src/popover.js +37 -0
- package/src/posestudio.jsx +15 -10
- package/src/project-browser-art.jsx +19 -0
- package/src/project-browser.css +74 -121
- package/src/project-browser.jsx +36 -60
- package/src/props.jsx +4 -7
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +3 -7
- package/src/scenes.js +4 -5
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +1 -1
- package/src/studio-elements.js +1 -1
- package/src/styles/themes.css +13 -0
- package/src/styles.css +6 -674
- package/src/text/text-object.jsx +1 -1
- package/src/ui.jsx +8 -16
- package/src/use-embed-bridge.js +3 -6
- package/src/use-render-activity.js +3 -1
- package/src/view-ghosts.jsx +5 -3
- package/src/viewport-visible-rect.js +37 -0
- package/src/workflow/AgentPanel.jsx +75 -42
- package/src/workflow/AgentProgressCard.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +102 -0
- package/src/workflow/TaskInline.jsx +114 -0
- package/src/workflow/agent-client.js +11 -25
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/attachment-image.js +10 -3
- package/src/workflow/attachment-video.js +91 -0
- package/src/workflow/task-board.css +98 -0
- package/src/workflow/task-board.js +169 -0
- package/tools/check-eslint.mjs +2 -1
- package/tools/check-knip.mjs +2 -1
- package/tools/dev/pages/greybox-to-video.html +2 -3
- package/tools/dev/pages/previs-software.html +2 -2
- package/tools/dev/pages/privacy.html +3 -7
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/dev-full.mjs +11 -2
- package/tools/eslint-baseline.json +6 -6
- package/tools/knip-baseline.json +0 -11
- package/tools/loc-baseline.json +30 -31
- package/tools/metrics-snapshot.mjs +43 -5
- package/tools/render-start-thumbs.mjs +175 -0
- package/tools/run-tests.mjs +7 -14
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-BibyE5ar.js +0 -6
- package/dist/assets/app-DhRJ-PBz.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
- package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
- package/dist/assets/landing-DYg87MDY.js +0 -1
- package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-BSA22Ofr.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
package/src/popover.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
// Shell popovers (the project menu, dropdowns, the Outliner context menu) are
|
|
4
|
+
// exclusive: opening one announces itself and every other open one closes, so
|
|
5
|
+
// two menus never stack over each other. Their surface and layer are the
|
|
6
|
+
// --popover-* / --z-popover tokens in src/styles/themes.css.
|
|
7
|
+
const POPOVER_OPEN_EVENT = "cozyclay:popover-open";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Join the exclusive popover group while `open` is truthy. A new truthy
|
|
11
|
+
* `open` value (a context menu re-opened at another row) announces again.
|
|
12
|
+
* With `insideRefs`, a mousedown outside every ref also closes the popover.
|
|
13
|
+
*/
|
|
14
|
+
export function usePopover(open, close, insideRefs = null) {
|
|
15
|
+
const live = useRef({ close, insideRefs });
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
live.current = { close, insideRefs };
|
|
18
|
+
});
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!open) return undefined;
|
|
21
|
+
const token = {};
|
|
22
|
+
window.dispatchEvent(new CustomEvent(POPOVER_OPEN_EVENT, { detail: token }));
|
|
23
|
+
const onOtherOpen = (event) => {
|
|
24
|
+
if (event.detail !== token) live.current.close();
|
|
25
|
+
};
|
|
26
|
+
const onDocMouseDown = (event) => {
|
|
27
|
+
const refs = live.current.insideRefs;
|
|
28
|
+
if (refs && !refs.some((ref) => ref.current?.contains(event.target))) live.current.close();
|
|
29
|
+
};
|
|
30
|
+
window.addEventListener(POPOVER_OPEN_EVENT, onOtherOpen);
|
|
31
|
+
document.addEventListener("mousedown", onDocMouseDown);
|
|
32
|
+
return () => {
|
|
33
|
+
window.removeEventListener(POPOVER_OPEN_EVENT, onOtherOpen);
|
|
34
|
+
document.removeEventListener("mousedown", onDocMouseDown);
|
|
35
|
+
};
|
|
36
|
+
}, [open]);
|
|
37
|
+
}
|
package/src/posestudio.jsx
CHANGED
|
@@ -423,13 +423,10 @@ export function IkHandles({ chains, fkJoints, ikState, enabled, focus, onFocus,
|
|
|
423
423
|
// not-yet-focused sphere just took focus (nothing to bake). An
|
|
424
424
|
// empty-canvas click releases focus too.
|
|
425
425
|
if (!d.moved) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (d.wasFocused && !d.axisDir) {
|
|
431
|
-
focusRef.current?.(null); // toggle off
|
|
432
|
-
}
|
|
426
|
+
// (An empty press is always wasFocused: it only begins while focused.)
|
|
427
|
+
// A gizmo volume can win the raycast over the sphere centre, so a
|
|
428
|
+
// no-move press on the centre (d.centerClick) toggles off too.
|
|
429
|
+
if (d.wasFocused && (!d.axisDir || d.centerClick)) focusRef.current?.(null); // toggle off
|
|
433
430
|
// axis-arrow clicks and fresh-sphere clicks: nothing to bake
|
|
434
431
|
return;
|
|
435
432
|
}
|
|
@@ -842,7 +839,7 @@ const CLICK_PX = 4;
|
|
|
842
839
|
if (kind === "empty") {
|
|
843
840
|
// An empty-canvas press: no drag, just a click candidate that
|
|
844
841
|
// releases focus on pointerup (see onUp).
|
|
845
|
-
dragRef.current = { kind: "empty", trackId: "empty", downXY, moved: false };
|
|
842
|
+
dragRef.current = { kind: "empty", trackId: "empty", downXY, moved: false, wasFocused: true };
|
|
846
843
|
window.addEventListener("pointermove", handlersRef.current.onMove);
|
|
847
844
|
window.addEventListener("pointerup", handlersRef.current.onUp);
|
|
848
845
|
window.addEventListener("pointercancel", handlersRef.current.onUp);
|
|
@@ -860,6 +857,7 @@ const CLICK_PX = 4;
|
|
|
860
857
|
}
|
|
861
858
|
raycaster.setFromCamera(ndc, camera);
|
|
862
859
|
let plane;
|
|
860
|
+
const rect = gl.domElement.getBoundingClientRect(), c = origin.clone().project(camera);
|
|
863
861
|
const ringDrag = !!axisDir && (kind === "fk" || kind === "swing");
|
|
864
862
|
if (ringDrag) {
|
|
865
863
|
// A ring turns about its axis: the pointer is read on the ring's own plane.
|
|
@@ -892,6 +890,8 @@ const CLICK_PX = 4;
|
|
|
892
890
|
// a no-move click toggles focus OFF only then; a click on a fresh
|
|
893
891
|
// (unfocused) handle must keep the focus it just took.
|
|
894
892
|
wasFocused: focusIdRef.current === track.id,
|
|
893
|
+
// Press landed within CLICK_PX of the sphere's projected centre.
|
|
894
|
+
centerClick: Math.hypot(downXY[0] - (rect.left + (c.x + 1) / 2 * rect.width), downXY[1] - (rect.top + (1 - c.y) / 2 * rect.height)) <= CLICK_PX,
|
|
895
895
|
};
|
|
896
896
|
if (kind === "fk" || kind === "swing" || kind === "body") {
|
|
897
897
|
// Swing/translate start state, frozen at drag start so moves stay
|
|
@@ -944,8 +944,7 @@ const CLICK_PX = 4;
|
|
|
944
944
|
handleRefs.current[p.track.id]?.userData.ikExposed !== false
|
|
945
945
|
);
|
|
946
946
|
if (focused && !selectionOnly) picks = picks.filter((p) => p.track.id === focused);
|
|
947
|
-
|
|
948
|
-
if (selectionOnly) picks = picks.filter((p) => !p.axisDir);
|
|
947
|
+
else picks = picks.filter((p) => !p.axisDir); // spheres only
|
|
949
948
|
if (!picks.length) {
|
|
950
949
|
if (focused) beginDrag("empty", { id: "empty" }, null, null, downXY);
|
|
951
950
|
return;
|
|
@@ -985,10 +984,16 @@ const CLICK_PX = 4;
|
|
|
985
984
|
}
|
|
986
985
|
setHoverAxis((current) => (current === axis ? current : axis));
|
|
987
986
|
};
|
|
987
|
+
// Esc releases the focused handle (same path as the toggle-off click).
|
|
988
|
+
const onKey = (ev) => {
|
|
989
|
+
if (ev.key === "Escape" && focusIdRef.current && !/^(INPUT|TEXTAREA|SELECT)$/.test(ev.target?.tagName)) focusRef.current?.(null);
|
|
990
|
+
};
|
|
988
991
|
raycaster.layers.enable(POSER_LAYER);
|
|
989
992
|
el.addEventListener("pointerdown", onDown, true);
|
|
990
993
|
el.addEventListener("pointermove", onHover);
|
|
994
|
+
window.addEventListener("keydown", onKey);
|
|
991
995
|
return () => {
|
|
996
|
+
window.removeEventListener("keydown", onKey);
|
|
992
997
|
el.removeEventListener("pointerdown", onDown, true);
|
|
993
998
|
el.removeEventListener("pointermove", onHover);
|
|
994
999
|
raycaster.layers.disable(POSER_LAYER);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Start-screen art: the brand mark and the template thumbnail that fills both
|
|
2
|
+
// the card and the larger preview. Thumbnails are real studio renders, one per
|
|
3
|
+
// theme, made by tools/render-start-thumbs.mjs into public/thumbs/. A template
|
|
4
|
+
// without a render (a starter added since the last run) keeps the plain tile
|
|
5
|
+
// its wrapper already paints.
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { useUiTheme } from "./theme.js";
|
|
8
|
+
|
|
9
|
+
export function TemplateThumb({ id }) {
|
|
10
|
+
const theme = useUiTheme();
|
|
11
|
+
const src = `/thumbs/${id}-${theme}.webp`;
|
|
12
|
+
const [failed, setFailed] = useState(null);
|
|
13
|
+
if (failed === src) return null;
|
|
14
|
+
return <img className="v2-start-art" src={src} alt="" aria-hidden="true" draggable="false" decoding="async" onError={() => setFailed(src)} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function BrandMark() {
|
|
18
|
+
return <img className="v2-start-brand-mark" src="/icons/icon-192.png" alt="" aria-hidden="true" draggable="false" width="26" height="26" />;
|
|
19
|
+
}
|
package/src/project-browser.css
CHANGED
|
@@ -102,11 +102,17 @@
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
.v2-start-screen .v2-start-brand-mark {
|
|
105
|
+
display: block;
|
|
106
|
+
box-sizing: border-box;
|
|
105
107
|
width: var(--start-brand-mark, 26px);
|
|
106
108
|
height: var(--start-brand-mark, 26px);
|
|
107
109
|
flex: 0 0 var(--start-brand-mark, 26px);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
padding: 1px;
|
|
111
|
+
border-radius: 7px;
|
|
112
|
+
/* The frog keeps its own blue on its own white tile in both themes. */
|
|
113
|
+
background: #fff;
|
|
114
|
+
box-shadow: 0 0 0 var(--start-border-width, 1px) color-mix(in srgb, var(--text-1, #e4e4e7) 10%, transparent);
|
|
115
|
+
object-fit: contain;
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
.v2-start-screen .v2-start-nav-list {
|
|
@@ -196,30 +202,7 @@
|
|
|
196
202
|
color: var(--text-1, #e4e4e7);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
|
|
200
|
-
display: flex;
|
|
201
|
-
align-items: center;
|
|
202
|
-
gap: var(--start-tab-gap, 20px);
|
|
203
|
-
min-height: var(--start-tab-height, 24px);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.v2-start-screen .v2-start-tab {
|
|
207
|
-
height: var(--start-tab-height, 24px);
|
|
208
|
-
padding: 0 0 var(--start-tab-underline-space, 8px);
|
|
209
|
-
border: 0;
|
|
210
|
-
border-bottom: var(--start-active-width, 1.5px) solid var(--transparent, transparent);
|
|
211
|
-
background: var(--transparent, transparent);
|
|
212
|
-
color: var(--text-3, #636369);
|
|
213
|
-
white-space: nowrap;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
.v2-start-screen .v2-start-tab:hover,
|
|
217
|
-
.v2-start-screen .v2-start-tab.active {
|
|
218
|
-
border-bottom-color: var(--select, #e8a33d);
|
|
219
|
-
color: var(--text-selected, #fff);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/* Project mode (previs flag): a two-up segmented switch above the tabs. Each
|
|
205
|
+
/* Project mode (previs flag): a two-up segmented switch above the cards. Each
|
|
223
206
|
segment carries its label and the one-line promise of what the mode makes. */
|
|
224
207
|
.v2-start-screen .v2-start-mode {
|
|
225
208
|
display: grid;
|
|
@@ -291,69 +274,27 @@
|
|
|
291
274
|
}
|
|
292
275
|
|
|
293
276
|
.v2-start-screen .v2-start-template-art {
|
|
294
|
-
|
|
277
|
+
display: block;
|
|
278
|
+
overflow: hidden;
|
|
295
279
|
width: var(--start-template-art-width, 100%);
|
|
296
280
|
aspect-ratio: 16 / 10;
|
|
297
|
-
overflow: hidden;
|
|
298
281
|
border-radius: var(--r-2, 6px);
|
|
282
|
+
outline: var(--start-border-width, 1px) solid color-mix(in srgb, var(--text-1, #e4e4e7) 9%, transparent);
|
|
283
|
+
outline-offset: -1px;
|
|
299
284
|
background: var(--surface-raised, #1e1e21);
|
|
300
|
-
box-shadow: inset 0 0 0 var(--start-border-width, 1px) var(--transparent, transparent);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.v2-start-screen .v2-start-template-art::before,
|
|
304
|
-
.v2-start-screen .v2-start-template-art::after {
|
|
305
|
-
content: "";
|
|
306
|
-
position: absolute;
|
|
307
|
-
display: block;
|
|
308
|
-
background: var(--surface-active, #26262a);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.v2-start-screen .v2-start-template-art::before {
|
|
312
|
-
left: var(--start-art-block-left, 12%);
|
|
313
|
-
bottom: var(--start-art-block-bottom, 16%);
|
|
314
|
-
width: var(--start-art-block-width, 36%);
|
|
315
|
-
height: var(--start-art-block-height, 48%);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.v2-start-screen .v2-start-template-art::after {
|
|
319
|
-
right: var(--start-art-block-right, 12%);
|
|
320
|
-
bottom: var(--start-art-block-bottom, 16%);
|
|
321
|
-
width: var(--start-art-small-width, 20%);
|
|
322
|
-
height: var(--start-art-small-height, 24%);
|
|
323
|
-
background: var(--field, #0f0f11);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
.v2-start-screen .v2-start-template-art.sample::before {
|
|
327
|
-
width: var(--start-sample-block-width, 54%);
|
|
328
|
-
height: var(--start-sample-block-height, 28%);
|
|
329
|
-
background: var(--primary, #7258a0);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
.v2-start-screen .v2-start-template-art.sample::after {
|
|
333
|
-
width: var(--start-sample-small-width, 10%);
|
|
334
|
-
height: var(--start-sample-small-height, 52%);
|
|
335
|
-
background: var(--camera, #a78bfa);
|
|
336
285
|
}
|
|
337
286
|
|
|
338
287
|
.v2-start-screen .v2-start-template.selected .v2-start-template-art {
|
|
339
|
-
|
|
288
|
+
outline: var(--start-selected-width, 1.5px) solid var(--select, #e8a33d);
|
|
289
|
+
outline-offset: -1.5px;
|
|
340
290
|
}
|
|
341
291
|
|
|
342
292
|
.v2-start-screen .v2-start-template:hover .v2-start-template-art {
|
|
343
|
-
|
|
293
|
+
outline-color: var(--text-3, #636369);
|
|
344
294
|
}
|
|
345
295
|
|
|
346
296
|
.v2-start-screen .v2-start-template.selected:hover .v2-start-template-art {
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
.v2-start-screen .v2-start-template-code {
|
|
351
|
-
position: absolute;
|
|
352
|
-
left: var(--sp-1, 8px);
|
|
353
|
-
bottom: var(--sp-1, 8px);
|
|
354
|
-
color: var(--text-off, #4f4f55);
|
|
355
|
-
font-family: var(--mono, 'JetBrains Mono'), ui-monospace, monospace;
|
|
356
|
-
font-size: var(--start-code-size, 10px);
|
|
297
|
+
outline-color: var(--select, #e8a33d);
|
|
357
298
|
}
|
|
358
299
|
|
|
359
300
|
.v2-start-screen .v2-start-template-copy {
|
|
@@ -382,6 +323,43 @@
|
|
|
382
323
|
white-space: nowrap;
|
|
383
324
|
}
|
|
384
325
|
|
|
326
|
+
/* Open-project card: same slot and caption as a template, dashed art. */
|
|
327
|
+
.v2-start-screen .v2-start-add-card {
|
|
328
|
+
min-width: var(--start-template-min-width, 0);
|
|
329
|
+
display: flex;
|
|
330
|
+
flex-direction: column;
|
|
331
|
+
gap: var(--sp-2, 12px);
|
|
332
|
+
padding: 0;
|
|
333
|
+
border: 0;
|
|
334
|
+
background: var(--transparent, transparent);
|
|
335
|
+
color: var(--text-1, #e4e4e7);
|
|
336
|
+
text-align: left;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.v2-start-screen .v2-start-add-art {
|
|
340
|
+
display: grid;
|
|
341
|
+
place-items: center;
|
|
342
|
+
width: var(--start-template-art-width, 100%);
|
|
343
|
+
aspect-ratio: 16 / 10;
|
|
344
|
+
border: var(--start-border-width, 1px) dashed var(--text-3, #636369);
|
|
345
|
+
border-radius: var(--r-2, 6px);
|
|
346
|
+
color: var(--text-3, #636369);
|
|
347
|
+
transition: border-color 120ms ease, color 120ms ease;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.v2-start-screen .v2-start-add-card:hover .v2-start-add-art,
|
|
351
|
+
.v2-start-screen .v2-start-add-card:focus-visible .v2-start-add-art {
|
|
352
|
+
border-color: var(--select, #e8a33d);
|
|
353
|
+
color: var(--select, #e8a33d);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.v2-start-screen .v2-start-add-card.selected .v2-start-add-art {
|
|
357
|
+
border-style: solid;
|
|
358
|
+
border-color: var(--select, #e8a33d);
|
|
359
|
+
box-shadow: inset 0 0 0 0.5px var(--select, #e8a33d);
|
|
360
|
+
color: var(--select, #e8a33d);
|
|
361
|
+
}
|
|
362
|
+
|
|
385
363
|
/* --------------------------------------------------------------- recent --- */
|
|
386
364
|
|
|
387
365
|
.v2-start-screen .v2-start-recent-block,
|
|
@@ -510,11 +488,6 @@
|
|
|
510
488
|
padding: var(--sp-1, 8px) 0;
|
|
511
489
|
}
|
|
512
490
|
|
|
513
|
-
.v2-start-screen .v2-start-file-actions {
|
|
514
|
-
margin-top: auto;
|
|
515
|
-
padding-top: var(--sp-1, 8px);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
491
|
.v2-start-screen .v2-start-learn {
|
|
519
492
|
max-width: var(--start-learn-max-width, 520px);
|
|
520
493
|
display: flex;
|
|
@@ -537,6 +510,11 @@
|
|
|
537
510
|
line-height: 1.55;
|
|
538
511
|
}
|
|
539
512
|
|
|
513
|
+
.v2-start-screen .v2-start-file-actions {
|
|
514
|
+
margin-top: auto;
|
|
515
|
+
padding-top: var(--sp-1, 8px);
|
|
516
|
+
}
|
|
517
|
+
|
|
540
518
|
/* --------------------------------------------------------------- preview --- */
|
|
541
519
|
|
|
542
520
|
.v2-start-screen .v2-start-preview {
|
|
@@ -550,51 +528,26 @@
|
|
|
550
528
|
}
|
|
551
529
|
|
|
552
530
|
.v2-start-screen .v2-start-preview-art {
|
|
553
|
-
|
|
554
|
-
|
|
531
|
+
display: block;
|
|
532
|
+
flex: 0 0 auto;
|
|
533
|
+
/* A framed card inset to the copy column below it, not an edge-to-edge band. */
|
|
534
|
+
width: auto;
|
|
535
|
+
margin: var(--start-preview-copy-pad-x, 20px) var(--start-preview-copy-pad-x, 20px) 0;
|
|
555
536
|
aspect-ratio: 16 / 9;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
537
|
+
overflow: hidden;
|
|
538
|
+
border-radius: var(--r-2, 6px);
|
|
539
|
+
outline: var(--start-border-width, 1px) solid color-mix(in srgb, var(--text-1, #e4e4e7) 9%, transparent);
|
|
540
|
+
outline-offset: -1px;
|
|
560
541
|
background: var(--surface-raised, #1e1e21);
|
|
561
542
|
}
|
|
562
543
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
position: absolute;
|
|
544
|
+
/* Template art: a real studio render (public/thumbs, one per theme). The
|
|
545
|
+
wrapper paints a plain tile underneath for a template without one. */
|
|
546
|
+
.v2-start-screen .v2-start-art {
|
|
567
547
|
display: block;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
.v2-start-screen .v2-start-preview-art::before {
|
|
572
|
-
left: var(--preview-block-left, 14%);
|
|
573
|
-
bottom: var(--preview-block-bottom, 16%);
|
|
574
|
-
width: var(--preview-block-width, 34%);
|
|
575
|
-
height: var(--preview-block-height, 50%);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
.v2-start-screen .v2-start-preview-art::after {
|
|
579
|
-
right: var(--preview-block-right, 14%);
|
|
580
|
-
bottom: var(--preview-block-bottom, 16%);
|
|
581
|
-
width: var(--preview-small-width, 18%);
|
|
582
|
-
height: var(--preview-small-height, 28%);
|
|
583
|
-
background: var(--primary, #7258a0);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
.v2-start-screen .v2-start-preview-art.sample::before {
|
|
587
|
-
width: var(--preview-sample-width, 58%);
|
|
588
|
-
height: var(--preview-sample-height, 30%);
|
|
589
|
-
background: var(--camera, #a78bfa);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
.v2-start-screen .v2-start-preview-art span {
|
|
593
|
-
position: relative;
|
|
594
|
-
z-index: var(--start-art-label-layer, 1);
|
|
595
|
-
color: var(--text-off, #4f4f55);
|
|
596
|
-
font-family: var(--mono, 'JetBrains Mono'), ui-monospace, monospace;
|
|
597
|
-
font-size: var(--start-preview-label-size, 10.5px);
|
|
548
|
+
width: 100%;
|
|
549
|
+
height: 100%;
|
|
550
|
+
object-fit: cover;
|
|
598
551
|
}
|
|
599
552
|
|
|
600
553
|
.v2-start-screen .v2-start-preview-copy {
|
package/src/project-browser.jsx
CHANGED
|
@@ -14,21 +14,16 @@ import {
|
|
|
14
14
|
storeProjectsDirectory,
|
|
15
15
|
DEFAULT_PREVIS_MODE,
|
|
16
16
|
} from "./project.js";
|
|
17
|
+
import { BrandMark, TemplateThumb } from "./project-browser-art.jsx";
|
|
17
18
|
import "./project-browser.css";
|
|
18
19
|
|
|
19
20
|
const APP_VERSION = packageInfo.version;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
name: "Blank Stage",
|
|
24
|
-
category: "All",
|
|
25
|
-
description: "An empty stage for blocking a new shot.",
|
|
26
|
-
kind: "blank",
|
|
27
|
-
code: "NEW",
|
|
28
|
-
});
|
|
21
|
+
// The "+" card is the one blank entry: an empty stage under a fresh name (no
|
|
22
|
+
// starterId, so Create takes the blank path). Its preview is the blank-stage render.
|
|
23
|
+
const NEW_PROJECT = Object.freeze({ id: "new-project", thumbId: "blank-stage", kind: "blank", defaultName: "Untitled Project", get name() { return ko("New project", "새 프로젝트"); }, get description() { return ko("Start from an empty stage.", "빈 무대에서 시작합니다."); } });
|
|
29
24
|
|
|
30
25
|
// The two project modes the New view offers.
|
|
31
|
-
// Storyboard starts from an empty stage only; Animation
|
|
26
|
+
// Storyboard starts from an empty stage only; Animation also offers the starters.
|
|
32
27
|
const MODE_OPTIONS = [
|
|
33
28
|
{
|
|
34
29
|
id: "storyboard",
|
|
@@ -46,6 +41,9 @@ const MODE_OPTIONS = [
|
|
|
46
41
|
// path. The v2 chooser already has the name, so this short-lived handoff seeds
|
|
47
42
|
// the existing ProjectNameDialog and lets its normal submit path commit it.
|
|
48
43
|
let pendingProjectName = null;
|
|
44
|
+
function stagePendingProjectName(name) {
|
|
45
|
+
pendingProjectName = name;
|
|
46
|
+
}
|
|
49
47
|
|
|
50
48
|
function formatDate(value) {
|
|
51
49
|
const date = new Date(value);
|
|
@@ -58,10 +56,8 @@ function sampleTemplate(scene) {
|
|
|
58
56
|
id: `sample-${scene.id}`,
|
|
59
57
|
starterId: scene.id,
|
|
60
58
|
name: scene.name,
|
|
61
|
-
category: "Action",
|
|
62
59
|
description: scene.blurb,
|
|
63
60
|
kind: "sample",
|
|
64
|
-
code: "SAMPLE",
|
|
65
61
|
};
|
|
66
62
|
}
|
|
67
63
|
|
|
@@ -103,20 +99,17 @@ export function ProjectNameDialog({ open, initialName = "My Project", onCancel,
|
|
|
103
99
|
|
|
104
100
|
/**
|
|
105
101
|
* v2 start / project screen. Recent projects still come from the browser-local
|
|
106
|
-
* project store, while
|
|
102
|
+
* project store, while the bundled starter scenes (none today) become template
|
|
103
|
+
* cards before the "+" card.
|
|
107
104
|
*/
|
|
108
105
|
export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew, onClose, startup = false, starters = [], onStarter }) {
|
|
109
106
|
const starterTemplates = useMemo(() => starters.map(sampleTemplate), [starters]);
|
|
110
107
|
const [previsMode, setPrevisMode] = useState(DEFAULT_PREVIS_MODE);
|
|
111
|
-
const templates =
|
|
112
|
-
|
|
113
|
-
[previsMode, starterTemplates],
|
|
114
|
-
);
|
|
115
|
-
const defaultTemplate = starterTemplates[0] ?? BLANK_TEMPLATE;
|
|
108
|
+
const templates = previsMode === "storyboard" ? [] : starterTemplates;
|
|
109
|
+
const defaultTemplate = starterTemplates[0] ?? NEW_PROJECT;
|
|
116
110
|
const [activeNav, setActiveNav] = useState(startup ? "new" : "recent");
|
|
117
|
-
const [category, setCategory] = useState("All");
|
|
118
111
|
const [selectedTemplateId, setSelectedTemplateId] = useState(defaultTemplate.id);
|
|
119
|
-
const [name, setName] = useState(currentName || defaultTemplate.name || "My Project");
|
|
112
|
+
const [name, setName] = useState(currentName || defaultTemplate.defaultName || defaultTemplate.name || "My Project");
|
|
120
113
|
const [frameRate, setFrameRate] = useState("24");
|
|
121
114
|
const [units, setUnits] = useState("Meters");
|
|
122
115
|
const [recents, setRecents] = useState([]);
|
|
@@ -156,15 +149,14 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
156
149
|
|
|
157
150
|
// File › New reaches this screen with unsaved work, so Create can be refused
|
|
158
151
|
// by the discard confirmation. A name left behind then must not seed a later
|
|
159
|
-
// Save's name dialog once the screen closes.
|
|
152
|
+
// Save's name dialog once the screen closes. The clear waits for the end of
|
|
153
|
+
// the commit: a Create that closes this screen opens the name dialog in the
|
|
154
|
+
// same commit, and its effect takes the name first.
|
|
160
155
|
useEffect(() => () => {
|
|
161
|
-
|
|
156
|
+
queueMicrotask(() => stagePendingProjectName(null));
|
|
162
157
|
}, []);
|
|
163
158
|
|
|
164
|
-
const selectedTemplate = templates.find((template) => template.id === selectedTemplateId) ??
|
|
165
|
-
const visibleTemplates = category === "All"
|
|
166
|
-
? templates
|
|
167
|
-
: templates.filter((template) => template.category === "All" || template.category === category);
|
|
159
|
+
const selectedTemplate = templates.find((template) => template.id === selectedTemplateId) ?? NEW_PROJECT;
|
|
168
160
|
const canCreate = name.trim().length > 0;
|
|
169
161
|
const createReason = ko("Enter a project name to create a project", "프로젝트를 만들려면 이름을 입력하세요");
|
|
170
162
|
const locationLabel = folder?.name || ko("Browser project store", "브라우저 프로젝트 저장소");
|
|
@@ -201,17 +193,17 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
201
193
|
const selectTemplate = (template) => {
|
|
202
194
|
setSelectedTemplateId(template.id);
|
|
203
195
|
// A name the previous card (or the open project) filled in follows the new card; a typed one stays.
|
|
204
|
-
setName((current) => !current.trim() ||
|
|
196
|
+
setName((current) => !current.trim() || [selectedTemplate?.name, selectedTemplate?.defaultName, currentName].includes(current.trim()) ? template.defaultName ?? template.name : current);
|
|
205
197
|
};
|
|
206
198
|
|
|
207
199
|
const choosePrevisMode = (mode) => {
|
|
208
200
|
setPrevisMode(mode);
|
|
209
201
|
// Storyboard offers the blank stage only, so a starter selection cannot
|
|
210
202
|
// survive the switch.
|
|
211
|
-
if (mode !== "storyboard" || selectedTemplate?.
|
|
212
|
-
setSelectedTemplateId(
|
|
203
|
+
if (mode !== "storyboard" || !selectedTemplate?.starterId) return;
|
|
204
|
+
setSelectedTemplateId(NEW_PROJECT.id);
|
|
213
205
|
// A name the starter card filled in would now describe the wrong stage.
|
|
214
|
-
setName((current) => (current.trim() === selectedTemplate?.name ?
|
|
206
|
+
setName((current) => (current.trim() === selectedTemplate?.name ? NEW_PROJECT.defaultName : current));
|
|
215
207
|
};
|
|
216
208
|
|
|
217
209
|
const createProject = (event) => {
|
|
@@ -222,7 +214,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
222
214
|
onStarter?.(selectedTemplate.starterId, projectName, { previsMode: "animation" });
|
|
223
215
|
return;
|
|
224
216
|
}
|
|
225
|
-
|
|
217
|
+
stagePendingProjectName(projectName);
|
|
226
218
|
onNew(projectName, { previsMode });
|
|
227
219
|
};
|
|
228
220
|
|
|
@@ -234,7 +226,6 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
234
226
|
const navItems = [
|
|
235
227
|
{ id: "recent", label: ko("Recent", "최근") },
|
|
236
228
|
{ id: "new", label: ko("New Project", "새 프로젝트") },
|
|
237
|
-
{ id: "samples", label: ko("Samples", "샘플") },
|
|
238
229
|
{ id: "learn", label: ko("Learn", "배우기") },
|
|
239
230
|
];
|
|
240
231
|
|
|
@@ -243,7 +234,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
243
234
|
<div className={`v2-start-window project-browser${startup ? " startup" : ""}`} role="dialog" aria-modal="true" aria-label={ko("Choose project", "프로젝트 선택")}>
|
|
244
235
|
<nav className="v2-start-nav" aria-label={ko("Project navigation", "프로젝트 탐색")}>
|
|
245
236
|
<div className="v2-start-brand">
|
|
246
|
-
<
|
|
237
|
+
<BrandMark />
|
|
247
238
|
<strong>CozyClay</strong>
|
|
248
239
|
</div>
|
|
249
240
|
<div className="v2-start-nav-list">
|
|
@@ -265,7 +256,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
265
256
|
<main className="v2-start-main">
|
|
266
257
|
<div className="v2-start-main-head">
|
|
267
258
|
<div className="v2-start-title-row">
|
|
268
|
-
<h1>{activeNav === "recent" ? ko("Recent", "최근") : activeNav === "
|
|
259
|
+
<h1>{activeNav === "recent" ? ko("Recent", "최근") : activeNav === "learn" ? ko("Learn", "배우기") : ko("New Project", "새 프로젝트")}</h1>
|
|
269
260
|
{activeNav !== "new" && <button type="button" className="v2-start-close x" onClick={onClose} aria-label={ko("Close", "닫기")}>×</button>}
|
|
270
261
|
</div>
|
|
271
262
|
{activeNav === "new" && (
|
|
@@ -286,22 +277,6 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
286
277
|
))}
|
|
287
278
|
</div>
|
|
288
279
|
)}
|
|
289
|
-
{(activeNav === "new" || activeNav === "samples") && (
|
|
290
|
-
<div className="v2-start-tabs" role="tablist" aria-label={ko("Template categories", "템플릿 카테고리")}>
|
|
291
|
-
{CATEGORIES.map((entry) => (
|
|
292
|
-
<button
|
|
293
|
-
type="button"
|
|
294
|
-
key={entry}
|
|
295
|
-
role="tab"
|
|
296
|
-
aria-selected={category === entry}
|
|
297
|
-
className={`v2-start-tab${category === entry ? " active" : ""}`}
|
|
298
|
-
onClick={() => setCategory(entry)}
|
|
299
|
-
>
|
|
300
|
-
{ko(entry, entry === "All" ? "전체" : entry === "Film" ? "영화" : entry === "Dialogue" ? "대화" : entry === "Action" ? "액션" : "차량")}
|
|
301
|
-
</button>
|
|
302
|
-
))}
|
|
303
|
-
</div>
|
|
304
|
-
)}
|
|
305
280
|
</div>
|
|
306
281
|
|
|
307
282
|
{activeNav === "learn" ? (
|
|
@@ -327,7 +302,7 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
327
302
|
) : (
|
|
328
303
|
<>
|
|
329
304
|
<div className="v2-start-template-grid" role="list" aria-label={ko("Project templates", "프로젝트 템플릿")}>
|
|
330
|
-
{
|
|
305
|
+
{templates.map((template) => (
|
|
331
306
|
<button
|
|
332
307
|
type="button"
|
|
333
308
|
role="listitem"
|
|
@@ -337,15 +312,20 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
337
312
|
onClick={() => selectTemplate(template)}
|
|
338
313
|
onDoubleClick={() => template.starterId && onStarter?.(template.starterId)}
|
|
339
314
|
>
|
|
340
|
-
<span className={`v2-start-template-art ${template.kind}`}
|
|
341
|
-
<span className="v2-start-template-code">{template.code}</span>
|
|
342
|
-
</span>
|
|
315
|
+
<span className={`v2-start-template-art ${template.kind}`}><TemplateThumb id={template.starterId ?? template.id} /></span>
|
|
343
316
|
<span className="v2-start-template-copy">
|
|
344
317
|
<strong>{template.name}</strong>
|
|
345
318
|
<span>{template.description}</span>
|
|
346
319
|
</span>
|
|
347
320
|
</button>
|
|
348
321
|
))}
|
|
322
|
+
<button type="button" role="listitem" className={`v2-start-add-card${selectedTemplate?.id === NEW_PROJECT.id ? " selected" : ""}`} data-testid="start-new-project-card" onClick={() => { selectTemplate(NEW_PROJECT); requestAnimationFrame(() => nameInputRef.current?.select()); }}>
|
|
323
|
+
<span className="v2-start-add-art" aria-hidden="true"><svg viewBox="0 0 20 20" width="20" height="20"><path d="M10 3.5v13M3.5 10h13" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /></svg></span>
|
|
324
|
+
<span className="v2-start-template-copy">
|
|
325
|
+
<strong>{ko("New project", "새 프로젝트")}</strong>
|
|
326
|
+
<span>{ko("Start from an empty stage", "빈 무대에서 시작")}</span>
|
|
327
|
+
</span>
|
|
328
|
+
</button>
|
|
349
329
|
</div>
|
|
350
330
|
|
|
351
331
|
<div className="v2-start-recent-block">
|
|
@@ -357,15 +337,11 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
357
337
|
</div>
|
|
358
338
|
</>
|
|
359
339
|
)}
|
|
360
|
-
{activeNav !== "recent" && activeNav !== "learn" && (
|
|
361
|
-
<div className="v2-start-file-actions">
|
|
362
|
-
<button type="button" className="v2-start-file-button" onClick={onOpenFile}>{ko("Open project file…", "프로젝트 파일 열기…")}</button>
|
|
363
|
-
</div>
|
|
364
|
-
)}
|
|
340
|
+
{activeNav !== "recent" && activeNav !== "learn" && <div className="v2-start-file-actions"><button type="button" className="v2-start-file-button" onClick={onOpenFile}>{ko("Open project file…", "프로젝트 파일 열기…")}</button></div>}
|
|
365
341
|
</main>
|
|
366
342
|
|
|
367
343
|
<aside className="v2-start-preview" data-testid="start-project-preview" aria-label={ko("Project preview", "프로젝트 미리보기")}>
|
|
368
|
-
<div className={`v2-start-preview-art ${selectedTemplate?.kind || "blank"}`}
|
|
344
|
+
<div className={`v2-start-preview-art ${selectedTemplate?.kind || "blank"}`}><TemplateThumb id={selectedTemplate.starterId ?? selectedTemplate.thumbId} /></div>
|
|
369
345
|
<div className="v2-start-preview-copy">
|
|
370
346
|
<strong>{selectedTemplate?.name}</strong>
|
|
371
347
|
<span>{selectedTemplate?.description}</span>
|
package/src/props.jsx
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Set props: low-poly clay maquettes to block against.
|
|
3
3
|
*
|
|
4
4
|
* The set is a bare room and a figure; a shot that needs "something in the
|
|
5
|
-
* world" (a
|
|
5
|
+
* world" (a wall to lean on, to walk past, to frame behind) has nothing
|
|
6
6
|
* to work with. These props are built from primitives in the same clay style
|
|
7
7
|
* as the Room and the character tint, so the whole frame stays one
|
|
8
|
-
* consistent maquette.
|
|
9
|
-
*
|
|
8
|
+
* consistent maquette. Practical lights and glow live in scene-lights.jsx
|
|
9
|
+
* and object-glow.js.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -25,7 +25,7 @@ import { EffectMarker, EffectsLayer } from "./effects/EffectsLayer.jsx";
|
|
|
25
25
|
import { EFFECT_KIND } from "./effects/effect-record.js";
|
|
26
26
|
import { applyPoseFade } from "./pose-fade.js";
|
|
27
27
|
import { coplanarDepthRanks } from "./coplanar-depth.js";
|
|
28
|
-
import { autoFlat
|
|
28
|
+
import { autoFlat } from "./auto-flat.js";
|
|
29
29
|
import { LIGHT_KIND, capPracticalShadows } from "./object-look.js";
|
|
30
30
|
import { SceneLight, useLookRefresh } from "./scene-lights.jsx";
|
|
31
31
|
import { placeObjectLook } from "./scene-light-place.js";
|
|
@@ -242,9 +242,6 @@ function SceneObjectContent({ object, depthRank = 0 }) {
|
|
|
242
242
|
if (renderer === MESH_KIND) return <ImportedMesh object={object} />;
|
|
243
243
|
if (renderer === SCULPT_KIND) return <Sculpt object={object} depthRank={depthRank} />;
|
|
244
244
|
if (renderer === TEXT_KIND) return <TextObject object={object} />;
|
|
245
|
-
if (renderer === "car") return <Car color={autoColor ?? color} autoColor={autoColor} />;
|
|
246
|
-
if (renderer === "small-plane") return <SmallPlane autoColor={autoColor} />;
|
|
247
|
-
if (renderer === "chair") return <Chair autoColor={autoColor} />;
|
|
248
245
|
if (PRIMITIVE_KINDS.has(renderer)) return <Primitive kind={renderer} color={color} autoColor={autoColor} depthRank={depthRank} />;
|
|
249
246
|
return null;
|
|
250
247
|
}
|