summer-engine 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +27 -5
  3. package/.codex-plugin/plugin.json +28 -6
  4. package/.cursor-plugin/plugin.json +27 -5
  5. package/.factory-plugin/plugin.json +27 -5
  6. package/.opencode/plugins/summer.js +1 -1
  7. package/CHANGELOG.md +102 -0
  8. package/README.md +18 -3
  9. package/assets/audit/scene_audit.gd +4136 -0
  10. package/assets/seeing/seeing_probe.gd +1873 -0
  11. package/dist/cli/commands/skills.js +20 -2
  12. package/dist/core/api-client.d.ts +9 -1
  13. package/dist/core/api-client.js +21 -4
  14. package/dist/core/capabilities/agent-playbook.js +46 -0
  15. package/dist/core/capabilities/audit/accept.d.ts +55 -0
  16. package/dist/core/capabilities/audit/accept.js +147 -0
  17. package/dist/core/capabilities/audit/args.d.ts +95 -0
  18. package/dist/core/capabilities/audit/args.js +89 -0
  19. package/dist/core/capabilities/audit/audit.d.ts +56 -0
  20. package/dist/core/capabilities/audit/audit.js +287 -0
  21. package/dist/core/capabilities/audit/frame.d.ts +14 -0
  22. package/dist/core/capabilities/audit/frame.js +35 -0
  23. package/dist/core/capabilities/audit/gaps.d.ts +96 -0
  24. package/dist/core/capabilities/audit/gaps.js +257 -0
  25. package/dist/core/capabilities/audit/judge.d.ts +162 -0
  26. package/dist/core/capabilities/audit/judge.js +1134 -0
  27. package/dist/core/capabilities/audit/math.d.ts +110 -0
  28. package/dist/core/capabilities/audit/math.js +263 -0
  29. package/dist/core/capabilities/audit/summary.d.ts +74 -0
  30. package/dist/core/capabilities/audit/summary.js +212 -0
  31. package/dist/core/capabilities/board.d.ts +31 -0
  32. package/dist/core/capabilities/board.js +43 -0
  33. package/dist/core/capabilities/camera-view.d.ts +4 -2
  34. package/dist/core/capabilities/camera-view.js +6 -3
  35. package/dist/core/capabilities/connect-signal.d.ts +85 -0
  36. package/dist/core/capabilities/connect-signal.js +285 -0
  37. package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
  38. package/dist/core/capabilities/engine-fallbacks.js +5 -0
  39. package/dist/core/capabilities/engine-ops.d.ts +5 -1
  40. package/dist/core/capabilities/engine-ops.js +0 -3
  41. package/dist/core/capabilities/engine-receipt.d.ts +12 -0
  42. package/dist/core/capabilities/engine-receipt.js +26 -0
  43. package/dist/core/capabilities/file-read.d.ts +64 -0
  44. package/dist/core/capabilities/file-read.js +325 -0
  45. package/dist/core/capabilities/game-task-plan.js +4 -2
  46. package/dist/core/capabilities/grep.d.ts +60 -0
  47. package/dist/core/capabilities/grep.js +190 -0
  48. package/dist/core/capabilities/inspect-node.d.ts +56 -0
  49. package/dist/core/capabilities/inspect-node.js +225 -0
  50. package/dist/core/capabilities/inspect-resource.d.ts +61 -0
  51. package/dist/core/capabilities/inspect-resource.js +376 -0
  52. package/dist/core/capabilities/math3d.d.ts +28 -0
  53. package/dist/core/capabilities/math3d.js +88 -0
  54. package/dist/core/capabilities/motion-generation.d.ts +62 -0
  55. package/dist/core/capabilities/motion-generation.js +138 -0
  56. package/dist/core/capabilities/placement-batch.d.ts +102 -0
  57. package/dist/core/capabilities/placement-batch.js +483 -0
  58. package/dist/core/capabilities/placement-math.d.ts +70 -0
  59. package/dist/core/capabilities/placement-math.js +209 -0
  60. package/dist/core/capabilities/placement-script.d.ts +42 -0
  61. package/dist/core/capabilities/placement-script.js +1096 -0
  62. package/dist/core/capabilities/placement.d.ts +485 -0
  63. package/dist/core/capabilities/placement.js +1731 -0
  64. package/dist/core/capabilities/project-context.d.ts +33 -2
  65. package/dist/core/capabilities/project-context.js +87 -4
  66. package/dist/core/capabilities/replace-node.d.ts +91 -0
  67. package/dist/core/capabilities/replace-node.js +423 -0
  68. package/dist/core/capabilities/runtime-control.d.ts +18 -2
  69. package/dist/core/capabilities/runtime-control.js +67 -2
  70. package/dist/core/capabilities/scene-batch.d.ts +63 -0
  71. package/dist/core/capabilities/scene-batch.js +394 -0
  72. package/dist/core/capabilities/scene-readback.d.ts +36 -0
  73. package/dist/core/capabilities/scene-readback.js +54 -0
  74. package/dist/core/capabilities/seeing/args.d.ts +348 -0
  75. package/dist/core/capabilities/seeing/args.js +178 -0
  76. package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
  77. package/dist/core/capabilities/seeing/candidates.js +282 -0
  78. package/dist/core/capabilities/seeing/math.d.ts +134 -0
  79. package/dist/core/capabilities/seeing/math.js +269 -0
  80. package/dist/core/capabilities/seeing/probe.d.ts +62 -0
  81. package/dist/core/capabilities/seeing/probe.js +188 -0
  82. package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
  83. package/dist/core/capabilities/seeing/scoring.js +639 -0
  84. package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
  85. package/dist/core/capabilities/seeing/seeing.js +1341 -0
  86. package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
  87. package/dist/core/capabilities/seeing/shot-store.js +204 -0
  88. package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
  89. package/dist/core/capabilities/surface-snap-mesh.js +425 -0
  90. package/dist/core/capabilities/surface-snap.d.ts +77 -0
  91. package/dist/core/capabilities/surface-snap.js +461 -0
  92. package/dist/core/capabilities/tool-dispatch.js +231 -72
  93. package/dist/core/capabilities/tscn.d.ts +85 -0
  94. package/dist/core/capabilities/tscn.js +391 -0
  95. package/dist/core/capabilities/world-snapshot.d.ts +58 -0
  96. package/dist/core/capabilities/world-snapshot.js +150 -0
  97. package/dist/core/capability-skew.d.ts +5 -5
  98. package/dist/core/capability-skew.js +14 -6
  99. package/dist/core/headless/worker-engine-client.d.ts +1 -1
  100. package/dist/core/headless/worker-engine-client.js +2 -2
  101. package/dist/core/library-read.d.ts +37 -5
  102. package/dist/core/library-read.js +264 -33
  103. package/dist/core/library-search.d.ts +2 -2
  104. package/dist/installer/agent-config.js +5 -3
  105. package/dist/installer/agent-table.d.ts +16 -0
  106. package/dist/installer/agent-table.js +52 -17
  107. package/dist/mcp/server.js +9 -3
  108. package/dist/mcp/tools/asset-tools.js +21 -3
  109. package/dist/mcp/tools/audit-tools.d.ts +23 -0
  110. package/dist/mcp/tools/audit-tools.js +60 -0
  111. package/dist/mcp/tools/debug-tools.js +3 -1
  112. package/dist/mcp/tools/file-tools.js +17 -4
  113. package/dist/mcp/tools/generate-tools.js +74 -66
  114. package/dist/mcp/tools/library-tools.js +2 -0
  115. package/dist/mcp/tools/perception-tools.js +6 -19
  116. package/dist/mcp/tools/placement-tools.d.ts +24 -0
  117. package/dist/mcp/tools/placement-tools.js +91 -0
  118. package/dist/mcp/tools/project-tools.js +18 -15
  119. package/dist/mcp/tools/scene-tools.js +71 -83
  120. package/dist/mcp/tools/seeing-tools.d.ts +24 -0
  121. package/dist/mcp/tools/seeing-tools.js +64 -0
  122. package/dist/mcp/tools/spatial-tools.js +17 -9
  123. package/dist/mcp/tools/visual-tools.js +59 -7
  124. package/gemini-extension.json +26 -4
  125. package/integrations/amp/README.md +1 -1
  126. package/integrations/crush/README.md +1 -1
  127. package/integrations/grok-build/README.md +1 -1
  128. package/integrations/hermes/README.md +1 -1
  129. package/integrations/kimi-code/README.md +1 -1
  130. package/integrations/mistral-vibe/README.md +1 -1
  131. package/integrations/opencode/README.md +1 -1
  132. package/integrations/rovo-dev/README.md +1 -1
  133. package/integrations/vscode-copilot/README.md +1 -1
  134. package/integrations/warp/README.md +1 -1
  135. package/integrations/windsurf/README.md +1 -1
  136. package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
  137. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  138. package/library/references/summer-folder/resource.yaml +1 -1
  139. package/library/references/summer-folder/summer-folder.md +1 -1
  140. package/library/skills/3d-lighting/SKILL.md +1 -1
  141. package/library/skills/README.md +36 -14
  142. package/library/skills/adaptive-music/SKILL.md +1 -1
  143. package/library/skills/agent-playtesting/SKILL.md +1 -1
  144. package/library/skills/ai-characters-in-games/SKILL.md +220 -0
  145. package/library/skills/ai-characters-in-games/resource.yaml +30 -0
  146. package/library/skills/ambient-bed/SKILL.md +1 -1
  147. package/library/skills/animated-loop/SKILL.md +1 -1
  148. package/library/skills/animation-tree/SKILL.md +1 -1
  149. package/library/skills/art-direction/SKILL.md +1 -1
  150. package/library/skills/asset-strategy/SKILL.md +2 -1
  151. package/library/skills/asset-strategy/resource.yaml +2 -1
  152. package/library/skills/audio-direction/SKILL.md +1 -1
  153. package/library/skills/auto-fire-targeting/SKILL.md +1 -1
  154. package/library/skills/brainstorm-game/SKILL.md +1 -1
  155. package/library/skills/brainstorming/SKILL.md +1 -1
  156. package/library/skills/browse-templates/SKILL.md +3 -3
  157. package/library/skills/browse-templates/resource.yaml +1 -1
  158. package/library/skills/camera-rigs/SKILL.md +128 -0
  159. package/library/skills/camera-rigs/resource.yaml +29 -0
  160. package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
  161. package/library/skills/character-animation-wiring/SKILL.md +1 -1
  162. package/library/skills/character-model/SKILL.md +1 -1
  163. package/library/skills/character-movement/SKILL.md +326 -0
  164. package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
  165. package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
  166. package/library/skills/character-movement/references/movement-3d.md +136 -0
  167. package/library/skills/character-movement/references/platformer-feel.md +83 -0
  168. package/library/skills/character-movement/resource.yaml +40 -0
  169. package/library/skills/character-portrait/SKILL.md +1 -1
  170. package/library/skills/cinematic-cutscene/SKILL.md +1 -1
  171. package/library/skills/combat-basics/SKILL.md +159 -0
  172. package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
  173. package/library/skills/combat-basics/resource.yaml +34 -0
  174. package/library/skills/concept-art/SKILL.md +1 -1
  175. package/library/skills/create-asset-sheet/SKILL.md +1 -1
  176. package/library/skills/debug/SKILL.md +1 -1
  177. package/library/skills/debugging-game-feel/SKILL.md +1 -1
  178. package/library/skills/design-level/SKILL.md +1 -1
  179. package/library/skills/design-mechanic/SKILL.md +1 -1
  180. package/library/skills/design-npc/SKILL.md +1 -1
  181. package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
  182. package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
  183. package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
  184. package/library/skills/environment-kit/SKILL.md +1 -1
  185. package/library/skills/export-and-ship/SKILL.md +1 -1
  186. package/library/skills/fabricating-assets/SKILL.md +1 -1
  187. package/library/skills/facial-and-lipsync/SKILL.md +1 -1
  188. package/library/skills/fps-controller/SKILL.md +9 -7
  189. package/library/skills/fps-controller/resource.yaml +4 -1
  190. package/library/skills/game-feel/SKILL.md +1 -1
  191. package/library/skills/gameskill/SKILL.md +1 -1
  192. package/library/skills/gdscript-patterns/SKILL.md +1 -1
  193. package/library/skills/generate-motion/SKILL.md +9 -9
  194. package/library/skills/generate-motion/resource.yaml +2 -1
  195. package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
  196. package/library/skills/headless-scripting/SKILL.md +1 -1
  197. package/library/skills/in-game-purchases/SKILL.md +254 -0
  198. package/library/skills/in-game-purchases/resource.yaml +29 -0
  199. package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
  200. package/library/skills/investigating-bugs/SKILL.md +1 -1
  201. package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
  202. package/library/skills/make-game/SKILL.md +25 -3
  203. package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
  204. package/library/skills/make-game/references/2d-platformer.md +117 -0
  205. package/library/skills/make-game/references/2d-rpg-content.md +234 -0
  206. package/library/skills/make-game/references/2d-rpg.md +241 -0
  207. package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
  208. package/library/skills/make-game/references/2d-survivors.md +237 -0
  209. package/library/skills/make-game/references/3d-fps-systems.md +216 -0
  210. package/library/skills/make-game/references/3d-fps.md +177 -0
  211. package/library/skills/make-game/references/3d-horror-systems.md +186 -0
  212. package/library/skills/make-game/references/3d-horror.md +201 -0
  213. package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
  214. package/library/skills/make-game/references/3d-third-person.md +215 -0
  215. package/library/skills/make-game/resource.yaml +6 -1
  216. package/library/skills/multiplayer/SKILL.md +150 -0
  217. package/library/skills/multiplayer/resource.yaml +33 -0
  218. package/library/skills/multiplayer-movement/SKILL.md +510 -0
  219. package/library/skills/multiplayer-movement/files.md +501 -0
  220. package/library/skills/multiplayer-movement/resource.yaml +30 -0
  221. package/library/skills/multiplayer-project/SKILL.md +521 -0
  222. package/library/skills/multiplayer-project/files.md +448 -0
  223. package/library/skills/multiplayer-project/resource.yaml +36 -0
  224. package/library/skills/multiplayer-publish/SKILL.md +229 -0
  225. package/library/skills/multiplayer-publish/export-errors.md +89 -0
  226. package/library/skills/multiplayer-publish/resource.yaml +28 -0
  227. package/library/skills/multiplayer-state/SKILL.md +438 -0
  228. package/library/skills/multiplayer-state/files.md +592 -0
  229. package/library/skills/multiplayer-state/resource.yaml +35 -0
  230. package/library/skills/multiplayer-testing/SKILL.md +236 -0
  231. package/library/skills/multiplayer-testing/resource.yaml +39 -0
  232. package/library/skills/music-track/SKILL.md +1 -1
  233. package/library/skills/navigate-summer/SKILL.md +1 -1
  234. package/library/skills/new-project/SKILL.md +1 -1
  235. package/library/skills/organic-model/SKILL.md +1 -1
  236. package/library/skills/pixel-art/SKILL.md +1 -1
  237. package/library/skills/play/SKILL.md +1 -1
  238. package/library/skills/playtesting-a-feature/SKILL.md +1 -1
  239. package/library/skills/procedural-animation/SKILL.md +2 -2
  240. package/library/skills/procedural-animation/resource.yaml +1 -1
  241. package/library/skills/prop-model/SKILL.md +1 -1
  242. package/library/skills/psx-retro-rendering/SKILL.md +1 -1
  243. package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
  244. package/library/skills/remote-deploy/SKILL.md +1 -1
  245. package/library/skills/retarget/SKILL.md +1 -1
  246. package/library/skills/running-in-the-cloud/SKILL.md +1 -1
  247. package/library/skills/save-load/SKILL.md +156 -0
  248. package/library/skills/save-load/resource.yaml +27 -0
  249. package/library/skills/scene-composition/SKILL.md +1 -1
  250. package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
  251. package/library/skills/scene-scripting/SKILL.md +1 -1
  252. package/library/skills/scene-to-level/SKILL.md +1 -1
  253. package/library/skills/skill-create/SKILL.md +1 -1
  254. package/library/skills/skill-improve/SKILL.md +1 -1
  255. package/library/skills/skill-test/SKILL.md +1 -1
  256. package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
  257. package/library/skills/skybox-panorama/SKILL.md +1 -1
  258. package/library/skills/sound-effect/SKILL.md +1 -1
  259. package/library/skills/spatial-placement/SKILL.md +134 -3
  260. package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
  261. package/library/skills/spatial-placement/resource.yaml +16 -2
  262. package/library/skills/sprite-sheet/SKILL.md +1 -1
  263. package/library/skills/summer-analytics/SKILL.md +147 -0
  264. package/library/skills/summer-analytics/resource.yaml +28 -0
  265. package/library/skills/summer-friends/SKILL.md +226 -0
  266. package/library/skills/summer-friends/resource.yaml +28 -0
  267. package/library/skills/summer-leaderboards/SKILL.md +231 -0
  268. package/library/skills/summer-leaderboards/resource.yaml +30 -0
  269. package/library/skills/summer-match-results/SKILL.md +286 -0
  270. package/library/skills/summer-match-results/resource.yaml +32 -0
  271. package/library/skills/summer-matchmaking/SKILL.md +301 -0
  272. package/library/skills/summer-matchmaking/resource.yaml +32 -0
  273. package/library/skills/summer-parties/SKILL.md +253 -0
  274. package/library/skills/summer-parties/resource.yaml +30 -0
  275. package/library/skills/summer-player-data/SKILL.md +322 -0
  276. package/library/skills/summer-player-data/resource.yaml +30 -0
  277. package/library/skills/summer-store/SKILL.md +271 -0
  278. package/library/skills/summer-store/resource.yaml +29 -0
  279. package/library/skills/summer-world-chat/SKILL.md +195 -0
  280. package/library/skills/summer-world-chat/resource.yaml +30 -0
  281. package/library/skills/summer-world-saves/SKILL.md +284 -0
  282. package/library/skills/summer-world-saves/resource.yaml +29 -0
  283. package/library/skills/text-to-motion/SKILL.md +99 -0
  284. package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
  285. package/library/skills/text-to-motion/resource.yaml +41 -0
  286. package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
  287. package/library/skills/tileable-texture/SKILL.md +1 -1
  288. package/library/skills/trailer-shot/SKILL.md +1 -1
  289. package/library/skills/tune-performance/SKILL.md +1 -1
  290. package/library/skills/ui-basics/SKILL.md +10 -1
  291. package/library/skills/ui-graphics/SKILL.md +1 -1
  292. package/library/skills/use-widget-asset/SKILL.md +1 -1
  293. package/library/skills/using-summer/SKILL.md +2 -2
  294. package/library/skills/using-summer/resource.yaml +2 -2
  295. package/library/skills/vehicle-model/SKILL.md +1 -1
  296. package/library/skills/verification-before-completion/SKILL.md +1 -1
  297. package/library/skills/verifying-scenes/SKILL.md +76 -2
  298. package/library/skills/verifying-scenes/resource.yaml +13 -2
  299. package/library/skills/vfx-dissolve/SKILL.md +1 -1
  300. package/library/skills/vfx-fire/SKILL.md +1 -1
  301. package/library/skills/vfx-hit-spark/SKILL.md +1 -1
  302. package/library/skills/vfx-lightning/SKILL.md +1 -1
  303. package/library/skills/vfx-magic-glow/SKILL.md +1 -1
  304. package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
  305. package/library/skills/vfx-smoke/SKILL.md +1 -1
  306. package/library/skills/vfx-water-ripple/SKILL.md +1 -1
  307. package/library/skills/voice-line/SKILL.md +1 -1
  308. package/library/skills/web-and-touch-input/SKILL.md +38 -0
  309. package/library/skills/web-and-touch-input/resource.yaml +28 -0
  310. package/library/skills/web-compatibility-look/SKILL.md +62 -0
  311. package/library/skills/web-compatibility-look/resource.yaml +30 -0
  312. package/library/skills/world-building-3d/SKILL.md +8 -1
  313. package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
  314. package/library/skills/world-building-3d/resource.yaml +2 -1
  315. package/library/skills/writing-plans/SKILL.md +1 -1
  316. package/library/skills/writing-skills/SKILL.md +1 -1
  317. package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
  318. package/library/tools/attach-to-surface/resource.yaml +142 -0
  319. package/library/tools/batch/resource.yaml +9 -3
  320. package/library/tools/connect-ports/resource.yaml +120 -0
  321. package/library/tools/connect-signal/resource.yaml +3 -2
  322. package/library/tools/debug-views/resource.yaml +94 -0
  323. package/library/tools/frame-nodes/resource.yaml +104 -0
  324. package/library/tools/frame-shot/resource.yaml +134 -0
  325. package/library/tools/generate-motion/resource.yaml +44 -9
  326. package/library/tools/get-board/resource.yaml +50 -0
  327. package/library/tools/get-project-context/resource.yaml +13 -2
  328. package/library/tools/grep/resource.yaml +77 -0
  329. package/library/tools/inspect-asset/resource.yaml +71 -0
  330. package/library/tools/inspect-node/resource.yaml +9 -2
  331. package/library/tools/inspect-resource/resource.yaml +16 -5
  332. package/library/tools/instantiate-scene/resource.yaml +27 -2
  333. package/library/tools/measure/resource.yaml +96 -0
  334. package/library/tools/place-adjacent/resource.yaml +113 -0
  335. package/library/tools/play/resource.yaml +15 -2
  336. package/library/tools/raycast/resource.yaml +101 -0
  337. package/library/tools/read-file/resource.yaml +36 -2
  338. package/library/tools/read-library/resource.yaml +2 -2
  339. package/library/tools/repeat-along/resource.yaml +129 -0
  340. package/library/tools/replace-node/resource.yaml +11 -5
  341. package/library/tools/scene-audit/resource.yaml +144 -0
  342. package/library/tools/screenshot/resource.yaml +13 -2
  343. package/library/tools/search-assets/resource.yaml +18 -0
  344. package/library/tools/shot-sheet/resource.yaml +110 -0
  345. package/library/tools/snap-to-surface/resource.yaml +4 -1
  346. package/library/tools/world-snapshot/resource.yaml +24 -3
  347. package/library/tools/zoom/resource.yaml +105 -0
  348. package/package.json +1 -1
  349. package/registry/generated/aliases.json +17 -3
  350. package/registry/generated/counts.json +3 -3
  351. package/registry/generated/gemini-extension.json +26 -4
  352. package/registry/generated/index.json +2154 -469
  353. package/registry/generated/marketplace.claude.json +2 -2
  354. package/registry/generated/plugin.claude.json +27 -5
  355. package/registry/generated/plugin.codex.json +28 -6
  356. package/registry/generated/plugin.cursor.json +27 -5
  357. package/registry/generated/plugin.factory.json +27 -5
  358. package/registry/generated/skills-index.md +36 -14
  359. package/registry/generated/skills-registry.json +421 -124
  360. package/registry/generated/templates-registry.json +2 -2
  361. package/library/skills/host-authoritative-state/SKILL.md +0 -332
  362. package/library/skills/host-authoritative-state/resource.yaml +0 -28
  363. package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
  364. package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
  365. package/library/skills/setup-multiplayer/SKILL.md +0 -336
  366. package/library/skills/setup-multiplayer/resource.yaml +0 -28
