cozyclay 3.1.6 → 3.2.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 +58 -0
- package/bin/agent/agent-runner.mjs +9 -4
- package/bin/agent/openai-compatible-provider.mjs +24 -1
- package/dist/app/index.html +5 -5
- package/dist/assets/TaskBoard-BUjjuvm5.js +1 -0
- package/dist/assets/{app-CEP7jzXB.css → app-Dige7q3X.css} +1 -1
- package/dist/assets/app-DxoI941o.js +5144 -0
- package/dist/assets/compile-worker-DRWRa9pr.js +6 -0
- package/dist/assets/{motion3-compile-Du7UXrWn.js → motion3-compile-B4SX4TGx.js} +1 -1
- package/dist/assets/scene-asset-sync-LeI3eSnO.js +13 -0
- package/dist/assets/task-board-5PKn7ZaA.js +15 -0
- package/dist/assets/{workflow-DJwOUzqJ.js → workflow-Dg9jCKOq.js} +3 -3
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +1 -1
- package/dist/workflow/index.html +3 -3
- package/package.json +2 -1
- package/src/App.jsx +5 -5
- package/src/commands/index.js +2 -1
- package/src/commands/physics.js +21 -0
- package/src/domains/motion.js +7 -9
- package/src/motion/center-of-mass.js +5 -23
- package/src/motion/contact-project.js +143 -0
- package/src/motion/fix-collisions.js +1 -1
- package/src/motion/ik-drag.js +93 -1
- package/src/motion/timeline.css +12 -0
- package/src/motion/timeline.jsx +2 -2
- package/src/motion3/compile-job.js +3 -2
- package/src/motion3/compiler/compile.js +3 -3
- package/src/motion3/compiler/spring.js +95 -0
- package/src/motion3/compiler/validate-objects.js +18 -2
- package/src/motion3/compiler/validate-parts.js +1 -0
- package/src/motion3/compiler/vocab.js +1 -1
- package/src/motion3/fix/index.js +8 -4
- package/src/motion3/fix/person.js +110 -0
- package/src/motion3/free-motion.js +49 -11
- package/src/motion3/object-pose.js +44 -2
- package/src/motion3/people.js +16 -1
- package/src/motion3/physics-frames.js +72 -0
- package/src/motion3/physics-trail.js +45 -0
- package/src/motion3/physics-view.js +79 -0
- package/src/motion3/pipeline.js +5 -1
- package/src/motion3/qa/ballistic.js +67 -0
- package/src/motion3/qa/com.js +140 -0
- package/src/motion3/qa/ground.js +10 -3
- package/src/motion3/qa/hints-contact.js +9 -7
- package/src/motion3/qa/hints-motion.js +38 -1
- package/src/motion3/qa/key-checks.js +5 -4
- package/src/motion3/qa/patch.js +28 -3
- package/src/motion3/qa/people-checks.js +1 -1
- package/src/motion3/qa/spring-pivot.js +23 -0
- package/src/motion3/qa/style.js +13 -30
- package/src/motion3/schema/fields.js +4 -2
- package/src/motion3/schema/topics.js +15 -11
- package/src/motion3/schema/types.js +13 -9
- package/src/physics-overlay-settings.js +41 -0
- package/src/physics-overlay.js +152 -0
- package/src/physics-overlay.jsx +54 -0
- package/src/physics-toggle.jsx +20 -0
- package/src/posestudio.jsx +1 -1
- package/tools/run-tests.mjs +9 -16
- package/dist/assets/TaskBoard-DwXhsJXz.js +0 -1
- package/dist/assets/app-BLS1Zaru.js +0 -5144
- package/dist/assets/compile-worker-or7djI2v.js +0 -6
- package/dist/assets/scene-asset-sync-t45pWCyq.js +0 -13
- package/dist/assets/task-board-CkLZGPkv.js +0 -15
|
@@ -1 +1 @@
|
|
|
1
|
-
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.
|
|
1
|
+
{"payload":"{\"distribution\":\"npm\",\"package\":\"cozyclay\",\"version\":\"3.2.0\",\"repository\":\"NomaDamas/CozyClay\",\"content_sha256\":\"6b804a847783301703e2a3b5f6b7df24949f4ec67afe0e0757342f15a57730b2\"}","signature":"kb5TnT8Rn5B5fOkGXZqgfujX5ADiLTS6LDq+SMZbBAxzeZWYmFC0ogOVS1BFyRC/hjsHI7M2YrM9i/wcSTZ7Cw=="}
|
package/dist/index.html
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"applicationCategory": "DesignApplication",
|
|
47
47
|
"applicationSubCategory": "3D previsualization",
|
|
48
48
|
"operatingSystem": "Any (Chromium-based browser)",
|
|
49
|
-
"softwareVersion": "3.
|
|
49
|
+
"softwareVersion": "3.2.0",
|
|
50
50
|
"description": "CozyClay is an open-source, browser-based 3D staging studio. Block a scene, pose characters, sequence motion prompts on a timeline, and preview generated motion in one local workspace that handles like the Unity Editor.",
|
|
51
51
|
"image": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
|
|
52
52
|
"screenshot": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
|
package/dist/workflow/index.html
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
<meta name="theme-color" content="#101116" />
|
|
9
9
|
<meta name="description" content="CozyClay visual workflow builder" />
|
|
10
10
|
<title>CozyClay Workflow</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/workflow-
|
|
11
|
+
<script type="module" crossorigin src="/assets/workflow-Dg9jCKOq.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/task-board-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/task-board-5PKn7ZaA.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/fflate.module-KbGj5-H6.js">
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/scene-asset-sync-LeI3eSnO.js">
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/assets/scene-asset-sync-BEjjhNEo.css">
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/workflow-DRdwoDBT.css">
|
|
18
18
|
</head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozyclay",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Open source previs software in the browser: block a scene, pose characters, author camera moves and cuts, then take the same shots to an AI video model.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "node tools/dev-full.mjs --host 127.0.0.1 --port 5180",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"eslint": "^10.12.0",
|
|
76
76
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
77
77
|
"globals": "^17.13.0",
|
|
78
|
+
"jpeg-js": "0.4.4",
|
|
78
79
|
"knip": "^6.41.0",
|
|
79
80
|
"mediabunny": "^1.55.2",
|
|
80
81
|
"posthog-js": "^1.418.12",
|
package/src/App.jsx
CHANGED
|
@@ -140,7 +140,7 @@ import {
|
|
|
140
140
|
pathMetrics,
|
|
141
141
|
strokeToPathPoints,
|
|
142
142
|
} from "./object-path.js";
|
|
143
|
-
import { carriedPointAt, sceneObjectsAt } from "./object-travel.js"; import { objectTracksOf } from "./motion3/object-tracks.js"; import { ViewGhosts } from "./view-ghosts.jsx"; import { DraftGhost } from "./draft-ghost.jsx"; import { SculptSketch } from "./sculpt-sketch.jsx";
|
|
143
|
+
import { carriedPointAt, sceneObjectsAt } from "./object-travel.js"; import { objectTracksOf } from "./motion3/object-tracks.js"; import { ViewGhosts } from "./view-ghosts.jsx"; import { DraftGhost } from "./draft-ghost.jsx"; import { SculptSketch } from "./sculpt-sketch.jsx"; import { PhysicsOverlayLayer } from "./physics-overlay.jsx"; import { syncPhysicsOverlays } from "./physics-overlay.js";
|
|
144
144
|
import { routeOwnerFor } from "./route-owner.js";
|
|
145
145
|
import {
|
|
146
146
|
exportFailureCode,
|
|
@@ -2874,7 +2874,7 @@ export default function App() {
|
|
|
2874
2874
|
// pass is the recorder's stand-in for the useFrame the preview gets.
|
|
2875
2875
|
// Riding characters move with their object first, then the props.
|
|
2876
2876
|
syncCarriers();
|
|
2877
|
-
propSyncRef.current?.();
|
|
2877
|
+
propSyncRef.current?.(); syncPhysicsOverlays(frame);
|
|
2878
2878
|
const sampled = sampleAt(context?.playbackScene ?? playbackScene, shotAtFrame(context?.shots ?? shots, frame), frame);
|
|
2879
2879
|
const framing = sampled.camera ?? context?.framing;
|
|
2880
2880
|
const cam = shotCamRef.current;
|
|
@@ -2959,7 +2959,7 @@ export default function App() {
|
|
|
2959
2959
|
look.current.pitch = cameraSnapshot.pitch;
|
|
2960
2960
|
propFrameRef.current = propFrame;
|
|
2961
2961
|
syncCarriers();
|
|
2962
|
-
propSyncRef.current?.();
|
|
2962
|
+
propSyncRef.current?.(); syncPhysicsOverlays();
|
|
2963
2963
|
}
|
|
2964
2964
|
}
|
|
2965
2965
|
|
|
@@ -3657,7 +3657,7 @@ export default function App() {
|
|
|
3657
3657
|
const point = new THREE.Vector3(); bone.getWorldPosition(point);
|
|
3658
3658
|
return point.toArray();
|
|
3659
3659
|
},
|
|
3660
|
-
committedIkEdits, waypoints,
|
|
3660
|
+
committedIkEdits, waypoints, rigs, characters, // rigs/characters: every cast member (QA reads a second rig)
|
|
3661
3661
|
// Scene props as the stage sees them, for QA that places a box and
|
|
3662
3662
|
// waits for its transform to land before fitting to it.
|
|
3663
3663
|
objects: sceneObjects,
|
|
@@ -4762,7 +4762,7 @@ export default function App() {
|
|
|
4762
4762
|
/>
|
|
4763
4763
|
</ObjectCarrier>
|
|
4764
4764
|
))}
|
|
4765
|
-
<ViewGhosts rig={rigs[activeChar.id]} take={motion} frame={tlFrame} objects={sceneObjects} propTake={{ frameCount: tlFrameCount, fps: tlFps, objectTracks }} /><DraftGhost rigs={rigs} objects={sceneObjects} /><SculptSketch objects={sceneObjects} />
|
|
4765
|
+
<ViewGhosts rig={rigs[activeChar.id]} take={motion} frame={tlFrame} objects={sceneObjects} propTake={{ frameCount: tlFrameCount, fps: tlFps, objectTracks }} /><DraftGhost rigs={rigs} objects={sceneObjects} /><SculptSketch objects={sceneObjects} /><PhysicsOverlayLayer rig={rigs[activeChar.id]} take={motion} objects={sceneObjects} />
|
|
4766
4766
|
|
|
4767
4767
|
{/* Selection marker: XYZ tripod + ring on the picked cast
|
|
4768
4768
|
member; the X/Z arrows drag it across the deck. */}
|
package/src/commands/index.js
CHANGED
|
@@ -17,8 +17,9 @@ import * as exporting from "./export.js";
|
|
|
17
17
|
import * as ai from "./ai.js";
|
|
18
18
|
import * as stage from "./stage.js";
|
|
19
19
|
import * as kits from "./kits.js";
|
|
20
|
+
import * as physics from "./physics.js";
|
|
20
21
|
|
|
21
|
-
export const COMMAND_MODULES = Object.freeze({ shot, cast, motion, motion3, objects, kits, view, scene, project, export: exporting, ai, stage });
|
|
22
|
+
export const COMMAND_MODULES = Object.freeze({ shot, cast, motion, motion3, physics, objects, kits, view, scene, project, export: exporting, ai, stage });
|
|
22
23
|
|
|
23
24
|
export function commandDeclarations(modules = COMMAND_MODULES) {
|
|
24
25
|
return Object.values(modules).flatMap(module => module.declarations.map(declaration => ({
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// motion.physicsOverlay: the physics overlay's viewer setting (physics-overlay-settings.js), the agent's door to the
|
|
2
|
+
// motion controls' Physics box. Transient: never saved, never undone.
|
|
3
|
+
import { describePhysicsOverlay, setPhysicsOverlay } from "../physics-overlay-settings.js";
|
|
4
|
+
|
|
5
|
+
const flag = description => ({ type: "boolean", description });
|
|
6
|
+
|
|
7
|
+
export const declarations = Object.freeze([
|
|
8
|
+
{ id: "motion.physicsOverlay", label: "Physics overlay", kind: "transient",
|
|
9
|
+
input: { type: "object", required: [], additionalProperties: false, properties: {
|
|
10
|
+
com: flag("the centre of mass: a 4 cm ball with a line down to the ground, green inside the feet, red past the balance limit (what QA calls BALANCE), grey off the ground"),
|
|
11
|
+
support: flag("the support polygon: the outline of the soles on the ground"),
|
|
12
|
+
arc: flag("per air run: the hips' path (white, dashed) and the gravity parabola through the same takeoff and landing (orange); far apart is a FLOAT"),
|
|
13
|
+
trail: flag("the last 24 frames of each spring object's tip"),
|
|
14
|
+
all: flag("every part at once, before the named ones; { all: false } turns the overlay off"),
|
|
15
|
+
} },
|
|
16
|
+
description: "Draw the physics QA measures over the active character's installed take, in the editor views and in captures, contact sheets and exports (capture_frame, verify_result frames, export.shotVideo) until it is turned off: com, support, arc and trail (see each field). Updated every frame, the same in scrub, playback and export. No field turns every part on; { all: false } turns it off. Turn it on before a screenshot to see why QA reports BALANCE or FLOAT. A viewer setting: off by default, never saved or undone." },
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function register(registry) {
|
|
20
|
+
registry.register({ ...declarations[0], available: () => true, run: args => ({ affectedIds: [], summary: describePhysicsOverlay(setPhysicsOverlay(args)) }) });
|
|
21
|
+
}
|
package/src/domains/motion.js
CHANGED
|
@@ -12,10 +12,7 @@ import {
|
|
|
12
12
|
ikKeyframes,
|
|
13
13
|
ikRemoveKeyframe,
|
|
14
14
|
ikSeedTargets,
|
|
15
|
-
clampIkTargetToFloor,
|
|
16
|
-
solveIk,
|
|
17
15
|
MID_TRACKS,
|
|
18
|
-
solveMidJoint,
|
|
19
16
|
solveEffectorSwing,
|
|
20
17
|
ikPlantFeet,
|
|
21
18
|
solveHipsTranslateToFloor,
|
|
@@ -35,7 +32,7 @@ import { ko } from "../locale.js";
|
|
|
35
32
|
import { isProxyFigure } from "../scenes.js";
|
|
36
33
|
import { refuseRigOnly } from "../character-kind.js";
|
|
37
34
|
import { DEFAULT_POSE, restoreBindPositions, applyPose, applyHipsOffset } from "../poses.js";
|
|
38
|
-
import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, ikDragRecord, ikDragTouch } from "../motion/ik-drag.js";
|
|
35
|
+
import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, clampDragTarget, ikDragRecord, ikDragTouch, solveDragTarget } from "../motion/ik-drag.js";
|
|
39
36
|
import { ikKeyJson, ikTrackKeyFromJson } from "../motion/ik-key-json.js";
|
|
40
37
|
import { chooseIkEntryPose } from "../ik-camera.js";
|
|
41
38
|
import { applyRangePin, captureRangePinTarget, normalizeRangePin, rangePinTargetWorld, rangePinTracks, removeRangePinKeys } from "../motion/range-pin.js";
|
|
@@ -741,7 +738,7 @@ export function useMotion(appContext) {
|
|
|
741
738
|
// chain toward the target; mid joints reposition the elbow/knee with both
|
|
742
739
|
// ends pinned (the handle snaps to the clamped position); FK joints swing
|
|
743
740
|
// toward the pointer. The solved pose stays a preview until Apply.
|
|
744
|
-
function ikSolve(kind, trackId, targetWorld) {
|
|
741
|
+
function ikSolve(kind, trackId, targetWorld, input = null) {
|
|
745
742
|
if (ikDragRef.current && ikDragRef.current.characterId !== appContext.shared.activeChar.id) return;
|
|
746
743
|
const state = appContext.shared.ikStateRef.current;
|
|
747
744
|
const ikChains = state.chains;
|
|
@@ -762,14 +759,15 @@ export function useMotion(appContext) {
|
|
|
762
759
|
ikDragRef.current = { frame: appContext.shared.tlFrame, ids: new Set(pending?.ids ?? []) };
|
|
763
760
|
}
|
|
764
761
|
ikDragRef.current = Object.assign(ikDragRecord(ikDragRef.current, appContext.shared.tlFrame), { characterId: appContext.shared.activeChar.id });
|
|
765
|
-
const touch = (id) => ikDragTouch(appContext.shared.ikStateRef.current, ikDragRef.current, id);
|
|
762
|
+
const touch = (id) => ikDragTouch(appContext.shared.ikStateRef.current, ikDragRef.current, id), activeId = appContext.shared.activeChar.id;
|
|
763
|
+
const clamp = () => clampDragTarget({ kind, trackId, targetWorld, bodyContact, chains: ikChains, record: ikDragRef.current, bypass: input?.bypass, open: () => ({ rigs: appContext.shared.rigs, activeId, cast: appContext.shared.characters, allow: domain.layer(activeId).take?.source?.spec?.checks?.allow, notify: appContext.notify }) });
|
|
766
764
|
if (kind === "chain") {
|
|
767
765
|
const chain = appContext.shared.ikStateRef.current.chains?.get(trackId);
|
|
768
766
|
if (!chain) return;
|
|
769
767
|
touch(trackId);
|
|
770
|
-
const clampedTarget =
|
|
768
|
+
const clampedTarget = clamp();
|
|
771
769
|
appContext.shared.ikStateRef.current.targets.set(trackId, clampedTarget.clone());
|
|
772
|
-
|
|
770
|
+
solveDragTarget(kind, chain, clampedTarget);
|
|
773
771
|
return;
|
|
774
772
|
}
|
|
775
773
|
if (kind === "mid") {
|
|
@@ -778,7 +776,7 @@ export function useMotion(appContext) {
|
|
|
778
776
|
const chain = midDef ? appContext.shared.ikStateRef.current.chains?.get(midDef.chain) : null;
|
|
779
777
|
if (!chain) return;
|
|
780
778
|
touch(chain.track.id);
|
|
781
|
-
|
|
779
|
+
solveDragTarget(kind, chain, clamp());
|
|
782
780
|
return;
|
|
783
781
|
}
|
|
784
782
|
// Effector swing: the rotation ring on a focused hand/foot. Rotates
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { findBone } from "./ik.js";
|
|
3
|
+
import { SEGMENT_FRACTIONS } from "../motion3/qa/com.js";
|
|
3
4
|
|
|
4
5
|
// Body segment masses and the centre of mass of a posed Mixamo rig. Read-only:
|
|
5
6
|
// motion verification and the support diagnostics measure with it.
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Segment mass fractions of total body mass,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Motor Control of Human Movement". The trunk (0.497) is split into pelvis
|
|
12
|
-
* (0.142) and thorax+abdomen (0.355); distal pairs are merged because their
|
|
13
|
-
* combined centroid still lands near the segment midpoint and Mixamo's hand
|
|
14
|
-
* and toe bones are too short to place reliably:
|
|
15
|
-
*
|
|
16
|
-
* head+neck 0.081 · trunk 0.497 · upper arm 0.028 · forearm 0.016 +
|
|
17
|
-
* hand 0.006 = 0.022 · thigh 0.100 · shank 0.0465 + foot 0.0145 = 0.061
|
|
9
|
+
* Segment mass fractions of total body mass (Dempster, via Winter), on Mixamo
|
|
10
|
+
* bone names. The table itself lives with motion3 QA (motion3/qa/com.js
|
|
11
|
+
* SEGMENT_FRACTIONS), which measures balance on the same segments.
|
|
18
12
|
*
|
|
19
13
|
* Each entry names the two Mixamo bones whose MIDPOINT carries the fraction.
|
|
20
14
|
* Dempster's true centres of mass sit ~43% down each segment rather than at
|
|
@@ -24,19 +18,7 @@ import { findBone } from "./ik.js";
|
|
|
24
18
|
*
|
|
25
19
|
* The fractions sum to 1, so the weighted average below is a true CoM.
|
|
26
20
|
*/
|
|
27
|
-
export const SEGMENT_MASSES =
|
|
28
|
-
{ id: "pelvis", start: "mixamorigHips", end: "mixamorigSpine1", mass: 0.142 },
|
|
29
|
-
{ id: "torso", start: "mixamorigSpine1", end: "mixamorigNeck", mass: 0.355 },
|
|
30
|
-
{ id: "head", start: "mixamorigNeck", end: "mixamorigHead", mass: 0.081 },
|
|
31
|
-
{ id: "leftUpperArm", start: "mixamorigLeftArm", end: "mixamorigLeftForeArm", mass: 0.028 },
|
|
32
|
-
{ id: "leftForeArmHand", start: "mixamorigLeftForeArm", end: "mixamorigLeftHand", mass: 0.022 },
|
|
33
|
-
{ id: "rightUpperArm", start: "mixamorigRightArm", end: "mixamorigRightForeArm", mass: 0.028 },
|
|
34
|
-
{ id: "rightForeArmHand", start: "mixamorigRightForeArm", end: "mixamorigRightHand", mass: 0.022 },
|
|
35
|
-
{ id: "leftThigh", start: "mixamorigLeftUpLeg", end: "mixamorigLeftLeg", mass: 0.100 },
|
|
36
|
-
{ id: "leftShankFoot", start: "mixamorigLeftLeg", end: "mixamorigLeftFoot", mass: 0.061 },
|
|
37
|
-
{ id: "rightThigh", start: "mixamorigRightUpLeg", end: "mixamorigRightLeg", mass: 0.100 },
|
|
38
|
-
{ id: "rightShankFoot", start: "mixamorigRightLeg", end: "mixamorigRightFoot", mass: 0.061 },
|
|
39
|
-
];
|
|
21
|
+
export const SEGMENT_MASSES = SEGMENT_FRACTIONS.map(({ id, start, end, mass }) => ({ id, start: `mixamorig${start}`, end: `mixamorig${end}`, mass }));
|
|
40
22
|
|
|
41
23
|
/**
|
|
42
24
|
* World-space centre of mass of the rig's CURRENT pose: the mass-weighted
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { closestPointsSegmentSegment } from "./fix-collisions.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contact projection for a Studio IK drag: the dragged target is moved out of
|
|
6
|
+
* the OTHER characters' capsules before the chain solves toward it, so a hand
|
|
7
|
+
* pushed into someone's chest stops on the chest instead of going through.
|
|
8
|
+
*
|
|
9
|
+
* One-sided by design: the blockers never move (the other character is never
|
|
10
|
+
* pushed; what they do is their own take). Fixed pass count, fixed order
|
|
11
|
+
* (probe capsules in the order given, blockers in the order given, the first
|
|
12
|
+
* deepest pair wins a tie), no clock, no randomness: the same target, the same
|
|
13
|
+
* blockers and the same `lastGood` give the same result.
|
|
14
|
+
*
|
|
15
|
+
* KEEP THE SIDE. The plain escape normal (probe point minus blocker point)
|
|
16
|
+
* flips once a deep target crosses the blocker's axis and then pushes the hand
|
|
17
|
+
* out of the BACK of the body. The escape keeps the side the hand came from:
|
|
18
|
+
* when the normal points away from `lastGood` (the last clean target of this
|
|
19
|
+
* drag), the push goes toward `lastGood` instead. Of the escape rules tried in
|
|
20
|
+
* the research prototype this was the only one that never tunnelled.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const EPS = 1e-9;
|
|
24
|
+
|
|
25
|
+
/** Push distance along unit `dir` that takes point `w` (relative to the
|
|
26
|
+
* blocker point) to distance `reach` from it: the far root of |w + t dir| = reach. */
|
|
27
|
+
function exitDistance(w, dir, reach) {
|
|
28
|
+
const along = w.dot(dir);
|
|
29
|
+
const inside = w.lengthSq() - reach * reach;
|
|
30
|
+
return -along + Math.sqrt(Math.max(0, along * along - inside));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* @param {Array<{id:string,a:THREE.Vector3,b:THREE.Vector3,radius:number}>} options.blockers
|
|
36
|
+
* the other characters' capsules, built once per drag; never moved
|
|
37
|
+
* @param {number|((probeId:string, blockerId:string) => number|null)} [options.allow]
|
|
38
|
+
* depth (m) a probe may go into a blocker: a number for every pair, or a
|
|
39
|
+
* lookup (null = 0) for an intended touch (spec.checks.allow person entries)
|
|
40
|
+
* @param {number} [options.margin] how far outside the surface a pushed target lands
|
|
41
|
+
* @param {number} [options.passes] fixed number of probe/push passes
|
|
42
|
+
* @param {number} [options.maxSteps] cap on the sweep's substeps per event
|
|
43
|
+
*/
|
|
44
|
+
export function createContactProjector({ blockers = [], allow = 0, margin = 0.004, passes = 4, maxSteps = 32 } = {}) {
|
|
45
|
+
// sweep spacing: no substep longer than the thinnest blocker's radius
|
|
46
|
+
const thinnest = Math.max(0.005, Math.min(...blockers.map((b) => b.radius)));
|
|
47
|
+
const allowed = typeof allow === "function" ? (p, b) => Math.max(0, allow(p, b) ?? 0) : () => Math.max(0, allow ?? 0);
|
|
48
|
+
const onProbe = new THREE.Vector3(), onBlocker = new THREE.Vector3(), w = new THREE.Vector3(), dir = new THREE.Vector3(), side = new THREE.Vector3();
|
|
49
|
+
const best = { depth: 0, reach: 0, probe: new THREE.Vector3(), blocker: new THREE.Vector3() };
|
|
50
|
+
|
|
51
|
+
/** The deepest probe/blocker pair past its allowance, written into `best`; false when none. */
|
|
52
|
+
function deepest(capsules) {
|
|
53
|
+
best.depth = 0;
|
|
54
|
+
for (const capsule of capsules) {
|
|
55
|
+
for (const blocker of blockers) {
|
|
56
|
+
const reach = capsule.radius + blocker.radius - allowed(capsule.id, blocker.id);
|
|
57
|
+
const dist = Math.sqrt(closestPointsSegmentSegment(capsule.a, capsule.b, blocker.a, blocker.b, onProbe, onBlocker));
|
|
58
|
+
if (reach - dist <= best.depth) continue;
|
|
59
|
+
best.depth = reach - dist;
|
|
60
|
+
best.reach = reach;
|
|
61
|
+
best.probe.copy(onProbe);
|
|
62
|
+
best.blocker.copy(onBlocker);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return best.depth > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* SWEEP. A clean endpoint is not enough: one pointer event can carry the
|
|
70
|
+
* limb clean across a thin blocker (a forearm, a hand). Walk from the
|
|
71
|
+
* handle as displayed (`from.at`) to the target in equal substeps no
|
|
72
|
+
* longer than the thinnest blocker's radius (at most `maxSteps`; a longer
|
|
73
|
+
* jump gets longer substeps) and stop at the first substep that touches
|
|
74
|
+
* after a clean one. Returns that substep (the keep-the-side push then runs
|
|
75
|
+
* there, with the clean substep before it as the side), `{ at: null }`
|
|
76
|
+
* when the target is clean and so was the way there, or null when the path
|
|
77
|
+
* never got clean (the limb as displayed is in contact, e.g. the drag
|
|
78
|
+
* opened touching, and a start in contact only stops once clean).
|
|
79
|
+
*
|
|
80
|
+
* The start is judged on the limb AS DISPLAYED (`from.capsules`), not by
|
|
81
|
+
* solving toward lastGood again: the drag's solve does not land its handle
|
|
82
|
+
* on the target in one call, so a second solve toward the same clean
|
|
83
|
+
* target moves the limb on, into the blocker it had stopped short of, and
|
|
84
|
+
* the whole crossing then read as a start in contact allowed to leave.
|
|
85
|
+
*/
|
|
86
|
+
function firstTouch(target, probe, from) {
|
|
87
|
+
const steps = Math.min(maxSteps, Math.max(1, Math.ceil(from.at.distanceTo(target) / thinnest)));
|
|
88
|
+
let wasClean = !deepest(from.capsules);
|
|
89
|
+
const clean = from.at.clone();
|
|
90
|
+
for (let k = 1; k <= steps; k++) {
|
|
91
|
+
const at = k === steps ? target.clone() : from.at.clone().lerp(target, k / steps);
|
|
92
|
+
if (!deepest(probe(at))) {
|
|
93
|
+
wasClean = true;
|
|
94
|
+
clean.copy(at);
|
|
95
|
+
} else if (wasClean) return { at, clean };
|
|
96
|
+
}
|
|
97
|
+
return wasClean ? { at: null, clean } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Move `target` out of the blockers.
|
|
102
|
+
* @param {THREE.Vector3} target the dragged point (not mutated)
|
|
103
|
+
* @param {(candidate:THREE.Vector3) => Array<{id:string,a:THREE.Vector3,b:THREE.Vector3,radius:number}>} probe
|
|
104
|
+
* the dragged limb's capsules when its handle sits at `candidate`
|
|
105
|
+
* @param {THREE.Vector3|null} lastGood the last clean target of this drag (the side to keep)
|
|
106
|
+
* @param {{at:THREE.Vector3, capsules:Array}|null} [from] the handle and the limb's
|
|
107
|
+
* capsules as displayed when the event began: where the sweep starts
|
|
108
|
+
* (default: lastGood, with the limb probed there)
|
|
109
|
+
* @returns {{ target: THREE.Vector3, contact: boolean, depth: number }}
|
|
110
|
+
* `contact` = a push happened; `depth` = what is left after the last pass (0 = clean)
|
|
111
|
+
*/
|
|
112
|
+
function project(target, probe, lastGood = null, from = null) {
|
|
113
|
+
let out = target.clone();
|
|
114
|
+
let contact = false;
|
|
115
|
+
if (!blockers.length) return { target: out, contact, depth: 0 };
|
|
116
|
+
const hit = lastGood ? firstTouch(out, probe, from ?? { at: lastGood, capsules: probe(lastGood) }) : null;
|
|
117
|
+
if (hit && !hit.at) return { target: out, contact, depth: 0 };
|
|
118
|
+
if (hit) out = hit.at;
|
|
119
|
+
const keep = hit?.clean ?? lastGood;
|
|
120
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
121
|
+
if (!deepest(probe(out))) return { target: out, contact, depth: 0 };
|
|
122
|
+
contact = true;
|
|
123
|
+
w.subVectors(best.probe, best.blocker);
|
|
124
|
+
dir.copy(w);
|
|
125
|
+
if (keep) {
|
|
126
|
+
side.subVectors(keep, best.blocker);
|
|
127
|
+
// a normal pointing away from the side the hand came from is the
|
|
128
|
+
// far side of the body: escape toward lastGood instead
|
|
129
|
+
if (side.lengthSq() > EPS && (dir.lengthSq() <= EPS || dir.dot(side) < 0)) dir.copy(side);
|
|
130
|
+
}
|
|
131
|
+
if (dir.lengthSq() <= EPS) dir.set(0, 1, 0);
|
|
132
|
+
dir.normalize();
|
|
133
|
+
out.addScaledVector(dir, exitDistance(w, dir, best.reach + margin));
|
|
134
|
+
}
|
|
135
|
+
const left = deepest(probe(out)) ? best.depth : 0;
|
|
136
|
+
// the passes did not get the probed (displayed) limb out: fall back to
|
|
137
|
+
// the last clean substep of the sweep, on the near side
|
|
138
|
+
if (left > 0 && hit) return { target: hit.clean.clone(), contact: true, depth: 0 };
|
|
139
|
+
return { target: out, contact, depth: left };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { project };
|
|
143
|
+
}
|
|
@@ -287,7 +287,7 @@ function restPairOverlaps(rig, radii = null) {
|
|
|
287
287
|
/** Closest points between two segments (Ericson, Real-Time Collision
|
|
288
288
|
* Detection — the textbook segment/segment distance). Writes into outA/outB
|
|
289
289
|
* and returns the squared distance. */
|
|
290
|
-
function closestPointsSegmentSegment(p1, q1, p2, q2, outA, outB) {
|
|
290
|
+
export function closestPointsSegmentSegment(p1, q1, p2, q2, outA, outB) {
|
|
291
291
|
const d1 = q1.clone().sub(p1);
|
|
292
292
|
const d2 = q2.clone().sub(p2);
|
|
293
293
|
const r = p1.clone().sub(p2);
|
package/src/motion/ik-drag.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { clampIkTargetToFloor, createIkState, ikBakeKeyframe, ikTouch, MID_TRACKS, solveIk, solveMidJoint } from "./ik.js";
|
|
3
|
+
import { buildCollisionCapsules } from "./fix-collisions.js";
|
|
4
|
+
import { blockerCharacterId, characterBlockers } from "./collision-blockers.js";
|
|
5
|
+
import { createContactProjector } from "./contact-project.js";
|
|
6
|
+
import { allowance } from "../motion3/qa/checks.js";
|
|
7
|
+
import { ko } from "../locale.js";
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Per-drag bookkeeping for manual IK drags over a loaded motion.
|
|
@@ -16,6 +22,92 @@ export function ikDragRecord(current, frame) {
|
|
|
16
22
|
return current && current.frame === frame ? current : { frame, ids: new Set() };
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
// Told once per drag, the first time the projection moves the handle.
|
|
26
|
+
const contactHint = () => ko("Stopped at the other character's body. Hold Alt while dragging to pass through",
|
|
27
|
+
"다른 인물의 몸에 닿아 멈췄어요. Alt를 누른 채 드래그하면 통과해요", "已停在另一个角色的身体上。按住 Alt 拖动即可穿过");
|
|
28
|
+
// Studio capsule id -> the motion3 QA part an allow entry names (qa/geometry.js CAPSULES).
|
|
29
|
+
const QA_PART = { UpperArm: "uarm", ForeArm: "farm", Hand: "hand", Thigh: "thigh", Shin: "shin", Foot: "foot" };
|
|
30
|
+
const qaPart = (capsuleId) => capsuleId.replace(/^(left|right)(\w+)$/, (_, side, part) => `${QA_PART[part] ?? part}_${side[0]}`);
|
|
31
|
+
|
|
32
|
+
/** The contact side of a drag, built once when it opens: the other cast members'
|
|
33
|
+
* capsules as they stand now (they do not move during a drag), the dragged
|
|
34
|
+
* chain's own capsules (upper arm, forearm, hand / thigh, shin, foot; no
|
|
35
|
+
* fingers) and the active take's person allowances (spec.checks.allow
|
|
36
|
+
* `{ part, object: "person:<id>", depth }`, read as QA reads them). */
|
|
37
|
+
function openDragContact(kind, chain, { rigs, activeId, cast = null, allow = null, notify = null }) {
|
|
38
|
+
const blockers = characterBlockers(rigs, activeId, { characterIds: cast });
|
|
39
|
+
const own = blockers.length ? buildCollisionCapsules(chain.rig) : null;
|
|
40
|
+
if (!own) return null;
|
|
41
|
+
const capsules = [...own.values()].filter((c) => c.def.movable?.chain === chain.track.id && !c.def.handId && !c.def.optional);
|
|
42
|
+
const allowFor = (probeId, blockerId) => allowance(allow, qaPart(probeId), `person:${blockerCharacterId(blockerId)}`);
|
|
43
|
+
const reads = capsules.map((c) => ({ id: c.def.id, start: c.bones[0], end: c.bones[1] ?? c.bones[0], radius: c.radius, a: c.a.clone(), b: c.b.clone() }));
|
|
44
|
+
const contact = {
|
|
45
|
+
projector: createContactProjector({ blockers, allow: allowFor }),
|
|
46
|
+
pose: null, // this event's starting locals, set by clampDragTarget
|
|
47
|
+
// the limb's capsules as the drag will DISPLAY them with its handle at
|
|
48
|
+
// `candidate`: the drag's own solve, from this event's starting pose
|
|
49
|
+
probe: (candidate) => {
|
|
50
|
+
restorePose(chain, contact.pose);
|
|
51
|
+
solveDragTarget(kind, chain, candidate);
|
|
52
|
+
return contact.shown();
|
|
53
|
+
},
|
|
54
|
+
// the limb's capsules as they stand (the pose on screen at event start)
|
|
55
|
+
shown: () => {
|
|
56
|
+
for (const read of reads) { read.start.getWorldPosition(read.a); read.end.getWorldPosition(read.b); }
|
|
57
|
+
return reads;
|
|
58
|
+
},
|
|
59
|
+
handle: chain.bones[kind === "mid" ? 1 : 2],
|
|
60
|
+
lastGood: chain.bones[kind === "mid" ? 1 : 2].getWorldPosition(new THREE.Vector3()),
|
|
61
|
+
notify,
|
|
62
|
+
};
|
|
63
|
+
return contact;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function restorePose(chain, pose) {
|
|
67
|
+
chain.bones.forEach((bone, i) => bone.quaternion.copy(pose[i]));
|
|
68
|
+
chain.bones[0].updateMatrixWorld(true);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The solve a chain or mid drag applies toward its handle (domains/motion.js
|
|
72
|
+
* ikSolve). The contact probe runs this same call from the same starting pose,
|
|
73
|
+
* so the pose it checks is exactly the pose the drag displays. (A probe on a
|
|
74
|
+
* different solve, e.g. exactHinge, lands the elbow and hand elsewhere from a
|
|
75
|
+
* bent pose and passed targets whose displayed limb sat inside the other body.) */
|
|
76
|
+
export function solveDragTarget(kind, chain, target) {
|
|
77
|
+
if (kind === "mid") solveMidJoint(chain, target);
|
|
78
|
+
else solveIk(chain, target);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Where a chain or mid drag may put its handle: on the floor with Body contact,
|
|
83
|
+
* then out of the other characters (contact-project.js). `open()` returns
|
|
84
|
+
* `{ rigs, activeId, cast, allow }` and is read once, on the first event of the
|
|
85
|
+
* drag (`record` keeps the result). `bypass` (Alt held) skips the contact
|
|
86
|
+
* projection for that event and leaves the side to keep untouched.
|
|
87
|
+
*/
|
|
88
|
+
export function clampDragTarget({ kind, trackId, targetWorld, bodyContact, chains, record = null, open = null, bypass = false }) {
|
|
89
|
+
const chain = chains?.get(kind === "mid" ? MID_TRACKS.find((t) => t.id === trackId)?.chain : trackId);
|
|
90
|
+
const heights = chain?.contactHeights ?? chains?.values().next().value?.contactHeights;
|
|
91
|
+
const floored = bodyContact ? clampIkTargetToFloor(trackId, targetWorld, 0, heights) : targetWorld;
|
|
92
|
+
if (!chain || !record || !open || (kind !== "chain" && kind !== "mid")) return floored;
|
|
93
|
+
if (record.contact === undefined) record.contact = openDragContact(kind, chain, open());
|
|
94
|
+
const contact = record.contact;
|
|
95
|
+
if (!contact || bypass) return floored;
|
|
96
|
+
// every probe starts from this event's pose, and the limb goes back to it
|
|
97
|
+
// so the caller's solveDragTarget starts there too
|
|
98
|
+
contact.pose = chain.bones.map((bone) => bone.quaternion.clone());
|
|
99
|
+
// the sweep starts from the limb as displayed, not from a re-solve toward lastGood
|
|
100
|
+
const from = { at: contact.handle.getWorldPosition(new THREE.Vector3()), capsules: contact.shown() };
|
|
101
|
+
const { target, depth } = contact.projector.project(floored, contact.probe, contact.lastGood, from);
|
|
102
|
+
restorePose(chain, contact.pose);
|
|
103
|
+
if (depth === 0) contact.lastGood = target.clone();
|
|
104
|
+
if (!target.equals(floored) && !contact.told) {
|
|
105
|
+
contact.told = true;
|
|
106
|
+
contact.notify?.(contactHint());
|
|
107
|
+
}
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
110
|
+
|
|
19
111
|
/** Mark `id` as written by this drag and touch it into the live layer. */
|
|
20
112
|
export function ikDragTouch(ikState, record, id) {
|
|
21
113
|
ikTouch(ikState, id);
|
package/src/motion/timeline.css
CHANGED
|
@@ -1427,3 +1427,15 @@
|
|
|
1427
1427
|
.v2-sequencer .objmo .sg { position: absolute; inset: 0; }
|
|
1428
1428
|
.v2-sequencer .objmo .sg-head { padding: 3px 10px 2px; }
|
|
1429
1429
|
.v2-sequencer .objmo .sg-body { min-height: 36px; margin: 0 0 6px; }
|
|
1430
|
+
|
|
1431
|
+
/* The motion controls' Physics box (physics-toggle.jsx) */
|
|
1432
|
+
.v2-sequencer .tl-btn.physics {
|
|
1433
|
+
display: inline-flex;
|
|
1434
|
+
align-items: center;
|
|
1435
|
+
gap: 4px;
|
|
1436
|
+
cursor: pointer;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.v2-sequencer .tl-btn.physics input {
|
|
1440
|
+
margin: 0;
|
|
1441
|
+
}
|
package/src/motion/timeline.jsx
CHANGED
|
@@ -10,7 +10,7 @@ import { insertPathMark, pathMarks, pathMetrics, pathProgressAt, MAX_PATH_LEAN }
|
|
|
10
10
|
import { NumberField } from "../ui.jsx";
|
|
11
11
|
import { flatTiming, timingIsFlat, envelopeDrag, insertCut, removeCut, CUT_MIN_GAP } from "../speed-envelope.js";
|
|
12
12
|
import { checkShotAgainstPreset, presetById } from "../model-presets.js";
|
|
13
|
-
import { TakeSegmentClips, TakeSegmentTools } from "./take-segments.jsx"; import { BeatBoard } from "./beat-board.jsx";
|
|
13
|
+
import { TakeSegmentClips, TakeSegmentTools } from "./take-segments.jsx"; import { BeatBoard } from "./beat-board.jsx"; import { PhysicsToggle } from "../physics-toggle.jsx";
|
|
14
14
|
import "./timeline.css";
|
|
15
15
|
export { timelineTake } from "./take-segments.jsx";
|
|
16
16
|
|
|
@@ -1708,7 +1708,7 @@ export default function Timeline({
|
|
|
1708
1708
|
</div>
|
|
1709
1709
|
{onClearMotion && (
|
|
1710
1710
|
<div className="tl-head-group tl-motion-tools" role="group" aria-label={ko("Motion controls", "모션 컨트롤")}>
|
|
1711
|
-
{motion?.segments && motionTools && <TakeSegmentTools motion={motion} frame={frame} />}
|
|
1711
|
+
{motion?.segments && motionTools && <TakeSegmentTools motion={motion} frame={frame} />}{motionTools && <PhysicsToggle />}
|
|
1712
1712
|
<button
|
|
1713
1713
|
type="button"
|
|
1714
1714
|
className="tl-btn clear"
|
|
@@ -65,8 +65,9 @@ export function runCompileJob({ model, scene, spec: asked, probes: sentProbes, c
|
|
|
65
65
|
// ms.repair: the whole repair's wall time, so a sweep that repairs every variant shows what it costs
|
|
66
66
|
const r = fixed.result, label = fixed.spec?.label ?? null, ms = fixed.repair ? { ...r.ms, repair: Math.round(performance.now() - t0) } : r.ms;
|
|
67
67
|
const take = bake ? bakeTake(sk, r.out, { position: bake.position, yaw: bake.yaw }, { spec: r.stored, prompt: ("prompt" in bake ? bake.prompt : label) ?? "" }) : undefined;
|
|
68
|
-
// a person anchor's joints ride with the take, so its saved source
|
|
69
|
-
|
|
68
|
+
// a person anchor's joints (and the bodies fix.person pushed an arm out of) ride with the take, so its saved source
|
|
69
|
+
// compiles the same without the others' takes (people.js)
|
|
70
|
+
const people = take && keptPeople(scene, [r.expanded, r.stored], r.out);
|
|
70
71
|
if (people) take.people = people;
|
|
71
72
|
return { stored: r.stored, qa: r.qa, observations: r.observations, used: r.used, fixed: r.fixed, search: r.search, timeline: r.timeline,
|
|
72
73
|
...(r.probes ? { probes: r.probes } : {}), ...(r.notes || corrected.length ? { notes: [...corrected, ...(r.notes ?? [])] } : {}), ms, frameCount: r.out.frameCount, label,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// ctx = { sk, scene: { objects: { id: { pos, R, half, ancestors } } } }; out.ctx.scene keeps the scene (object-pose.js)
|
|
21
21
|
import { add, sub, scale, norm, len, dot, apply, axisRot } from "./math.js";
|
|
22
22
|
import { isCarried, isFollow, hingePose, movesRecord, compose, relative } from "../carried.js";
|
|
23
|
-
import { isFree,
|
|
23
|
+
import { isFree, freeSwungAt } from "../free-motion.js";
|
|
24
24
|
import { groupsOf } from "../scene.js";
|
|
25
25
|
import { pose, SIDES } from "./pose.js";
|
|
26
26
|
import { graspAnchor } from "./socket-grip.js";
|
|
@@ -46,7 +46,7 @@ const REACH_SLACK = 0.02;
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Scene boxes by frame. A body moves with its own spec.objects entry or its nearest group's (a
|
|
49
|
-
* switch entry moves nothing; hinge: turned; free (C17): where its keys and falls put it, under the time map `mapOf()
|
|
49
|
+
* switch entry moves nothing; hinge: turned; free (C17): where its keys and falls put it, under the time map `mapOf()`, swung by its spring;
|
|
50
50
|
* carry and follow: left at its scene pose and marked `carried` with the moving id). `obstacles` drops the carried
|
|
51
51
|
* ones: QA, refs and object anchors read a carry where it rides (object-pose.js objectPoseAt, qa/object-checks.js);
|
|
52
52
|
* a follow entry rides the body and is no obstacle.
|
|
@@ -63,7 +63,7 @@ function objectTracks(spec, scene, baked, mapOf = () => undefined) {
|
|
|
63
63
|
const mover = [id, ...(o.ancestors ?? [])].find(k => movesRecord(spec.objects?.[k])), entry = spec.objects?.[mover];
|
|
64
64
|
if (!entry) out[id] = o;
|
|
65
65
|
else if (isCarried(entry) || isFollow(entry)) out[id] = { ...o, carried: mover };
|
|
66
|
-
else if (isFree(entry)) out[id] = { ...o, ...compose(
|
|
66
|
+
else if (isFree(entry)) out[id] = { ...o, ...compose(freeSwungAt({ spec, scene, timeMap: map }, mover, f), relative(scene.objects[mover] ?? groupsOf(scene)[mover], o)) };
|
|
67
67
|
else out[id] = { ...o, ...hingePose(entry, o, baked, f) };
|
|
68
68
|
}
|
|
69
69
|
cache.set(f, out);
|