cozyclay 2.1.1 → 3.0.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 (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -0,0 +1,96 @@
1
+ // Measured, not judged (output.observations.near): how close each hand and foot comes to the scene
2
+ // objects. A hand is its palm and its five fingertips, a foot its sole (geometry.js solePoints);
3
+ // every scene body is a box at its pose that frame (object-pose.js: carried, hinged or still).
4
+ // Per frame the nearest body within FAR m names its top group (the object) and the body (the
5
+ // part); consecutive frames near the same object make a run, and the RUNS closest runs are kept,
6
+ // in frame order. gap is the signed distance to the box (sdBox), < 0 inside. No run: nothing
7
+ // within FAR m. A hand is not measured against what it carries (it is on it), and a follow
8
+ // record (it rides the body) is none of these objects.
9
+ // Why: a hand meant to strum floated 20 cm in front of a guitar with QA clean (session df388a9b),
10
+ // and a toe went 4 cm into a stair's edge during a swing (session e7edb718); no other answer said so.
11
+ import { sdBox, solePoints } from "./geometry.js";
12
+ import { objectPoseAt, handOf } from "../object-pose.js";
13
+ import { isFollow } from "../carried.js";
14
+ import { add, apply, dist, len, scale } from "../compiler/math.js";
15
+
16
+ const FAR = 0.3, RUNS = 2, TIPS = ["Thumb4", "Index4", "Middle4", "Ring4", "Pinky4"];
17
+ const r3 = v => Math.round(v * 1000) / 1000;
18
+
19
+ export const NEAR_UNITS = "near: hand (palm, fingertips), foot (sole) to the nearest object within 0.3 m (top group; part: the body at the closest frame), 2 closest runs; gap [min, f, max, f] m, < 0 inside"
20
+
21
+ /** The effector points of a solved pose s: { hand_l, hand_r, foot_l, foot_r } -> [point, ...]. */
22
+ function effectorPoints(sk, s) {
23
+ const I = sk.index, out = {};
24
+ for (const [S, k] of [["Left", "l"], ["Right", "r"]]) {
25
+ const palm = scale(add(s.P[I[`${S}Hand`]], s.P[I[`${S}HandMiddle1`]]), 0.5);
26
+ out[`hand_${k}`] = [palm, ...TIPS.map(t => s.P[I[`${S}Hand${t}`]]).filter(Boolean)];
27
+ out[`foot_${k}`] = Object.values(solePoints(sk, s, S));
28
+ }
29
+ return out;
30
+ }
31
+
32
+ /** True while a spec.objects entry over body `id` (its own or a group's) hides it at absolute frame f. */
33
+ const hiddenAt = (out, id, f) => [id, ...(out.ctx.scene.objects[id].ancestors ?? [])]
34
+ .some(k => { const o = out.ctx.spec.objects?.[k]; return o?.visible && !((out.baked?.[o.visible]?.[f] ?? 0) > 0.5); });
35
+
36
+ /** The scene bodies at absolute frame f as boxes with their top group, the hand carrying them and a bounding sphere; follow records left out. */
37
+ function boxesAt(out, f) {
38
+ const { scene, spec } = out.ctx, boxes = [];
39
+ for (const [id, o] of Object.entries(scene?.objects ?? {})) {
40
+ const up = o.ancestors ?? [];
41
+ if ([id, ...up].some(k => isFollow(spec.objects?.[k])) || hiddenAt(out, id, f)) continue;
42
+ const pose = objectPoseAt(out, id, f);
43
+ if (!pose) continue;
44
+ const box = { pos: pose.pos, R: pose.R, half: o.half };
45
+ boxes.push({ id, root: up[up.length - 1] ?? id, hand: handOf(out, id, f), box, c: add(box.pos, apply(box.R, [0, o.half[1], 0])), r: len(o.half) });
46
+ }
47
+ return boxes;
48
+ }
49
+
50
+ /** Nearest box to a point set: [root, part, gap] within FAR, else null; the boxes hand `side` carries are left out. */
51
+ function nearest(points, boxes, side) {
52
+ const c = scale(points.reduce((a, p) => add(a, p), [0, 0, 0]), 1 / points.length), spread = Math.max(...points.map(p => dist(p, c)));
53
+ let best = null;
54
+ for (const b of boxes) {
55
+ if (side && b.hand === side) continue;
56
+ if (dist(c, b.c) - b.r - spread > (best ? best[2] : FAR)) continue;
57
+ const gap = Math.min(...points.map(p => sdBox(p, b.box)));
58
+ if (gap <= FAR && (!best || gap < best[2])) best = [b.root, b.id, gap];
59
+ }
60
+ return best;
61
+ }
62
+
63
+ /** Runs of frames with the same nearest object -> the RUNS closest, in frame order. */
64
+ function runsOf(rows, f0) {
65
+ const runs = [];
66
+ rows.forEach((row, i) => {
67
+ const last = runs[runs.length - 1];
68
+ if (!row) return;
69
+ const [root, part, gap] = row, f = i + f0;
70
+ if (last && last.object === root && last.frames[1] === f - 1) {
71
+ last.frames[1] = f;
72
+ if (gap < last.gap[0]) { last.gap[0] = gap; last.gap[1] = f; last.part = part; }
73
+ if (gap > last.gap[2]) { last.gap[2] = gap; last.gap[3] = f; }
74
+ } else runs.push({ object: root, part, frames: [f, f], gap: [gap, f, gap, f] });
75
+ });
76
+ return runs.sort((a, b) => a.gap[0] - b.gap[0]).slice(0, RUNS).sort((a, b) => a.frames[0] - b.frames[0])
77
+ .map(({ object, part, frames, gap }) => ({ object, ...(part !== object ? { part } : {}), frames, gap: [r3(gap[0]), gap[1], r3(gap[2]), gap[3]] }));
78
+ }
79
+
80
+ /**
81
+ * observeNear(sk, out) -> { hand_l?, hand_r?, foot_l?, foot_r? } (null when nothing is within FAR m
82
+ * of any of them, or the scene has no bodies). Frames are absolute (out.f0 added).
83
+ */
84
+ export function observeNear(sk, out) {
85
+ if (!Object.keys(out.ctx.scene?.objects ?? {}).length) return null;
86
+ const f0 = out.f0 ?? 0, rows = { hand_l: [], hand_r: [], foot_l: [], foot_r: [] };
87
+ out.frames.forEach(({ s }, i) => {
88
+ const f = i + f0, boxes = boxesAt(out, f), pts = effectorPoints(sk, s);
89
+ for (const k of Object.keys(rows)) {
90
+ // a hand is on what it carries: measure it against everything else
91
+ rows[k].push(nearest(pts[k], boxes, k.startsWith("hand_") ? k.slice(-1) : null));
92
+ }
93
+ });
94
+ const near = Object.fromEntries(Object.entries(rows).map(([k, r]) => [k, runsOf(r, f0)]).filter(([, r]) => r.length));
95
+ return Object.keys(near).length ? near : null;
96
+ }
@@ -0,0 +1,135 @@
1
+ // QA of scene objects the spec carries (C3 and 7.1), as plain issues { kind, msg, frame, value,
2
+ // limit } (frame indexes out.frames):
3
+ // GRAB "<id>: hand 12.0 cm from it at grab", frame = grab, limit = checks.limits.grab
4
+ // (default 0.03 m); the distance runs from the prop origin to the part boxes' surface
5
+ // at their scene pose (where the hand finds them). An object its visible channel shows
6
+ // only from grab on appears in the hand (a cigarette out of a pocket), and one grabbed
7
+ // at frame 0 is in hand from the take's first frame (C3, as with no grab): nothing to
8
+ // pick up (session d5c110a7: a bat kit left on the floor, grab 0, "63.6 cm").
9
+ // COLLIDE "<part> x <id>": a body capsule in a carried object (every part box of a group) at
10
+ // any frame it is shown, the holding hand and forearm excused (the fingers ride on the
11
+ // hand), and a second hand on the frames it grips the object (../second-hand.js): at
12
+ // weight 0.5 or more, or with its palm inside the object (a grip the arm turns only
13
+ // partly still holds the handle: session 6262a69c, f37-40, the agent allowed it);
14
+ // "<id> x <other>": a carried object in another scene box from grab on, the
15
+ // support it stood on at grab excused for +-3 frames around grab and release. The
16
+ // depths and checks.allow work as for the body (checks.js); <id> is the spec.objects
17
+ // key, so a group reads as one object.
18
+ // REACH "<id>: 6.2 cm off its keys": a keyed object the hand could not hold where its keys ask (key-checks.js)
19
+ import { QA_LIMITS, allowance } from "./checks.js";
20
+ import { keyIssues } from "./key-checks.js";
21
+ import { CAPSULES, segPoints, sdBox } from "./geometry.js";
22
+ import { isCarried, propOrigin } from "../carried.js";
23
+ import { objectBoxesAt, partsOf, spanOf, restingAt } from "../object-pose.js";
24
+ import { add, sub, apply, dot, cross, len, dist, scale, norm } from "../compiler/math.js";
25
+
26
+ const NEAR_SUPPORT = 3;
27
+
28
+ const centre = b => add(b.pos, apply(b.R, [0, b.half[1], 0]));
29
+ const col = (R, i) => [R[0][i], R[1][i], R[2][i]];
30
+
31
+ /**
32
+ * Overlap of two boxes (separating axes): { depth, axis } with depth > 0 when they overlap and
33
+ * axis the unit direction that moves `a` out of `b` by depth; depth <= 0 when apart. With
34
+ * `side` (a direction from b toward where a came from) the exit is taken on that side.
35
+ */
36
+ export function boxOverlap(a, b, side = null) {
37
+ const ca = centre(a), cb = centre(b), d = sub(ca, cb), A = [0, 1, 2].map(i => col(a.R, i)), B = [0, 1, 2].map(i => col(b.R, i));
38
+ const axes = [...A, ...B, ...A.flatMap(u => B.map(v => cross(u, v)))];
39
+ let best = { depth: Infinity, axis: [0, 1, 0] };
40
+ for (const raw of axes) {
41
+ if (len(raw) < 1e-6) continue;
42
+ const L = norm(raw), r = (box, U) => U.reduce((s, u, i) => s + Math.abs(dot(u, L)) * box.half[i], 0), reach = r(a, A) + r(b, B);
43
+ if (reach - Math.abs(dot(d, L)) <= 0) return { depth: reach - Math.abs(dot(d, L)), axis: L };
44
+ const out = dot(side ?? d, L) < 0 ? scale(L, -1) : L, depth = reach - dot(d, out);
45
+ if (depth < best.depth) best = { depth, axis: out };
46
+ }
47
+ return best;
48
+ }
49
+
50
+ /** True when the palm of hand k (between its index and middle finger, where a grip holds) is inside one of `boxes`: the hand holds the object there. */
51
+ const palmIn = (sk, s, k, boxes) => {
52
+ const S = k === "l" ? "Left" : "Right", p = scale(add(s.P[sk.index[`${S}HandIndex2`]], s.P[sk.index[`${S}HandMiddle2`]]), 0.5);
53
+ return boxes.some(b => sdBox(p, b) <= 0);
54
+ };
55
+
56
+ /** True while entry o's record is shown (its `visible` channel above 0.5, or no channel) at absolute f. */
57
+ const shownAt = (out, o, f) => !o.visible || (out.baked?.[o.visible]?.[f] ?? 0) > 0.5;
58
+
59
+ /** The part boxes of `id` where the hand finds it (any frame f, the hint asks later ones too): its scene pose, or where a fall before its grab left it (C17). */
60
+ const restingBoxes = (out, id, f) => partsOf(out.ctx.scene, id).map(part => ({ ...out.ctx.scene.objects[part], ...restingAt(out, part, f) }));
61
+
62
+ /** Prop origin to the nearest part box surface of `id` where it rests before grab, at absolute frame f (0 inside). */
63
+ export function grabGap(out, id, f) {
64
+ const { sk, spec } = out.ctx, fr = out.frames[f - (out.f0 ?? 0)];
65
+ if (!fr) return null;
66
+ const p = propOrigin(sk, spec, fr.s, spec.objects[id]);
67
+ return Math.max(0, Math.min(...restingBoxes(out, id, f).map(box => sdBox(p, box))));
68
+ }
69
+
70
+ /** The scene box right below `id` at grab (absolute frame), the support it may touch near grab and release. */
71
+ function supportOf(out, id, grab) {
72
+ const parts = restingBoxes(out, id, grab);
73
+ if (!parts.length) return null;
74
+ const low = Math.min(...parts.map(b => b.pos[1] - Math.abs(b.R[1][0]) * b.half[0] - Math.abs(b.R[1][2]) * b.half[2]));
75
+ const c = scale(parts.map(centre).reduce((s, p) => add(s, p), [0, 0, 0]), 1 / parts.length), probe = [c[0], low - 0.01, c[2]];
76
+ let best = null;
77
+ for (const [oid, box] of Object.entries(out.ctx.objectsAt(grab))) {
78
+ if (box.carried || !box.half) continue;
79
+ const d = sdBox(probe, box);
80
+ if (d <= 0.02 && (!best || d < best[1])) best = [oid, d];
81
+ }
82
+ return best?.[0] ?? null;
83
+ }
84
+
85
+ /**
86
+ * objectChecks(sk, out, spec) -> issues
87
+ * @param {object} sk the rig
88
+ * @param {object} out compileMotion result (out.ctx.spec, out.ctx.scene; object-pose.js)
89
+ * @param {object} spec the expanded spec QA measures (checks, objects)
90
+ * @returns {{ kind: string, msg: string, frame: number, value: number, limit: number }[]}
91
+ */
92
+ export function objectChecks(sk, out, spec) {
93
+ const carried = Object.entries(out.ctx.spec.objects ?? {}).filter(([, o]) => isCarried(o));
94
+ if (!carried.length) return [];
95
+ const checks = spec.checks ?? {}, L = { ...QA_LIMITS, ...checks.limits }, allow = checks.allow, issues = [], f0 = out.f0 ?? 0, I = sk.index;
96
+ const push = (kind, msg, f, value, limit) => issues.push({ kind, msg, frame: f, value: +value.toFixed(4), limit });
97
+ // checks.allow names the carried id or one of its parts, on either side
98
+ const lim = (parts, objs) => parts.reduce((v, p) => objs.reduce((w, x) => w ?? allowance(allow, p, x), v), null) ?? L.allow;
99
+ for (const [id, o] of carried) {
100
+ const [grab, release] = spanOf(out, o), k = spec.props?.[o.prop]?.hand, excused = new Set([`hand_${k}`, `farm_${k}`]);
101
+ if (o.grab !== undefined && grab > 0 && out.frames[grab - f0] && shownAt(out, o, grab - 1)) {
102
+ const gap = grabGap(out, id, grab);
103
+ if (gap > L.grab) push("GRAB", `${id}: hand ${(gap * 100).toFixed(1)} cm from it at grab`, grab - f0, gap, L.grab);
104
+ }
105
+ const support = o.grab !== undefined ? supportOf(out, id, grab) : null;
106
+ const nearSupport = f => Math.abs(f - grab) <= NEAR_SUPPORT || (o.release !== undefined && Math.abs(f - release) <= NEAR_SUPPORT);
107
+ for (let i = 0; i < out.frames.length; i++) {
108
+ const f = i + f0, boxes = objectBoxesAt(out, id, f);
109
+ if (!boxes || !shownAt(out, o, f)) continue;
110
+ const ids = boxes.map(b => b.id), spheres = boxes.map(b => [b, centre(b), len(b.half)]), { s } = out.frames[i];
111
+ const grips = Object.entries(out.frames[i].diag).filter(([k, v]) => /^hold_[lr]$/.test(k) && v?.second === id && (v.w >= 0.5 || palmIn(sk, s, k.slice(-1), boxes))).map(([k]) => `hand_${k.slice(-1)}`);
112
+ for (const [part, a, b, r] of CAPSULES) {
113
+ if (excused.has(part) || grips.includes(part)) continue;
114
+ const A = s.P[I[a]], B = s.P[I[b]], mid = scale(add(A, B), 0.5), reach = r + dist(A, B) / 2, l = lim([part], [id, ...ids]);
115
+ let pts = null, pen = -Infinity;
116
+ for (const [box, c, rb] of spheres) {
117
+ if (reach - (dist(mid, c) - rb) <= l) continue;
118
+ pts ??= segPoints(A, B);
119
+ pen = Math.max(pen, r - Math.min(...pts.map(p => sdBox(p, box))));
120
+ }
121
+ if (pen > l) push("COLLIDE", `${part} x ${id}`, i, pen, l);
122
+ }
123
+ if (f < grab) continue;
124
+ for (const [oid, ob] of Object.entries(out.objectsAt(i))) {
125
+ if (!ob.half || ob.ignore || (oid === support && nearSupport(f))) continue;
126
+ const l = lim([id, ...ids], [oid]), oc = centre(ob), orad = len(ob.half);
127
+ let depth = -Infinity;
128
+ for (const [box, c, rb] of spheres) if (dist(c, oc) < rb + orad + l) depth = Math.max(depth, boxOverlap(box, ob).depth);
129
+ if (depth > l) push("COLLIDE", `${id} x ${oid}`, i, depth, l);
130
+ }
131
+ }
132
+ }
133
+ keyIssues(out, L, push);
134
+ return issues;
135
+ }
@@ -0,0 +1,161 @@
1
+ // C6. Hint fixes -> JSON Patch (RFC 6902, json-patch.js) on the agent's own spec.
2
+ //
3
+ // A hint function may return fix (non-enumerable on the group, never in the output):
4
+ // { channel: "hand_l", f: 106, delta: [dx, dy, dz] | n } move a keyed channel by delta at f
5
+ // { channel: "elbow_r", f: 120, value: [x, y, z] | n } set a channel at f
6
+ // { path: "/fix/retime", value: true } set a spec path
7
+ // { path: "/anchors/cupHandle/p", delta: [dx, dy, dz] } move a spec value by delta
8
+ // { channel: "root", f, delta, hold: true } move the body that stands at f
9
+ // Each becomes g.patch = [{ op, path, value }] against opts.userSpec (the spec as the agent sent
10
+ // it, use[] not expanded): a user key that sets the channel within f +-2 -> replace
11
+ // /keys/<i>/set/<ch>; a standIdle stance moved sideways -> /use/<i>/args/at; otherwise add
12
+ // /keys/- with the frame's value moved. A path fix replaces the value, or adds it when only the
13
+ // last step is missing (or the top-level /fix object). A hold fix moves what placed the body
14
+ // at f by the delta's x and z: a standIdle args.at, a walkTo's length (firstStep with 2 steps,
15
+ // else step; its to when steps follow from it), or the run of the agent's root keys around f.
16
+ // g.patch is set only when the patch applies to userSpec and changes it.
17
+ import { applyPatch } from "../json-patch.js";
18
+
19
+ const NEAR = 2;
20
+ const round4 = v => (Array.isArray(v) ? v.map(round4) : typeof v === "number" ? Math.round(v * 1e4) / 1e4 : v);
21
+ const tokens = path => path.split("/").slice(1).map(t => t.replaceAll("~1", "/").replaceAll("~0", "~"));
22
+ const pointer = parts => parts.map(t => `/${String(t).replaceAll("~", "~0").replaceAll("/", "~1")}`).join("");
23
+ const isNum = x => typeof x === "number" && Number.isFinite(x);
24
+
25
+ /** The value at a JSON pointer, or undefined. */
26
+ function valueAt(doc, parts) {
27
+ let node = doc;
28
+ for (const t of parts) {
29
+ if (node === null || typeof node !== "object" || !Object.hasOwn(node, t)) return undefined;
30
+ node = node[t];
31
+ }
32
+ return node;
33
+ }
34
+
35
+ /** v moved by d (same shape: a number, or a vector of numbers), else undefined. */
36
+ function moved(v, d) {
37
+ if (isNum(v) && isNum(d)) return v + d;
38
+ if (Array.isArray(v) && Array.isArray(d) && v.length === d.length && v.every(isNum) && d.every(isNum)) return v.map((x, i) => x + d[i]);
39
+ return undefined;
40
+ }
41
+
42
+ /** The user key nearest f (within NEAR frames) that sets channel ch, as its index; -1 if none. */
43
+ function userKey(userSpec, ch, f) {
44
+ let best = -1;
45
+ (userSpec.keys ?? []).forEach((k, i) => {
46
+ if (!k?.set || !Object.hasOwn(k.set, ch) || !isNum(k.f) || Math.abs(k.f - f) > NEAR) return;
47
+ if (best < 0 || Math.abs(k.f - f) < Math.abs(userSpec.keys[best].f - f)) best = i;
48
+ });
49
+ return best;
50
+ }
51
+
52
+ /** A standIdle stance moved by the root's sideways delta: /use/<i>/args/at. */
53
+ function standPatch(X, userSpec, fix, near) {
54
+ const m = /^use\[(\d+)\] standIdle$/.exec(near?.src ?? ""), d = fix.delta;
55
+ if (!m || fix.channel !== "root" || !Array.isArray(d) || Math.hypot(d[0], d[2]) < Math.abs(d[1])) return null;
56
+ const i = Number(m[1]), u = userSpec.use?.[i], root = X.fr(near.f).c.root;
57
+ if (u?.skill !== "standIdle") return null;
58
+ const at = Array.isArray(u.args?.at) ? u.args.at : root && [root[0], root[2]];
59
+ if (!at) return null;
60
+ const value = round4(at.map((v, j) => v + (j === 0 ? d[0] : j === at.length - 1 ? d[2] : 0)));
61
+ if (!u.args) return [{ op: "add", path: `/use/${i}/args`, value: { at: value } }];
62
+ return [{ op: Object.hasOwn(u.args, "at") ? "replace" : "add", path: `/use/${i}/args/at`, value }];
63
+ }
64
+
65
+ /** A walkTo made longer or shorter by the delta along the body's heading at f (null when it is mostly sideways). */
66
+ function walkPatch(X, userSpec, i, fix) {
67
+ const u = userSpec.use?.[i], a = u?.args ?? {}, d = fix.delta, h = (X.fr(fix.f).c.yaw ?? 0) * Math.PI / 180;
68
+ const along = d[0] * Math.sin(h) + d[2] * Math.cos(h);
69
+ if (u?.skill !== "walkTo" || Math.abs(d[0] * Math.cos(h) - d[2] * Math.sin(h)) > Math.abs(along)) return null;
70
+ const set = (arg, value) => [u.args ? { op: Object.hasOwn(a, arg) ? "replace" : "add", path: `/use/${i}/args/${arg}`, value: round4(value) } : { op: "add", path: `/use/${i}/args`, value: { [arg]: round4(value) } }];
71
+ if (a.steps === undefined && Array.isArray(a.to)) return set("to", a.to.map((v, j) => v + (j === 0 ? d[0] : j === a.to.length - 1 ? d[2] : 0)));
72
+ const steps = a.steps ?? 8, arg = steps <= 2 ? "firstStep" : "step", now = a[arg] ?? (arg === "step" ? 0.5 : 0.4), value = now + along / Math.max(1, steps - 2);
73
+ return steps >= 2 && value > 0.05 ? set(arg, value) : null;
74
+ }
75
+
76
+ /** The agent's root keys around f (no skill root key between them) moved by the delta's x and z. */
77
+ function rootRunPatch(X, userSpec, fix) {
78
+ const list = X.keys("root"), mine = k => /^keys\[\d+\]$/.test(k.src);
79
+ const at = list.findLastIndex(k => k.f <= fix.f);
80
+ if (at < 0 || !mine(list[at])) return null;
81
+ let a = at, b = at;
82
+ while (a > 0 && mine(list[a - 1])) a -= 1;
83
+ while (b < list.length - 1 && mine(list[b + 1])) b += 1;
84
+ return list.slice(a, b + 1).map(k => {
85
+ const j = Number(/\d+/.exec(k.src)[0]), r = userSpec.keys?.[j]?.set?.root;
86
+ return Array.isArray(r) && r.length === 3 ? { op: "replace", path: `/keys/${j}/set/root`, value: round4([r[0] + fix.delta[0], r[1], r[2] + fix.delta[2]]) } : null;
87
+ }).filter(Boolean);
88
+ }
89
+
90
+ /** fix.hold: what placed the body at f (the root key at or before it) moves. */
91
+ function bodyPatch(X, userSpec, fix) {
92
+ if (!Array.isArray(fix.delta) || !X?.fr) return null;
93
+ const prev = X.bracket("root", fix.f).prev, m = /^use\[(\d+)\] (standIdle|walkTo)\b/.exec(prev?.src ?? "");
94
+ if (m?.[2] === "standIdle") return standPatch(X, userSpec, fix, prev);
95
+ if (m?.[2] === "walkTo") return walkPatch(X, userSpec, Number(m[1]), fix);
96
+ const run = prev && rootRunPatch(X, userSpec, fix);
97
+ return run?.length ? run : null;
98
+ }
99
+
100
+ function channelPatch(X, userSpec, fix) {
101
+ const { channel: ch, f } = fix;
102
+ if (typeof ch !== "string" || !isNum(f)) return null;
103
+ if (fix.hold) return bodyPatch(X, userSpec, fix);
104
+ const to = v => (fix.value !== undefined ? fix.value : moved(v, fix.delta));
105
+ const i = userKey(userSpec, ch, f);
106
+ if (i >= 0) {
107
+ const value = to(userSpec.keys[i].set[ch]);
108
+ return value === undefined ? null : [{ op: "replace", path: pointer(["keys", i, "set", ch]), value: round4(value) }];
109
+ }
110
+ if (!X?.fr) return null;
111
+ const { prev, next } = X.bracket(ch, f);
112
+ const near = prev && f - prev.f <= NEAR ? prev : next && next.f - f <= NEAR ? next : null;
113
+ const stand = fix.delta !== undefined && standPatch(X, userSpec, fix, near);
114
+ if (stand) return stand;
115
+ const value = to(X.fr(f).c[ch]);
116
+ return value === undefined ? null : [{ op: "add", path: "/keys/-", value: { f: Math.round(f), set: { [ch]: round4(value) } } }];
117
+ }
118
+
119
+ function pathPatch(userSpec, fix) {
120
+ if (typeof fix.path !== "string" || !fix.path.startsWith("/")) return null;
121
+ const parts = tokens(fix.path), now = valueAt(userSpec, parts);
122
+ const value = fix.delta === undefined ? fix.value : now === undefined && fix.path.endsWith("/offset") ? fix.delta : moved(now, fix.delta);
123
+ if (value === undefined) return null;
124
+ if (now !== undefined) return [{ op: "replace", path: fix.path, value: round4(value) }];
125
+ const parent = valueAt(userSpec, parts.slice(0, -1));
126
+ if (parent !== undefined) return [{ op: "add", path: fix.path, value: round4(value) }];
127
+ // a missing top-level object (spec.fix): add it with the one member
128
+ if (parts.length === 2 && parts[0] === "fix") return [{ op: "add", path: pointer([parts[0]]), value: { [parts[1]]: round4(value) } }];
129
+ return null;
130
+ }
131
+
132
+ /** The patch for one fix, or null when it does not apply mechanically or changes nothing. */
133
+ function patchOf(fix, X, userSpec) {
134
+ const patch = fix.channel !== undefined ? channelPatch(X, userSpec, fix) : fix.path !== undefined ? pathPatch(userSpec, fix) : null;
135
+ if (!patch) return null;
136
+ try {
137
+ return JSON.stringify(applyPatch(userSpec, patch)) === JSON.stringify(userSpec) ? null : patch;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * attachPatches(groups, { X, userSpec }) -> groups (the same array)
145
+ * @param {object[]} groups ranked QA groups, each maybe with a non-enumerable g.fix
146
+ * @param {{ X: object, userSpec?: object }} ctx X = the explain context (qa/explain.js: X.out,
147
+ * X.spec the expanded spec, X.keys(ch), X.bracket(ch, f)); userSpec undefined = no patches
148
+ */
149
+ export function attachPatches(groups, { X, userSpec } = {}) {
150
+ if (!userSpec || typeof userSpec !== "object") return groups;
151
+ for (const g of groups) {
152
+ if (!g.fix) continue;
153
+ try {
154
+ const patch = patchOf(g.fix, X, userSpec);
155
+ if (patch) g.patch = patch;
156
+ } catch {
157
+ // a fix the context cannot place (no frame, no key table) gets no patch
158
+ }
159
+ }
160
+ return groups;
161
+ }
@@ -0,0 +1,37 @@
1
+ // PERSON: the compiled body against the other people in the scene (people.js), capsule against capsule (geometry.js
2
+ // CAPSULES on both bodies, at the same timeline frame). "<part> x person:<id>": how deep the part goes into the
3
+ // other body (its deepest capsule there; the hint names which), past QA_LIMITS.allow. An intended touch (a push, a hand
4
+ // on a shoulder) is allowed like any contact: checks.allow [{ part, object: "person:<id>", depth }].
5
+ import { segDist, CAPSULES } from "./geometry.js";
6
+ import { allowance, QA_LIMITS } from "./checks.js";
7
+ import { dist } from "../compiler/math.js";
8
+
9
+ /** Two bodies whose hips are this far apart (m) cannot touch: a body is under 1 m from its hips to any skin. */
10
+ const APART = 2.2;
11
+
12
+ /** The person's capsule endpoints at timeline frame f: [part, A, B, r] with the joints read from P. */
13
+ export function personCapsules(p, f) {
14
+ const at = Math.max(0, Math.min(p.frames - 1, f)) * p.names.length, J = name => { const j = p.names.indexOf(name), i = (at + j) * 3; return [p.P[i], p.P[i + 1], p.P[i + 2]]; };
15
+ return CAPSULES.map(([part, a, b, r]) => [part, J(a), J(b), r]);
16
+ }
17
+
18
+ /** PERSON issues of compile result out (frames indexed in out.frames; reported at their absolute frame by qa/index.js). */
19
+ export function peopleChecks(sk, out, checks = {}) {
20
+ const people = Object.entries(out.ctx?.scene?.people ?? {}), issues = [];
21
+ if (!people.length) return issues;
22
+ const I = sk.index, f0 = out.f0 ?? 0, lim = { ...QA_LIMITS, ...checks.limits }.allow;
23
+ out.frames.forEach(({ s }, i) => {
24
+ const f = i + f0, mine = CAPSULES.map(([part, a, b, r]) => [part, s.P[I[a]], s.P[I[b]], r]);
25
+ for (const [id, p] of people) {
26
+ // a saved take keeps only the joints its anchors read (people.js keptPeople): no body to collide with there
27
+ if (!CAPSULES.every(([, a, b]) => p.names.includes(a) && p.names.includes(b))) continue;
28
+ const theirs = personCapsules(p, f), hips = theirs.find(c => c[0] === "hips")[1];
29
+ if (dist(s.P[I.Hips], hips) > APART) continue;
30
+ for (const [part, A, B, r] of mine) {
31
+ const depth = Math.max(...theirs.map(([, C, D, q]) => r + q - segDist(A, B, C, D))), allow = allowance(checks.allow, part, `person:${id}`) ?? lim;
32
+ if (depth > allow) issues.push({ kind: "PERSON", msg: `${part} x person:${id}`, frame: i, value: +depth.toFixed(4), limit: allow });
33
+ }
34
+ }
35
+ });
36
+ return issues;
37
+ }
@@ -0,0 +1,38 @@
1
+ // C6. How bad a QA issue group is, one formula for the agent (qa.groups order and severity),
2
+ // the open-parameter search (search.js cost) and repair (repair.js): the issue count weighted by
3
+ // how far the worst issue is past its limit, at most 4x. See docs/motion-api-v3.1.md.
4
+
5
+ /** Kinds whose value must stay ABOVE the limit (a cosine, a drop, a clearance). */
6
+ export const LOW_IS_BAD = new Set(["ORIENT", "BELOW", "CLEARANCE"]);
7
+
8
+ /** How far the worst issue of group g is past its limit (>= 0, in the kind's unit). */
9
+ export const excessOf = g => Math.max(0, LOW_IS_BAD.has(g.kind) ? g.limit - g.worst : g.worst - g.limit);
10
+
11
+ /** Unrounded severity; search.js sums these as its cost. */
12
+ export const weightOf = g => g.count * (1 + Math.min(3, excessOf(g) / Math.max(Math.abs(g.limit), 0.01)));
13
+
14
+ /** Severity of group g, rounded to 2 decimals. */
15
+ export const severityOf = g => Math.round(weightOf(g) * 100) / 100;
16
+
17
+ /** A measurement in a subject: a free-standing number ("12.0 cm", "120deg", "-0.30") or a frame ("f150"). */
18
+ const MEASURE = /(?<![\w\-.[])[-+]?\d+(\.\d+)?|\bf\d+\b/g;
19
+
20
+ /**
21
+ * The grouping key of an issue (kind + subject): every measurement in the subject becomes "#",
22
+ * so one problem at many frames and depths is one group. Digits that are part of a name stay:
23
+ * an id ("cube-6", "empty-2"), a bone ("LeftHandIndex3"), a bracketed index ("contacts[1]", a
24
+ * default check name, qa/check-names.js) and the whole "<name>: " prefix of a named check or goal.
25
+ */
26
+ export const groupKey = g => {
27
+ const cut = g.subject.indexOf(": "), head = cut >= 0 ? g.subject.slice(0, cut + 2) : "";
28
+ return `${g.kind} ${head}${g.subject.slice(head.length).replace(MEASURE, m => (m[0] === "f" ? "f#" : "#"))}`;
29
+ };
30
+
31
+ /**
32
+ * Adds excess (4 decimals) and severity to every group and returns them ordered: GOAL groups
33
+ * first, then severity, then count, both descending (ties keep their order).
34
+ */
35
+ export function rankGroups(groups) {
36
+ for (const g of groups) { g.excess = Math.round(excessOf(g) * 1e4) / 1e4; g.severity = severityOf(g); }
37
+ return groups.sort((a, b) => (b.kind === "GOAL") - (a.kind === "GOAL") || b.severity - a.severity || b.count - a.count);
38
+ }
@@ -0,0 +1,65 @@
1
+ // Task checks the agent declares in spec.checks (balance is style: style.js).
2
+ // contacts: [{ name, point, target, tol, when }] point on target (e.g. filter on lips)
3
+ // orient: [{ name, from, to, along, minCos, when }] direction from->to against "face" | "up" | [x,y,z]
4
+ // below: [{ name, joint, ref, min, when }] joint at least `min` m below ref (no chicken wing)
5
+ // clearance: [{ name, part, object, min, frames }] body part keeps `min` m from an object
6
+ // appear: [{ name, channel, joint, anchor, tol }] a visibility switch happens with joint at anchor
7
+ // supports: [{ object, part, tol }] frames where `part` rests on `object` skip balance (style.js)
8
+ // point/from/to: "joint:Name" | "prop:name.point" | "anchor:name"; when: weight channel (> 0.99) or [f0, f1].
9
+ // f below indexes out.frames (frame out.f0 + f when the compile was windowed).
10
+ import { sub, dot, norm, len } from "../compiler/math.js";
11
+ import { carried } from "../compiler/limbs.js";
12
+ import { resolveAnchor } from "../compiler/anchors.js";
13
+ import { CAPSULES, segPoints, sdBox } from "./geometry.js";
14
+
15
+ export function pointOf(out, f, ref) {
16
+ const { s, diag, c } = out.frames[f], [kind, rest] = ref.split(/:(.*)/s);
17
+ if (kind === "joint") return s.P[out.ctx.sk.index[rest]];
18
+ if (kind === "prop") { const [p, pt] = rest.split("."); return diag.props?.[p]?.[pt] ?? null; }
19
+ if (kind === "anchor") return resolveAnchor(out.ctx, rest, s, diag, c);
20
+ throw new Error(`MOTION_SPEC_INVALID: check point "${ref}"`);
21
+ }
22
+
23
+ const active = (out, f, when) => (Array.isArray(when) ? f + (out.f0 ?? 0) >= when[0] && f + (out.f0 ?? 0) <= when[1] : when ? (out.frames[f].c[when] ?? 0) > 0.99 : true);
24
+
25
+ function alongDir(out, f, along) {
26
+ if (along === "up") return [0, 1, 0];
27
+ if (along === "face") return carried(out.ctx.sk, out.frames[f].s, out.ctx.sk.index.Head, [0, 0, 1]);
28
+ return norm(along);
29
+ }
30
+
31
+ export function specChecks(out, checks = {}) {
32
+ const issues = [], n = out.frames.length, sk = out.ctx.sk, I = sk.index;
33
+ const push = (kind, msg, f, value, limit) => issues.push({ kind, msg, frame: f, value: +value.toFixed(4), limit });
34
+ for (let f = 0; f < n; f++) {
35
+ for (const k of checks.contacts ?? []) if (active(out, f, k.when)) {
36
+ const a = pointOf(out, f, k.point), b = pointOf(out, f, k.target);
37
+ if (!a || !b) continue;
38
+ const e = len(sub(a, b));
39
+ if (e > (k.tol ?? 0.01)) push("CONTACT", `${k.name}: ${(e * 100).toFixed(1)} cm apart`, f, e, k.tol ?? 0.01);
40
+ }
41
+ for (const k of checks.orient ?? []) if (active(out, f, k.when)) {
42
+ const a = pointOf(out, f, k.from), b = pointOf(out, f, k.to);
43
+ if (!a || !b) continue;
44
+ const cosv = dot(norm(sub(b, a)), alongDir(out, f, k.along));
45
+ if (cosv < (k.minCos ?? 0.64)) push("ORIENT", `${k.name}: cos ${cosv.toFixed(2)}`, f, cosv, k.minCos ?? 0.64);
46
+ }
47
+ for (const k of checks.below ?? []) if (active(out, f, k.when)) {
48
+ const { P } = out.frames[f].s, drop = P[I[k.ref]][1] - P[I[k.joint]][1];
49
+ if (drop < (k.min ?? 0.08)) push("BELOW", `${k.name}: ${k.joint} only ${(drop * 100).toFixed(1)} cm below ${k.ref}`, f, drop, k.min ?? 0.08);
50
+ }
51
+ for (const k of checks.clearance ?? []) if (active(out, f, k.frames)) {
52
+ const box = out.objectsAt(f)[k.object], cap = CAPSULES.find(c => c[0] === k.part);
53
+ if (!box || !cap) continue;
54
+ const { P } = out.frames[f].s, d = Math.min(...segPoints(P[I[cap[1]]], P[I[cap[2]]]).map(p => sdBox(p, box))) - cap[3];
55
+ if (d < k.min) push("CLEARANCE", `${k.name}: ${(d * 100).toFixed(1)} cm`, f, d, k.min);
56
+ }
57
+ }
58
+ for (const k of checks.appear ?? []) for (let f = 1; f < n; f++) {
59
+ const was = (out.frames[f - 1].c[k.channel] ?? 0) > 0.5, is = (out.frames[f].c[k.channel] ?? 0) > 0.5;
60
+ if (was === is) continue;
61
+ const e = len(sub(pointOf(out, f, `joint:${k.joint}`), pointOf(out, f, `anchor:${k.anchor}`)));
62
+ if (e > (k.tol ?? 0.035)) push("APPEAR", `${k.name}: switches with the hand ${(e * 100).toFixed(1)} cm from ${k.anchor}`, f, e, k.tol ?? 0.035);
63
+ }
64
+ return issues;
65
+ }