cozyclay 3.0.3 → 3.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 +93 -0
- package/INSTALL_FOR_AGENTS.md +146 -0
- package/README.md +74 -192
- package/THIRD_PARTY_NOTICES.md +0 -21
- package/bin/agent/agent-routes.mjs +29 -62
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/agent/main-agent.mjs +70 -0
- package/bin/agent/main-prompt.mjs +17 -0
- package/bin/agent/plan-tool.mjs +114 -0
- package/bin/agent/script-tool.mjs +2 -0
- package/bin/agent/sculpt-sketch.mjs +190 -0
- package/bin/agent/session-store.mjs +4 -3
- package/bin/agent/sketch-code.mjs +151 -0
- package/bin/agent/sketch-sandbox.mjs +102 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-task-routes.mjs +185 -0
- package/bin/agent/studio-tasks.mjs +156 -0
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/agent/studio-turn-transaction.mjs +76 -0
- package/bin/agent/turn-frames.mjs +4 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-ngdqiu7u.js +1 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/app-DmR7jBkz.js +5144 -0
- package/dist/assets/compile-worker-D0pKioa0.js +5 -0
- package/dist/assets/landing-y3EqDgSZ.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BB3_1-EB.js +1 -0
- package/dist/assets/scene-asset-sync-CBx7XRti.css +1 -0
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +13 -0
- package/dist/assets/task-board-ErQvfoaz.js +14 -0
- package/dist/assets/workflow-zQM-EKW1.js +179 -0
- package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
- package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +2 -3
- package/dist/index.html +7 -57
- package/dist/llms.txt +2 -1
- package/dist/previs-software/index.html +2 -2
- package/dist/privacy/index.html +3 -7
- package/dist/seedance-camera-control/index.html +2 -2
- package/dist/sitemap.xml +5 -5
- package/dist/sw.js +5 -1
- package/dist/thumbs/blank-stage-dark.webp +0 -0
- package/dist/thumbs/blank-stage-light.webp +0 -0
- package/dist/workflow/index.html +5 -4
- package/mcp/mesh-file.mjs +3 -1
- package/mcp/server.mjs +3 -0
- package/mcp/tool-handlers.mjs +2 -2
- package/package.json +5 -3
- package/src/App.jsx +51 -282
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-tasks.js +11 -5
- package/src/command-bus.js +1 -1
- package/src/commands/elements.js +4 -3
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +3 -3
- package/src/commands/objects.js +1 -1
- package/src/commands/project.js +1 -1
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/material-look.js +189 -0
- package/src/material-presets.js +78 -0
- package/src/material-textures.js +157 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/compile.js +7 -1
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/fix/index.js +5 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +3 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +10 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +9 -8
- package/src/motion3/schema/types.js +4 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +10 -3
- package/src/motion3/skills/info.js +13 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-look.js +9 -5
- package/src/object-path.js +1 -1
- package/src/panels/ProjectPanel.jsx +42 -21
- package/src/panels/details.css +15 -4
- package/src/popover.js +37 -0
- package/src/posestudio.jsx +15 -10
- package/src/primitive-look.js +38 -0
- package/src/project-browser-art.jsx +19 -0
- package/src/project-browser.css +74 -121
- package/src/project-browser.jsx +36 -60
- package/src/props.jsx +24 -35
- package/src/render-passes.js +7 -1
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +5 -9
- package/src/scenes.js +4 -5
- package/src/sculpt-colours.js +65 -0
- package/src/sculpt-compare.js +13 -50
- package/src/sculpt-csg.js +112 -0
- package/src/sculpt-curve.js +123 -0
- package/src/sculpt-cutters.js +43 -0
- package/src/sculpt-detail.js +207 -0
- package/src/sculpt-geometry-loft-convex.js +100 -0
- package/src/sculpt-geometry-shapes.js +250 -0
- package/src/sculpt-geometry.js +33 -15
- package/src/sculpt-loft-rings.js +225 -0
- package/src/sculpt-noise.js +65 -0
- package/src/sculpt-part-fields.js +27 -0
- package/src/sculpt-recipe-read.js +65 -0
- package/src/sculpt-recipe.js +33 -75
- package/src/sculpt-run.js +1 -1
- package/src/sculpt-scatter-items.js +183 -0
- package/src/sculpt-scatter-shape.js +57 -0
- package/src/sculpt-scatter.js +124 -0
- package/src/sculpt-shapes-curved.js +211 -0
- package/src/sculpt-shapes-loft-convex.js +218 -0
- package/src/sculpt-shapes.js +138 -0
- package/src/sculpt-sketch-state.js +76 -0
- package/src/sculpt-sketch.jsx +124 -0
- package/src/sculpt-surface-fields.js +65 -0
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +1 -1
- package/src/studio-elements.js +6 -2
- package/src/styles/themes.css +13 -0
- package/src/styles.css +6 -674
- package/src/text/text-object.jsx +1 -1
- package/src/ui.jsx +8 -16
- package/src/use-embed-bridge.js +3 -6
- package/src/use-render-activity.js +3 -1
- package/src/view-ghosts.jsx +5 -3
- package/src/viewport-visible-rect.js +37 -0
- package/src/workflow/AgentPanel.jsx +79 -46
- package/src/workflow/AgentPlanCard.jsx +41 -0
- package/src/workflow/AgentProgressCard.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +102 -0
- package/src/workflow/TaskInline.jsx +114 -0
- package/src/workflow/agent-client.js +25 -36
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/agent-plan.css +93 -0
- package/src/workflow/agent-plan.js +59 -0
- package/src/workflow/attachment-image.js +10 -3
- package/src/workflow/attachment-video.js +91 -0
- package/src/workflow/task-board.css +98 -0
- package/src/workflow/task-board.js +169 -0
- package/tools/check-eslint.mjs +2 -1
- package/tools/check-knip.mjs +2 -1
- package/tools/dev/pages/greybox-to-video.html +2 -3
- package/tools/dev/pages/previs-software.html +2 -2
- package/tools/dev/pages/privacy.html +3 -7
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/dev-full.mjs +11 -2
- package/tools/eslint-baseline.json +6 -6
- package/tools/knip-baseline.json +0 -11
- package/tools/loc-baseline.json +30 -31
- package/tools/metrics-snapshot.mjs +43 -5
- package/tools/render-start-thumbs.mjs +175 -0
- package/tools/run-tests.mjs +10 -17
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-BibyE5ar.js +0 -6
- package/dist/assets/app-DhRJ-PBz.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
- package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
- package/dist/assets/landing-DYg87MDY.js +0 -1
- package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-BSA22Ofr.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Material presets: named surface looks for sculpt parts (`material`, `finish`)
|
|
3
|
+
* and plain primitives (`object.material`, `object.finish`). Pure data and the
|
|
4
|
+
* record rules, no three.js, so the recipe reader, the scene record and Node
|
|
5
|
+
* tests share one list; material-look.js turns an entry into a material.
|
|
6
|
+
*
|
|
7
|
+
* A record keeps only the names. `color` stays the base tint ("#c24a3b" with
|
|
8
|
+
* wood is red-painted wood), and a colour left at the blockout grey takes the
|
|
9
|
+
* preset's own (brick with no colour draws brick red). `finish` replaces the
|
|
10
|
+
* preset's roughness: chrome is metal + gloss, frosted glass is glass + matte.
|
|
11
|
+
* Clay with no finish is exactly the look every prop wore before presets.
|
|
12
|
+
*
|
|
13
|
+
* An entry (every field optional): roughness, metalness, color (its natural
|
|
14
|
+
* tint), physical (MeshPhysicalMaterial fields), env (reflection strength on
|
|
15
|
+
* a stage with no environment of its own), and a pattern with its tile in
|
|
16
|
+
* metres, the axis its grain runs along (up: the part's own y; long: its
|
|
17
|
+
* longest side; hang: its most upright side, skipping a slab's thickness),
|
|
18
|
+
* albedo contrast, bump depth in metres, roughness variation and a mask
|
|
19
|
+
* colour (brick mortar).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { GREY_BOX } from "./object-colors.js";
|
|
23
|
+
|
|
24
|
+
export const MATERIAL_LOOKS = Object.freeze({
|
|
25
|
+
clay: { roughness: 0.82 },
|
|
26
|
+
wood: { roughness: 0.68, color: "#9a6b3f", pattern: "wood", tile: 0.5, grain: "long", contrast: 0.32, bump: 0.0015, roughVar: 0.1 },
|
|
27
|
+
straw: { roughness: 0.88, color: "#c8a85c", pattern: "straw", tile: 0.6, grain: "hang", contrast: 0.5, bump: 0.02, roughVar: 0.08 },
|
|
28
|
+
stone: { roughness: 0.9, color: "#8d8a83", pattern: "stone", tile: 1, contrast: 0.32, bump: 0.008, roughVar: 0.1 },
|
|
29
|
+
brick: { roughness: 0.88, color: "#a5503a", pattern: "brick", tile: 0.5, grain: "hang", contrast: 0.22, bump: 0.008, roughVar: 0.08, mask: "#b9b3a8" },
|
|
30
|
+
concrete: { roughness: 0.92, color: "#a7a49d", pattern: "concrete", tile: 1.2, contrast: 0.2, bump: 0.0015, roughVar: 0.08 },
|
|
31
|
+
metal: { roughness: 0.32, metalness: 1, color: "#c9ccd1", env: 1, pattern: "brushed", tile: 0.4, contrast: 0.06, bump: 0.0001, roughVar: 0.12 },
|
|
32
|
+
glass: { roughness: 0.04, color: "#f2f7f7", env: 1, physical: { transmission: 1, ior: 1.5 } },
|
|
33
|
+
ceramic: { roughness: 0.35, color: "#efece4", env: 0.45, physical: { clearcoat: 1, clearcoatRoughness: 0.06 } },
|
|
34
|
+
plastic: { roughness: 0.3, env: 0.35 },
|
|
35
|
+
rubber: { roughness: 0.92, color: "#2d2d2f", pattern: "stipple", tile: 0.15, contrast: 0.08, bump: 0.0003, roughVar: 0.06 },
|
|
36
|
+
fabric: { roughness: 0.95, pattern: "weave", tile: 0.12, contrast: 0.22, bump: 0.0008, roughVar: 0.05, physical: { sheen: 1, sheenRoughness: 0.6 } },
|
|
37
|
+
paper: { roughness: 0.88, color: "#ebe6da", pattern: "paper", tile: 0.25, contrast: 0.08, bump: 0.0002 },
|
|
38
|
+
foliage: { roughness: 0.62, color: "#4f7d34", pattern: "leaves", tile: 0.6, contrast: 0.45, bump: 0.015, roughVar: 0.15, physical: { sheen: 0.8, sheenRoughness: 0.45 } },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const MATERIAL_PRESETS = Object.freeze(Object.keys(MATERIAL_LOOKS));
|
|
42
|
+
/** What a finish sets the roughness to, whatever the preset's own. */
|
|
43
|
+
export const FINISH_ROUGHNESS = Object.freeze({ matte: 0.85, satin: 0.45, gloss: 0.12 });
|
|
44
|
+
export const MATERIAL_FINISHES = Object.freeze(Object.keys(FINISH_ROUGHNESS));
|
|
45
|
+
|
|
46
|
+
/** The colour a surface draws in: the authored one, or the preset's own while
|
|
47
|
+
* the authored one is still the blockout grey. */
|
|
48
|
+
export function lookTint(color, preset) {
|
|
49
|
+
const own = MATERIAL_LOOKS[preset]?.color;
|
|
50
|
+
return own && (!color || color.toLowerCase() === GREY_BOX) ? own : color ?? GREY_BOX;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The record renderers that draw a preset (props.jsx Primitive). */
|
|
54
|
+
const LOOK_RENDERERS = new Set(["cube", "sphere", "capsule", "cylinder", "cone", "plane"]);
|
|
55
|
+
const LOOK_KEYS = [["material", MATERIAL_PRESETS], ["finish", MATERIAL_FINISHES]];
|
|
56
|
+
|
|
57
|
+
/** The patch keys object.update reads for the look, on the renderers that draw one. */
|
|
58
|
+
export const objectMaterialKeys = (renderer) => (LOOK_RENDERERS.has(renderer) ? LOOK_KEYS.map(([key]) => key) : []);
|
|
59
|
+
|
|
60
|
+
/** The material/finish a normalized record keeps: known names on a primitive,
|
|
61
|
+
* nothing otherwise, so records written before presets come back unchanged. */
|
|
62
|
+
export function objectMaterialFields(record, renderer) {
|
|
63
|
+
if (!LOOK_RENDERERS.has(renderer)) return {};
|
|
64
|
+
return Object.fromEntries(LOOK_KEYS.filter(([key, names]) => names.includes(record?.[key])).map(([key]) => [key, record[key]]));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The material/finish part of an object.update patch: a known name sets it,
|
|
68
|
+
* null removes it (back to the preset default), anything else is ignored. */
|
|
69
|
+
export function objectMaterialPatch(object, patch) {
|
|
70
|
+
if (!LOOK_RENDERERS.has(object?.renderer)) return {};
|
|
71
|
+
const update = {};
|
|
72
|
+
for (const [key, names] of LOOK_KEYS) {
|
|
73
|
+
const value = patch?.[key];
|
|
74
|
+
if (value === undefined || (value !== null && !names.includes(value))) continue;
|
|
75
|
+
if ((value ?? undefined) !== object[key]) update[key] = value;
|
|
76
|
+
}
|
|
77
|
+
return update;
|
|
78
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedural surface detail for the material presets: one small seamless tile
|
|
3
|
+
* per pattern, from integer hashes, so every machine draws the same pixels.
|
|
4
|
+
* Pure arithmetic, no three.js and no DOM; material-look.js wraps the bytes in
|
|
5
|
+
* a DataTexture the first time a preset is drawn, and never in Node.
|
|
6
|
+
*
|
|
7
|
+
* Channels, each 0..255: R albedo detail (128 is the authored colour), G
|
|
8
|
+
* height (0..255 spans the preset's bump depth), B roughness offset (128 is
|
|
9
|
+
* none), A where a second colour shows (brick mortar). Each pattern is drawn
|
|
10
|
+
* in tile coordinates u (across) and v (along the grain, or up), both 0..1;
|
|
11
|
+
* R and B are re-centred on their mean afterwards, so a preset never shifts
|
|
12
|
+
* the colour the author picked.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const PATTERN_SIZE = 256;
|
|
16
|
+
|
|
17
|
+
function hash(x, y, seed) {
|
|
18
|
+
let h = Math.imul(x, 0x27d4eb2d) ^ Math.imul(y, 0x165667b1) ^ Math.imul(seed, 0x9e3779b1);
|
|
19
|
+
h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
|
|
20
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
21
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
22
|
+
}
|
|
23
|
+
const wrap = (i, n) => ((i % n) + n) % n;
|
|
24
|
+
const smooth = (t) => t * t * (3 - 2 * t);
|
|
25
|
+
const step = (a, b, x) => smooth(Math.min(1, Math.max(0, (x - a) / (b - a))));
|
|
26
|
+
|
|
27
|
+
/** Value noise on a px by py lattice over the tile, seamless at its edges. */
|
|
28
|
+
function noise(u, v, px, py, seed) {
|
|
29
|
+
const x = u * px, y = v * py, ix = Math.floor(x), iy = Math.floor(y);
|
|
30
|
+
const fx = smooth(x - ix), fy = smooth(y - iy);
|
|
31
|
+
const x0 = wrap(ix, px), x1 = wrap(ix + 1, px), y0 = wrap(iy, py), y1 = wrap(iy + 1, py);
|
|
32
|
+
const a = hash(x0, y0, seed), b = hash(x1, y0, seed), c = hash(x0, y1, seed), d = hash(x1, y1, seed);
|
|
33
|
+
return a + (b - a) * fx + (c - a) * fy + (a - b - c + d) * fx * fy;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fbm(u, v, px, py, octaves, seed) {
|
|
37
|
+
let sum = 0, amp = 0.5, total = 0;
|
|
38
|
+
for (let o = 0; o < octaves; o += 1) {
|
|
39
|
+
sum += amp * noise(u, v, px << o, py << o, seed + o);
|
|
40
|
+
total += amp;
|
|
41
|
+
amp /= 2;
|
|
42
|
+
}
|
|
43
|
+
return sum / total;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Worley cells on an n by n jittered grid, seamless: the nearest and second
|
|
47
|
+
* nearest feature distances (tile units) and the nearest cell's hash. */
|
|
48
|
+
function cells(u, v, n, seed) {
|
|
49
|
+
const x = u * n, y = v * n, ix = Math.floor(x), iy = Math.floor(y);
|
|
50
|
+
let f1 = 9, f2 = 9, id = 0;
|
|
51
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
52
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
53
|
+
const cx = ix + dx, cy = iy + dy, wx = wrap(cx, n), wy = wrap(cy, n);
|
|
54
|
+
const d = Math.hypot(cx + hash(wx, wy, seed) - x, cy + hash(wx, wy, seed + 1) - y) / n;
|
|
55
|
+
if (d < f1) {
|
|
56
|
+
f2 = f1;
|
|
57
|
+
f1 = d;
|
|
58
|
+
id = hash(wx, wy, seed + 2);
|
|
59
|
+
} else if (d < f2) f2 = d;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { f1, f2, id };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Each pattern: (u, v) to [albedo, height, roughness, mask]. */
|
|
66
|
+
const PATTERNS = {
|
|
67
|
+
// Ten rings across the tile, wandering along it, over fine fibres.
|
|
68
|
+
wood(u, v) {
|
|
69
|
+
const late = (0.5 + 0.5 * Math.sin(2 * Math.PI * (u * 10 + fbm(u, v, 2, 1, 3, 11) * 2.2))) ** 6;
|
|
70
|
+
const fibre = noise(u, v, 128, 4, 12), fleck = fbm(u, v, 16, 2, 2, 13);
|
|
71
|
+
return [0.5 - 0.45 * late + 0.25 * fibre + 0.2 * fleck, 0.6 * fibre - 0.5 * late, 0.5 + 0.4 * late - 0.2 * fibre, 0];
|
|
72
|
+
},
|
|
73
|
+
// Four overlapping courses of strands; each course is thickest at its ragged
|
|
74
|
+
// lower edge and shaded just under the edge of the course above.
|
|
75
|
+
straw(u, v) {
|
|
76
|
+
const strand = 0.6 * noise(u, v, 192, 3, 21) + 0.4 * noise(u, v, 96, 2, 22);
|
|
77
|
+
const course = v * 4 + 0.12 * noise(u, v, 48, 1, 23), cv = course - Math.floor(course);
|
|
78
|
+
const shade = step(0.55, 1, cv);
|
|
79
|
+
return [0.5 * strand - 0.45 * shade + 0.1 * fbm(u, v, 6, 2, 2, 24), 0.7 * (1 - cv) + 0.3 * strand, 0.5 + 0.2 * shade, 0];
|
|
80
|
+
},
|
|
81
|
+
// Weathered fieldstone: lumpy noise, stones of their own tone, dark joints.
|
|
82
|
+
stone(u, v) {
|
|
83
|
+
const lump = fbm(u, v, 4, 4, 5, 31), cell = cells(u, v, 5, 32), grain = noise(u, v, 128, 128, 33);
|
|
84
|
+
const edge = cell.f2 - cell.f1, crack = 1 - step(0, 0.012, edge);
|
|
85
|
+
return [0.35 * lump + 0.25 * cell.id + 0.12 * grain - 0.35 * crack, 0.5 * lump + 0.35 * step(0, 0.08, edge) + 0.1 * grain, 0.25 * lump + 0.15 * crack, 0];
|
|
86
|
+
},
|
|
87
|
+
// Running bond: six 83 mm courses of two 250 mm bricks on the 0.5 m tile,
|
|
88
|
+
// 10 mm recessed mortar joints (the mask), each brick its own tone.
|
|
89
|
+
brick(u, v) {
|
|
90
|
+
const row = Math.floor(v * 6), cv = v * 6 - row;
|
|
91
|
+
const across = u * 2 + (row % 2 ? 0.5 : 0), col = Math.floor(across), cu = across - col;
|
|
92
|
+
const gap = Math.min(Math.min(cu, 1 - cu) / 2, Math.min(cv, 1 - cv) / 6), joint = 0.01;
|
|
93
|
+
const grit = noise(u, v, 96, 96, 42), mortar = 1 - step(joint * 0.6, joint, gap);
|
|
94
|
+
return [0.35 * hash(wrap(col, 2), row, 41) + 0.15 * grit + 0.15 * fbm(u, v, 8, 8, 3, 43), 0.85 * step(joint * 0.6, joint * 2.2, gap) + 0.15 * grit, 0.1 * grit + 0.15 * mortar, mortar];
|
|
95
|
+
},
|
|
96
|
+
// Cast concrete: soft mottling, fine grit, scattered air pits.
|
|
97
|
+
concrete(u, v) {
|
|
98
|
+
const mottle = fbm(u, v, 3, 3, 5, 51), grit = noise(u, v, 160, 160, 52), pit = step(0.86, 0.95, noise(u, v, 48, 48, 53));
|
|
99
|
+
return [0.3 * mottle + 0.1 * grit - 0.25 * pit, 0.4 * mottle + 0.25 * grit - 0.6 * pit, 0.2 * mottle, 0];
|
|
100
|
+
},
|
|
101
|
+
// Brushed metal: fine lines across the tile and faint smudges in the sheen.
|
|
102
|
+
brushed(u, v) {
|
|
103
|
+
const brush = 0.6 * noise(u, v, 3, 220, 61) + 0.4 * noise(u, v, 2, 110, 62), smudge = fbm(u, v, 3, 3, 4, 63);
|
|
104
|
+
return [0.15 * brush + 0.1 * smudge, brush, 0.35 * brush + 0.4 * smudge, 0];
|
|
105
|
+
},
|
|
106
|
+
stipple(u, v) {
|
|
107
|
+
const fine = noise(u, v, 128, 128, 71), wear = fbm(u, v, 6, 6, 3, 72);
|
|
108
|
+
return [0.2 * wear + 0.1 * fine, fine, 0.3 * wear, 0];
|
|
109
|
+
},
|
|
110
|
+
// Plain weave: 32 threads each way, over and under, each thread its own tone.
|
|
111
|
+
weave(u, v) {
|
|
112
|
+
const x = u * 32, y = v * 32, i = Math.floor(x), j = Math.floor(y);
|
|
113
|
+
const across = Math.sin(Math.PI * (x - i)), along = Math.sin(Math.PI * (y - j)), warp = (i + j) % 2 === 0;
|
|
114
|
+
const height = warp ? across * (0.75 + 0.25 * along) : along * (0.75 + 0.25 * across);
|
|
115
|
+
return [0.3 * height + 0.2 * (warp ? hash(i, 0, 81) : hash(0, j, 82)) + 0.1 * noise(u, v, 256, 256, 83), height, -0.2 * height, 0];
|
|
116
|
+
},
|
|
117
|
+
paper(u, v) {
|
|
118
|
+
const pulp = fbm(u, v, 8, 8, 4, 91), fibre = noise(u, v, 64, 8, 92) * noise(u, v, 8, 64, 93);
|
|
119
|
+
return [0.12 * pulp + 0.1 * fibre, pulp + 0.5 * fibre, 0.1 * pulp, 0];
|
|
120
|
+
},
|
|
121
|
+
// Two layers of leaf cells; the leaf nearer its own centre lies on top, and
|
|
122
|
+
// where neither covers the surface a dark gap shows through.
|
|
123
|
+
leaves(u, v) {
|
|
124
|
+
const big = cells(u, v, 6, 101), small = cells(u, v, 9, 104);
|
|
125
|
+
const domeBig = Math.max(0, 1 - big.f1 * 6 * 1.6), domeSmall = 0.9 * Math.max(0, 1 - small.f1 * 9 * 1.6);
|
|
126
|
+
const top = domeBig >= domeSmall ? big : small, dome = Math.max(domeBig, domeSmall), gap = 1 - step(0, 0.15, dome);
|
|
127
|
+
return [0.45 * top.id + 0.2 * dome - 0.5 * gap + 0.05 * noise(u, v, 64, 64, 107), dome, 0.3 * top.id + 0.3 * gap, 0];
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const PATTERN_NAMES = Object.freeze(Object.keys(PATTERNS));
|
|
132
|
+
|
|
133
|
+
/** The RGBA bytes of one pattern's tile, PATTERN_SIZE square, row 0 at v = 0. */
|
|
134
|
+
export function patternPixels(name) {
|
|
135
|
+
const draw = PATTERNS[name];
|
|
136
|
+
if (!draw) throw new Error(`unknown material pattern "${name}"`);
|
|
137
|
+
const count = PATTERN_SIZE * PATTERN_SIZE, raw = new Float32Array(count * 4);
|
|
138
|
+
for (let y = 0; y < PATTERN_SIZE; y += 1) {
|
|
139
|
+
for (let x = 0; x < PATTERN_SIZE; x += 1) raw.set(draw((x + 0.5) / PATTERN_SIZE, (y + 0.5) / PATTERN_SIZE), (y * PATTERN_SIZE + x) * 4);
|
|
140
|
+
}
|
|
141
|
+
let albedo = 0, rough = 0, low = Infinity, high = -Infinity;
|
|
142
|
+
for (let i = 0; i < count; i += 1) {
|
|
143
|
+
albedo += raw[i * 4] / count;
|
|
144
|
+
rough += raw[i * 4 + 2] / count;
|
|
145
|
+
low = Math.min(low, raw[i * 4 + 1]);
|
|
146
|
+
high = Math.max(high, raw[i * 4 + 1]);
|
|
147
|
+
}
|
|
148
|
+
const span = high - low || 1, byte = (value) => Math.round(255 * Math.min(1, Math.max(0, value)));
|
|
149
|
+
const out = new Uint8Array(count * 4);
|
|
150
|
+
for (let i = 0; i < count; i += 1) {
|
|
151
|
+
out[i * 4] = byte(raw[i * 4] - albedo + 0.5);
|
|
152
|
+
out[i * 4 + 1] = byte((raw[i * 4 + 1] - low) / span);
|
|
153
|
+
out[i * 4 + 2] = byte(raw[i * 4 + 2] - rough + 0.5);
|
|
154
|
+
out[i * 4 + 3] = byte(raw[i * 4 + 3]);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
package/src/motion/ground.js
CHANGED
|
@@ -3,20 +3,11 @@ import { isEffectivelyHidden, objectFootprintBounds } from "../scene-objects.js"
|
|
|
3
3
|
/**
|
|
4
4
|
* Ground height under a point, from the scene's objects (issue #250).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* surface actually under the foot.
|
|
11
|
-
*
|
|
12
|
-
* DRAFT - the sampler exists and is tested; physics-review.js does not call
|
|
13
|
-
* it yet. Wiring plan:
|
|
14
|
-
* 1. supportIntervals(): compare the sole to groundAt(toe.x, toe.z), not
|
|
15
|
-
* floorY, in the "low enough to be a contact" test.
|
|
16
|
-
* 2. Span anchors carry y = groundAt(anchor.x, anchor.z); footLockTrack
|
|
17
|
-
* anchors and relaxToePath's toeMinHeight take that value per frame.
|
|
18
|
-
* 3. Penetration / float metrics and the root lift measure against the
|
|
19
|
-
* local ground under each support site.
|
|
6
|
+
* Against ONE flat floor (`floorY`), a character standing on a box or a step
|
|
7
|
+
* would read as sunk into it. This module answers "what is the ground here?":
|
|
8
|
+
* studio-agent-motion.js samples it per support site, and physics-review.js
|
|
9
|
+
* takes it as `groundAt` for contact spans, foot anchors and the floor/float
|
|
10
|
+
* metrics, so they measure against the surface actually under the foot.
|
|
20
11
|
*
|
|
21
12
|
* Surface model: each scene object is an axis-aligned box from
|
|
22
13
|
* objectFootprintBounds (yaw-exact XZ footprint, flat top at topY). The
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import { shotIndexAtFrame } from "../cuts.js";
|
|
3
|
+
|
|
4
|
+
// The playhead lives here, outside React: playback ticks write it and only the
|
|
5
|
+
// components that show the frame subscribe, so App does not re-render per frame.
|
|
6
|
+
let currentFrame = 0;
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
let applyPlaybackFrame = null;
|
|
9
|
+
|
|
10
|
+
export function playbackFrame() {
|
|
11
|
+
return currentFrame;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setPlaybackFrame(next) {
|
|
15
|
+
const frame = typeof next === "function" ? next(currentFrame) : next;
|
|
16
|
+
if (!Number.isFinite(frame) || frame === currentFrame) return currentFrame;
|
|
17
|
+
currentFrame = frame;
|
|
18
|
+
if (typeof window !== "undefined") window.__cozyclayPlaybackFrame = frame;
|
|
19
|
+
for (const listener of listeners) listener();
|
|
20
|
+
return currentFrame;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function advancePlaybackFrame(frameCount, count) {
|
|
24
|
+
const frame = setPlaybackFrame((current) => (current + count) % frameCount);
|
|
25
|
+
applyPlaybackFrame?.(frame);
|
|
26
|
+
return frame;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerPlaybackFrameApplier(applier) {
|
|
30
|
+
applyPlaybackFrame = applier;
|
|
31
|
+
return () => {
|
|
32
|
+
if (applyPlaybackFrame === applier) applyPlaybackFrame = null;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createPlaybackFrameSetter({ getFrameCount, setReactFrame, onFrame }) {
|
|
37
|
+
return (update) => {
|
|
38
|
+
const next = typeof update === "function" ? update(playbackFrame()) : update;
|
|
39
|
+
const frame = Math.max(0, Math.min(getFrameCount() - 1, Math.round(next)));
|
|
40
|
+
setPlaybackFrame(frame);
|
|
41
|
+
setReactFrame(frame);
|
|
42
|
+
onFrame?.(frame);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function commitPlaybackFrame(setReactFrame, onFrame) {
|
|
47
|
+
const frame = playbackFrame();
|
|
48
|
+
setReactFrame(frame);
|
|
49
|
+
onFrame?.(frame);
|
|
50
|
+
return frame;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function usePlaybackSnapshot(getSnapshot) {
|
|
54
|
+
return useSyncExternalStore(subscribePlaybackFrame, getSnapshot, getSnapshot);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function usePlaybackEffects({ playing = false, commit = () => {}, onStop = null } = {}) {
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!playing) {
|
|
60
|
+
commit();
|
|
61
|
+
onStop?.();
|
|
62
|
+
}
|
|
63
|
+
// Runs on play/stop transitions only; commit and onStop are recreated every render.
|
|
64
|
+
}, [playing]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The shots domain's timeline frame: React state (owned by the caller) for edits, the store for playback.
|
|
68
|
+
* activeShotIdx subscribes to the store, so it re-renders only when playback crosses a cut. */
|
|
69
|
+
export function usePlaybackFrameState({ frameCount, setReactFrame, shots }) {
|
|
70
|
+
const setTlFrame = createPlaybackFrameSetter({ getFrameCount: () => frameCount, setReactFrame });
|
|
71
|
+
const activeShotIdx = usePlaybackSnapshot(() => shotIndexAtFrame(shots, playbackFrame()));
|
|
72
|
+
const activeShot = shots[activeShotIdx] ?? null;
|
|
73
|
+
return { setTlFrame, commitPlaybackFrame: () => commitPlaybackFrame(setReactFrame), activeShotIdx, activeShot };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function subscribePlaybackFrame(listener) {
|
|
77
|
+
listeners.add(listener);
|
|
78
|
+
return () => listeners.delete(listener);
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sampleAt } from "../sample-at.js";
|
|
2
|
+
|
|
3
|
+
export function playbackProbe(probe, frame, fallback) {
|
|
4
|
+
const sampled = probe.scene ? sampleAt(probe.scene, null, frame).subject : null;
|
|
5
|
+
const carried = sampled && probe.carry ? probe.carry(sampled, frame) : sampled;
|
|
6
|
+
return carried ?? fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function probeVisibility(vector, camera, subject, height) {
|
|
10
|
+
let visible = false;
|
|
11
|
+
for (const y of [0.1, height * 0.94]) {
|
|
12
|
+
vector.set(subject.x, y, subject.z).project(camera);
|
|
13
|
+
if (vector.z < 1 && Math.abs(vector.x) <= 1 && Math.abs(vector.y) <= 1) visible = true;
|
|
14
|
+
}
|
|
15
|
+
return visible;
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { sampleAt } from "../sample-at.js";
|
|
2
|
+
|
|
3
|
+
export function usePlaybackSubjectPosition({ motion, subjectCarry, scene, frame }) {
|
|
4
|
+
if (!motion && !subjectCarry) return null;
|
|
5
|
+
const subject = sampleAt(scene, null, frame).subject;
|
|
6
|
+
return subjectCarry ? subjectCarry(subject, frame) : subject;
|
|
7
|
+
}
|
package/src/motion/timeline.css
CHANGED
|
@@ -778,11 +778,13 @@
|
|
|
778
778
|
box-shadow: none;
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
+
/* Shots cut against each other: a hairline between neighbours, not a wide gutter, so a run of cuts reads as
|
|
782
|
+
one sequence. Free-camera time between shots still shows as its own gap. */
|
|
781
783
|
.v2-sequencer .tl-shot-block {
|
|
782
784
|
top: var(--tl-clip-inset, 7px);
|
|
783
785
|
bottom: var(--tl-clip-inset, 7px);
|
|
784
|
-
left: calc(var(--tl-f-start) * 100% + var(--tl-
|
|
785
|
-
width: max(var(--tl-min-clip-width, 1px), calc((var(--tl-f-end) - var(--tl-f-start)) * 100% - (var(--tl-
|
|
786
|
+
left: calc(var(--tl-f-start) * 100% + var(--tl-shot-gap, 1px));
|
|
787
|
+
width: max(var(--tl-min-clip-width, 1px), calc((var(--tl-f-end) - var(--tl-f-start)) * 100% - (var(--tl-shot-gap, 1px) * 2)));
|
|
786
788
|
padding: var(--sp-1, 8px) var(--sp-1, 8px) var(--tl-shot-footer-height, 28px);
|
|
787
789
|
border: 0;
|
|
788
790
|
border-radius: var(--r-1, 4px);
|
|
@@ -854,6 +856,10 @@
|
|
|
854
856
|
background: var(--select-tint, rgba(232, 163, 61, .14));
|
|
855
857
|
}
|
|
856
858
|
|
|
859
|
+
/* The trim grips appear with the shot under the pointer, not as a column of marks at every cut. */
|
|
860
|
+
.v2-sequencer .tl-shot-edge { opacity: 0; transition: opacity .12s; }
|
|
861
|
+
.v2-sequencer .tl-shot-block:is(:hover, :focus-within) .tl-shot-edge { opacity: 1; }
|
|
862
|
+
|
|
857
863
|
/* The bar hangs above the block's top-left corner instead of inside it: a
|
|
858
864
|
short cut (40 frames ~ 38px) is narrower than Split + Duplicate + Delete,
|
|
859
865
|
and an in-block bar would sit on the shot's name, turning a click on the
|
|
@@ -926,13 +932,22 @@
|
|
|
926
932
|
pointer-events: auto;
|
|
927
933
|
}
|
|
928
934
|
|
|
935
|
+
/* A camera key sits on its Shot's key strip (the line along the box's foot), as a small diamond: at the
|
|
936
|
+
lane's first-row height it landed on the box's top corner, in the gutter between two Shots. */
|
|
929
937
|
.v2-sequencer .tl-marker.cam {
|
|
930
|
-
top:
|
|
931
|
-
bottom:
|
|
938
|
+
top: auto;
|
|
939
|
+
bottom: calc(var(--tl-clip-inset, 7px) + var(--tl-key-strip-height, 12px) / 2);
|
|
940
|
+
width: 7px;
|
|
941
|
+
height: 7px;
|
|
942
|
+
border-radius: 1.5px;
|
|
932
943
|
background: var(--camera, #a78bfa);
|
|
933
|
-
|
|
944
|
+
box-shadow: 0 0 0 1.5px var(--tl-camera-clip, #3d3354);
|
|
945
|
+
transform: translate(-50%, 50%) rotate(45deg);
|
|
934
946
|
z-index: 7;
|
|
935
947
|
}
|
|
948
|
+
/* A key on a Shot's first or last frame steps just inside the box instead of straddling its edge. */
|
|
949
|
+
.v2-sequencer .tl-marker.cam[data-edge="start"] { margin-left: 7px; }
|
|
950
|
+
.v2-sequencer .tl-marker.cam[data-edge="end"] { margin-left: -7px; }
|
|
936
951
|
|
|
937
952
|
.v2-sequencer .tl-marker.wp {
|
|
938
953
|
background: var(--ok, #4fbf7a);
|
|
@@ -1151,6 +1166,29 @@
|
|
|
1151
1166
|
background: var(--select, #e8a33d);
|
|
1152
1167
|
}
|
|
1153
1168
|
|
|
1169
|
+
/* In a Shot box the height graph is a ~20 px strip: the points are small dots ON the curve, quiet on other
|
|
1170
|
+
shots and full on the selected or hovered one, and their hit area stays a full finger wide. Hovering a curve
|
|
1171
|
+
thickens it and the cursor says which way it moves. The axis labels have no room. */
|
|
1172
|
+
.v2-sequencer .tl-shot-block .tl-crane-knob {
|
|
1173
|
+
width: 7px;
|
|
1174
|
+
height: 7px;
|
|
1175
|
+
border: 1.5px solid var(--tl-camera-rail, #302a40);
|
|
1176
|
+
box-shadow: none;
|
|
1177
|
+
opacity: .5;
|
|
1178
|
+
transition: opacity .12s;
|
|
1179
|
+
}
|
|
1180
|
+
.v2-sequencer .tl-shot-block .sg-shot.height { z-index: 2; }
|
|
1181
|
+
.v2-sequencer .tl-shot-block .tl-crane-knob::before { content: ""; position: absolute; inset: -6px; border-radius: 50%; }
|
|
1182
|
+
.v2-sequencer .tl-shot-block .tl-crane-knob.selected { width: 9px; height: 9px; }
|
|
1183
|
+
.v2-sequencer .tl-shot-block:is(.selected, .active, :hover) .tl-crane-knob { opacity: 1; }
|
|
1184
|
+
.v2-sequencer .tl-shot-block .tl-crane-knob:hover, .v2-sequencer .tl-shot-block .tl-crane-knob:focus-visible { outline: none; transform: translate(-50%, -50%) scale(1.3); }
|
|
1185
|
+
.v2-sequencer .tl-shot-block .tl-crane-line { stroke-width: 1.5px; }
|
|
1186
|
+
.v2-sequencer .tl-shot-block .sg-shot .sg-body svg { cursor: ns-resize; }
|
|
1187
|
+
.v2-sequencer .tl-shot-block .sg-shot:hover :is(.sg-line, .tl-crane-line) { stroke-width: 2.5px; }
|
|
1188
|
+
.v2-sequencer .tl-shot-block .tl-crane-time-pick { opacity: 0; }
|
|
1189
|
+
.v2-sequencer .tl-shot-block .tl-crane-time-pick.selected { opacity: 1; }
|
|
1190
|
+
.v2-sequencer .tl-shot-block .tl-crane-editor :is(.sg-scale-top, .sg-scale-avg, .sg-scale-zero) { display: none; }
|
|
1191
|
+
|
|
1154
1192
|
.v2-sequencer .sg-shot {
|
|
1155
1193
|
left: var(--sp-1, 8px);
|
|
1156
1194
|
right: var(--sp-1, 8px);
|
package/src/motion/timeline.jsx
CHANGED
|
@@ -273,8 +273,9 @@ function CraneHeightEditor({ crane, railRange, durationFrames, selectedIndex, on
|
|
|
273
273
|
key={index}
|
|
274
274
|
type="button"
|
|
275
275
|
className={"tl-crane-knob" + (index === selectedIndex ? " selected" : "")}
|
|
276
|
-
// an end point sits ON the window edge; clamp it inside so the knob stays
|
|
277
|
-
|
|
276
|
+
// an end point sits ON the window edge; clamp it inside so the knob stays whole. The clamp is the
|
|
277
|
+
// knob's own radius: a wider one pinned every knob of a 20 px Shot strip to one row, off its curve.
|
|
278
|
+
style={{ left: `clamp(5px, ${xFor(point.t) * 100}%, calc(100% - 5px))`, top: `clamp(5px, ${yFor(point.height) * 100}%, calc(100% - 5px))` }}
|
|
278
279
|
aria-label={ko(`Crane point ${index + 1}, ${point.height.toFixed(1)} metres`, `크레인 점 ${index + 1}, ${point.height.toFixed(1)}미터`)}
|
|
279
280
|
onPointerDown={(event) => {
|
|
280
281
|
event.preventDefault();
|
|
@@ -2199,7 +2200,7 @@ export default function Timeline({
|
|
|
2199
2200
|
{name === SHOTS_LANE && shots.flatMap((shot, index) => (shot.cameraKeys ?? []).map((key) => (
|
|
2200
2201
|
<span
|
|
2201
2202
|
key={`${shot.id}:${key.frame}`}
|
|
2202
|
-
className="tl-marker cam"
|
|
2203
|
+
className="tl-marker cam" data-edge={key.frame <= shot.startFrame ? "start" : key.frame >= shot.endFrame ? "end" : undefined}
|
|
2203
2204
|
style={{ "--tl-f": framePct(key.frame, displayFrameCount) }}
|
|
2204
2205
|
title={isKo ? `${key.frame}프레임의 카메라 키 — 클릭해 이동, 드래그로 시간 변경, 오른쪽 클릭으로 삭제` : `Camera key at frame ${key.frame} — click to jump, drag to re-time, right-click to remove`}
|
|
2205
2206
|
onPointerDown={(e) => beginCameraKeyDrag(e, key, index)}
|
package/src/motion3/body.js
CHANGED
|
@@ -3,11 +3,43 @@
|
|
|
3
3
|
// outside positive, metres) and, for the limbs, the skin radius along the bone. A limb collides as
|
|
4
4
|
// spheres on its bone axis (limbProbes); bodyDistance is the nearest skin of every other bone but
|
|
5
5
|
// the limb's own neighbours (SKIP), so an arm reads the torso, the head, the legs and the other arm.
|
|
6
|
-
import yBody from "./rigs/y-bot-tpose-body.json" with { type: "json" };
|
|
7
|
-
import xBody from "./rigs/x-bot-tpose-body.json" with { type: "json" };
|
|
8
6
|
import { add, sub, scale, apply, T, len, dot, clamp } from "./compiler/math.js";
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
// The fits are ~0.5 MB of base64 grids, most of the editor's first-load JavaScript were they imported: each
|
|
9
|
+
// is an asset file instead, read the first time a compile needs it. Node reads it from disk; a worker (or the
|
|
10
|
+
// editor, should a compile run before preloadBodies has it) reads it synchronously over HTTP, so bodyFor
|
|
11
|
+
// stays synchronous everywhere. The editor fetches both in the background after its first paint (main.jsx).
|
|
12
|
+
const BODIES = {
|
|
13
|
+
"y-bot-tpose": new URL("./rigs/y-bot-tpose-body.json", import.meta.url),
|
|
14
|
+
"x-bot-tpose": new URL("./rigs/x-bot-tpose-body.json", import.meta.url),
|
|
15
|
+
};
|
|
16
|
+
const fits = new Map();
|
|
17
|
+
|
|
18
|
+
function readFit(url) {
|
|
19
|
+
const fs = url.protocol === "file:" ? globalThis.process?.getBuiltinModule?.("node:fs") : null;
|
|
20
|
+
if (fs) return JSON.parse(fs.readFileSync(url, "utf8"));
|
|
21
|
+
const xhr = new XMLHttpRequest();
|
|
22
|
+
xhr.open("GET", url.href, false);
|
|
23
|
+
xhr.send();
|
|
24
|
+
if (xhr.status !== 200) throw new Error(`motion body fit ${url.pathname}: HTTP ${xhr.status}`);
|
|
25
|
+
return JSON.parse(xhr.responseText);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fitOf(model) {
|
|
29
|
+
if (!fits.has(model)) fits.set(model, readFit(BODIES[model]));
|
|
30
|
+
return fits.get(model);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Fetches every rig's fit in the background, so a compile on this thread never waits on one. */
|
|
34
|
+
export function preloadBodies() {
|
|
35
|
+
return Promise.all(Object.entries(BODIES).map(async ([model, url]) => {
|
|
36
|
+
if (fits.has(model)) return;
|
|
37
|
+
const response = await fetch(url);
|
|
38
|
+
if (!response.ok) throw new Error(`motion body fit ${url.pathname}: HTTP ${response.status}`);
|
|
39
|
+
const fit = await response.json();
|
|
40
|
+
if (!fits.has(model)) fits.set(model, fit);
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
11
43
|
/** Distance read outside a grid: far enough that no probe counts it as touching. */
|
|
12
44
|
const FAR = 0.1;
|
|
13
45
|
const cache = new Map();
|
|
@@ -28,9 +60,9 @@ function skipOf(sk, b) {
|
|
|
28
60
|
|
|
29
61
|
/** The fitted body of skeleton sk, or null for a rig without one. */
|
|
30
62
|
export function bodyFor(sk) {
|
|
31
|
-
|
|
32
|
-
if (!fit) return null;
|
|
63
|
+
if (!BODIES[sk.model]) return null;
|
|
33
64
|
if (cache.has(sk.model)) return cache.get(sk.model);
|
|
65
|
+
const fit = fitOf(sk.model);
|
|
34
66
|
const grids = Object.entries(fit.grids).map(([name, g]) => {
|
|
35
67
|
const half = g.dims.map(d => ((d - 1) * fit.step) / 2), centre = g.min.map((m, k) => m + half[k]);
|
|
36
68
|
return { b: sk.index[name], min: g.min, dims: g.dims, data: decode(g.data), centre, reach: len(half) };
|
package/src/motion3/carried.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { palmAxes } from "./compiler/limbs.js";
|
|
9
9
|
import { SIDES } from "./compiler/pose.js";
|
|
10
10
|
import { add, scale, norm, sub, dot, cross, apply, mul, T, axisRot, I3 } from "./compiler/math.js";
|
|
11
|
+
import { bonePoint } from "./compiler/skeleton.js";
|
|
11
12
|
|
|
12
13
|
export const isCarried = o => typeof o?.prop === "string";
|
|
13
14
|
/** A follow entry { follow: <ref>, offset?, aim?, visible?, level? } (C16, follow.js): the record rides a point of the body. */
|
|
@@ -23,9 +24,16 @@ export function propOrigin(sk, spec, s, o) {
|
|
|
23
24
|
return scale(prop.origin.map(b => s.P[sk.index[b]]).reduce((a, p) => add(a, p), [0, 0, 0]), 1 / prop.origin.length);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
const
|
|
27
|
+
/** The prop origin carried by its hand's bone on pose s, where the bind pose has it against the hand: closing fingers do not move it (#816). */
|
|
28
|
+
function handOrigin(sk, prop, s) {
|
|
29
|
+
const h = sk.index[`${SIDES[prop.hand]}Hand`], P = sk.restWorldPos;
|
|
30
|
+
const mean = scale(prop.origin.map(b => P[sk.index[b]]).reduce((a, p) => add(a, p), [0, 0, 0]), 1 / prop.origin.length);
|
|
31
|
+
return bonePoint(s, h, apply(T(sk.restWorldRot[h]), sub(mean, P[h])));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** World { pos, R } of a carried record on pose s (at/along offset, PR #14); `fixed`: the origin rides the hand bone (seat.js). */
|
|
35
|
+
export function carriedPose(sk, spec, s, o, fixed = false) {
|
|
36
|
+
const ax = palmAxes(sk, s, SIDES[spec.props[o.prop].hand]), origin = fixed ? handOrigin(sk, spec.props[o.prop], s) : propOrigin(sk, spec, s, o);
|
|
29
37
|
const palm = v => add(scale(ax.d, v[0]), add(scale(ax.l, v[1]), scale(ax.n, v[2])));
|
|
30
38
|
const y = norm(palm(o.along ?? [0, 0, 1]));
|
|
31
39
|
let x = sub(ax.d, scale(y, dot(ax.d, y)));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// One motion compile as a job of plain data, the same in the compile worker (compile-worker.js) and on
|
|
2
2
|
// the calling thread (commands/motion3-compile.js, where no worker runs: Node, tests): the rig by model
|
|
3
|
-
// name, the motion scene (scene.js motionScene of the editor's objects: boxes
|
|
3
|
+
// name, the motion scene (scene.js motionScene of the editor's objects: oriented boxes; shapes.js reads a sphere or a cylinder round from them), the spec as sent, and
|
|
4
4
|
// what to do with it (probes, checks, lenient, repair, bake). The answer is plain data and typed arrays
|
|
5
5
|
// only (the compiler's out holds closures), so it crosses a worker boundary: QA, observations, the stored
|
|
6
6
|
// spec, and the baked take when asked.
|
|
@@ -35,6 +35,8 @@ import { fitWeight } from "../fix/self.js";
|
|
|
35
35
|
import { holdHook } from "../object-keys.js";
|
|
36
36
|
import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
|
|
37
37
|
import { objectPoseAt, handOf, restingAt, frameAt, spanOf } from "../object-pose.js";
|
|
38
|
+
import { heldFor } from "./finger-close.js";
|
|
39
|
+
import { sagOf } from "../load.js";
|
|
38
40
|
|
|
39
41
|
const smooth = x => x * x * (3 - 2 * x);
|
|
40
42
|
// how far past the straight arm a grasp fit's wrist target may end before it counts as wound up (solveFrame), m
|
|
@@ -136,6 +138,10 @@ function hooksFor(ctx, c, f, inFit = false, lead = true, fitSide = null) {
|
|
|
136
138
|
afterArm: (k, s, diag) => measureProps(ctx, k, s, diag),
|
|
137
139
|
// a fit's passes only place the wrist; the final pose (and a plain one) turns the hand onto the body
|
|
138
140
|
grip: inFit ? undefined : (k, s, diag) => gripHand(ctx, sk, s, SIDES[k], k, c, f, diag),
|
|
141
|
+
// a carry's weight lowers a plain IK hand's target, on the final pose only (load.js)
|
|
142
|
+
sag: inFit ? undefined : (k, s, target) => sagOf(ctx, k, f, s, target, (1 - fitWeight(ctx.spec, c, k)) * Math.min(1, Math.max(0, c[`armw_${k}`] ?? 0))),
|
|
143
|
+
// fingers close on what the hand holds, on the final pose only (finger-close.js)
|
|
144
|
+
heldShapes: inFit || !ctx.grasp ? undefined : (k, s) => heldFor(ctx, c, f, k, s),
|
|
139
145
|
};
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -186,7 +192,7 @@ export function compileMotion(spec, { sk, scene }, opts = {}) {
|
|
|
186
192
|
if (!(frameCount >= 1 && frameCount <= 20000)) throw new Error("MOTION_SPEC_INVALID: frames must be 1..20000");
|
|
187
193
|
const fix = fixOptions(spec.fix);
|
|
188
194
|
const [a, b] = opts.window ? [Math.max(0, opts.window[0]), Math.min(frameCount - 1, opts.window[1])] : [0, frameCount - 1];
|
|
189
|
-
const ctx = { sk, spec, scene, softReach: fix.reach, push: { l: new Map(), r: new Map() }, swivel: { l: new Map(), r: new Map() }, skinPush: { l: new Map(), r: new Map() }, keepOut: fix.self ? wristBody(sk) : null };
|
|
195
|
+
const ctx = { sk, spec, scene, grasp: fix.grasp, softReach: fix.reach, push: { l: new Map(), r: new Map() }, swivel: { l: new Map(), r: new Map() }, skinPush: { l: new Map(), r: new Map() }, keepOut: fix.self ? wristBody(sk) : null };
|
|
190
196
|
const out = { ctx, fps: spec.fps ?? 30, frameCount, f0: a, frames: [] };
|
|
191
197
|
// object anchors read the pose refs read (object-pose.js), on the frame being solved
|
|
192
198
|
ctx.objectPose = (id, f, now) => objectPoseAt(out, id, f, now);
|