cozyclay 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +219 -38
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +6 -5
- package/bin/cozyclay.mjs +12 -3
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +12 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +5 -5
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +801 -259
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +196 -3
- package/src/ardy/timeline.jsx +117 -29
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +1 -4
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +43 -24
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +16 -3
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +89 -32
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion/generation.js +1 -1
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +52 -3
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +62 -17
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +111 -10
- package/src/theme.js +1 -1
- package/src/timeline-fps.js +13 -0
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +110 -20
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +96 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-C1sdVbg4.css +0 -1
- package/dist/assets/app-CA55tcI3.js +0 -4876
- package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
- package/dist/assets/workflow-dNPGY9xz.js +0 -179
package/src/matte-editor.js
CHANGED
|
@@ -43,10 +43,10 @@ const OVERLAY_ALPHA = 0.62;
|
|
|
43
43
|
const STAGE_GROUND = "#141819";
|
|
44
44
|
/** Interaction runs on a preview this wide at most — the paint underneath it
|
|
45
45
|
* stays at the picture's own resolution. */
|
|
46
|
-
|
|
46
|
+
const EDIT_PREVIEW_MAX = 512;
|
|
47
47
|
/** Zoom range. 1 is the whole picture — there is no reason to look at less of
|
|
48
48
|
* it than that — and 16 puts a preview pixel under a fingertip. */
|
|
49
|
-
|
|
49
|
+
const MIN_ZOOM = 1;
|
|
50
50
|
export const MAX_ZOOM = 16;
|
|
51
51
|
/** How many edits back you can walk. Each step is a full-resolution copy of
|
|
52
52
|
* the selection, so this is a memory budget as much as a usability one: seven
|
|
@@ -251,8 +251,6 @@ export function createMatteEditor(canvas, {
|
|
|
251
251
|
report();
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/* --------------------------------------------------------- pointer --- */
|
|
255
|
-
|
|
256
254
|
/** Where a pointer is, in FULL-resolution picture pixels: stage → picture,
|
|
257
255
|
* through the same placement the paint is drawn with. */
|
|
258
256
|
function at(event) {
|
|
@@ -370,8 +368,6 @@ export function createMatteEditor(canvas, {
|
|
|
370
368
|
growFromStroke();
|
|
371
369
|
if (canvas.hasPointerCapture?.(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
|
|
372
370
|
};
|
|
373
|
-
/* ------------------------------------------------------------ view --- */
|
|
374
|
-
|
|
375
371
|
/** Zoom about a point on screen, so what is under the cursor stays under
|
|
376
372
|
* the cursor — the only zoom that lets you keep working on the thing you
|
|
377
373
|
* were looking at. */
|
|
@@ -416,8 +412,6 @@ export function createMatteEditor(canvas, {
|
|
|
416
412
|
canvas.addEventListener("keydown", onKeyDown);
|
|
417
413
|
canvas.addEventListener("keyup", onKeyUp);
|
|
418
414
|
|
|
419
|
-
/* ------------------------------------------------------------- api --- */
|
|
420
|
-
|
|
421
415
|
return {
|
|
422
416
|
/**
|
|
423
417
|
* Decode an asset and show it AS IT IS. Nothing is marked until someone
|
package/src/matte.js
CHANGED
|
@@ -109,15 +109,6 @@ export function borderSeeds({ data, width, height }, points = null) {
|
|
|
109
109
|
return { seeds: Float64Array.from(seeds), starts };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/**
|
|
113
|
-
* Drop the background, in place on a copy.
|
|
114
|
-
*
|
|
115
|
-
* `tolerance` (0–1) is the one control a person should ever need: it scales
|
|
116
|
-
* both the bound against the seed colours and the local step. `shrink` eats
|
|
117
|
-
* the fringe of background-coloured pixels that survives any threshold, and
|
|
118
|
-
* `feather` softens what is left so the card's edge does not read as a
|
|
119
|
-
* cut-out-with-scissors line against the set.
|
|
120
|
-
*/
|
|
121
112
|
/**
|
|
122
113
|
* `within`, when given, fences the growth: a pixel can only be entered if it is
|
|
123
114
|
* marked there. That is what lets the eraser be the same tool as the brush —
|
|
@@ -177,8 +168,6 @@ export function backgroundMask(pixels, { points = null, tolerance = 0.18, within
|
|
|
177
168
|
return background;
|
|
178
169
|
}
|
|
179
170
|
|
|
180
|
-
/* -------------------------------------------------------------- brush ---- */
|
|
181
|
-
|
|
182
171
|
/**
|
|
183
172
|
* A round stroke into the paint layer: `value` 1 means "this is background
|
|
184
173
|
* after all", -1 means "put that back". The paint layer is kept SEPARATE from
|
|
@@ -235,8 +224,6 @@ export function combineMask(auto, paint, { width, height, paintWidth = width, pa
|
|
|
235
224
|
return out;
|
|
236
225
|
}
|
|
237
226
|
|
|
238
|
-
/* -------------------------------------------------------------- apply ---- */
|
|
239
|
-
|
|
240
227
|
/**
|
|
241
228
|
* Turn a mask into transparency: erode the fringe, soften the boundary, write
|
|
242
229
|
* the alpha. Split from the growth above so the editor can show a mask, let
|
|
@@ -257,7 +244,6 @@ function dropEdgeLines(data, width, height) {
|
|
|
257
244
|
let changed = 0;
|
|
258
245
|
for (let i = 0; i < total; i += 1) {
|
|
259
246
|
if (data[i * 4 + 3] < 8 || visited[i]) continue;
|
|
260
|
-
// flood this kept component
|
|
261
247
|
stack.length = 0;
|
|
262
248
|
stack.push(i);
|
|
263
249
|
visited[i] = 1;
|
|
@@ -336,7 +322,6 @@ export function applyMask(pixels, mask, { shrink = 1, feather = 1 } = {}) {
|
|
|
336
322
|
grown = next;
|
|
337
323
|
}
|
|
338
324
|
|
|
339
|
-
/* -- alpha, then a soft edge that only touches the boundary ------------- */
|
|
340
325
|
const alpha = new Float32Array(total);
|
|
341
326
|
let removed = 0;
|
|
342
327
|
for (let pixel = 0; pixel < total; pixel++) {
|
|
@@ -387,8 +372,6 @@ export function removeBackground(pixels, { points = null, tolerance = 0.18, shri
|
|
|
387
372
|
return applyMask(pixels, grown, { shrink, feather });
|
|
388
373
|
}
|
|
389
374
|
|
|
390
|
-
/* --------------------------------------------------------------- trim ---- */
|
|
391
|
-
|
|
392
375
|
/** The box the picture still occupies, or null when nothing survived. */
|
|
393
376
|
export function opaqueBounds({ data, width, height }, threshold = 8) {
|
|
394
377
|
let minX = width;
|
|
@@ -422,8 +405,6 @@ export function cropPixels({ data, width, height }, box) {
|
|
|
422
405
|
return { data: out, width: box.width, height: box.height };
|
|
423
406
|
}
|
|
424
407
|
|
|
425
|
-
/* ------------------------------------------------------------- browser ---- */
|
|
426
|
-
|
|
427
408
|
function defaultCanvas(width, height) {
|
|
428
409
|
if (typeof OffscreenCanvas === "function") return new OffscreenCanvas(width, height);
|
|
429
410
|
throw new Error("this browser cannot edit images — OffscreenCanvas is unavailable");
|
package/src/mesh-graph-clone.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* a Mixamo-as-statue (or any skinned GLB) can bind to the wrong bones. Graphs
|
|
6
6
|
* that actually contain a SkinnedMesh go through SkeletonUtils; everything
|
|
7
7
|
* else keeps the cheap deep clone.
|
|
8
|
+
*
|
|
9
|
+
* Both clones share materials with the cache, so each instance gets its own
|
|
10
|
+
* copies (marked `instanceOwned`, disposed with the instance): a per-object
|
|
11
|
+
* write such as `object.opacity` must not leak into other copies.
|
|
8
12
|
*/
|
|
9
13
|
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
|
10
14
|
|
|
@@ -18,6 +22,16 @@ export function graphHasSkinnedMesh(root) {
|
|
|
18
22
|
|
|
19
23
|
export function cloneMeshGraph(source) {
|
|
20
24
|
if (!source) return source;
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
const root = graphHasSkinnedMesh(source) ? cloneSkeleton(source) : source.clone(true);
|
|
26
|
+
const copies = new Map();
|
|
27
|
+
const own = (material) => {
|
|
28
|
+
if (!copies.has(material)) copies.set(material, material.clone());
|
|
29
|
+
return copies.get(material);
|
|
30
|
+
};
|
|
31
|
+
root.traverse((node) => {
|
|
32
|
+
if (!node.isMesh || !node.material) return;
|
|
33
|
+
node.material = Array.isArray(node.material) ? node.material.map(own) : own(node.material);
|
|
34
|
+
node.userData.instanceOwned = true;
|
|
35
|
+
});
|
|
36
|
+
return root;
|
|
23
37
|
}
|
package/src/mesh-sniff.js
CHANGED
|
@@ -61,7 +61,6 @@ export function looksLikeObj(bytes) {
|
|
|
61
61
|
const line = raw.trim();
|
|
62
62
|
if (!/^v\s+/.test(line)) continue;
|
|
63
63
|
const parts = line.split(/\s+/);
|
|
64
|
-
if (parts[0] !== "v") continue;
|
|
65
64
|
if ([parts[1], parts[2], parts[3]].every((part) => Number.isFinite(Number(part)))) return true;
|
|
66
65
|
}
|
|
67
66
|
return false;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A monotone piecewise cubic through authored knots (harmonic-mean tangents,
|
|
3
|
+
* Fritsch–Carlson family): it hits every knot exactly, never overshoots between
|
|
4
|
+
* two of them, runs flat across a local extremum and degrades to the exact
|
|
5
|
+
* straight lerp for two knots. The camera crane's height profile and a route's
|
|
6
|
+
* lean (bank / pitch) marks share it, so a dot means the same thing on both rails.
|
|
7
|
+
*
|
|
8
|
+
* @param {object[]} knots ascending by `t`; at least one
|
|
9
|
+
* @param {number} x position along the profile (clamped to the knots' span)
|
|
10
|
+
* @param {(knot: object) => number} read the knot's value; defaults to `knot.v`
|
|
11
|
+
*/
|
|
12
|
+
export function monotoneCubicAt(knots, x, read = (knot) => knot.v) {
|
|
13
|
+
const last = knots.length - 1;
|
|
14
|
+
const clamped = Math.max(knots[0].t, Math.min(knots[last].t, x));
|
|
15
|
+
if (clamped <= knots[0].t) return read(knots[0]);
|
|
16
|
+
if (clamped >= knots[last].t) return read(knots[last]);
|
|
17
|
+
let i = 0;
|
|
18
|
+
while (i < knots.length - 2 && clamped > knots[i + 1].t) i += 1;
|
|
19
|
+
const p0 = knots[i];
|
|
20
|
+
const p1 = knots[i + 1];
|
|
21
|
+
const h = p1.t - p0.t;
|
|
22
|
+
if (h < 1e-9) return read(p1);
|
|
23
|
+
const v0 = read(p0);
|
|
24
|
+
const v1 = read(p1);
|
|
25
|
+
const secant = (a, b) => (read(b) - read(a)) / Math.max(b.t - a.t, 1e-9);
|
|
26
|
+
const d = secant(p0, p1);
|
|
27
|
+
// harmonic mean of neighbouring secants; zero across a local extremum
|
|
28
|
+
const mono = (sa, sb) => (sa * sb <= 0 ? 0 : (2 * sa * sb) / (sa + sb));
|
|
29
|
+
const m0 = i > 0 ? mono(secant(knots[i - 1], p0), d) : d;
|
|
30
|
+
const m1 = i < knots.length - 2 ? mono(d, secant(p1, knots[i + 2])) : d;
|
|
31
|
+
const u = (clamped - p0.t) / h;
|
|
32
|
+
const u2 = u * u;
|
|
33
|
+
const u3 = u2 * u;
|
|
34
|
+
return (
|
|
35
|
+
(2 * u3 - 3 * u2 + 1) * v0 +
|
|
36
|
+
(u3 - 2 * u2 + u) * h * m0 +
|
|
37
|
+
(-2 * u3 + 3 * u2) * v1 +
|
|
38
|
+
(u3 - u2) * h * m1
|
|
39
|
+
);
|
|
40
|
+
}
|
package/src/motion/generation.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pure request construction. The caller supplies one rolled seed and sampled
|
|
2
2
|
// poses; no rig, store, random source, notification or network is touched here.
|
|
3
3
|
import { StudioProtocolError, compileStudioBeats } from '../studio-agent-protocol.js';
|
|
4
|
-
import { TIMELINE_FRAME_FPS as FPS } from '../
|
|
4
|
+
import { TIMELINE_FRAME_FPS as FPS } from '../timeline-fps.js';
|
|
5
5
|
import { judgeAuthoredPath, alignArdyPath } from '../ardy/waypoints.js';
|
|
6
6
|
import { planPosePin, PIN_BLOCKED } from '../ardy/pose-pin.js';
|
|
7
7
|
import { blocksFromRequest, replayPayload, replayTruncated } from '../take-recipe.js';
|
package/src/motion-store.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const MOTION_DB_NAME = "cozyclay.motions";
|
|
2
|
+
const MOTION_DB_VERSION = 1;
|
|
3
|
+
const MOTION_STORE_NAME = "motions";
|
|
4
4
|
|
|
5
5
|
const validId = (id) => typeof id === "string" && /^[0-9a-f]{64}$/i.test(id);
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
function normalizeMotion(record) {
|
|
8
8
|
if (!record || typeof record !== "object" || !validId(record.motionId)) return null;
|
|
9
9
|
if (record.encoding !== "base64" || typeof record.data !== "string") return null;
|
|
10
10
|
return { ...record, motionId: record.motionId.toLowerCase(), stored: true };
|
package/src/motion-trail.js
CHANGED
|
@@ -28,10 +28,11 @@ const LIMB_CHAINS = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* limbs-first so an overlapping grab
|
|
31
|
+
* The available trail tracks: root plus every IK chain endpoint and the head,
|
|
32
|
+
* coloured exactly like their viewport IK/FK handles (posestudio.jsx coding:
|
|
33
|
+
* arms orange, legs blue, torso yellow, head purple). The viewport can focus
|
|
34
|
+
* one track after selection; pick order is limbs-first so an overlapping grab
|
|
35
|
+
* prefers the finer target; hips last.
|
|
35
36
|
*/
|
|
36
37
|
export const TRAIL_TRACKS = [
|
|
37
38
|
{ id: "leftHand", joint: "LeftHand", color: "#ff8a3d" },
|
|
@@ -295,13 +296,6 @@ const matVec = (m, v) => [
|
|
|
295
296
|
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
|
|
296
297
|
m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
|
|
297
298
|
];
|
|
298
|
-
/** Inverse of a row-major 3x3 (callers keep it well conditioned). */
|
|
299
|
-
function inverse3(m) {
|
|
300
|
-
const [a, b, c, d, e, f, g, h, i] = m;
|
|
301
|
-
const A = e * i - f * h, B = f * g - d * i, C = d * h - e * g;
|
|
302
|
-
const det = a * A + b * B + c * C;
|
|
303
|
-
return [A, c * h - b * i, b * f - c * e, B, a * i - c * g, c * d - a * f, C, b * g - a * h, a * e - b * d].map((v) => v / det);
|
|
304
|
-
}
|
|
305
299
|
/** Shortest-arc rotation taking unit `a` onto unit `b` (Rodrigues). */
|
|
306
300
|
function arcRotation(a, b) {
|
|
307
301
|
const c = dot(a, b);
|
package/src/multimodel-ingest.js
CHANGED
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
// and a silent fallback to a plausible-looking default is how footage that was
|
|
9
9
|
// never read ends up looking ingested.
|
|
10
10
|
|
|
11
|
-
export const INGEST_STAGES = Object.freeze(["idle", "fetching", "probing", "ready", "error"]);
|
|
12
|
-
|
|
13
11
|
/** A successful extraction is not proof that vertical recovery was applied. */
|
|
14
12
|
export function trajectoryReceipt(report, korean = false) {
|
|
15
13
|
if (!report) return "";
|
|
@@ -242,7 +240,7 @@ export function fpsFromFrameTimes(times, fallback = 30) {
|
|
|
242
240
|
return { fps: Math.round(1 / median), measured: true };
|
|
243
241
|
}
|
|
244
242
|
|
|
245
|
-
|
|
243
|
+
function formatBytes(bytes) {
|
|
246
244
|
if (!Number.isFinite(bytes) || bytes < 0) return "";
|
|
247
245
|
if (bytes < 1024) return `${bytes} B`;
|
|
248
246
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
package/src/object-catalog.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ko, isKo } from "./locale.js";
|
|
3
|
-
import {
|
|
3
|
+
import { EMPTY_KIND, OBJECT_MENU_ENTRIES } from "./scene-objects.js";
|
|
4
4
|
|
|
5
5
|
const GROUP_LABELS_KO = {
|
|
6
6
|
Primitives: "기본 도형",
|
|
@@ -8,6 +8,7 @@ const GROUP_LABELS_KO = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
const OBJECT_LABELS_KO = {
|
|
11
|
+
Empty: "빈 오브젝트",
|
|
11
12
|
Cube: "큐브",
|
|
12
13
|
Sphere: "구",
|
|
13
14
|
Capsule: "캡슐",
|
|
@@ -19,7 +20,7 @@ const OBJECT_LABELS_KO = {
|
|
|
19
20
|
"Plane (aircraft)": "비행기",
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
function displayObjectGroupName(name) {
|
|
23
24
|
return isKo ? (GROUP_LABELS_KO[name] ?? name) : name;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -39,9 +40,9 @@ export function displayObjectLabel(label) {
|
|
|
39
40
|
/** The catalogue grouped by section. Shared with the hierarchy's right-click
|
|
40
41
|
* create menu so both entry points list the same objects
|
|
41
42
|
* (docs/unity-reference.md §9.7). */
|
|
42
|
-
|
|
43
|
+
function buildObjectMenuGroups() {
|
|
43
44
|
const groups = [];
|
|
44
|
-
for (const entry of
|
|
45
|
+
for (const entry of OBJECT_MENU_ENTRIES) {
|
|
45
46
|
const group = groups.find((item) => item.name === entry.group);
|
|
46
47
|
if (group) group.entries.push(entry);
|
|
47
48
|
else groups.push({ name: entry.group, entries: [entry] });
|
|
@@ -65,7 +66,8 @@ export function CatalogueEntries({ onPick }) {
|
|
|
65
66
|
>
|
|
66
67
|
<span className={`add-object-swatch ${entry.kind}`} style={{ background: entry.color }} aria-hidden="true" />
|
|
67
68
|
<span>{displayObjectLabel(entry.label)}</span>
|
|
68
|
-
|
|
69
|
+
{/* An empty has no size to quote: say what it is instead of "0 × 0 m". */}
|
|
70
|
+
<small>{entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
|
|
69
71
|
</button>
|
|
70
72
|
))}
|
|
71
73
|
</div>
|
package/src/object-gizmo.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
3
|
import * as THREE from "three";
|
|
4
4
|
import { GIZMO_LAYER, SHOT_ASPECT, fitAspect } from "./dualview.jsx";
|
|
@@ -228,21 +228,6 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
228
228
|
rootRef.current?.traverse((node) => node.layers?.set(GIZMO_LAYER));
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
/** pointer -> NDC inside the rect the shot camera actually rendered into */
|
|
232
|
-
const pointerRay = (event) => {
|
|
233
|
-
const pane = paneRef.current;
|
|
234
|
-
const camera = camRef.current;
|
|
235
|
-
if (!pane || !camera) return null;
|
|
236
|
-
const bounds = pane.getBoundingClientRect();
|
|
237
|
-
if (bounds.width < 2 || bounds.height < 2) return null;
|
|
238
|
-
const rect = imageRect(bounds);
|
|
239
|
-
tools.ndc.set(
|
|
240
|
-
((event.clientX - rect.x) / rect.w) * 2 - 1,
|
|
241
|
-
-((event.clientY - rect.y) / rect.h) * 2 + 1,
|
|
242
|
-
);
|
|
243
|
-
return Math.abs(tools.ndc.x) <= 1 && Math.abs(tools.ndc.y) <= 1 ? tools.ndc : null;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
231
|
/** the scene object under the pointer, if any (meshes carry the id on an
|
|
247
232
|
* ancestor group, so the hit walks up to find it) */
|
|
248
233
|
const pickObject = () => {
|
|
@@ -256,9 +241,18 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
256
241
|
// whichever surface is nearer.
|
|
257
242
|
tools.raycaster.layers.set(GIZMO_LAYER);
|
|
258
243
|
let ghostId = null;
|
|
244
|
+
let ghostIsEmpty = false;
|
|
259
245
|
const ghostHit = tools.raycaster.intersectObjects(scene.children, true).find((entry) => {
|
|
260
246
|
if (!entry.object.isMesh) return false;
|
|
261
247
|
for (let node = entry.object; node; node = node.parent) {
|
|
248
|
+
// An Empty's marker: its pick volume is a gizmo-layer mesh under
|
|
249
|
+
// the object's own group, so the owning record is the first
|
|
250
|
+
// ancestor carrying a sceneObjectId.
|
|
251
|
+
if (entry.object.userData?.emptyPick && node.userData?.sceneObjectId) {
|
|
252
|
+
ghostId = node.userData.sceneObjectId;
|
|
253
|
+
ghostIsEmpty = true;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
262
256
|
if (node.userData?.shotCameraPick) {
|
|
263
257
|
ghostId = "__shotcam__";
|
|
264
258
|
return true;
|
|
@@ -271,7 +265,9 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
271
265
|
return false;
|
|
272
266
|
});
|
|
273
267
|
tools.raycaster.layers.set(0);
|
|
274
|
-
|
|
268
|
+
// The Empty's marker is drawn over the set (no depth test), so what you
|
|
269
|
+
// see is what you click: it wins over whatever surface is behind it.
|
|
270
|
+
if (ghostHit && (ghostIsEmpty || !hit || ghostHit.distance < hit.distance)) return { id: ghostId, point: ghostHit.point.clone() };
|
|
275
271
|
if (!hit) return null;
|
|
276
272
|
for (let node = hit.object; node; node = node.parent) {
|
|
277
273
|
if (node.userData?.sceneObjectId) return { id: node.userData.sceneObjectId, point: hit.point.clone() };
|
|
@@ -478,9 +474,15 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
478
474
|
* null for the uniform-scale knob and for plane handles, which carry
|
|
479
475
|
* their own `plane` axes instead. */
|
|
480
476
|
const beginDrag = (kind, axis, dir, camera) => {
|
|
481
|
-
const
|
|
482
|
-
if (!
|
|
483
|
-
|
|
477
|
+
const shown = stateRef.current.object;
|
|
478
|
+
if (!shown) return false;
|
|
479
|
+
// The gizmo can be SHOWN on the pose a routed record is drawn at, while
|
|
480
|
+
// every channel a drag writes starts from the AUTHORED record: a start
|
|
481
|
+
// taken from the drawn numbers would land them in the authored ones
|
|
482
|
+
// (a jump of however far the car has driven). The pivot, and the yaw
|
|
483
|
+
// the card's corners are seen at, are the shown ones.
|
|
484
|
+
const live = stateRef.current.objects.find((entry) => entry.id === shown.id) ?? shown;
|
|
485
|
+
tools.origin.set(shown.x, gizmoHeight(shown), shown.z);
|
|
484
486
|
camera.getWorldDirection(tools.eye);
|
|
485
487
|
let plane;
|
|
486
488
|
let drag;
|
|
@@ -525,7 +527,7 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
525
527
|
// The card's own plane, so the grab tracks the picture rather than a
|
|
526
528
|
// world axis: a standee that has been turned is still resized by the
|
|
527
529
|
// corner the eye sees.
|
|
528
|
-
const yaw = ((
|
|
530
|
+
const yaw = ((shown.rot ?? 0) * Math.PI) / 180;
|
|
529
531
|
const right = new THREE.Vector3(Math.cos(yaw), 0, -Math.sin(yaw));
|
|
530
532
|
const up = new THREE.Vector3(0, 1, 0);
|
|
531
533
|
const normal = new THREE.Vector3().crossVectors(right, up).normalize();
|
|
@@ -631,17 +633,22 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
631
633
|
// side-effect free: it only answers "would you grab here?".
|
|
632
634
|
if (stateRef.current.claimPointer?.(event)) return;
|
|
633
635
|
const grabbed = pickHandle(event);
|
|
634
|
-
// A press ON the key-light sun
|
|
635
|
-
// the puck's own body-drag
|
|
636
|
-
//
|
|
636
|
+
// A press ON the key-light sun, or on a route or crane dot, outranks
|
|
637
|
+
// any handle overlapping it: the puck's own body-drag and the dot's
|
|
638
|
+
// own grab are the primary interaction there, and the centre
|
|
639
|
+
// plane-square would otherwise silently claim the grab. (A routed
|
|
640
|
+
// object drawn at frame 0 sits exactly on its route's start dot.)
|
|
637
641
|
if (grabbed && rayFrom(event)) {
|
|
638
642
|
tools.raycaster.layers.set(GIZMO_LAYER);
|
|
639
|
-
const
|
|
640
|
-
for (let node = entry.object; node; node = node.parent)
|
|
643
|
+
const dotClaims = tools.raycaster.intersectObjects(scene.children, true).some((entry) => {
|
|
644
|
+
for (let node = entry.object; node; node = node.parent) {
|
|
645
|
+
const data = node.userData;
|
|
646
|
+
if (data?.keyLightPick || data?.pathIndex !== undefined || data?.pathRing || data?.craneIndex !== undefined) return true;
|
|
647
|
+
}
|
|
641
648
|
return false;
|
|
642
649
|
});
|
|
643
650
|
tools.raycaster.layers.set(0);
|
|
644
|
-
if (
|
|
651
|
+
if (dotClaims) return;
|
|
645
652
|
}
|
|
646
653
|
if (
|
|
647
654
|
grabbed &&
|
|
@@ -769,9 +776,9 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
769
776
|
|
|
770
777
|
// QA hook: where each handle sits on screen, through the exact rect the
|
|
771
778
|
// picker builds its ray from. Headless checks drive real pointer events
|
|
772
|
-
// with it (harmless in normal use).
|
|
773
|
-
|
|
774
|
-
|
|
779
|
+
// with it (harmless in normal use). The character gizmo is a separate
|
|
780
|
+
// pick-only instance, so it needs its own mirror hook.
|
|
781
|
+
const getHandles = useCallback(() => {
|
|
775
782
|
const camera = camRef?.current;
|
|
776
783
|
const pane = paneRef?.current;
|
|
777
784
|
if (!camera || !pane) return [];
|
|
@@ -799,8 +806,15 @@ export default function ObjectGizmo({ object, objects = [], mode = "move", snap
|
|
|
799
806
|
y: rect.y + ((1 - point.y) / 2) * rect.h,
|
|
800
807
|
};
|
|
801
808
|
});
|
|
809
|
+
}, [camRef, paneRef, shotAspect]);
|
|
810
|
+
if (typeof window !== "undefined" && !pickOnly) window.__gizmoHandles = getHandles;
|
|
811
|
+
useEffect(() => {
|
|
812
|
+
if (typeof window === "undefined" || !pickOnly) return undefined;
|
|
813
|
+
window.__characterGizmoHandles = getHandles;
|
|
814
|
+
return () => {
|
|
815
|
+
if (window.__characterGizmoHandles === getHandles) delete window.__characterGizmoHandles;
|
|
802
816
|
};
|
|
803
|
-
}
|
|
817
|
+
}, [getHandles, pickOnly]);
|
|
804
818
|
|
|
805
819
|
if (!enabled || !object) {
|
|
806
820
|
handlesRef.current.clear();
|