cozyclay 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +219 -38
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +6 -5
- package/bin/cozyclay.mjs +12 -3
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +12 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +5 -5
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +801 -259
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +196 -3
- package/src/ardy/timeline.jsx +117 -29
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +1 -4
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +43 -24
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +16 -3
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +89 -32
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion/generation.js +1 -1
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +52 -3
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +62 -17
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +111 -10
- package/src/theme.js +1 -1
- package/src/timeline-fps.js +13 -0
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +110 -20
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +96 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-C1sdVbg4.css +0 -1
- package/dist/assets/app-CA55tcI3.js +0 -4876
- package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
- package/dist/assets/workflow-dNPGY9xz.js +0 -179
package/src/posestudio.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
-
import { Line } from "@react-three/drei";
|
|
3
|
+
import { Html, Line } from "@react-three/drei";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { POSE_BONES, normalizeBoneName, primeBindPose } from "./poses.js";
|
|
6
6
|
import { poseThumbnail, warmThumbnailModels } from "./pose-thumbs.js";
|
|
@@ -8,6 +8,8 @@ import { IK_TRACKS, MID_TRACKS, ikControlIsExposed } from "./ardy/ik.js";
|
|
|
8
8
|
import { shouldRefreshIkExposure } from "./use-render-activity.js";
|
|
9
9
|
import { POSER_LAYER } from "./dualview.jsx";
|
|
10
10
|
import { ko, isKo } from "./locale.js";
|
|
11
|
+
import { ROTATION_AXES, backFaceFade, bindQuaternionOf, handleColor as sideColor, ringAngle, rotationFromBindDeg, rotationReadout } from "./pose-gizmo.js";
|
|
12
|
+
import "./pose-gizmo.css";
|
|
11
13
|
|
|
12
14
|
/** Same normalised-name match rule as poses.js: equal, or one is a suffix of
|
|
13
15
|
* the other, so `mixamorig:LeftArm`, `mixamorigLeftArm` and `LeftArm` all hit. */
|
|
@@ -38,7 +40,7 @@ function handleColor(entry) {
|
|
|
38
40
|
* best matches the drag direction in screen space). Each move calls
|
|
39
41
|
* `onChange()`.
|
|
40
42
|
*/
|
|
41
|
-
export function PoseHandles({ root, enabled, onChange }) {
|
|
43
|
+
export function PoseHandles({ root, enabled, onChange, onCommit }) {
|
|
42
44
|
const { camera, gl, raycaster } = useThree();
|
|
43
45
|
const meshRefs = useRef([]);
|
|
44
46
|
const jointsRef = useRef(new Map());
|
|
@@ -46,6 +48,10 @@ export function PoseHandles({ root, enabled, onChange }) {
|
|
|
46
48
|
const dragHandlersRef = useRef(null);
|
|
47
49
|
const onChangeRef = useRef(onChange);
|
|
48
50
|
onChangeRef.current = onChange;
|
|
51
|
+
// Called once when a drag that actually moved a joint ends: the pose the
|
|
52
|
+
// rig now stands in is the author's, and the host keeps it.
|
|
53
|
+
const onCommitRef = useRef(onCommit);
|
|
54
|
+
onCommitRef.current = onCommit;
|
|
49
55
|
const tmp = useRef({
|
|
50
56
|
origin: new THREE.Vector3(),
|
|
51
57
|
limb: new THREE.Vector3(),
|
|
@@ -114,6 +120,7 @@ export function PoseHandles({ root, enabled, onChange }) {
|
|
|
114
120
|
bone.quaternion.copy(tmp.qNew);
|
|
115
121
|
bone.updateMatrixWorld(true);
|
|
116
122
|
}
|
|
123
|
+
d.moved = true;
|
|
117
124
|
onChangeRef.current?.(before, d.writable.map((bone) => bone.quaternion.toArray()));
|
|
118
125
|
};
|
|
119
126
|
const onUp = () => {
|
|
@@ -124,7 +131,9 @@ export function PoseHandles({ root, enabled, onChange }) {
|
|
|
124
131
|
window.removeEventListener("pointermove", onMove);
|
|
125
132
|
window.removeEventListener("pointerup", onUp);
|
|
126
133
|
window.removeEventListener("pointercancel", onUp);
|
|
134
|
+
const moved = dragRef.current?.moved === true;
|
|
127
135
|
dragRef.current = null;
|
|
136
|
+
if (moved) onCommitRef.current?.();
|
|
128
137
|
gl.domElement.style.cursor = "";
|
|
129
138
|
};
|
|
130
139
|
dragHandlersRef.current = { onMove, onUp };
|
|
@@ -231,28 +240,18 @@ export function PoseHandles({ root, enabled, onChange }) {
|
|
|
231
240
|
);
|
|
232
241
|
}
|
|
233
242
|
|
|
234
|
-
/**
|
|
235
|
-
|
|
236
|
-
function ikHandleColor(track) {
|
|
237
|
-
return track.kind === "arm" ? "#ff8a3d" : "#4dd2ff";
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** World-axis gizmo colours (Blender / Cascadeur convention). */
|
|
241
|
-
const AXIS_DEFS = [
|
|
242
|
-
{ axis: "X", dir: new THREE.Vector3(1, 0, 0), color: "#ff5340" },
|
|
243
|
-
{ axis: "Y", dir: new THREE.Vector3(0, 1, 0), color: "#54e05c" },
|
|
244
|
-
{ axis: "Z", dir: new THREE.Vector3(0, 0, 1), color: "#3d8bff" },
|
|
245
|
-
];
|
|
243
|
+
/** World-axis gizmo colours (Blender / Cascadeur convention), shared with the rotation rings. */
|
|
244
|
+
const AXIS_DEFS = ROTATION_AXES.map(({ axis, dir, color }) => ({ axis: axis.toUpperCase(), dir, color }));
|
|
246
245
|
const GIZMO_LEN = 0.22;
|
|
247
|
-
const GIZMO_SHAFT_R = 0.
|
|
248
|
-
const GIZMO_TIP_R = 0.
|
|
246
|
+
const GIZMO_SHAFT_R = 0.0065;
|
|
247
|
+
const GIZMO_TIP_R = 0.022;
|
|
249
248
|
const GIZMO_PICK_SHAFT_R = 0.045;
|
|
250
249
|
const GIZMO_PICK_TIP_R = 0.055;
|
|
251
250
|
const HANDLE_R = 0.055; // chain target spheres
|
|
252
251
|
const JOINT_R = 0.042; // mid-joint + FK swing spheres
|
|
253
|
-
const SWING_RING_R = 0.
|
|
254
|
-
const SWING_RING_TUBE = 0.
|
|
255
|
-
const SWING_RING_PICK_TUBE = 0.
|
|
252
|
+
const SWING_RING_R = 0.17; // rotation ring radius, clear of the hand/foot mesh
|
|
253
|
+
const SWING_RING_TUBE = 0.0045; // visible ring line
|
|
254
|
+
const SWING_RING_PICK_TUBE = 0.035; // invisible grab tube
|
|
256
255
|
/** Unfocused handles fade back so the focused one reads clearly. */
|
|
257
256
|
const OPACITY_FOCUSED = 1;
|
|
258
257
|
const OPACITY_UNFOCUSED = 0.5;
|
|
@@ -299,7 +298,11 @@ export function IkHandles({ chains, fkJoints, ikState, enabled, focus, onFocus,
|
|
|
299
298
|
const skinnedBlockerProxiesRef = useRef(new Map());
|
|
300
299
|
const occlusionRay = useRef(new THREE.Raycaster()).current;
|
|
301
300
|
const gizmoRef = useRef(null);
|
|
302
|
-
const ringRef = useRef(null); //
|
|
301
|
+
const ringRef = useRef(null); // rotation rings around the focused joint
|
|
302
|
+
const readoutRef = useRef(null);
|
|
303
|
+
const [hoverAxis, setHoverAxis] = useState(null);
|
|
304
|
+
const [rotationDrag, setRotationDrag] = useState(null); // { axis, angle } while a ring drags
|
|
305
|
+
const [rotationDeg, setRotationDeg] = useState(null);
|
|
303
306
|
const dragRef = useRef(null);
|
|
304
307
|
const handlersRef = useRef(null);
|
|
305
308
|
const solveRef = useRef(onSolve);
|
|
@@ -359,6 +362,13 @@ export function IkHandles({ chains, fkJoints, ikState, enabled, focus, onFocus,
|
|
|
359
362
|
);
|
|
360
363
|
raycaster.setFromCamera(tmp.ndc, camera);
|
|
361
364
|
if (!raycaster.ray.intersectPlane(d.plane, tmp.hit)) return;
|
|
365
|
+
if ((d.kind === "fk" || d.kind === "swing") && d.axisDir) {
|
|
366
|
+
const angle = ringAngle(d.axisDir, d.targetStart, d.hitStart, tmp.hit);
|
|
367
|
+
if (Math.hypot(ev.clientX - d.downXY[0], ev.clientY - d.downXY[1]) > CLICK_PX) d.moved = true;
|
|
368
|
+
solveRef.current?.(d.kind, d.trackId, { axis: d.axisDir, angle, startQuat: d.startQuat, startParentQuat: d.startParentQuat });
|
|
369
|
+
setRotationDrag({ axis: d.axisName, angle });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
362
372
|
if (d.kind === "fk" || d.kind === "swing" || (d.kind === "body" && !d.axisDir)) {
|
|
363
373
|
// Swing = trackball rotation (TransformControls rotate model):
|
|
364
374
|
// axis = offset × eye, angle = (offset · tangent) × speed/camDist,
|
|
@@ -407,6 +417,7 @@ export function IkHandles({ chains, fkJoints, ikState, enabled, focus, onFocus,
|
|
|
407
417
|
const d = dragRef.current;
|
|
408
418
|
dragRef.current = null;
|
|
409
419
|
gl.domElement.style.cursor = "";
|
|
420
|
+
setRotationDrag(null);
|
|
410
421
|
if (!d) return;
|
|
411
422
|
// Click (never dragged): the focused sphere toggles focus OFF; a
|
|
412
423
|
// not-yet-focused sphere just took focus (nothing to bake). An
|
|
@@ -739,11 +750,12 @@ export function IkHandles({ chains, fkJoints, ikState, enabled, focus, onFocus,
|
|
|
739
750
|
: clickable
|
|
740
751
|
? THREE.MathUtils.lerp(OPACITY_FAR, OPACITY_NEAR, emphasis)
|
|
741
752
|
: THREE.MathUtils.lerp(OPACITY_DISABLED_FAR, OPACITY_DISABLED_NEAR, emphasis);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
753
|
+
const [core, halo] = mesh.children;
|
|
754
|
+
if (core) core.material.opacity = mesh.material.opacity;
|
|
755
|
+
if (halo) {
|
|
756
|
+
halo.quaternion.copy(camera.quaternion);
|
|
757
|
+
halo.material.opacity = mesh.material.opacity * (focused ? 0.6 : 0.4);
|
|
758
|
+
}
|
|
747
759
|
}
|
|
748
760
|
const focusExposed = !!focus && visibilityRef.current.get(focus) !== false;
|
|
749
761
|
if (gizmoRef.current) gizmoRef.current.visible = focusExposed;
|
|
@@ -773,10 +785,16 @@ export function IkHandles({ chains, fkJoints, ikState, enabled, focus, onFocus,
|
|
|
773
785
|
// the camera: a trackball ring, not a bone-aligned one — the drag math
|
|
774
786
|
// is trackball, so the affordance must match.
|
|
775
787
|
if (ringRef.current && focus) {
|
|
776
|
-
const
|
|
777
|
-
if (
|
|
778
|
-
|
|
779
|
-
ringRef.current.
|
|
788
|
+
const bone = chains?.get(focus)?.bones[2] ?? fkJoints?.get(focus)?.bone ?? null;
|
|
789
|
+
if (bone) {
|
|
790
|
+
bone.getWorldPosition(tmp.pos);
|
|
791
|
+
ringRef.current.position.copy(tmp.pos);
|
|
792
|
+
const deg = rotationFromBindDeg(bindQuaternionOf(bone), bone.quaternion);
|
|
793
|
+
if (!rotationDeg || deg.x !== rotationDeg.x || deg.y !== rotationDeg.y || deg.z !== rotationDeg.z) setRotationDeg(deg);
|
|
794
|
+
if (readoutRef.current) {
|
|
795
|
+
readoutRef.current.position.copy(tmp.pos).add(tmp.delta.set(SWING_RING_R * 0.8, SWING_RING_R * 0.8, 0).applyQuaternion(camera.quaternion));
|
|
796
|
+
readoutRef.current.visible = focusExposed;
|
|
797
|
+
}
|
|
780
798
|
}
|
|
781
799
|
}
|
|
782
800
|
});
|
|
@@ -842,7 +860,11 @@ const CLICK_PX = 4;
|
|
|
842
860
|
}
|
|
843
861
|
raycaster.setFromCamera(ndc, camera);
|
|
844
862
|
let plane;
|
|
845
|
-
|
|
863
|
+
const ringDrag = !!axisDir && (kind === "fk" || kind === "swing");
|
|
864
|
+
if (ringDrag) {
|
|
865
|
+
// A ring turns about its axis: the pointer is read on the ring's own plane.
|
|
866
|
+
plane = new THREE.Plane().setFromNormalAndCoplanarPoint(axisDir, origin);
|
|
867
|
+
} else if (axisDir) {
|
|
846
868
|
// TransformControls: plane normal = axis × (eye × axis) — the plane
|
|
847
869
|
// contains the axis and is as view-perpendicular as possible.
|
|
848
870
|
const eye = camera.getWorldDirection(new THREE.Vector3());
|
|
@@ -883,6 +905,7 @@ const CLICK_PX = 4;
|
|
|
883
905
|
dragRef.current = null;
|
|
884
906
|
return;
|
|
885
907
|
}
|
|
908
|
+
if (ringDrag) dragRef.current.axisName = ROTATION_AXES.find((a) => a.dir === axisDir)?.axis ?? "x";
|
|
886
909
|
dragRef.current.startQuat = bone.quaternion.clone();
|
|
887
910
|
dragRef.current.startParentQuat = bone.parent.getWorldQuaternion(new THREE.Quaternion());
|
|
888
911
|
dragRef.current.eye = origin.clone().sub(camera.getWorldPosition(new THREE.Vector3())).negate().normalize();
|
|
@@ -950,10 +973,24 @@ const CLICK_PX = 4;
|
|
|
950
973
|
};
|
|
951
974
|
// The handles live on POSER_LAYER so they render only in the poser
|
|
952
975
|
// (IK working) view; the raycaster must see that layer to hit them.
|
|
976
|
+
const onHover = (ev) => {
|
|
977
|
+
if (ev.buttons || dragRef.current) return;
|
|
978
|
+
const rings = pickRefs.current.filter((p) => p.part === "ring" && p.mesh?.parent && p.track.id === focusIdRef.current);
|
|
979
|
+
let axis = null;
|
|
980
|
+
if (rings.length) {
|
|
981
|
+
const rect = el.getBoundingClientRect();
|
|
982
|
+
raycaster.setFromCamera(new THREE.Vector2(((ev.clientX - rect.left) / rect.width) * 2 - 1, -((ev.clientY - rect.top) / rect.height) * 2 + 1), camera);
|
|
983
|
+
const hit = raycaster.intersectObjects(rings.map((p) => p.mesh), false)[0];
|
|
984
|
+
axis = hit ? ROTATION_AXES.find((a) => a.dir === rings.find((p) => p.mesh === hit.object)?.axisDir)?.axis ?? null : null;
|
|
985
|
+
}
|
|
986
|
+
setHoverAxis((current) => (current === axis ? current : axis));
|
|
987
|
+
};
|
|
953
988
|
raycaster.layers.enable(POSER_LAYER);
|
|
954
989
|
el.addEventListener("pointerdown", onDown, true);
|
|
990
|
+
el.addEventListener("pointermove", onHover);
|
|
955
991
|
return () => {
|
|
956
992
|
el.removeEventListener("pointerdown", onDown, true);
|
|
993
|
+
el.removeEventListener("pointermove", onHover);
|
|
957
994
|
raycaster.layers.disable(POSER_LAYER);
|
|
958
995
|
};
|
|
959
996
|
});
|
|
@@ -983,19 +1020,17 @@ const CLICK_PX = 4;
|
|
|
983
1020
|
register(track, kind)(m);
|
|
984
1021
|
}}
|
|
985
1022
|
renderOrder={999}
|
|
986
|
-
scale={focused ? 1.2 : 1}
|
|
987
1023
|
>
|
|
988
|
-
<sphereGeometry args={[radius,
|
|
989
|
-
<
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
transparent
|
|
997
|
-
|
|
998
|
-
/>
|
|
1024
|
+
<sphereGeometry args={[radius, 12, 8]} />
|
|
1025
|
+
<meshBasicMaterial colorWrite={false} depthTest={false} depthWrite={false} transparent opacity={focused ? OPACITY_FOCUSED : OPACITY_UNFOCUSED} />
|
|
1026
|
+
<mesh renderOrder={999} ref={(m) => m?.layers.set(POSER_LAYER)} raycast={() => {}}>
|
|
1027
|
+
<sphereGeometry args={[radius * (focused ? 0.62 : 0.52), 20, 14]} />
|
|
1028
|
+
<meshBasicMaterial color={color} toneMapped={false} depthTest={false} depthWrite={false} transparent />
|
|
1029
|
+
</mesh>
|
|
1030
|
+
<mesh renderOrder={998} ref={(m) => m?.layers.set(POSER_LAYER)} raycast={() => {}}>
|
|
1031
|
+
<ringGeometry args={[radius * (focused ? 0.86 : 0.8), radius * (focused ? 1.0 : 0.92), 48]} />
|
|
1032
|
+
<meshBasicMaterial color={color} toneMapped={false} depthTest={false} depthWrite={false} transparent side={THREE.DoubleSide} />
|
|
1033
|
+
</mesh>
|
|
999
1034
|
</mesh>
|
|
1000
1035
|
);
|
|
1001
1036
|
};
|
|
@@ -1008,17 +1043,17 @@ const CLICK_PX = 4;
|
|
|
1008
1043
|
MID_TRACKS.find((t) => t.id === focus) ||
|
|
1009
1044
|
(focus === "hips" ? (fkJoints?.get("hips")?.track ?? null) : null);
|
|
1010
1045
|
const gizmoKind = IK_TRACKS.find((t) => t.id === focus) ? "chain" : MID_TRACKS.find((t) => t.id === focus) ? "mid" : focus === "hips" ? "body" : null;
|
|
1011
|
-
// The focused
|
|
1012
|
-
// the
|
|
1013
|
-
const
|
|
1046
|
+
// The focused hand/foot gets rotation rings beside its move arrows (the
|
|
1047
|
+
// rings turn the hand/foot itself); a focused FK joint gets rings only.
|
|
1048
|
+
const rotateTrack = IK_TRACKS.find((t) => t.id === focus) ?? (focus && focus !== "hips" ? fkJoints?.get(focus)?.track ?? null : null);
|
|
1049
|
+
const rotateKind = IK_TRACKS.find((t) => t.id === focus) ? "swing" : "fk";
|
|
1014
1050
|
|
|
1015
1051
|
return (
|
|
1016
1052
|
<group>
|
|
1017
|
-
{IK_TRACKS.map((track) => (chains.has(track.id) ? sphereFor(track, "chain",
|
|
1018
|
-
{MID_TRACKS.map((track) => (chains.has(track.chain) ? sphereFor(track, "mid",
|
|
1019
|
-
{[...(fkJoints ?? [])].map(([id, joint]) => sphereFor(joint.track, id === "hips" ? "body" : "fk",
|
|
1053
|
+
{IK_TRACKS.map((track) => (chains.has(track.id) ? sphereFor(track, "chain", sideColor(track.id), HANDLE_R) : null))}
|
|
1054
|
+
{MID_TRACKS.map((track) => (chains.has(track.chain) ? sphereFor(track, "mid", sideColor(track.id), JOINT_R) : null))}
|
|
1055
|
+
{[...(fkJoints ?? [])].map(([id, joint]) => sphereFor(joint.track, id === "hips" ? "body" : "fk", sideColor(id), id === "hips" ? HANDLE_R : JOINT_R))}
|
|
1020
1056
|
|
|
1021
|
-
{/* Mini move-gizmo on the focused position handle: world-axis arrows. */}
|
|
1022
1057
|
{gizmoTrack && (
|
|
1023
1058
|
<group ref={gizmoRef} renderOrder={999}>
|
|
1024
1059
|
{AXIS_DEFS.map(({ axis, dir, color }) => {
|
|
@@ -1033,7 +1068,7 @@ const CLICK_PX = 4;
|
|
|
1033
1068
|
}}
|
|
1034
1069
|
>
|
|
1035
1070
|
<cylinderGeometry args={[GIZMO_SHAFT_R, GIZMO_SHAFT_R, GIZMO_LEN, 8]} />
|
|
1036
|
-
<
|
|
1071
|
+
<meshBasicMaterial color={color} toneMapped={false} depthTest={false} depthWrite={false} transparent opacity={0.85} />
|
|
1037
1072
|
</mesh>
|
|
1038
1073
|
<mesh
|
|
1039
1074
|
position={[0, GIZMO_LEN + 0.04, 0]}
|
|
@@ -1042,8 +1077,8 @@ const CLICK_PX = 4;
|
|
|
1042
1077
|
register(gizmoTrack, gizmoKind, dir, "tip")(m);
|
|
1043
1078
|
}}
|
|
1044
1079
|
>
|
|
1045
|
-
<coneGeometry args={[GIZMO_TIP_R, 0.
|
|
1046
|
-
<
|
|
1080
|
+
<coneGeometry args={[GIZMO_TIP_R, 0.06, 16]} />
|
|
1081
|
+
<meshBasicMaterial color={color} toneMapped={false} depthTest={false} depthWrite={false} transparent opacity={0.95} />
|
|
1047
1082
|
</mesh>
|
|
1048
1083
|
{/* Invisible forgiving hit volumes keep the target
|
|
1049
1084
|
at least as easy to grab as the visible arrow. */}
|
|
@@ -1073,44 +1108,60 @@ const CLICK_PX = 4;
|
|
|
1073
1108
|
</group>
|
|
1074
1109
|
)}
|
|
1075
1110
|
|
|
1076
|
-
{
|
|
1077
|
-
effector itself while the position gizmo keeps moving it. A thin
|
|
1078
|
-
visible torus plus a fat invisible one — a 3 px tube raycasts
|
|
1079
|
-
terribly, so the pick target is generous. */}
|
|
1080
|
-
{swingTrack && (
|
|
1111
|
+
{rotateTrack && (
|
|
1081
1112
|
<group ref={(g) => { ringRef.current = g; }} renderOrder={999}>
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1113
|
+
{ROTATION_AXES.map(({ axis, dir, color }) => {
|
|
1114
|
+
const quat = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), dir);
|
|
1115
|
+
const active = rotationDrag?.axis === axis;
|
|
1116
|
+
const hot = active || (!rotationDrag && hoverAxis === axis);
|
|
1117
|
+
return (
|
|
1118
|
+
<group key={axis} quaternion={quat}>
|
|
1119
|
+
<mesh ref={(m) => m?.layers.set(POSER_LAYER)}>
|
|
1120
|
+
<torusGeometry args={[SWING_RING_R, hot ? SWING_RING_TUBE * 2.2 : SWING_RING_TUBE, 6, 96]} />
|
|
1121
|
+
<meshBasicMaterial ref={(m) => m && backFaceFade(m)} color={color} toneMapped={false} depthTest={false} depthWrite={false} transparent opacity={rotationDrag && !active ? 0.15 : hot ? 1 : 0.7} />
|
|
1122
|
+
</mesh>
|
|
1123
|
+
<mesh
|
|
1124
|
+
ref={(m) => {
|
|
1125
|
+
if (m) m.layers.set(POSER_LAYER);
|
|
1126
|
+
register(rotateTrack, rotateKind, dir, "ring")(m);
|
|
1127
|
+
}}
|
|
1128
|
+
>
|
|
1129
|
+
<torusGeometry args={[SWING_RING_R, SWING_RING_PICK_TUBE, 8, 48]} />
|
|
1130
|
+
<meshBasicMaterial transparent opacity={0} depthTest={false} depthWrite={false} />
|
|
1131
|
+
</mesh>
|
|
1132
|
+
</group>
|
|
1133
|
+
);
|
|
1134
|
+
})}
|
|
1135
|
+
</group>
|
|
1136
|
+
)}
|
|
1137
|
+
{rotateTrack && rotationDeg && (
|
|
1138
|
+
<group ref={readoutRef}>
|
|
1139
|
+
<Html zIndexRange={[20, 0]} style={{ pointerEvents: "none" }}>
|
|
1140
|
+
<RotationReadout readout={rotationReadout(rotationDeg, rotationDrag)} />
|
|
1141
|
+
</Html>
|
|
1108
1142
|
</group>
|
|
1109
1143
|
)}
|
|
1110
1144
|
</group>
|
|
1111
1145
|
);
|
|
1112
1146
|
}
|
|
1113
1147
|
|
|
1148
|
+
function RotationReadout({ readout }) {
|
|
1149
|
+
return (
|
|
1150
|
+
<div className="pose-rotation-readout" data-testid="pose-rotation-readout">
|
|
1151
|
+
{readout.delta && (
|
|
1152
|
+
<div className="pose-rotation-delta" data-axis={readout.delta.axis}>
|
|
1153
|
+
<span>{readout.delta.axis.toUpperCase()}</span>{readout.delta.value}
|
|
1154
|
+
</div>
|
|
1155
|
+
)}
|
|
1156
|
+
<div className="pose-rotation-axes">
|
|
1157
|
+
{readout.axes.map(({ axis, value, active }) => (
|
|
1158
|
+
<span key={axis} data-axis={axis} className={active ? "active" : undefined}><i>{axis.toUpperCase()}</i>{value}</span>
|
|
1159
|
+
))}
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1114
1165
|
/** A poser-only target marker for the active range pin. The target is supplied
|
|
1115
1166
|
* in world space; the line is rebuilt every render frame so object travel and
|
|
1116
1167
|
* the effector's keyed pose stay visually connected while the playhead moves. */
|
package/src/project-browser.css
CHANGED
|
@@ -219,6 +219,59 @@
|
|
|
219
219
|
color: var(--text-selected, #fff);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/* Project mode (previs flag): a two-up segmented switch above the tabs. Each
|
|
223
|
+
segment carries its label and the one-line promise of what the mode makes. */
|
|
224
|
+
.v2-start-screen .v2-start-mode {
|
|
225
|
+
display: grid;
|
|
226
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
227
|
+
gap: var(--start-mode-gap, 4px);
|
|
228
|
+
max-width: var(--start-mode-max-width, 640px);
|
|
229
|
+
padding: var(--start-mode-pad, 4px);
|
|
230
|
+
border-radius: var(--r-2, 6px);
|
|
231
|
+
background: var(--field, #0f0f11);
|
|
232
|
+
box-shadow: inset 0 0 0 var(--start-border-width, 1px) var(--row-divider, #1d1d20);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.v2-start-screen .v2-start-mode-option {
|
|
236
|
+
min-width: var(--start-template-min-width, 0);
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
gap: var(--start-copy-gap, 3px);
|
|
240
|
+
padding: var(--sp-1, 8px) var(--sp-2, 12px);
|
|
241
|
+
border: 0;
|
|
242
|
+
border-radius: var(--r-1, 4px);
|
|
243
|
+
background: var(--transparent, transparent);
|
|
244
|
+
color: var(--text-2, #9b9ba1);
|
|
245
|
+
text-align: left;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.v2-start-screen .v2-start-mode-option strong {
|
|
249
|
+
color: inherit;
|
|
250
|
+
font-size: var(--start-template-title-size, 12px);
|
|
251
|
+
font-weight: 500;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.v2-start-screen .v2-start-mode-option span {
|
|
255
|
+
color: var(--text-3, #636369);
|
|
256
|
+
font-size: var(--start-template-meta-size, 11px);
|
|
257
|
+
line-height: 1.35;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.v2-start-screen .v2-start-mode-option:hover {
|
|
261
|
+
background: var(--surface-raised, #1e1e21);
|
|
262
|
+
color: var(--text-1, #e4e4e7);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.v2-start-screen .v2-start-mode-option.active {
|
|
266
|
+
background: var(--surface-raised, #1e1e21);
|
|
267
|
+
box-shadow: inset 0 0 0 var(--start-selected-width, 1.5px) var(--select, #e8a33d);
|
|
268
|
+
color: var(--text-selected, #fff);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.v2-start-screen .v2-start-mode-option.active span {
|
|
272
|
+
color: var(--text-2, #9b9ba1);
|
|
273
|
+
}
|
|
274
|
+
|
|
222
275
|
.v2-start-screen .v2-start-template-grid {
|
|
223
276
|
display: grid;
|
|
224
277
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
package/src/project-browser.jsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
removeRecentProject,
|
|
13
13
|
requestHandlePermission,
|
|
14
14
|
storeProjectsDirectory,
|
|
15
|
+
DEFAULT_PREVIS_MODE,
|
|
15
16
|
} from "./project.js";
|
|
16
17
|
import "./project-browser.css";
|
|
17
18
|
|
|
@@ -26,6 +27,21 @@ const BLANK_TEMPLATE = Object.freeze({
|
|
|
26
27
|
code: "NEW",
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
// The two project modes the New view offers.
|
|
31
|
+
// Storyboard starts from an empty stage only; Animation keeps every template.
|
|
32
|
+
const MODE_OPTIONS = [
|
|
33
|
+
{
|
|
34
|
+
id: "storyboard",
|
|
35
|
+
label: () => ko("Storyboard", "스토리보드"),
|
|
36
|
+
blurb: () => ko("Describe scenes in words and direct one panel at a time. Output: image references.", "말로 장면을 설명하면 한 장씩 패널을 연출합니다. 결과물: 이미지 레퍼런스."),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "animation",
|
|
40
|
+
label: () => ko("Animation", "애니메이션"),
|
|
41
|
+
blurb: () => ko("Block and shoot continuous motion. Output: clip references.", "연속 동작을 블로킹하고 촬영합니다. 결과물: 클립 레퍼런스."),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
29
45
|
// App.jsx keeps the project mutation behind its named requestNewProject/newProject
|
|
30
46
|
// path. The v2 chooser already has the name, so this short-lived handoff seeds
|
|
31
47
|
// the existing ProjectNameDialog and lets its normal submit path commit it.
|
|
@@ -91,9 +107,10 @@ export function ProjectNameDialog({ open, initialName = "My Project", onCancel,
|
|
|
91
107
|
*/
|
|
92
108
|
export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew, onClose, startup = false, starters = [], onStarter }) {
|
|
93
109
|
const starterTemplates = useMemo(() => starters.map(sampleTemplate), [starters]);
|
|
110
|
+
const [previsMode, setPrevisMode] = useState(DEFAULT_PREVIS_MODE);
|
|
94
111
|
const templates = useMemo(
|
|
95
|
-
() => [BLANK_TEMPLATE, ...starterTemplates],
|
|
96
|
-
[starterTemplates],
|
|
112
|
+
() => (previsMode === "storyboard" ? [BLANK_TEMPLATE] : [BLANK_TEMPLATE, ...starterTemplates]),
|
|
113
|
+
[previsMode, starterTemplates],
|
|
97
114
|
);
|
|
98
115
|
const defaultTemplate = starterTemplates[0] ?? BLANK_TEMPLATE;
|
|
99
116
|
const [activeNav, setActiveNav] = useState(startup ? "new" : "recent");
|
|
@@ -186,12 +203,26 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
186
203
|
setName((current) => current.trim() ? current : template.name);
|
|
187
204
|
};
|
|
188
205
|
|
|
206
|
+
const choosePrevisMode = (mode) => {
|
|
207
|
+
setPrevisMode(mode);
|
|
208
|
+
// Storyboard offers the blank stage only, so a starter selection cannot
|
|
209
|
+
// survive the switch.
|
|
210
|
+
if (mode !== "storyboard" || selectedTemplate?.id === BLANK_TEMPLATE.id) return;
|
|
211
|
+
setSelectedTemplateId(BLANK_TEMPLATE.id);
|
|
212
|
+
// A name the starter card filled in would now describe the wrong stage.
|
|
213
|
+
setName((current) => (current.trim() === selectedTemplate?.name ? BLANK_TEMPLATE.name : current));
|
|
214
|
+
};
|
|
215
|
+
|
|
189
216
|
const createProject = (event) => {
|
|
190
217
|
event.preventDefault();
|
|
191
218
|
const projectName = name.trim();
|
|
192
219
|
if (!projectName) return;
|
|
220
|
+
if (selectedTemplate?.starterId) {
|
|
221
|
+
onStarter?.(selectedTemplate.starterId, projectName, { previsMode: "animation" });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
193
224
|
pendingProjectName = projectName;
|
|
194
|
-
onNew(projectName);
|
|
225
|
+
onNew(projectName, { previsMode });
|
|
195
226
|
};
|
|
196
227
|
|
|
197
228
|
const recentEntries = [
|
|
@@ -236,6 +267,24 @@ export default function ProjectBrowser({ currentName, onOpen, onOpenFile, onNew,
|
|
|
236
267
|
<h1>{activeNav === "recent" ? ko("Recent", "최근") : activeNav === "samples" ? ko("Samples", "샘플") : activeNav === "learn" ? ko("Learn", "배우기") : ko("New Project", "새 프로젝트")}</h1>
|
|
237
268
|
{activeNav !== "new" && <button type="button" className="v2-start-close x" onClick={onClose} aria-label={ko("Close", "닫기")}>×</button>}
|
|
238
269
|
</div>
|
|
270
|
+
{activeNav === "new" && (
|
|
271
|
+
<div className="v2-start-mode" role="radiogroup" aria-label={ko("Project mode", "프로젝트 모드")} data-testid="start-previs-mode">
|
|
272
|
+
{MODE_OPTIONS.map((option) => (
|
|
273
|
+
<button
|
|
274
|
+
type="button"
|
|
275
|
+
key={option.id}
|
|
276
|
+
role="radio"
|
|
277
|
+
aria-checked={previsMode === option.id}
|
|
278
|
+
className={`v2-start-mode-option${previsMode === option.id ? " active" : ""}`}
|
|
279
|
+
data-previs-mode={option.id}
|
|
280
|
+
onClick={() => choosePrevisMode(option.id)}
|
|
281
|
+
>
|
|
282
|
+
<strong>{option.label()}</strong>
|
|
283
|
+
<span>{option.blurb()}</span>
|
|
284
|
+
</button>
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
239
288
|
{(activeNav === "new" || activeNav === "samples") && (
|
|
240
289
|
<div className="v2-start-tabs" role="tablist" aria-label={ko("Template categories", "템플릿 카테고리")}>
|
|
241
290
|
{CATEGORIES.map((entry) => (
|