cozyclay 2.0.2 → 2.1.1
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 +61 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +23 -8
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-C9pMpW7s.js +1 -0
- package/dist/assets/app-B_2XB9rw.css +1 -0
- package/dist/assets/app-DAuvKPP8.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.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-BPk2Ni95.js} +1 -1
- package/dist/assets/workflow-jJRBR94Z.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/llms.txt +14 -0
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +813 -278
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +121 -36
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +60 -16
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +18 -3
- package/src/shell/ViewportToolbar.jsx +36 -31
- 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/shell/viewport.css +12 -0
- 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 +213 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +60 -24
- package/src/studio-app-binding.js +56 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/gt-render/browser.mjs +23 -0
- package/tools/headless-studio.mjs +85 -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 +95 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
package/src/planview.jsx
CHANGED
|
@@ -4,9 +4,12 @@ import { Line, Text } from "@react-three/drei";
|
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { aimAt } from "./controls.jsx";
|
|
6
6
|
import { PLAN_LAYER } from "./dualview.jsx";
|
|
7
|
-
import { objectSize } from "./scene-objects.js";
|
|
7
|
+
import { EMPTY_MARKER_SIZE, isEmptyObject, objectSize } from "./scene-objects.js";
|
|
8
8
|
import { displayObjectLabel } from "./object-catalog.jsx";
|
|
9
9
|
import { ko } from "./locale.js";
|
|
10
|
+
import { isProxyFigure } from "./scenes.js";
|
|
11
|
+
import { prepareRailBend } from "./camera-follow.js";
|
|
12
|
+
import { MARK_GAP, MAX_PATH_MARKS, MAX_PATH_POINTS, nearestPathFraction, pathCurve, pathMarkFractions, pathPointAtFraction } from "./object-path.js";
|
|
10
13
|
|
|
11
14
|
const ROOM_LIMIT = 240; // stay on the open stage (matches scene-objects' clamp)
|
|
12
15
|
const ACTOR_LIMIT = 4; // matches the Subject sliders' range
|
|
@@ -186,7 +189,33 @@ function CastRing({ color }) {
|
|
|
186
189
|
);
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
/** An Empty on the board: no footprint, just a small cross and a ring where
|
|
193
|
+
* its origin stands, so a group node can be found, selected and dragged like
|
|
194
|
+
* any other row of the plan. */
|
|
195
|
+
function EmptyPlanMarker({ object, selected, dragging, turning }) {
|
|
196
|
+
const half = EMPTY_MARKER_SIZE / 2;
|
|
197
|
+
const color = selected ? SELECTED_COLOR : PROP_LABEL_COLOR;
|
|
198
|
+
const rotation = (object.rot * Math.PI) / 180;
|
|
199
|
+
const cross = useMemo(() => [[[-half, 0.04, 0], [half, 0.04, 0]], [[0, 0.04, -half], [0, 0.04, half]]], [half]);
|
|
200
|
+
return (
|
|
201
|
+
<group position={[object.x, 0, object.z]} rotation={[0, rotation, 0]}>
|
|
202
|
+
{cross.map((points, index) => (
|
|
203
|
+
<Line key={index} points={points} color={color} lineWidth={selected ? 2 : 1.5} transparent opacity={selected ? 1 : 0.85} depthWrite={false} depthTest={false} renderOrder={12} />
|
|
204
|
+
))}
|
|
205
|
+
<mesh position={[0, 0.036, 0]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={11}>
|
|
206
|
+
<ringGeometry args={[half * 0.8, half * 0.95, 24]} />
|
|
207
|
+
<meshBasicMaterial color={color} transparent opacity={selected ? 0.9 : 0.6} depthWrite={false} depthTest={false} />
|
|
208
|
+
</mesh>
|
|
209
|
+
<group position={[0, 0, half + 0.35]} rotation={[0, -rotation, 0]}>
|
|
210
|
+
<PlanLabel text={displayObjectLabel(object.name).slice(0, 8)} color={color} offset={0} muted={!selected} />
|
|
211
|
+
</group>
|
|
212
|
+
{selected && <Puck color={SELECTED_COLOR} showBody={false} handleDist={half + 0.65} dragging={dragging} turning={turning} />}
|
|
213
|
+
</group>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
189
217
|
function SceneObjectFootprint({ object, selected, dragging, turning }) {
|
|
218
|
+
if (isEmptyObject(object)) return <EmptyPlanMarker object={object} selected={selected} dragging={dragging} turning={turning} />;
|
|
190
219
|
const { width, height, depth } = objectSize(object);
|
|
191
220
|
const objectLabel = displayObjectLabel(object.name).slice(0, 8);
|
|
192
221
|
const handleDist = depth / 2 + 0.65;
|
|
@@ -303,11 +332,6 @@ function SubjectMovementGuide({ track }) {
|
|
|
303
332
|
);
|
|
304
333
|
}
|
|
305
334
|
|
|
306
|
-
/**
|
|
307
|
-
* The drawn camera rail (and the live stroke while drawing): a flat violet
|
|
308
|
-
* line on the deck. Display only — authoring happens through the pointer
|
|
309
|
-
* handlers, exactly like the pucks.
|
|
310
|
-
*/
|
|
311
335
|
/**
|
|
312
336
|
* A scene object's travel path on the plan board. The camera's rail owns
|
|
313
337
|
* RAIL_COLOR; a prop's route gets its own green so the two never read as the
|
|
@@ -315,54 +339,41 @@ function SubjectMovementGuide({ track }) {
|
|
|
315
339
|
*/
|
|
316
340
|
const OBJECT_PATH_COLOR = "#6fcf97";
|
|
317
341
|
const OBJECT_PATH_SELECTED_COLOR = "#ffb454";
|
|
342
|
+
const OBJECT_PATH_LIT_COLOR = "#d4f7e2";
|
|
318
343
|
// How near the floor pointer must be to grab a route point, in metres.
|
|
319
344
|
const PATH_POINT_GRAB = 0.34;
|
|
320
|
-
function ObjectPathLine({
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
345
|
+
function ObjectPathLine({ path, selectedIndex = null, hoverIndex = null, lit = false }) {
|
|
346
|
+
const curve = useMemo(() => (path?.points?.length > 1 ? pathCurve(path) : null), [path]);
|
|
347
|
+
const marks = useMemo(() => (path ? pathMarkFractions(path).map((t) => pathPointAtFraction(path, t)) : []), [path]);
|
|
348
|
+
if (!curve || curve.points.length < 2) return null;
|
|
324
349
|
return (
|
|
325
350
|
<group>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
depthTest={false}
|
|
334
|
-
renderOrder={9}
|
|
335
|
-
/>
|
|
336
|
-
{/* Every point is a handle here, not just the ends: the route is
|
|
337
|
-
drawn on this board, so it should also be editable on it. */}
|
|
338
|
-
{points.map((point, index) => (
|
|
351
|
+
{/* The camera rail's own line: the curve the object travels, with the
|
|
352
|
+
same start disc, START label, heading arrow and end ring. Only the
|
|
353
|
+
colour is the prop's. */}
|
|
354
|
+
<CameraRailLine points={curve.points} color={lit ? OBJECT_PATH_LIT_COLOR : OBJECT_PATH_COLOR} />
|
|
355
|
+
{/* The marks, like the rail's crane dots: the ends plus any added by
|
|
356
|
+
double-clicking the line. These are what the board lets you grab. */}
|
|
357
|
+
{marks.map((point, index) => point && (
|
|
339
358
|
<mesh
|
|
340
359
|
key={index}
|
|
341
|
-
position={[point.x, 0.
|
|
360
|
+
position={[point.x, 0.042, point.z]}
|
|
342
361
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
343
|
-
renderOrder={
|
|
362
|
+
renderOrder={12}
|
|
344
363
|
>
|
|
345
|
-
<circleGeometry args={[index === selectedIndex ? 0.13 : 0.1, 14]} />
|
|
364
|
+
<circleGeometry args={[index === selectedIndex ? 0.13 : index === hoverIndex ? 0.12 : 0.1, 14]} />
|
|
346
365
|
<meshBasicMaterial
|
|
347
|
-
color={index === selectedIndex ? OBJECT_PATH_SELECTED_COLOR :
|
|
366
|
+
color={index === selectedIndex ? OBJECT_PATH_SELECTED_COLOR : index === hoverIndex ? "#d4f7e2" : "#2f8f5b"}
|
|
348
367
|
depthWrite={false}
|
|
349
368
|
depthTest={false}
|
|
350
369
|
/>
|
|
351
370
|
</mesh>
|
|
352
371
|
))}
|
|
353
|
-
<mesh position={[first.x, 0.038, first.z]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={11}>
|
|
354
|
-
<circleGeometry args={[0.16, 18]} />
|
|
355
|
-
<meshBasicMaterial color={OBJECT_PATH_COLOR} depthWrite={false} depthTest={false} />
|
|
356
|
-
</mesh>
|
|
357
|
-
<mesh position={[last.x, 0.038, last.z]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={11}>
|
|
358
|
-
<ringGeometry args={[0.13, 0.19, 18]} />
|
|
359
|
-
<meshBasicMaterial color={OBJECT_PATH_COLOR} depthWrite={false} depthTest={false} />
|
|
360
|
-
</mesh>
|
|
361
372
|
</group>
|
|
362
373
|
);
|
|
363
374
|
}
|
|
364
375
|
|
|
365
|
-
function CameraRailLine({ points, live = false }) {
|
|
376
|
+
function CameraRailLine({ points, live = false, color = RAIL_COLOR }) {
|
|
366
377
|
const directionGeometry = useMemo(() => {
|
|
367
378
|
if (live || !points || points.length < 2) return null;
|
|
368
379
|
const head = points[0];
|
|
@@ -394,22 +405,22 @@ function CameraRailLine({ points, live = false }) {
|
|
|
394
405
|
const last = points[points.length - 1];
|
|
395
406
|
return (
|
|
396
407
|
<group>
|
|
397
|
-
<Line points={points.map((point) => [point.x, 0.03, point.z])} color={
|
|
408
|
+
<Line points={points.map((point) => [point.x, 0.03, point.z])} color={color} lineWidth={live ? 2.5 : 3.5} transparent opacity={live ? 0.72 : 0.96} depthWrite={false} depthTest={false} renderOrder={9} />
|
|
398
409
|
{!live && (
|
|
399
410
|
<>
|
|
400
411
|
<mesh position={[first.x, 0.04, first.z]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={11}>
|
|
401
412
|
<circleGeometry args={[0.22, 20]} />
|
|
402
|
-
<meshBasicMaterial color={
|
|
413
|
+
<meshBasicMaterial color={color} depthWrite={false} depthTest={false} />
|
|
403
414
|
</mesh>
|
|
404
415
|
<mesh position={[first.x, 0.039, first.z]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={10}>
|
|
405
416
|
<ringGeometry args={[0.27, 0.32, 20]} />
|
|
406
|
-
<meshBasicMaterial color={
|
|
417
|
+
<meshBasicMaterial color={color} transparent opacity={0.72} depthWrite={false} depthTest={false} />
|
|
407
418
|
</mesh>
|
|
408
419
|
<Text
|
|
409
420
|
position={[first.x, 0.05, first.z + 0.48]}
|
|
410
421
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
411
422
|
fontSize={0.24}
|
|
412
|
-
color={
|
|
423
|
+
color={color}
|
|
413
424
|
anchorX="center"
|
|
414
425
|
anchorY="middle"
|
|
415
426
|
outlineWidth={0.035}
|
|
@@ -422,12 +433,12 @@ function CameraRailLine({ points, live = false }) {
|
|
|
422
433
|
</Text>
|
|
423
434
|
{directionGeometry && (
|
|
424
435
|
<mesh geometry={directionGeometry} renderOrder={11}>
|
|
425
|
-
<meshBasicMaterial color={
|
|
436
|
+
<meshBasicMaterial color={color} depthWrite={false} depthTest={false} side={THREE.DoubleSide} />
|
|
426
437
|
</mesh>
|
|
427
438
|
)}
|
|
428
439
|
<mesh position={[last.x, 0.04, last.z]} rotation={[-Math.PI / 2, 0, 0]} renderOrder={10}>
|
|
429
440
|
<ringGeometry args={[0.1, 0.15, 16]} />
|
|
430
|
-
<meshBasicMaterial color={
|
|
441
|
+
<meshBasicMaterial color={color} depthWrite={false} depthTest={false} />
|
|
431
442
|
</mesh>
|
|
432
443
|
</>
|
|
433
444
|
)}
|
|
@@ -490,10 +501,18 @@ function WaypointPath({ waypoints, start, activeWaypointId }) {
|
|
|
490
501
|
* reports moves that still hit it, so a fast drag off the edge silently strands
|
|
491
502
|
* the puck.
|
|
492
503
|
*/
|
|
493
|
-
export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, characters = [], onMoveCharacter, onCharacterGestureStart, onWaypointGestureStart, onCameraGestureStart, pathStart = null, waypoints, activeWaypointId, onSelectWaypoint, onMoveWaypoint, onSelectEntity, sceneObjects = [], selectedSceneObjectId, onMoveSceneObject, onObjectMoveStart, onObjectMoveEnd, cameraRailPoints = null, railDraw = false, onRailStroke, pathDraw = false, onPathStroke,
|
|
504
|
+
export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, characters = [], characterPlacements = [], onMoveCharacter, onCharacterGestureStart, onWaypointGestureStart, onCameraGestureStart, pathStart = null, waypoints, activeWaypointId, onSelectWaypoint, onMoveWaypoint, onSelectEntity, sceneObjects = [], selectedSceneObjectId, onMoveSceneObject, onObjectMoveStart, onObjectMoveEnd, cameraRailPoints = null, railDraw = false, onRailStroke, pathDraw = false, onPathStroke, objectPath = null, objectPathSelectedIndex = null, onObjectPathPointSelect, onObjectPathChange, onObjectPathMarkInsert, onObjectPathGestureStart, onObjectPathGestureEnd, subjectTrack = null, onCameraChange, keyLight = null, minimal = false }) {
|
|
505
|
+
characters = characters.map(entry => {
|
|
506
|
+
const placement = isProxyFigure(entry) && characterPlacements.find(view => view.id === entry.id);
|
|
507
|
+
return placement ? { ...entry, x: placement.position[0], z: placement.position[2], rot: placement.rot } : entry;
|
|
508
|
+
});
|
|
494
509
|
const [drag, setDrag] = useState(null); // { id, mode }
|
|
495
510
|
// live stroke while the rail is being drawn; world XZ, display only
|
|
496
511
|
const [railStroke, setRailStroke] = useState(null);
|
|
512
|
+
// the route mark under the pointer, for the hover highlight
|
|
513
|
+
const [hoverMark, setHoverMark] = useState(null);
|
|
514
|
+
// The pointer is on the route where a double-click would still drop a dot.
|
|
515
|
+
const [hoverLine, setHoverLine] = useState(false);
|
|
497
516
|
const rootRef = useRef();
|
|
498
517
|
const camPos = useRef();
|
|
499
518
|
const camRot = useRef();
|
|
@@ -520,8 +539,8 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
520
539
|
// clears dragRef, so a dep that changes while dragging (charA.x does, on the
|
|
521
540
|
// very first move) would kill the drag after one frame. Read live values
|
|
522
541
|
// through a ref and keep the effect's deps stable.
|
|
523
|
-
const latest = useRef({
|
|
524
|
-
latest.current = {
|
|
542
|
+
const latest = useRef({ objectPath, objectPathSelectedIndex, onObjectPathPointSelect, onObjectPathChange, onObjectPathMarkInsert, onObjectPathGestureStart, onObjectPathGestureEnd, characters, waypoints, onSelectWaypoint, onMoveWaypoint, onMoveCharacter, onCharacterGestureStart, onWaypointGestureStart, onCameraGestureStart, onSelectEntity, sceneObjects, selectedSceneObjectId, onMoveSceneObject, onObjectMoveStart, onObjectMoveEnd, railDraw, onRailStroke, pathDraw, onPathStroke, onCameraChange });
|
|
543
|
+
latest.current = { objectPath, objectPathSelectedIndex, onObjectPathPointSelect, onObjectPathChange, onObjectPathMarkInsert, onObjectPathGestureStart, onObjectPathGestureEnd, characters, waypoints, onSelectWaypoint, onMoveWaypoint, onMoveCharacter, onCharacterGestureStart, onWaypointGestureStart, onCameraGestureStart, onSelectEntity, sceneObjects, selectedSceneObjectId, onMoveSceneObject, onObjectMoveStart, onObjectMoveEnd, railDraw, onRailStroke, pathDraw, onPathStroke, onCameraChange };
|
|
525
544
|
|
|
526
545
|
const targets = () => {
|
|
527
546
|
const cast = latest.current.characters;
|
|
@@ -539,7 +558,9 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
539
558
|
});
|
|
540
559
|
});
|
|
541
560
|
for (const object of latest.current.sceneObjects) {
|
|
542
|
-
|
|
561
|
+
// An Empty has no footprint; it is picked by its marker's size.
|
|
562
|
+
const size = isEmptyObject(object) ? { width: EMPTY_MARKER_SIZE, depth: EMPTY_MARKER_SIZE } : objectSize(object);
|
|
563
|
+
const { width, depth } = size;
|
|
543
564
|
out.push({
|
|
544
565
|
id: `object:${object.id}`,
|
|
545
566
|
objectId: object.id,
|
|
@@ -612,13 +633,15 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
612
633
|
// "the puck doesn't move". Pick the NEAREST target instead.
|
|
613
634
|
// A route's own points outrank pucks: they are smaller, they sit on
|
|
614
635
|
// top of the deck, and a press near one is always meant for it.
|
|
615
|
-
const
|
|
616
|
-
if (
|
|
636
|
+
const route = latest.current.objectPath;
|
|
637
|
+
if (route && route.points?.length >= 2) {
|
|
617
638
|
let nearest = null;
|
|
618
|
-
|
|
619
|
-
const
|
|
639
|
+
pathMarkFractions(route).forEach((t, i) => {
|
|
640
|
+
const at = pathPointAtFraction(route, t);
|
|
641
|
+
if (!at) return;
|
|
642
|
+
const d = (p.x - at.x) ** 2 + (p.z - at.z) ** 2;
|
|
620
643
|
if (d < PATH_POINT_GRAB * PATH_POINT_GRAB && (!nearest || d < nearest.d)) nearest = { d, index: i };
|
|
621
|
-
}
|
|
644
|
+
});
|
|
622
645
|
if (nearest) return { id: `path:${nearest.index}`, mode: "pathPoint", origin: { pathIndex: nearest.index } };
|
|
623
646
|
}
|
|
624
647
|
const list = targets();
|
|
@@ -637,8 +660,6 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
637
660
|
|
|
638
661
|
const onDown = (event) => {
|
|
639
662
|
if (event.button !== 0) return;
|
|
640
|
-
// Rail drawing owns the pointer: a stroke starts anywhere on the
|
|
641
|
-
// deck, so puck picking would fight every stroke that crosses one.
|
|
642
663
|
// Rail drawing and object-path drawing share one stroke gesture:
|
|
643
664
|
// a stroke starts anywhere on the deck, so puck picking would
|
|
644
665
|
// fight every stroke that crosses one.
|
|
@@ -670,8 +691,17 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
670
691
|
// instantly (plan §6.4). Camera, character and waypoint grips
|
|
671
692
|
// write separate state and must never open a scene transaction.
|
|
672
693
|
if (grip.mode === "pathPoint") {
|
|
673
|
-
// One undo entry per
|
|
694
|
+
// One undo entry per mark drag, like every other plan gesture.
|
|
674
695
|
latest.current.onObjectPathGestureStart?.();
|
|
696
|
+
// The bend this drag applies (the camera rail's own preparation):
|
|
697
|
+
// the route's points around the mark follow, fading out by the
|
|
698
|
+
// neighbouring marks.
|
|
699
|
+
const route = latest.current.objectPath;
|
|
700
|
+
const fractions = pathMarkFractions(route);
|
|
701
|
+
const base = pathPointAtFraction(route, fractions[grip.origin.pathIndex]);
|
|
702
|
+
const bend = base && prepareRailBend(route.points, base, fractions.map((t) => ({ t })), grip.origin.pathIndex, { maxControls: MAX_PATH_POINTS });
|
|
703
|
+
dragRef.current.bend = bend;
|
|
704
|
+
dragRef.current.pressed = { x: p.x, z: p.z };
|
|
675
705
|
} else if (grip.origin.objectId) {
|
|
676
706
|
dragRef.current.token = latest.current.onObjectMoveStart?.({
|
|
677
707
|
owner: "plan",
|
|
@@ -702,11 +732,27 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
702
732
|
}
|
|
703
733
|
const p = toFloor(event);
|
|
704
734
|
const grip = p && pick(p);
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
735
|
+
const markIndex = grip?.mode === "pathPoint" ? grip.origin.pathIndex : null;
|
|
736
|
+
setHoverMark((current) => (current === markIndex ? current : markIndex));
|
|
737
|
+
// The route answers too: it brightens and the cursor says "copy" while a
|
|
738
|
+
// double-click here would still add a dot.
|
|
739
|
+
let addable = false;
|
|
740
|
+
const route = latest.current.objectPath;
|
|
741
|
+
if (!grip && p && route && route.points?.length >= 2) {
|
|
742
|
+
const fractions = pathMarkFractions(route);
|
|
743
|
+
if (fractions.length < MAX_PATH_MARKS) {
|
|
744
|
+
const best = nearestPathFraction(route, (point) => ({ x: point.x, y: point.z }), p.x, p.z);
|
|
745
|
+
addable = !!best && best.d <= PATH_POINT_GRAB && !fractions.some((entry) => Math.abs(entry - best.t) < MARK_GAP + 0.01);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
setHoverLine((current) => (current === addable ? current : addable));
|
|
749
|
+
host.style.cursor = addable
|
|
750
|
+
? "copy"
|
|
751
|
+
: !grip
|
|
752
|
+
? "default"
|
|
753
|
+
: grip.mode === "turn"
|
|
754
|
+
? "ew-resize"
|
|
755
|
+
: "grab";
|
|
710
756
|
};
|
|
711
757
|
|
|
712
758
|
const onMove = (event) => {
|
|
@@ -726,10 +772,18 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
726
772
|
}
|
|
727
773
|
|
|
728
774
|
if (grip.mode === "pathPoint") {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
775
|
+
if (!grip.bend) return;
|
|
776
|
+
// Floor only: a point's height is the scene's business, so y rides
|
|
777
|
+
// through untouched. The pull is snapped like every board move.
|
|
778
|
+
const dx = Math.round((p.x - grip.pressed.x) / 0.05) * 0.05;
|
|
779
|
+
const dz = Math.round((p.z - grip.pressed.z) / 0.05) * 0.05;
|
|
780
|
+
const moved = grip.bend.startControls.map((entry, i) => {
|
|
781
|
+
const weight = grip.bend.weights[i];
|
|
782
|
+
return weight > 0
|
|
783
|
+
? { ...entry, x: THREE.MathUtils.clamp(entry.x + dx * weight, -ROOM_LIMIT, ROOM_LIMIT), z: THREE.MathUtils.clamp(entry.z + dz * weight, -ROOM_LIMIT, ROOM_LIMIT) }
|
|
784
|
+
: entry;
|
|
732
785
|
});
|
|
786
|
+
latest.current.onObjectPathChange?.({ points: moved });
|
|
733
787
|
return;
|
|
734
788
|
}
|
|
735
789
|
|
|
@@ -831,27 +885,20 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
831
885
|
// 3D scene uses, offered here because this is the board the route was
|
|
832
886
|
// drawn on, so this is where a hand goes looking for it.
|
|
833
887
|
const onDouble = (event) => {
|
|
834
|
-
const
|
|
835
|
-
if (!
|
|
888
|
+
const route = latest.current.objectPath;
|
|
889
|
+
if (!route || route.points?.length < 2) return;
|
|
836
890
|
if (latest.current.railDraw || latest.current.pathDraw) return;
|
|
837
891
|
const p = toFloor(event);
|
|
838
892
|
if (!p) return;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const lenSq = dx * dx + dz * dz;
|
|
846
|
-
const t = lenSq < 1e-9 ? 0 : Math.min(1, Math.max(0, ((p.x - a.x) * dx + (p.z - a.z) * dz) / lenSq));
|
|
847
|
-
const d = Math.hypot(a.x + dx * t - p.x, a.z + dz * t - p.z);
|
|
848
|
-
if (!best || d < best.d) best = { d, index: i, t };
|
|
849
|
-
}
|
|
850
|
-
// Near the line, and not on top of a point that is already there.
|
|
851
|
-
if (!best || best.d > PATH_POINT_GRAB || best.t < 0.04 || best.t > 0.96) return;
|
|
893
|
+
const fractions = pathMarkFractions(route);
|
|
894
|
+
if (fractions.length >= MAX_PATH_MARKS) return;
|
|
895
|
+
const best = nearestPathFraction(route, (point) => ({ x: point.x, y: point.z }), p.x, p.z);
|
|
896
|
+
// Near the line, and not on top of a mark that is already there.
|
|
897
|
+
if (!best || best.d > PATH_POINT_GRAB) return;
|
|
898
|
+
if (fractions.some((entry) => Math.abs(entry - best.t) < MARK_GAP + 0.01)) return;
|
|
852
899
|
event.preventDefault();
|
|
853
900
|
event.stopPropagation();
|
|
854
|
-
latest.current.
|
|
901
|
+
latest.current.onObjectPathMarkInsert?.(best.t);
|
|
855
902
|
};
|
|
856
903
|
|
|
857
904
|
const onEscape = (event) => {
|
|
@@ -901,7 +948,7 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
901
948
|
const puckId = listIndex === 0 ? "a" : listIndex === 1 ? "b" : `char:${entry.id}`;
|
|
902
949
|
const color = listIndex === 0 ? SUBJECT_ONE_COLOR : SUBJECT_TWO_COLOR;
|
|
903
950
|
return (
|
|
904
|
-
<group key={entry.id} position={[entry.x, 0, entry.z]}>
|
|
951
|
+
<group key={entry.id} position={[entry.x, 0, entry.z]} userData={{ characterPuck: entry.id }}>
|
|
905
952
|
<PlanLabel text={ko(`S${listIndex + 1}`, `인물 ${listIndex + 1}`)} color={color} offset={-(CAST_RING_OUTER + 0.3)} />
|
|
906
953
|
<CastRing color={color} />
|
|
907
954
|
<group rotation={[0, (entry.rot * Math.PI) / 180, 0]}>
|
|
@@ -931,7 +978,7 @@ export function PlanBoard({ hostRef, planCamRef, shotCamRef, look, fovDeg, chara
|
|
|
931
978
|
{railStroke && railStroke.length > 1 && <CameraRailLine points={railStroke} live />}
|
|
932
979
|
{/* The selected object's travel path, drawn in its own colour so a
|
|
933
980
|
prop's route never reads as the camera's rail. */}
|
|
934
|
-
{!minimal &&
|
|
981
|
+
{!minimal && objectPath && objectPath.points?.length > 1 && <ObjectPathLine path={objectPath} selectedIndex={objectPathSelectedIndex} hoverIndex={hoverMark} lit={hoverLine} />}
|
|
935
982
|
{/* The sun on the floor plan: a gold disc + a stem toward the stage
|
|
936
983
|
centre, so blocking can read where the light comes from without
|
|
937
984
|
switching to the 3D scene. Not draggable here — the 3D puck owns
|
package/src/playground.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// playground must never overwrite scenes they authored in the real studio.
|
|
6
6
|
|
|
7
7
|
import { readProjectDocument } from "./project.js";
|
|
8
|
-
import {
|
|
8
|
+
import { migrateScenesDocument as migrateToCurrentScenes } from "./scenes.js";
|
|
9
9
|
import { resolveSceneParam } from "./starter-scenes.js";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const PLAYGROUND_EMBED = "playground";
|
|
12
12
|
const PLAYGROUND_GLOBAL = "__cozyclayPlayground";
|
|
13
13
|
|
|
14
14
|
export function isPlaygroundEmbed(search) {
|
|
@@ -42,9 +42,7 @@ export async function fetchPlaygroundProject(url) {
|
|
|
42
42
|
const result = readProjectDocument(await response.text());
|
|
43
43
|
if (!result.ok) return null;
|
|
44
44
|
const source = result.project.scenesDocument;
|
|
45
|
-
const document =
|
|
46
|
-
? { ...source, version: SCENES_VERSION, scenes: source.scenes.map((scene) => ({ ...scene, stage: migrateStageFrames(scene.stage) })) }
|
|
47
|
-
: source;
|
|
45
|
+
const document = migrateToCurrentScenes(source);
|
|
48
46
|
return { name: result.project.name, document };
|
|
49
47
|
} catch {
|
|
50
48
|
return null;
|
package/src/pose-fade.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const POSE_FADE_DEFAULT = 0.2;
|
|
2
|
+
export const POSE_FADE_STORAGE_KEY = "cozyclay.poseObjectOpacity";
|
|
3
|
+
|
|
4
|
+
export function readPoseFadeOpacity(storage = globalThis.localStorage) {
|
|
5
|
+
const value = Number(storage?.getItem?.(POSE_FADE_STORAGE_KEY));
|
|
6
|
+
return storage?.getItem?.(POSE_FADE_STORAGE_KEY) == null || !Number.isFinite(value)
|
|
7
|
+
? POSE_FADE_DEFAULT
|
|
8
|
+
: Math.min(1, Math.max(0, value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function writePoseFadeOpacity(value, storage = globalThis.localStorage) {
|
|
12
|
+
try { storage?.setItem?.(POSE_FADE_STORAGE_KEY, String(value)); } catch { return; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Keyed by material: a material shared by several meshes is recorded once, so the restore is exact.
|
|
16
|
+
const originals = new WeakMap();
|
|
17
|
+
const noRaycast = () => {};
|
|
18
|
+
|
|
19
|
+
function materialsOf(mesh) {
|
|
20
|
+
return Array.isArray(mesh.material) ? mesh.material : mesh.material ? [mesh.material] : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Image captures in Pose mode want the set as authored: the working fade is lifted while this is set.
|
|
24
|
+
let fadeSuspended = false;
|
|
25
|
+
function suspendPoseFade(suspended) {
|
|
26
|
+
fadeSuspended = suspended;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Runs `draw` with the working fade lifted. `sync` re-applies the fade to every prop; it runs
|
|
30
|
+
again afterwards because a demand frame loop may not render another frame on its own. */
|
|
31
|
+
export function withPoseFadeLifted(sync, draw) {
|
|
32
|
+
suspendPoseFade(true);
|
|
33
|
+
sync?.();
|
|
34
|
+
try {
|
|
35
|
+
return draw();
|
|
36
|
+
} finally {
|
|
37
|
+
suspendPoseFade(false);
|
|
38
|
+
sync?.();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `fade` is Pose mode's working fade (null = off; a faded prop also stops catching clicks);
|
|
43
|
+
`own` is the object's authored opacity (1 = solid). */
|
|
44
|
+
export function applyPoseFade(root, fade, own = 1) {
|
|
45
|
+
const workingFade = fadeSuspended || fade == null || fade >= 1 ? 1 : fade;
|
|
46
|
+
const opacity = workingFade * own;
|
|
47
|
+
const faded = opacity < 1;
|
|
48
|
+
const unpickable = workingFade < 1;
|
|
49
|
+
root.traverse((node) => {
|
|
50
|
+
if (!node.isMesh) return;
|
|
51
|
+
for (const material of materialsOf(node)) {
|
|
52
|
+
if (faded) {
|
|
53
|
+
if (!originals.has(material)) {
|
|
54
|
+
originals.set(material, { transparent: material.transparent, opacity: material.opacity, depthWrite: material.depthWrite });
|
|
55
|
+
}
|
|
56
|
+
const target = originals.get(material).opacity * opacity;
|
|
57
|
+
if (material.opacity !== target || !material.transparent || material.depthWrite) {
|
|
58
|
+
material.transparent = true;
|
|
59
|
+
material.opacity = target;
|
|
60
|
+
material.depthWrite = false;
|
|
61
|
+
material.needsUpdate = true;
|
|
62
|
+
}
|
|
63
|
+
} else if (originals.has(material)) {
|
|
64
|
+
Object.assign(material, originals.get(material));
|
|
65
|
+
originals.delete(material);
|
|
66
|
+
material.needsUpdate = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (unpickable && node.raycast !== noRaycast) {
|
|
70
|
+
node.userData.poseFadeRaycast = node.raycast;
|
|
71
|
+
node.raycast = noRaycast;
|
|
72
|
+
} else if (!unpickable && node.raycast === noRaycast) {
|
|
73
|
+
node.raycast = node.userData.poseFadeRaycast;
|
|
74
|
+
delete node.userData.poseFadeRaycast;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.pose-rotation-readout {
|
|
2
|
+
transform: translate(10px, -100%);
|
|
3
|
+
display: grid;
|
|
4
|
+
gap: 3px;
|
|
5
|
+
padding: 5px 8px;
|
|
6
|
+
border-radius: 7px;
|
|
7
|
+
background: rgba(22, 22, 26, 0.72);
|
|
8
|
+
backdrop-filter: blur(6px);
|
|
9
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 4px 14px rgba(0, 0, 0, 0.18);
|
|
10
|
+
color: rgba(255, 255, 255, 0.86);
|
|
11
|
+
font: 500 11px/1.25 ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
12
|
+
font-variant-numeric: tabular-nums;
|
|
13
|
+
white-space: nowrap;
|
|
14
|
+
user-select: none;
|
|
15
|
+
}
|
|
16
|
+
.pose-rotation-axes { display: flex; gap: 9px; }
|
|
17
|
+
.pose-rotation-axes span { display: inline-flex; gap: 3px; opacity: 0.72; }
|
|
18
|
+
.pose-rotation-axes span.active { opacity: 1; }
|
|
19
|
+
.pose-rotation-readout i { font-style: normal; font-weight: 600; }
|
|
20
|
+
.pose-rotation-readout [data-axis="x"] i, .pose-rotation-delta[data-axis="x"] span { color: #ff6b5e; }
|
|
21
|
+
.pose-rotation-readout [data-axis="y"] i, .pose-rotation-delta[data-axis="y"] span { color: #7ee07a; }
|
|
22
|
+
.pose-rotation-readout [data-axis="z"] i, .pose-rotation-delta[data-axis="z"] span { color: #5c9dff; }
|
|
23
|
+
.pose-rotation-delta { display: flex; gap: 4px; font-size: 13px; font-weight: 600; color: #fff; }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
|
|
3
|
+
// Left side runs cool (blue arm, teal leg), right side warm (red arm, magenta leg); the pelvis is
|
|
4
|
+
// its own amber so the body's root reads apart from the spine above it.
|
|
5
|
+
export const HANDLE_COLORS = Object.freeze({
|
|
6
|
+
pelvis: "#ffb21e",
|
|
7
|
+
torso: "#2fd66b",
|
|
8
|
+
head: "#a37bff",
|
|
9
|
+
leftArm: "#3d8bff",
|
|
10
|
+
leftLeg: "#14c8e6",
|
|
11
|
+
rightArm: "#ff4d6a",
|
|
12
|
+
rightLeg: "#e44dff",
|
|
13
|
+
});
|
|
14
|
+
export const ROTATION_AXES = Object.freeze([
|
|
15
|
+
{ axis: "x", dir: new THREE.Vector3(1, 0, 0), color: "#ff6b5e" },
|
|
16
|
+
{ axis: "y", dir: new THREE.Vector3(0, 1, 0), color: "#7ee07a" },
|
|
17
|
+
{ axis: "z", dir: new THREE.Vector3(0, 0, 1), color: "#5c9dff" },
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const RING_BACK_OPACITY = 0.22;
|
|
21
|
+
|
|
22
|
+
// Fades the half of a ring that faces away from the camera (relative to the ring's centre), the way
|
|
23
|
+
// DCC rotate gizmos do, so the three rings stop crossing over the bone like a cage.
|
|
24
|
+
export function backFaceFade(material) {
|
|
25
|
+
material.onBeforeCompile = (shader) => {
|
|
26
|
+
shader.uniforms.ringBackOpacity = { value: RING_BACK_OPACITY };
|
|
27
|
+
shader.vertexShader = shader.vertexShader
|
|
28
|
+
.replace("#include <common>", "#include <common>\nvarying float vRingFront;")
|
|
29
|
+
.replace("#include <project_vertex>", `#include <project_vertex>
|
|
30
|
+
vec4 ringCentre = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
|
31
|
+
vRingFront = smoothstep(-0.02, 0.02, mvPosition.z - ringCentre.z);`);
|
|
32
|
+
shader.fragmentShader = shader.fragmentShader
|
|
33
|
+
.replace("#include <common>", "#include <common>\nvarying float vRingFront;\nuniform float ringBackOpacity;")
|
|
34
|
+
.replace("#include <opaque_fragment>", "#include <opaque_fragment>\ngl_FragColor.a *= mix(ringBackOpacity, 1.0, vRingFront);");
|
|
35
|
+
};
|
|
36
|
+
material.customProgramCacheKey = () => "pose-ring-backfade";
|
|
37
|
+
return material;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handlePart(id = "") {
|
|
41
|
+
const match = /^(left|right)(.*)$/.exec(String(id));
|
|
42
|
+
if (match) return `${match[1]}${/knee|foot|leg|toe|hip/i.test(match[2]) ? "Leg" : "Arm"}`;
|
|
43
|
+
if (id === "hips") return "pelvis";
|
|
44
|
+
return id === "head" ? "head" : "torso";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function handleColor(id) {
|
|
48
|
+
return HANDLE_COLORS[handlePart(id)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const v0 = new THREE.Vector3();
|
|
52
|
+
const v1 = new THREE.Vector3();
|
|
53
|
+
const cross = new THREE.Vector3();
|
|
54
|
+
|
|
55
|
+
/** Signed angle (radians) that turns `start` into `current` about `axis`, both measured from `origin`. */
|
|
56
|
+
export function ringAngle(axis, origin, start, current) {
|
|
57
|
+
v0.subVectors(start, origin);
|
|
58
|
+
v1.subVectors(current, origin);
|
|
59
|
+
if (v0.lengthSq() < 1e-12 || v1.lengthSq() < 1e-12) return 0;
|
|
60
|
+
cross.crossVectors(v0, v1);
|
|
61
|
+
return Math.atan2(cross.dot(axis), v0.dot(v1));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const relative = new THREE.Quaternion();
|
|
65
|
+
const euler = new THREE.Euler();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Rotation away from the bind pose in the parent frame (the frame the rings turn in), as XYZ Euler
|
|
69
|
+
* degrees rounded to 0.1. The pose system applies a correction as `delta · bind`, so the delta is
|
|
70
|
+
* recovered as `current · bind⁻¹`.
|
|
71
|
+
*/
|
|
72
|
+
export function rotationFromBindDeg(bindQuat, quat) {
|
|
73
|
+
relative.copy(bindQuat ?? relative.identity()).invert().premultiply(quat);
|
|
74
|
+
euler.setFromQuaternion(relative, "XYZ");
|
|
75
|
+
const round = (rad) => Math.round(THREE.MathUtils.radToDeg(rad) * 10) / 10 || 0;
|
|
76
|
+
return { x: round(euler.x), y: round(euler.y), z: round(euler.z) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function rotationReadout(deg, drag = null) {
|
|
80
|
+
const axes = ["x", "y", "z"].map((axis) => ({ axis, value: `${Math.round(deg[axis])}°`, active: drag?.axis === axis }));
|
|
81
|
+
if (!drag) return { delta: null, axes };
|
|
82
|
+
const value = THREE.MathUtils.radToDeg(drag.angle);
|
|
83
|
+
return { delta: { axis: drag.axis, value: `${value >= 0 ? "+" : "−"}${Math.abs(value).toFixed(1)}°` }, axes };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bindCache = new WeakMap();
|
|
87
|
+
|
|
88
|
+
export function bindQuaternionOf(bone) {
|
|
89
|
+
if (bindCache.has(bone)) return bindCache.get(bone);
|
|
90
|
+
let node = bone;
|
|
91
|
+
while (node && !node.userData?.poseBind) node = node.parent;
|
|
92
|
+
const saved = node?.userData.poseBind.get(bone);
|
|
93
|
+
const quat = saved ? new THREE.Quaternion(saved.x, saved.y, saved.z, saved.w) : null;
|
|
94
|
+
bindCache.set(bone, quat);
|
|
95
|
+
return quat;
|
|
96
|
+
}
|