@@ -0,0 +1,461 @@
1
+ /**
2
+ * surface-snap — ONE implementation of `summer_snap_to_surface` for both faces
3
+ * (src/mcp/tools/spatial-tools.ts and tool-dispatch.ts), around the engine's
4
+ * SnapToSurface op (modules/1summer_engine/editor/ops/surface_snap_ops.cpp).
5
+ *
6
+ * Two gaps in the engine op, closed here with ops main already ships:
7
+ *
8
+ * 1. Its two recovery failures say nothing about WHY. gap_exceeds_hit_travel
9
+ * and overlap_recovery_exceeded name neither the node in the way nor
10
+ * whether the subject started out overlapping. After either failure a
11
+ * read-only Starcast3D at the subject's current pose names the contacts,
12
+ * the start-overlap state and what is below, and the failure gains a
13
+ * concrete next_step.
14
+ *
15
+ * 2. It cannot lift a prop SUNK into its support. When the start pose
16
+ * overlaps, the engine backs the subject off against the cast direction,
17
+ * sweeps, and measures hitTravel from the ORIGINAL pose — negative for a
18
+ * sunk prop — then refuses any gap above it ("would move opposite the
19
+ * verified sweep"), although the backed-off sweep did verify that space
20
+ * (_solve_physics compares p_gap with hit_travel instead of with the swept
21
+ * distance from the backed-off start). So on gap_exceeds_hit_travel with a
22
+ * start overlap this module lifts the subject against the cast direction
23
+ * by the overlap depth plus a margin (exact local position from the saved
24
+ * scene), snaps again from there, and keeps the result only when it
25
+ * settles back on a node the subject was sunk into. Otherwise the original
26
+ * position is restored and the failure is reported.
27
+ *
28
+ * 3. Without a collider on the subject or the support it falls back to
29
+ * visual_aabb: AABBs swept against AABBs. One big AABB around the subject
30
+ * (a tree, a building block, a post quad with a huge custom AABB) is an
31
+ * overlap no back-off clears, so collider-less props on a PlaneMesh failed
32
+ * with overlap_recovery_exceeded, and a success can rest on an AABB above
33
+ * the real surface. Whenever the engine answers with visual_aabb evidence
34
+ * (overlap_recovery_exceeded, gap_exceeds_hit_travel, surface_not_found,
35
+ * or a seat), a read-only probe measures the move from visible triangles
36
+ * instead (surface-snap-mesh.ts), one SetProp places the subject, a second
37
+ * read verifies the gap and SaveScene saves it: evidence visual_mesh.
38
+ */
39
+ import { missingEngineOpResult, resolveSingleOnlyOps } from "../capability-skew.js";
40
+ import { asRecord } from "../util/json.js";
41
+ import { executeOpsChunked, sceneMutationOps } from "./engine-ops.js";
42
+ import { extractOpError } from "./engine-receipt.js";
43
+ import { STARCAST_FALLBACK } from "./engine-fallbacks.js";
44
+ import { applyMat3, IDENTITY3, invertMat3, mulMat3, parseTransform3D, vec3Literal, vec3LiteralExact, } from "./math3d.js";
45
+ import { findTscnNode, isSceneCreatedNode, normalizeNodePath, parentNodePath, parseTscn } from "./tscn.js";
46
+ import { measureMeshSnap } from "./surface-snap-mesh.js";
47
+ /** Clearance added above the support before the second sweep. */
48
+ export const SNAP_LIFT_MARGIN = 0.02;
49
+ /** Never lift further than this automatically (a deeper overlap is a placement mistake to report). */
50
+ export const SNAP_MAX_AUTO_LIFT = 0.5;
51
+ const RECOVERY_FAILURES = new Set(["gap_exceeds_hit_travel", "overlap_recovery_exceeded", "aligned_overlap_recovery_exceeded"]);
52
+ /** Engine failures with visual_aabb evidence that the visible-mesh measurement retries. */
53
+ const MESH_FALLBACK_FAILURES = new Set(["overlap_recovery_exceeded", "gap_exceeds_hit_travel", "surface_not_found"]);
54
+ /** A visible-mesh gap this close to the requested one needs no move. */
55
+ export const MESH_SNAP_TOLERANCE = 0.001;
56
+ /** The verify read must find the requested gap within this, or the move is undone. */
57
+ export const MESH_SNAP_VERIFY_TOLERANCE = 0.005;
58
+ export function buildSnapToSurfaceOp(args) {
59
+ return {
60
+ op: "SnapToSurface",
61
+ subject_path: args.subjectPath,
62
+ direction: args.direction,
63
+ max_distance: args.maxDistance,
64
+ gap: args.gap,
65
+ align_up: args.alignUp,
66
+ };
67
+ }
68
+ function snapResult(receipt) {
69
+ const results = asRecord(receipt)?.results;
70
+ if (!Array.isArray(results))
71
+ return null;
72
+ return asRecord(results.find((entry) => asRecord(entry)?.op === "SnapToSurface")) ?? null;
73
+ }
74
+ function round3(value) {
75
+ return Math.round(value * 1000) / 1000;
76
+ }
77
+ async function diagnose(client, args) {
78
+ const missing = missingEngineOpResult(client, "Starcast3D", STARCAST_FALLBACK);
79
+ if (missing)
80
+ return { available: false, startOverlap: null, contacts: [], note: "this engine build has no Starcast3D" };
81
+ let receipt;
82
+ try {
83
+ receipt = await client.executeIdentityBoundOps([{
84
+ op: "Starcast3D",
85
+ path: args.subjectPath,
86
+ detail: "summary",
87
+ max_distance: Math.min(args.maxDistance, 20),
88
+ nearby_radius: 0,
89
+ direction_space: "world",
90
+ collision_mask: 0xffffffff,
91
+ collide_with_areas: false,
92
+ max_hits_per_direction: 1,
93
+ max_results: 8,
94
+ margin: 0.001,
95
+ }], { scenePath: args.scenePath });
96
+ }
97
+ catch (err) {
98
+ return { available: false, startOverlap: null, contacts: [], note: `starcast failed: ${err instanceof Error ? err.message : String(err)}` };
99
+ }
100
+ if (extractOpError(receipt)) {
101
+ return { available: false, startOverlap: null, contacts: [], note: `starcast failed: ${extractOpError(receipt)}` };
102
+ }
103
+ const results = asRecord(receipt)?.results;
104
+ const cast = asRecord(Array.isArray(results) ? results.find((r) => asRecord(r)?.op === "Starcast3D") ?? results[0] : receipt);
105
+ if (!cast)
106
+ return { available: false, startOverlap: null, contacts: [], note: "starcast returned no result" };
107
+ const contacts = Array.isArray(cast.contacts) ? cast.contacts.map(String) : [];
108
+ const status = typeof cast.contactStatus === "string" ? cast.contactStatus : undefined;
109
+ const down = asRecord(asRecord(cast.directions)?.down);
110
+ const sizeRaw = asRecord(cast.subject)?.size;
111
+ const size = Array.isArray(sizeRaw) && sizeRaw.length === 3 && sizeRaw.every((v) => typeof v === "number") ? sizeRaw : undefined;
112
+ return {
113
+ available: true,
114
+ startOverlap: status === undefined ? contacts.length > 0 : status !== "none_detected",
115
+ contacts,
116
+ ...(down ? { below: { status: down.status, object: down.object, distance: down.distance } } : {}),
117
+ ...(size ? { size } : {}),
118
+ };
119
+ }
120
+ /** Local position of the subject and the world basis of its parent, read
121
+ * from the saved .tscn (exact floats). Ancestors without a transform line
122
+ * are identity; an ancestor the file does not list (a node inside an
123
+ * instanced scene) makes the plan unavailable. */
124
+ async function planLift(client, args, lift) {
125
+ const read = asRecord(await client.readProjectFile(args.scenePath, 1_000_000));
126
+ const content = asRecord(read?.data)?.content;
127
+ if (read?.ok === false || typeof content !== "string" || asRecord(read?.data)?.truncated === true) {
128
+ return { reason: `${args.scenePath} could not be read back as a whole text scene` };
129
+ }
130
+ const parsed = parseTscn(content);
131
+ const subjectPath = normalizeNodePath(args.subjectPath);
132
+ const subject = findTscnNode(parsed, subjectPath);
133
+ if (!subject || !isSceneCreatedNode(subject)) {
134
+ return { reason: `${subjectPath} is not a node the scene file creates (it may live inside an instanced scene)` };
135
+ }
136
+ const local = (node) => {
137
+ const raw = node?.props.find((p) => p.key === "transform")?.value;
138
+ return raw ? parseTransform3D(raw) : { basis: IDENTITY3, origin: [0, 0, 0] };
139
+ };
140
+ const own = local(subject);
141
+ if (!own)
142
+ return { reason: `${subjectPath} has a transform the planner cannot read` };
143
+ if (subject.props.some((p) => p.key === "top_level" && p.value.trim() === "true")) {
144
+ return { reason: `${subjectPath} is top_level` };
145
+ }
146
+ const chain = [];
147
+ for (let p = parentNodePath(subjectPath); p !== null; p = parentNodePath(p)) {
148
+ const node = findTscnNode(parsed, p);
149
+ if (!node)
150
+ return { reason: `ancestor ${p} is not listed in the scene file` };
151
+ const t = local(node);
152
+ if (!t)
153
+ return { reason: `ancestor ${p} has a transform the planner cannot read` };
154
+ chain.unshift(t.basis);
155
+ }
156
+ const parentBasis = chain.reduce((acc, basis) => mulMat3(acc, basis), IDENTITY3);
157
+ const inverse = invertMat3(parentBasis);
158
+ if (!inverse)
159
+ return { reason: "the parent transform is singular" };
160
+ const length = Math.hypot(...args.direction);
161
+ const worldDelta = args.direction.map((d) => (-d / length) * lift);
162
+ const localDelta = applyMat3(inverse, worldDelta);
163
+ const original = own.origin;
164
+ return { original, lifted: [original[0] + localDelta[0], original[1] + localDelta[1], original[2] + localDelta[2]] };
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // Entry point
168
+ // ---------------------------------------------------------------------------
169
+ function extentAlong(size, direction) {
170
+ if (!size)
171
+ return undefined;
172
+ const length = Math.hypot(...direction);
173
+ return Math.abs(size[0] * direction[0] / length) + Math.abs(size[1] * direction[1] / length) + Math.abs(size[2] * direction[2] / length);
174
+ }
175
+ function nextStepFor(reason, failed, args, diagnosis, lift, liftNote) {
176
+ const blocking = diagnosis.contacts.length ? diagnosis.contacts.join(", ") : undefined;
177
+ if (reason === "gap_exceeds_hit_travel") {
178
+ const hitTravel = typeof failed.hitTravel === "number" ? failed.hitTravel : undefined;
179
+ if (diagnosis.startOverlap !== false && lift !== undefined) {
180
+ return (`The subject starts ${hitTravel !== undefined && hitTravel < 0 ? `${round3(-hitTravel)} m ` : ""}inside ${blocking ?? "its support"}` +
181
+ `${liftNote ? ` and the automatic lift was not applied (${liftNote})` : ""}. ` +
182
+ `Raise it about ${round3(lift)} m against the cast direction with summer_set_prop position, then call summer_snap_to_surface again; ` +
183
+ "check the result with summer_starcast.");
184
+ }
185
+ return (`The first surface along the cast is ${hitTravel !== undefined ? `${round3(hitTravel)} m` : "closer than the requested gap"} away, so gap ${args.gap} cannot be kept. ` +
186
+ `Pass gap of at most ${hitTravel !== undefined ? round3(Math.max(0, hitTravel)) : "that distance"}, or move the subject away from ${blocking ?? diagnosis.below?.object ?? "the surface"} first.`);
187
+ }
188
+ const visual = failed.evidence === "visual_aabb";
189
+ return (`The subject starts overlapping ${blocking ?? "something"} and moving it ${args.maxDistance} m against the cast direction did not clear it. ` +
190
+ (visual
191
+ ? "It has no enabled collider, so its visible bounding box was tested against the bounding boxes around it. Set its position by hand with summer_set_prop (read the support height with summer_starcast or summer_inspect_node), or give it a CollisionShape3D. "
192
+ : `Move it clear of ${blocking ?? "the overlapping node"} with summer_set_prop position (summer_test_placement checks a candidate pose first), or raise maxDistance. `) +
193
+ "Then call summer_snap_to_surface again.");
194
+ }
195
+ function failureEnvelope(receipt, failed, args, diagnosis, extra = {}) {
196
+ const reason = String(failed.failure_reason);
197
+ const nextStep = nextStepFor(reason, failed, args, diagnosis, extra.lift, extra.liftNote);
198
+ const startState = diagnosis.startOverlap === null
199
+ ? `start overlap unknown (${diagnosis.note ?? "no starcast"})`
200
+ : diagnosis.startOverlap
201
+ ? `the subject starts in contact with or inside: ${diagnosis.contacts.join(", ") || "(unnamed)"}`
202
+ : "the subject does not start overlapping anything";
203
+ const augmented = {
204
+ ...failed,
205
+ start_overlap: diagnosis.startOverlap,
206
+ ...(diagnosis.contacts.length ? { blocking: diagnosis.contacts } : {}),
207
+ ...(diagnosis.below ? { below: diagnosis.below } : {}),
208
+ ...(extra.recovery ? { recovery: extra.recovery } : {}),
209
+ ...(extra.meshFallback ? { mesh_fallback: extra.meshFallback } : {}),
210
+ next_step: nextStep,
211
+ };
212
+ const root = asRecord(receipt) ?? {};
213
+ const results = Array.isArray(root.results) ? root.results.map((r) => (asRecord(r)?.op === "SnapToSurface" ? augmented : r)) : [augmented];
214
+ return {
215
+ ...root,
216
+ ok: false,
217
+ error: `${String(failed.error ?? reason)} (${reason}): ${startState}. Next step: ${nextStep}`,
218
+ results,
219
+ };
220
+ }
221
+ export async function snapToSurface(client, args) {
222
+ const op = buildSnapToSurfaceOp(args);
223
+ const singleOnly = resolveSingleOnlyOps(client);
224
+ const send = (ops, options = {}) => executeOpsChunked((chunk) => client.executeIdentityBoundOps(chunk, { ...options, scenePath: args.scenePath }), ops, singleOnly);
225
+ const first = await send(sceneMutationOps([op]));
226
+ if (!extractOpError(first)) {
227
+ const seated = snapResult(first);
228
+ // A seat on visual AABBs may rest on a box above the real surface: check it on triangles.
229
+ if (seated?.evidence === "visual_aabb") {
230
+ const checked = await snapOnVisibleMesh(client, args, send, { engineResult: seated });
231
+ return checked.receipt ?? withMeshNote(first, checked.note);
232
+ }
233
+ return first;
234
+ }
235
+ const failed = snapResult(first);
236
+ const reason = typeof failed?.failure_reason === "string" ? failed.failure_reason : "";
237
+ let meshFallback;
238
+ if (failed && failed.evidence === "visual_aabb" && MESH_FALLBACK_FAILURES.has(reason)) {
239
+ const mesh = await snapOnVisibleMesh(client, args, send, { engineFailure: failed });
240
+ if (mesh.receipt)
241
+ return mesh.receipt;
242
+ meshFallback = mesh.note;
243
+ }
244
+ if (!failed || !RECOVERY_FAILURES.has(reason))
245
+ return meshFallback ? withMeshNote(first, meshFallback) : first;
246
+ const diagnosis = await diagnose(client, args);
247
+ if (reason !== "gap_exceeds_hit_travel" || typeof failed.hitTravel !== "number") {
248
+ return failureEnvelope(first, failed, args, diagnosis, { meshFallback });
249
+ }
250
+ // A sunk subject: hitTravel is how far along the cast the contact is from
251
+ // the CURRENT pose (negative = behind it). Lift past it, then settle.
252
+ const lift = args.gap - failed.hitTravel + SNAP_LIFT_MARGIN;
253
+ const extent = extentAlong(diagnosis.size, args.direction);
254
+ const cap = Math.min(SNAP_MAX_AUTO_LIFT, args.maxDistance, extent !== undefined ? Math.max(extent, 0.05) : SNAP_MAX_AUTO_LIFT);
255
+ // Lift only a subject that provably starts in or on its support: starcast
256
+ // contacts, or (without starcast) a contact behind the current pose.
257
+ const sunk = diagnosis.startOverlap === true || (diagnosis.startOverlap === null && failed.hitTravel < 0);
258
+ if (!sunk)
259
+ return failureEnvelope(first, failed, args, diagnosis, { meshFallback });
260
+ if (lift > cap) {
261
+ return failureEnvelope(first, failed, args, diagnosis, {
262
+ meshFallback,
263
+ lift,
264
+ liftNote: `it would need ${round3(lift)} m, more than the ${round3(cap)} m allowed automatically`,
265
+ });
266
+ }
267
+ // Save first: the lift is computed from the exact transform in the file.
268
+ const preSave = await send([{ op: "SaveScene" }]);
269
+ if (extractOpError(preSave)) {
270
+ return failureEnvelope(first, failed, args, diagnosis, { meshFallback, lift, liftNote: "the scene could not be saved before lifting" });
271
+ }
272
+ const plan = await planLift(client, args, lift);
273
+ if ("reason" in plan)
274
+ return failureEnvelope(first, failed, args, diagnosis, { meshFallback, lift, liftNote: plan.reason });
275
+ const position = (v) => ({ op: "SetProp", path: args.subjectPath, key: "position", value: vec3LiteralExact(v) });
276
+ const restore = async () => !extractOpError(await send(sceneMutationOps([position(plan.original)])));
277
+ const recovery = {
278
+ lifted_by: round3(lift),
279
+ original_local_position: vec3Literal(plan.original),
280
+ lifted_local_position: vec3Literal(plan.lifted),
281
+ };
282
+ const second = await send([position(plan.lifted), op], { groupUndo: true });
283
+ const settled = snapResult(second);
284
+ if (extractOpError(second) || !settled || settled.ok === false) {
285
+ const restored = await restore();
286
+ return failureEnvelope(first, settled ?? failed, args, diagnosis, {
287
+ meshFallback,
288
+ lift,
289
+ liftNote: `the snap from the lifted pose failed too (${String(settled?.failure_reason ?? extractOpError(second))}); ${restored ? "the original position was restored" : "restoring the original position FAILED — check it"}`,
290
+ recovery: { ...recovery, restored },
291
+ });
292
+ }
293
+ const support = typeof settled.supportPath === "string" ? settled.supportPath : "";
294
+ const sameSupport = diagnosis.contacts.length === 0 || support === "" || diagnosis.contacts.some((c) => normalizeNodePath(c) === normalizeNodePath(support));
295
+ if (!sameSupport) {
296
+ const restored = await restore();
297
+ return failureEnvelope(first, failed, args, diagnosis, {
298
+ meshFallback,
299
+ lift,
300
+ liftNote: `from the lifted pose it settled on ${support}, not on a node it was sunk into; ${restored ? "the original position was restored" : "restoring the original position FAILED — check it"}`,
301
+ recovery: { ...recovery, settled_on: support, restored },
302
+ });
303
+ }
304
+ const saved = await send([{ op: "SaveScene" }]);
305
+ const warnings = Array.isArray(settled.warnings) ? [...settled.warnings] : [];
306
+ warnings.push(`The subject started ${round3(-failed.hitTravel)} m inside ${diagnosis.contacts.join(", ") || "its support"}; it was lifted ${round3(lift)} m against the cast direction and settled from there. 'before' is the lifted pose; recovery.original_local_position is where it was.`);
307
+ const result = { ...settled, warnings, recovery: { ...recovery, recovered_from: "gap_exceeds_hit_travel", start_contacts: diagnosis.contacts } };
308
+ const root = asRecord(second) ?? {};
309
+ const results = Array.isArray(root.results) ? root.results.map((r) => (asRecord(r)?.op === "SnapToSurface" ? result : r)) : [result];
310
+ if (extractOpError(saved)) {
311
+ return {
312
+ ...root,
313
+ ok: false,
314
+ error: `The subject was lifted and settled in the editor, but SaveScene failed, so ${args.scenePath} on disk still holds the sunk pose: ${extractOpError(saved)}. Call summer_save_scene.`,
315
+ results,
316
+ };
317
+ }
318
+ return { ...root, results: [...results, ...(Array.isArray(asRecord(saved)?.results) ? asRecord(saved).results : [])] };
319
+ }
320
+ function round4(value) {
321
+ return Math.round(value * 10000) / 10000 || 0; // never -0
322
+ }
323
+ function roundVec3(value) {
324
+ return Array.isArray(value) && value.length === 3 && value.every((v) => typeof v === "number") ? value.map(round4) : undefined;
325
+ }
326
+ /** The engine's receipt with the fallback's reason on its SnapToSurface entry. */
327
+ function withMeshNote(receipt, note) {
328
+ if (!note)
329
+ return receipt;
330
+ const root = asRecord(receipt) ?? {};
331
+ if (!Array.isArray(root.results))
332
+ return receipt;
333
+ return { ...root, results: root.results.map((r) => (asRecord(r)?.op === "SnapToSurface" ? { ...asRecord(r), mesh_fallback: note } : r)) };
334
+ }
335
+ /**
336
+ * Measure the seat on visible triangles, move the subject with one SetProp,
337
+ * read the gap again and save. Returns the receipt, or a note saying why the
338
+ * fallback could not place it (the caller then reports the engine's answer).
339
+ */
340
+ async function snapOnVisibleMesh(client, args, send, context) {
341
+ const probeArgs = { scenePath: args.scenePath, subjectPath: args.subjectPath, direction: args.direction, maxDistance: args.maxDistance, gap: args.gap };
342
+ const measure = await measureMeshSnap(client, probeArgs);
343
+ const probeRan = measure.ok || measure.failure_reason !== "engine_lacks_op";
344
+ const finish = async (note) => {
345
+ // The engine's own seat is saved; the probe run marked the tab unsaved.
346
+ if (context.engineResult && probeRan)
347
+ await send([{ op: "SaveScene" }]);
348
+ return { note };
349
+ };
350
+ if (!measure.ok)
351
+ return finish({ failure_reason: measure.failure_reason, error: measure.error });
352
+ if (!measure.found || typeof measure.shift !== "number" || typeof measure.travel !== "number" || typeof measure.position_after !== "string") {
353
+ return finish({
354
+ failure_reason: "surface_not_found",
355
+ error: `No visible triangles below the subject within maxDistance ${args.maxDistance} (${measure.support_meshes ?? 0} mesh(es), ${measure.support_triangles ?? 0} triangle(s) in its column).`,
356
+ });
357
+ }
358
+ const liftCap = Math.min(SNAP_MAX_AUTO_LIFT, args.maxDistance);
359
+ if (-measure.shift > liftCap) {
360
+ return finish({
361
+ failure_reason: "lift_too_large",
362
+ error: `The subject starts ${round3(-measure.travel)} m inside ${measure.support || "its support"}; lifting it ${round3(-measure.shift)} m is more than the ${round3(liftCap)} m allowed automatically. Raise it with summer_set_prop position first.`,
363
+ });
364
+ }
365
+ const position = (value) => ({ op: "SetProp", path: args.subjectPath, key: "position", value });
366
+ const moved = Math.abs(measure.shift) > MESH_SNAP_TOLERANCE;
367
+ let setReceipt;
368
+ if (moved) {
369
+ setReceipt = await send([position(measure.position_after)]);
370
+ if (extractOpError(setReceipt))
371
+ return finish({ failure_reason: "mutation_failed", error: `SetProp position failed: ${extractOpError(setReceipt)}` });
372
+ }
373
+ const verify = moved ? await measureMeshSnap(client, probeArgs) : measure;
374
+ const finalGap = verify.ok && verify.found && typeof verify.travel === "number" ? verify.travel : undefined;
375
+ if (moved && finalGap !== undefined && Math.abs(finalGap - args.gap) > MESH_SNAP_VERIFY_TOLERANCE) {
376
+ const restored = typeof measure.position === "string" && !extractOpError(await send([position(measure.position)]));
377
+ return finish({
378
+ failure_reason: "verify_mismatch",
379
+ error: `After the move the visible-mesh read found a gap of ${round4(finalGap)} m instead of ${args.gap}; the position was ${restored ? "restored" : "NOT restored, check it"}.`,
380
+ });
381
+ }
382
+ const saved = await send([{ op: "SaveScene" }]);
383
+ const snap = meshSnapResult(args, measure, finalGap, verify, context, moved);
384
+ const resultsOf = (receipt) => (Array.isArray(asRecord(receipt)?.results) ? asRecord(receipt).results : []);
385
+ const results = [snap, ...resultsOf(setReceipt), ...resultsOf(saved)];
386
+ const root = asRecord(saved) ?? {};
387
+ if (extractOpError(saved)) {
388
+ return {
389
+ receipt: {
390
+ ...root,
391
+ ok: false,
392
+ error: `The subject was placed on ${measure.support || "its support"} in the editor (visual_mesh), but SaveScene failed, so ${args.scenePath} on disk does not hold it yet: ${extractOpError(saved)}. Call summer_save_scene.`,
393
+ results,
394
+ },
395
+ };
396
+ }
397
+ return { receipt: { ...root, results } };
398
+ }
399
+ function meshSnapResult(args, measure, finalGap, verify, context, moved) {
400
+ const travel = measure.travel ?? 0;
401
+ const warnings = [
402
+ "evidence visual_mesh: the contact was measured between visible triangles (the subject's vertices cast along the direction onto the support's triangles, and the support's vertices under it back onto the subject's); no collider was used.",
403
+ ];
404
+ if (travel < -MESH_SNAP_TOLERANCE)
405
+ warnings.push(`The subject started ${round3(-travel)} m inside ${measure.support || "its support"}; it was lifted out.`);
406
+ if (measure.subject_colliders === 0)
407
+ warnings.push("The subject has no enabled collider: physics bodies will not rest on it and physics queries do not see it.");
408
+ if (args.alignUp)
409
+ warnings.push("alignUp was not applied: the visible-mesh fallback keeps the subject's rotation.");
410
+ const screenSpace = Number(asRecord(measure.skipped)?.screen_space ?? 0);
411
+ if (screenSpace > 0)
412
+ warnings.push(`${screenSpace} screen-space mesh(es) (their shader writes POSITION) were not treated as surfaces.`);
413
+ if (measure.reverse_partial)
414
+ warnings.push("The support has more vertices under the subject than the back-cast budget; contact between subject vertices is approximate.");
415
+ if (moved && finalGap === undefined)
416
+ warnings.push(`The read after the move did not complete (${verify.ok ? "no support found" : verify.error}); finalGap is the planned gap.`);
417
+ const normal = roundVec3(measure.hit_normal);
418
+ const length = Math.hypot(...args.direction);
419
+ const slopeDeg = normal
420
+ ? round3((Math.acos(Math.min(1, Math.abs(normal[0] * args.direction[0] + normal[1] * args.direction[1] + normal[2] * args.direction[2]) / length)) * 180) / Math.PI)
421
+ : undefined;
422
+ const engine = context.engineFailure
423
+ ? { evidence: "visual_aabb", failure_reason: context.engineFailure.failure_reason, error: context.engineFailure.error }
424
+ : {
425
+ evidence: "visual_aabb",
426
+ supportPath: context.engineResult?.supportPath,
427
+ finalGap: context.engineResult?.finalGap,
428
+ note: moved ? "the engine's AABB seat was corrected on triangles" : "the engine's AABB seat matches the triangles",
429
+ };
430
+ return {
431
+ ok: true,
432
+ op: "SnapToSurface",
433
+ evidence: "visual_mesh",
434
+ subjectPath: args.subjectPath,
435
+ supportPath: measure.support ?? "",
436
+ finalGap: round4(finalGap ?? args.gap),
437
+ requestedGap: args.gap,
438
+ hitTravel: round4(travel),
439
+ ...(slopeDeg !== undefined ? { slopeDeg } : {}),
440
+ changed: moved || context.engineResult?.changed === true,
441
+ before: { origin: roundVec3(asRecord(context.engineResult?.before)?.origin) ?? roundVec3(measure.origin_before) },
442
+ after: { origin: roundVec3(moved ? measure.origin_after : measure.origin_before) },
443
+ evidenceDetails: {
444
+ method: "visible_triangles",
445
+ contactFrom: measure.contact_from,
446
+ samples: measure.samples,
447
+ subjectVertices: measure.subject_vertices,
448
+ supportMeshes: measure.support_meshes,
449
+ supportTriangles: measure.support_triangles,
450
+ subjectColliders: measure.subject_colliders,
451
+ supportHasCollider: measure.support_has_collider,
452
+ },
453
+ engine,
454
+ verify: {
455
+ final_gap: finalGap !== undefined ? round4(finalGap) : null,
456
+ ok: finalGap !== undefined && Math.abs(finalGap - args.gap) <= MESH_SNAP_VERIFY_TOLERANCE,
457
+ read: moved ? "after the move" : "the measurement itself (no move needed)",
458
+ },
459
+ warnings,
460
+ };
461
+ }