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.
Files changed (246) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/INSTALL_FOR_AGENTS.md +146 -0
  3. package/README.md +74 -192
  4. package/THIRD_PARTY_NOTICES.md +0 -21
  5. package/bin/agent/agent-routes.mjs +29 -62
  6. package/bin/agent/agent-runner.mjs +5 -3
  7. package/bin/agent/main-agent.mjs +70 -0
  8. package/bin/agent/main-prompt.mjs +17 -0
  9. package/bin/agent/plan-tool.mjs +114 -0
  10. package/bin/agent/script-tool.mjs +2 -0
  11. package/bin/agent/sculpt-sketch.mjs +190 -0
  12. package/bin/agent/session-store.mjs +4 -3
  13. package/bin/agent/sketch-code.mjs +151 -0
  14. package/bin/agent/sketch-sandbox.mjs +102 -0
  15. package/bin/agent/studio-prompt.mjs +1 -1
  16. package/bin/agent/studio-task-routes.mjs +185 -0
  17. package/bin/agent/studio-tasks.mjs +156 -0
  18. package/bin/agent/studio-tools.mjs +3 -2
  19. package/bin/agent/studio-turn-transaction.mjs +76 -0
  20. package/bin/agent/turn-frames.mjs +4 -0
  21. package/bin/cozyclay.mjs +5 -7
  22. package/dist/app/index.html +7 -6
  23. package/dist/assets/TaskBoard-ngdqiu7u.js +1 -0
  24. package/dist/assets/app-BlwOpest.css +1 -0
  25. package/dist/assets/app-DmR7jBkz.js +5144 -0
  26. package/dist/assets/compile-worker-D0pKioa0.js +5 -0
  27. package/dist/assets/landing-y3EqDgSZ.js +1 -0
  28. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  29. package/dist/assets/motion3-compile-BB3_1-EB.js +1 -0
  30. package/dist/assets/scene-asset-sync-CBx7XRti.css +1 -0
  31. package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +13 -0
  32. package/dist/assets/task-board-ErQvfoaz.js +14 -0
  33. package/dist/assets/workflow-zQM-EKW1.js +179 -0
  34. package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
  35. package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
  36. package/dist/cozyclay-package.json +1 -1
  37. package/dist/greybox-to-video/index.html +2 -3
  38. package/dist/index.html +7 -57
  39. package/dist/llms.txt +2 -1
  40. package/dist/previs-software/index.html +2 -2
  41. package/dist/privacy/index.html +3 -7
  42. package/dist/seedance-camera-control/index.html +2 -2
  43. package/dist/sitemap.xml +5 -5
  44. package/dist/sw.js +5 -1
  45. package/dist/thumbs/blank-stage-dark.webp +0 -0
  46. package/dist/thumbs/blank-stage-light.webp +0 -0
  47. package/dist/workflow/index.html +5 -4
  48. package/mcp/mesh-file.mjs +3 -1
  49. package/mcp/server.mjs +3 -0
  50. package/mcp/tool-handlers.mjs +2 -2
  51. package/package.json +5 -3
  52. package/src/App.jsx +51 -282
  53. package/src/analytics.js +3 -148
  54. package/src/app-stage.jsx +1 -1
  55. package/src/asset-pane.jsx +5 -10
  56. package/src/auto-flat.js +10 -0
  57. package/src/bus-tasks.js +11 -5
  58. package/src/command-bus.js +1 -1
  59. package/src/commands/elements.js +4 -3
  60. package/src/commands/motion3-compile.js +51 -27
  61. package/src/commands/motion3.js +3 -3
  62. package/src/commands/objects.js +1 -1
  63. package/src/commands/project.js +1 -1
  64. package/src/commands/shot.js +16 -4
  65. package/src/controls.jsx +0 -11
  66. package/src/cuts.js +11 -0
  67. package/src/domains/cast.js +3 -2
  68. package/src/domains/motion.js +3 -10
  69. package/src/domains/playback.js +32 -0
  70. package/src/domains/scenes.js +3 -22
  71. package/src/domains/shots.js +5 -5
  72. package/src/dualview.jsx +11 -15
  73. package/src/hierarchy-panel.css +18 -4
  74. package/src/hierarchy-panel.jsx +12 -11
  75. package/src/kits/held.js +3 -2
  76. package/src/light-blockers.js +2 -2
  77. package/src/main.jsx +5 -0
  78. package/src/material-look.js +189 -0
  79. package/src/material-presets.js +78 -0
  80. package/src/material-textures.js +157 -0
  81. package/src/motion/ground.js +5 -14
  82. package/src/motion/playback-frame-store.js +79 -0
  83. package/src/motion/playback-probe.js +16 -0
  84. package/src/motion/playback-subject.js +7 -0
  85. package/src/motion/timeline.css +43 -5
  86. package/src/motion/timeline.jsx +4 -3
  87. package/src/motion3/body.js +37 -5
  88. package/src/motion3/carried.js +11 -3
  89. package/src/motion3/compile-job.js +1 -1
  90. package/src/motion3/compiler/compile.js +7 -1
  91. package/src/motion3/compiler/finger-close.js +137 -0
  92. package/src/motion3/compiler/limbs.js +18 -3
  93. package/src/motion3/compiler/pose.js +6 -0
  94. package/src/motion3/compiler/validate-objects.js +17 -2
  95. package/src/motion3/compiler/validate-parts.js +2 -1
  96. package/src/motion3/compiler/vocab.js +4 -4
  97. package/src/motion3/describe.js +2 -2
  98. package/src/motion3/fix/index.js +5 -3
  99. package/src/motion3/flight.js +38 -0
  100. package/src/motion3/free-motion.js +10 -7
  101. package/src/motion3/load.js +125 -0
  102. package/src/motion3/mass.js +23 -0
  103. package/src/motion3/object-keys.js +11 -6
  104. package/src/motion3/object-pose.js +11 -3
  105. package/src/motion3/pipeline.js +3 -0
  106. package/src/motion3/qa/explain.js +3 -1
  107. package/src/motion3/qa/grasp-checks.js +123 -0
  108. package/src/motion3/qa/hints-grasp.js +71 -0
  109. package/src/motion3/qa/index.js +3 -1
  110. package/src/motion3/qa/object-checks.js +7 -2
  111. package/src/motion3/qa/patch.js +2 -1
  112. package/src/motion3/qa/spec-checks.js +10 -1
  113. package/src/motion3/schema/fields.js +3 -3
  114. package/src/motion3/schema/topics.js +9 -8
  115. package/src/motion3/schema/types.js +4 -1
  116. package/src/motion3/seat.js +84 -0
  117. package/src/motion3/settle.js +11 -6
  118. package/src/motion3/shapes.js +70 -0
  119. package/src/motion3/skills/catch.js +71 -0
  120. package/src/motion3/skills/index.js +10 -3
  121. package/src/motion3/skills/info.js +13 -0
  122. package/src/motion3/skills/throw.js +72 -0
  123. package/src/object-catalog.jsx +0 -4
  124. package/src/object-gizmo.jsx +2 -1
  125. package/src/object-look.js +9 -5
  126. package/src/object-path.js +1 -1
  127. package/src/panels/ProjectPanel.jsx +42 -21
  128. package/src/panels/details.css +15 -4
  129. package/src/popover.js +37 -0
  130. package/src/posestudio.jsx +15 -10
  131. package/src/primitive-look.js +38 -0
  132. package/src/project-browser-art.jsx +19 -0
  133. package/src/project-browser.css +74 -121
  134. package/src/project-browser.jsx +36 -60
  135. package/src/props.jsx +24 -35
  136. package/src/render-passes.js +7 -1
  137. package/src/retired-kinds.js +37 -0
  138. package/src/scene-objects.js +5 -9
  139. package/src/scenes.js +4 -5
  140. package/src/sculpt-colours.js +65 -0
  141. package/src/sculpt-compare.js +13 -50
  142. package/src/sculpt-csg.js +112 -0
  143. package/src/sculpt-curve.js +123 -0
  144. package/src/sculpt-cutters.js +43 -0
  145. package/src/sculpt-detail.js +207 -0
  146. package/src/sculpt-geometry-loft-convex.js +100 -0
  147. package/src/sculpt-geometry-shapes.js +250 -0
  148. package/src/sculpt-geometry.js +33 -15
  149. package/src/sculpt-loft-rings.js +225 -0
  150. package/src/sculpt-noise.js +65 -0
  151. package/src/sculpt-part-fields.js +27 -0
  152. package/src/sculpt-recipe-read.js +65 -0
  153. package/src/sculpt-recipe.js +33 -75
  154. package/src/sculpt-run.js +1 -1
  155. package/src/sculpt-scatter-items.js +183 -0
  156. package/src/sculpt-scatter-shape.js +57 -0
  157. package/src/sculpt-scatter.js +124 -0
  158. package/src/sculpt-shapes-curved.js +211 -0
  159. package/src/sculpt-shapes-loft-convex.js +218 -0
  160. package/src/sculpt-shapes.js +138 -0
  161. package/src/sculpt-sketch-state.js +76 -0
  162. package/src/sculpt-sketch.jsx +124 -0
  163. package/src/sculpt-surface-fields.js +65 -0
  164. package/src/settings-menu.jsx +1 -20
  165. package/src/shell/BottomDock.jsx +9 -9
  166. package/src/shell/MenuBar.jsx +1 -9
  167. package/src/shell/PreferencesDialog.jsx +3 -4
  168. package/src/shell/ViewportToolbar.jsx +3 -3
  169. package/src/shell/add-camera-shot.js +11 -0
  170. package/src/shell/glass-regions.css +3 -2
  171. package/src/shell/glass.css +0 -5
  172. package/src/shell/lens-scrub-math.js +51 -0
  173. package/src/shell/lens-scrub.css +73 -0
  174. package/src/shell/lens-scrub.jsx +98 -0
  175. package/src/shell/mode.css +6 -5
  176. package/src/shell/preferences.css +1 -32
  177. package/src/shell/shell.css +18 -1
  178. package/src/shell/studio-shell-context.js +5 -0
  179. package/src/shot-frame.js +26 -0
  180. package/src/stage-canvas-rigs.jsx +7 -8
  181. package/src/stage-characters.jsx +26 -0
  182. package/src/stage-gizmo.js +43 -0
  183. package/src/stage-labels-ko.js +0 -6
  184. package/src/stage-startup-defaults.js +3 -9
  185. package/src/starter-scenes.js +5 -11
  186. package/src/studio-agent-commands.js +2 -2
  187. package/src/studio-agent-protocol.js +2 -7
  188. package/src/studio-app-binding.js +1 -1
  189. package/src/studio-elements.js +6 -2
  190. package/src/styles/themes.css +13 -0
  191. package/src/styles.css +6 -674
  192. package/src/text/text-object.jsx +1 -1
  193. package/src/ui.jsx +8 -16
  194. package/src/use-embed-bridge.js +3 -6
  195. package/src/use-render-activity.js +3 -1
  196. package/src/view-ghosts.jsx +5 -3
  197. package/src/viewport-visible-rect.js +37 -0
  198. package/src/workflow/AgentPanel.jsx +79 -46
  199. package/src/workflow/AgentPlanCard.jsx +41 -0
  200. package/src/workflow/AgentProgressCard.jsx +1 -1
  201. package/src/workflow/TaskBoard.jsx +102 -0
  202. package/src/workflow/TaskInline.jsx +114 -0
  203. package/src/workflow/agent-client.js +25 -36
  204. package/src/workflow/agent-panel.css +14 -1
  205. package/src/workflow/agent-plan.css +93 -0
  206. package/src/workflow/agent-plan.js +59 -0
  207. package/src/workflow/attachment-image.js +10 -3
  208. package/src/workflow/attachment-video.js +91 -0
  209. package/src/workflow/task-board.css +98 -0
  210. package/src/workflow/task-board.js +169 -0
  211. package/tools/check-eslint.mjs +2 -1
  212. package/tools/check-knip.mjs +2 -1
  213. package/tools/dev/pages/greybox-to-video.html +2 -3
  214. package/tools/dev/pages/previs-software.html +2 -2
  215. package/tools/dev/pages/privacy.html +3 -7
  216. package/tools/dev/pages/seedance-camera-control.html +2 -2
  217. package/tools/dev-full.mjs +11 -2
  218. package/tools/eslint-baseline.json +6 -6
  219. package/tools/knip-baseline.json +0 -11
  220. package/tools/loc-baseline.json +30 -31
  221. package/tools/metrics-snapshot.mjs +43 -5
  222. package/tools/render-start-thumbs.mjs +175 -0
  223. package/tools/run-tests.mjs +10 -17
  224. package/tools/sitemap.mjs +1 -1
  225. package/dist/assets/analytics-BibyE5ar.js +0 -6
  226. package/dist/assets/app-DhRJ-PBz.js +0 -5088
  227. package/dist/assets/app-f-fkV5uP.css +0 -1
  228. package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
  229. package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
  230. package/dist/assets/landing-DYg87MDY.js +0 -1
  231. package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
  232. package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
  233. package/dist/assets/workflow-BSA22Ofr.js +0 -179
  234. package/dist/scenes/city-block.cclayproject +0 -1
  235. package/src/burn-in.js +0 -91
  236. package/src/camera-tutorial.jsx +0 -520
  237. package/src/facing-marks.js +0 -88
  238. package/src/first-shot-handoff.js +0 -44
  239. package/src/panels/motion.css +0 -197
  240. package/src/set-pieces.jsx +0 -183
  241. package/src/tutorial-analytics.js +0 -86
  242. package/src/tutorial-walk.js +0 -17
  243. package/src/use-camera-tutorial.js +0 -50
  244. package/src/use-semantic-state.js +0 -12
  245. package/tools/metrics/archive-traffic.mjs +0 -138
  246. 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
