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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useThree } from "@react-three/fiber";
|
|
3
|
+
import { Line, useFBX } from "@react-three/drei";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { SkeletonUtils } from "three/examples/jsm/Addons.js";
|
|
6
|
+
import { ProxyFigure } from "./proxy-figure.jsx";
|
|
7
|
+
import { isProxyFigure } from "./scenes.js";
|
|
8
|
+
import { applyPartColours } from "./part-colours.js";
|
|
9
|
+
import { applyHipsOffset, applyPose, primeBindPose } from "./poses.js";
|
|
10
|
+
import { TRAIL_TRACKS, falloffWeight, jointTrailPoints, trailEditRange } from "./motion-trail.js";
|
|
11
|
+
import { pickTrailPoint } from "./trail-pick.js";
|
|
12
|
+
import { REST_BONES } from "./stage-startup-defaults.js";
|
|
13
|
+
|
|
14
|
+
// Memoized: unchanged cast members skip re-rendering on every playhead tick.
|
|
15
|
+
export const Character = memo(function Character(props) {
|
|
16
|
+
return isProxyFigure(props) ? <ProxyFigure {...props} /> : <RiggedCharacter {...props} />;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const RiggedCharacter = memo(function RiggedCharacter({ url, position, rot, tint, pose, scale = 1, onRig, pickId, partColoursEnabled = false, partColoursMode = "shaded" }) {
|
|
20
|
+
const fbx = useFBX(url);
|
|
21
|
+
const model = useMemo(() => {
|
|
22
|
+
const clone = SkeletonUtils.clone(fbx);
|
|
23
|
+
// Mixamo exports centimetres; `scale` is the FILMED person's stature.
|
|
24
|
+
// THE INVARIANT: extraction divided this take's root travel by that
|
|
25
|
+
// stature, so the clip and the scale must travel together — apply one
|
|
26
|
+
// without the other and every stride overshoots by the same factor and
|
|
27
|
+
// the feet skate. It belongs HERE, on the world transform, and never
|
|
28
|
+
// inside rig space: playback.js derives prep.scale from the bind pose,
|
|
29
|
+
// so a scaled bone hierarchy would be measured as a different rig.
|
|
30
|
+
clone.scale.setScalar(0.01 * scale);
|
|
31
|
+
// The joint shells (Alpha_Joints / Beta_Joints — elbows, knees, the
|
|
32
|
+
// exoskeleton bands) render in a darkened shade of the body tint so
|
|
33
|
+
// the segments read as separate pieces.
|
|
34
|
+
// The shipped clay is intentionally pale, but the stage deck is pale too.
|
|
35
|
+
// Lift neither with emissive nor a second light; a small value drop keeps
|
|
36
|
+
// the same authored hue while restoring silhouette contrast in the editor.
|
|
37
|
+
const bodyTint = new THREE.Color(tint).multiplyScalar(0.9);
|
|
38
|
+
const jointTint = bodyTint.clone().multiplyScalar(0.62);
|
|
39
|
+
clone.traverse((child) => {
|
|
40
|
+
if (child.isMesh) {
|
|
41
|
+
// warm clay reads as a maquette; cold grey reads as a broken render
|
|
42
|
+
child.material = new THREE.MeshStandardMaterial({
|
|
43
|
+
color: /_Joints$/i.test(child.name) ? jointTint : bodyTint,
|
|
44
|
+
// A slightly tighter highlight gives the clay enough form to read
|
|
45
|
+
// at blocking distance without turning it into shiny plastic.
|
|
46
|
+
roughness: 0.66,
|
|
47
|
+
metalness: 0,
|
|
48
|
+
envMapIntensity: 0.35,
|
|
49
|
+
});
|
|
50
|
+
child.frustumCulled = false;
|
|
51
|
+
// The subject's own shadow is what plants it on the deck; a blocking
|
|
52
|
+
// frame without one leaves the figure floating over flat colour.
|
|
53
|
+
child.castShadow = true;
|
|
54
|
+
child.receiveShadow = true;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (partColoursEnabled) applyPartColours(clone, partColoursMode);
|
|
58
|
+
// Stamp the bind pose while the rig is still untouched: the pose effect
|
|
59
|
+
// below runs immediately after and would otherwise be baked into "rest".
|
|
60
|
+
primeBindPose(clone);
|
|
61
|
+
return clone;
|
|
62
|
+
}, [fbx, tint, partColoursEnabled, partColoursMode]);
|
|
63
|
+
|
|
64
|
+
// A new stature (a fresh extraction on this character) must not rebuild the
|
|
65
|
+
// clone — that would drop the rig the playback effects hold. Only the world
|
|
66
|
+
// transform moves; the same invariant as above applies.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
model.scale.setScalar(0.01 * scale);
|
|
69
|
+
}, [model, scale]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
// During playback the pose prop is null and the motion drives the rig;
|
|
73
|
+
// this effect can flush AFTER the playback effect in a later commit
|
|
74
|
+
// (measured: playback at t, pose reset at t+1.8ms), and resetting to
|
|
75
|
+
// REST then would clobber the animation back to the bind pose. So a
|
|
76
|
+
// null pose means "playback owns the rig" — do not touch it.
|
|
77
|
+
if (!pose) return;
|
|
78
|
+
// every joint is reset first, otherwise switching presets leaves stale limbs
|
|
79
|
+
applyPose(model, { ...REST_BONES, ...pose.bones });
|
|
80
|
+
// A measured pose (photo, bottled take frame) carries its hips height;
|
|
81
|
+
// rotations alone would leave a crouch floating at standing height.
|
|
82
|
+
applyHipsOffset(model, pose.rootY ?? 0);
|
|
83
|
+
}, [model, pose]);
|
|
84
|
+
|
|
85
|
+
const onRigRef = useRef(onRig);
|
|
86
|
+
onRigRef.current = onRig;
|
|
87
|
+
// Report on MODEL change only — a per-render callback identity change
|
|
88
|
+
// must not re-run this effect.
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
onRigRef.current?.(model);
|
|
91
|
+
}, [model]);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
// characterPick makes the body a first-class click target: the Scene
|
|
95
|
+
// picker walks up from any hit mesh, finds the tag, and routes the
|
|
96
|
+
// selection to the hierarchy so the Inspector owns the controls.
|
|
97
|
+
<group position={position} rotation={[0, (rot * Math.PI) / 180, 0]} userData={pickId ? { characterPick: pickId } : undefined}>
|
|
98
|
+
<primitive object={model} />
|
|
99
|
+
</group>
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/* ------------------------- IK-mode motion trails --------------------------
|
|
104
|
+
* World-space trajectory polylines of the loaded take: the selected track,
|
|
105
|
+
* or all tracks when the user asks for an overview. The whole clip is
|
|
106
|
+
* drawn as a faint line; while a grab is active the falloff window is
|
|
107
|
+
* re-drawn on top as a bright highlight. Grabbing any point of a line starts
|
|
108
|
+
* a drag on a camera-facing plane through the grab point; the caller deforms
|
|
109
|
+
* the take (motion-trail.js falloff math) so the preview updates live. */
|
|
110
|
+
export const MotionTrails = memo(function MotionTrails({ motion, rig = null, baseY, charScale, ikFocus, activeTrackId = null, falloffFrames, playheadFrame, pendingEdit, enabled, visible = true, onDragStart, onDragPreview, onDragEnd }) {
|
|
111
|
+
const { camera, gl, invalidate } = useThree();
|
|
112
|
+
const [drag, setDrag] = useState(null);
|
|
113
|
+
const callbacksRef = useRef({ onDragStart, onDragPreview, onDragEnd });
|
|
114
|
+
callbacksRef.current = { onDragStart, onDragPreview, onDragEnd };
|
|
115
|
+
const toTriples = (flat) => {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (let index = 0; index + 2 < flat.length; index += 3) out.push([flat[index], flat[index + 1], flat[index + 2]]);
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
120
|
+
// Every trail track (root + IK endpoints + head) in its handle colour,
|
|
121
|
+
// sampled where playback renders each bone on this rig (not the clip's
|
|
122
|
+
// own joints, which sit 4-15 cm off the body), so draw and pick agree.
|
|
123
|
+
const tracks = useMemo(
|
|
124
|
+
() => TRAIL_TRACKS.map((track) => {
|
|
125
|
+
const flat = jointTrailPoints(motion, track.joint, { baseY, scale: charScale, rig });
|
|
126
|
+
return flat ? { ...track, flat, points: toTriples(flat) } : null;
|
|
127
|
+
}).filter(Boolean),
|
|
128
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
|
+
[motion, rig, baseY, charScale],
|
|
130
|
+
);
|
|
131
|
+
const trackById = (id) => tracks.find((track) => track.id === id) ?? null;
|
|
132
|
+
const focusTrack = activeTrackId ? tracks.find((track) => track.id === activeTrackId)?.id ?? null : null;
|
|
133
|
+
const visibleTracks = focusTrack ? tracks.filter((track) => track.id === focusTrack) : tracks;
|
|
134
|
+
// The falloff window rides whichever line is being (or was last) grabbed.
|
|
135
|
+
const highlightCandidate = drag ?? pendingEdit;
|
|
136
|
+
const highlight = highlightCandidate && (!focusTrack || highlightCandidate.track === focusTrack) ? highlightCandidate : null;
|
|
137
|
+
const highlightPoints = useMemo(() => {
|
|
138
|
+
if (!highlight || !motion?.frames) return null;
|
|
139
|
+
const flat = trackById(highlight.track)?.flat ?? trackById("hips")?.flat;
|
|
140
|
+
if (!flat) return null;
|
|
141
|
+
const { startFrame, endFrame } = trailEditRange(motion.frames, highlight.grabFrame, falloffFrames);
|
|
142
|
+
const out = [];
|
|
143
|
+
for (let frame = startFrame; frame < endFrame; frame += 1) {
|
|
144
|
+
out.push([flat[frame * 3], flat[frame * 3 + 1], flat[frame * 3 + 2]]);
|
|
145
|
+
}
|
|
146
|
+
return out.length > 1 ? out : null;
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, [highlight, motion, tracks, falloffFrames]);
|
|
149
|
+
|
|
150
|
+
// Manual capture-phase picking instead of r3f pointer handlers: handlers on
|
|
151
|
+
// a drei Line make react-three-fiber raycast the fat-line shader geometry on
|
|
152
|
+
// EVERY pointermove for hover bookkeeping, which is exactly the IK-mode lag.
|
|
153
|
+
// A single pointerdown listener that measures point-to-ray distance against
|
|
154
|
+
// the cached trail arrays costs nothing while the mouse merely moves.
|
|
155
|
+
const pickRef = useRef(null);
|
|
156
|
+
pickRef.current = { tracks: visibleTracks, enabled, falloffFrames, playheadFrame };
|
|
157
|
+
// Line2 instances for in-place geometry rewrites during a drag.
|
|
158
|
+
const lineRefs = useRef({});
|
|
159
|
+
const highlightRef = useRef(null);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const dom = gl.domElement;
|
|
162
|
+
const raycaster = new THREE.Raycaster();
|
|
163
|
+
const hit = new THREE.Vector3();
|
|
164
|
+
const down = (event) => {
|
|
165
|
+
const pick = pickRef.current;
|
|
166
|
+
if (!pick?.enabled || event.button !== 0) return;
|
|
167
|
+
const rect = dom.getBoundingClientRect();
|
|
168
|
+
const ndc = new THREE.Vector2(
|
|
169
|
+
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
|
170
|
+
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
|
171
|
+
);
|
|
172
|
+
raycaster.setFromCamera(ndc, camera);
|
|
173
|
+
const { origin, direction } = raycaster.ray;
|
|
174
|
+
const picked = pickTrailPoint({
|
|
175
|
+
tracks: pick.tracks,
|
|
176
|
+
playheadFrame: pick.playheadFrame,
|
|
177
|
+
falloffFrames: pick.falloffFrames,
|
|
178
|
+
rayOrigin: origin,
|
|
179
|
+
rayDirection: direction,
|
|
180
|
+
maxDistance: 0.2,
|
|
181
|
+
});
|
|
182
|
+
if (!picked) return;
|
|
183
|
+
const { track, grabFrame } = picked;
|
|
184
|
+
// The grab wins over the camera controls listening in the bubble phase.
|
|
185
|
+
event.stopPropagation();
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
const flat = pick.tracks.find((candidate) => candidate.id === track)?.flat;
|
|
188
|
+
if (!flat) return;
|
|
189
|
+
const start = new THREE.Vector3(flat[grabFrame * 3], flat[grabFrame * 3 + 1], flat[grabFrame * 3 + 2]);
|
|
190
|
+
const normal = camera.getWorldDirection(new THREE.Vector3()).negate();
|
|
191
|
+
const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, start);
|
|
192
|
+
setDrag({ track, grabFrame });
|
|
193
|
+
callbacksRef.current.onDragStart?.({ track, grabFrame });
|
|
194
|
+
// The drag never touches React state: per rAF the grabbed line and the
|
|
195
|
+
// falloff highlight are rewritten in place on their Line2 geometries,
|
|
196
|
+
// and onDragPreview poses the rig imperatively. Re-rendering the whole
|
|
197
|
+
// app per pointermove is exactly the drag lag this avoids; the real
|
|
198
|
+
// commit (setMotion + pending edit) happens once, on pointerup.
|
|
199
|
+
const frames = flat.length / 3;
|
|
200
|
+
const { startFrame, endFrame } = trailEditRange(frames, grabFrame, pickRef.current.falloffFrames);
|
|
201
|
+
const deformedFlat = new Float32Array(flat);
|
|
202
|
+
const windowFlat = new Float32Array((endFrame - startFrame) * 3);
|
|
203
|
+
let lastDelta = null;
|
|
204
|
+
let queued = null;
|
|
205
|
+
let rafId = 0;
|
|
206
|
+
const applyPreview = (delta) => {
|
|
207
|
+
for (let frame = startFrame; frame < endFrame; frame += 1) {
|
|
208
|
+
const weight = falloffWeight(frame - grabFrame, pickRef.current.falloffFrames);
|
|
209
|
+
deformedFlat[frame * 3] = flat[frame * 3] + delta.x * weight;
|
|
210
|
+
deformedFlat[frame * 3 + 1] = flat[frame * 3 + 1] + delta.y * weight;
|
|
211
|
+
deformedFlat[frame * 3 + 2] = flat[frame * 3 + 2] + delta.z * weight;
|
|
212
|
+
}
|
|
213
|
+
windowFlat.set(deformedFlat.subarray(startFrame * 3, endFrame * 3));
|
|
214
|
+
lineRefs.current[track]?.geometry?.setPositions(deformedFlat);
|
|
215
|
+
highlightRef.current?.geometry?.setPositions(windowFlat);
|
|
216
|
+
callbacksRef.current.onDragPreview?.({ track, grabFrame, delta });
|
|
217
|
+
invalidate();
|
|
218
|
+
};
|
|
219
|
+
const flush = () => {
|
|
220
|
+
rafId = 0;
|
|
221
|
+
if (queued) applyPreview(queued);
|
|
222
|
+
queued = null;
|
|
223
|
+
};
|
|
224
|
+
const move = (pointerEvent) => {
|
|
225
|
+
const moveRect = dom.getBoundingClientRect();
|
|
226
|
+
const moveNdc = new THREE.Vector2(
|
|
227
|
+
((pointerEvent.clientX - moveRect.left) / moveRect.width) * 2 - 1,
|
|
228
|
+
-((pointerEvent.clientY - moveRect.top) / moveRect.height) * 2 + 1,
|
|
229
|
+
);
|
|
230
|
+
raycaster.setFromCamera(moveNdc, camera);
|
|
231
|
+
if (!raycaster.ray.intersectPlane(plane, hit)) return;
|
|
232
|
+
lastDelta = { x: hit.x - start.x, y: hit.y - start.y, z: hit.z - start.z };
|
|
233
|
+
queued = lastDelta;
|
|
234
|
+
if (!rafId) rafId = requestAnimationFrame(flush);
|
|
235
|
+
};
|
|
236
|
+
const up = () => {
|
|
237
|
+
window.removeEventListener("pointermove", move);
|
|
238
|
+
window.removeEventListener("pointerup", up);
|
|
239
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
240
|
+
if (queued) applyPreview(queued);
|
|
241
|
+
setDrag(null);
|
|
242
|
+
callbacksRef.current.onDragEnd?.({ track, grabFrame, delta: lastDelta });
|
|
243
|
+
};
|
|
244
|
+
window.addEventListener("pointermove", move);
|
|
245
|
+
window.addEventListener("pointerup", up);
|
|
246
|
+
};
|
|
247
|
+
dom.addEventListener("pointerdown", down, true);
|
|
248
|
+
return () => dom.removeEventListener("pointerdown", down, true);
|
|
249
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
250
|
+
}, [gl, camera]);
|
|
251
|
+
|
|
252
|
+
if (!tracks.length) return null;
|
|
253
|
+
// depthTest off + high renderOrder: the trails read through the character
|
|
254
|
+
// and the floor instead of vanishing into them. Every part rides its own
|
|
255
|
+
// IK-handle colour; the focused part draws thicker.
|
|
256
|
+
return (
|
|
257
|
+
<group
|
|
258
|
+
visible={visible}
|
|
259
|
+
renderOrder={900}
|
|
260
|
+
userData={{ editorOverlay: true }}
|
|
261
|
+
ref={(group) => {
|
|
262
|
+
// QA-only escape hatch (same spirit as window.__cozyclay): lets
|
|
263
|
+
// headless perf probes toggle the trails without a rebuild.
|
|
264
|
+
if (typeof window !== "undefined") {
|
|
265
|
+
window.__cozyclayTrails = group;
|
|
266
|
+
window.__cozyclayVisibleTrailTracks = visibleTracks.map((track) => track.id);
|
|
267
|
+
}
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
{visibleTracks.map((track) => track.points.length > 1 && (
|
|
271
|
+
<Line
|
|
272
|
+
key={track.id}
|
|
273
|
+
ref={(line) => {
|
|
274
|
+
if (line) lineRefs.current[track.id] = line;
|
|
275
|
+
else delete lineRefs.current[track.id];
|
|
276
|
+
}}
|
|
277
|
+
points={track.points}
|
|
278
|
+
color={track.color}
|
|
279
|
+
lineWidth={track.id === focusTrack ? 4 : 2.5}
|
|
280
|
+
depthTest={false}
|
|
281
|
+
transparent
|
|
282
|
+
opacity={track.id === focusTrack ? 1 : 0.85}
|
|
283
|
+
/>
|
|
284
|
+
))}
|
|
285
|
+
{highlightPoints && (
|
|
286
|
+
<Line ref={highlightRef} points={highlightPoints} color={drag ? "#3dff7a" : "#ff8c42"} lineWidth={5} depthTest={false} />
|
|
287
|
+
)}
|
|
288
|
+
</group>
|
|
289
|
+
);
|
|
290
|
+
}, (previous, next) => (
|
|
291
|
+
previous.motion === next.motion &&
|
|
292
|
+
previous.rig === next.rig &&
|
|
293
|
+
previous.baseY === next.baseY &&
|
|
294
|
+
previous.charScale === next.charScale &&
|
|
295
|
+
previous.ikFocus === next.ikFocus &&
|
|
296
|
+
previous.activeTrackId === next.activeTrackId &&
|
|
297
|
+
previous.falloffFrames === next.falloffFrames &&
|
|
298
|
+
previous.playheadFrame === next.playheadFrame &&
|
|
299
|
+
previous.pendingEdit === next.pendingEdit &&
|
|
300
|
+
previous.enabled === next.enabled &&
|
|
301
|
+
previous.visible === next.visible
|
|
302
|
+
));
|