+ }
@@ -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
- * AutoPhysics review measures every contact against ONE flat floor
7
- * (`floorY`). When a character stands on a box or a step its feet get pulled
8
- * down to the floor plane. This module answers "what is the ground here?" so
9
- * contact detection, span anchors, and toe minimum heights can use the
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
+ }
@@ -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-clip-inset, 7px));
785
- width: max(var(--tl-min-clip-width, 1px), calc((var(--tl-f-end) - var(--tl-f-start)) * 100% - (var(--tl-clip-inset, 7px) * 2)));
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: calc(var(--tl-track-row-height, 38px) / 2);
931
- bottom: auto;
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
- transform: translate(-50%, -50%) rotate(45deg);
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);
@@ -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 a whole, grabbable circle
277
- style={{ left: `clamp(9px, ${xFor(point.t) * 100}%, calc(100% - 9px))`, top: `clamp(9px, ${yFor(point.height) * 100}%, calc(100% - 9px))` }}
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)}
@@ -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
- const BODIES = { "y-bot-tpose": yBody, "x-bot-tpose": xBody };
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
- const fit = BODIES[sk.model];
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) };
@@ -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
- /** World { pos, R } of a carried record on pose s (at/along offset, PR #14). */
27
- export function carriedPose(sk, spec, s, o) {
28
- const ax = palmAxes(sk, s, SIDES[spec.props[o.prop].hand]), origin = propOrigin(sk, spec, s, o);
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 only), the spec as sent, and
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);