summer-engine 3.0.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 (413) 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 +120 -0
  8. package/README.md +22 -7
  9. package/assets/audit/scene_audit.gd +4136 -0
  10. package/assets/seeing/seeing_probe.gd +1873 -0
  11. package/dist/cli/commands/setup.js +6 -19
  12. package/dist/cli/commands/skills.js +57 -32
  13. package/dist/core/api-client.d.ts +9 -1
  14. package/dist/core/api-client.js +21 -4
  15. package/dist/core/capabilities/agent-playbook.js +46 -0
  16. package/dist/core/capabilities/audit/accept.d.ts +55 -0
  17. package/dist/core/capabilities/audit/accept.js +147 -0
  18. package/dist/core/capabilities/audit/args.d.ts +95 -0
  19. package/dist/core/capabilities/audit/args.js +89 -0
  20. package/dist/core/capabilities/audit/audit.d.ts +56 -0
  21. package/dist/core/capabilities/audit/audit.js +287 -0
  22. package/dist/core/capabilities/audit/frame.d.ts +14 -0
  23. package/dist/core/capabilities/audit/frame.js +35 -0
  24. package/dist/core/capabilities/audit/gaps.d.ts +96 -0
  25. package/dist/core/capabilities/audit/gaps.js +257 -0
  26. package/dist/core/capabilities/audit/judge.d.ts +162 -0
  27. package/dist/core/capabilities/audit/judge.js +1134 -0
  28. package/dist/core/capabilities/audit/math.d.ts +110 -0
  29. package/dist/core/capabilities/audit/math.js +263 -0
  30. package/dist/core/capabilities/audit/summary.d.ts +74 -0
  31. package/dist/core/capabilities/audit/summary.js +212 -0
  32. package/dist/core/capabilities/board.d.ts +31 -0
  33. package/dist/core/capabilities/board.js +43 -0
  34. package/dist/core/capabilities/camera-view.d.ts +4 -2
  35. package/dist/core/capabilities/camera-view.js +6 -3
  36. package/dist/core/capabilities/connect-signal.d.ts +85 -0
  37. package/dist/core/capabilities/connect-signal.js +285 -0
  38. package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
  39. package/dist/core/capabilities/engine-fallbacks.js +5 -0
  40. package/dist/core/capabilities/engine-ops.d.ts +5 -1
  41. package/dist/core/capabilities/engine-ops.js +0 -3
  42. package/dist/core/capabilities/engine-receipt.d.ts +12 -0
  43. package/dist/core/capabilities/engine-receipt.js +26 -0
  44. package/dist/core/capabilities/file-read.d.ts +64 -0
  45. package/dist/core/capabilities/file-read.js +325 -0
  46. package/dist/core/capabilities/game-task-plan.js +4 -2
  47. package/dist/core/capabilities/grep.d.ts +60 -0
  48. package/dist/core/capabilities/grep.js +190 -0
  49. package/dist/core/capabilities/inspect-node.d.ts +56 -0
  50. package/dist/core/capabilities/inspect-node.js +225 -0
  51. package/dist/core/capabilities/inspect-resource.d.ts +61 -0
  52. package/dist/core/capabilities/inspect-resource.js +376 -0
  53. package/dist/core/capabilities/math3d.d.ts +28 -0
  54. package/dist/core/capabilities/math3d.js +88 -0
  55. package/dist/core/capabilities/motion-generation.d.ts +62 -0
  56. package/dist/core/capabilities/motion-generation.js +138 -0
  57. package/dist/core/capabilities/placement-batch.d.ts +102 -0
  58. package/dist/core/capabilities/placement-batch.js +483 -0
  59. package/dist/core/capabilities/placement-math.d.ts +70 -0
  60. package/dist/core/capabilities/placement-math.js +209 -0
  61. package/dist/core/capabilities/placement-script.d.ts +42 -0
  62. package/dist/core/capabilities/placement-script.js +1096 -0
  63. package/dist/core/capabilities/placement.d.ts +485 -0
  64. package/dist/core/capabilities/placement.js +1731 -0
  65. package/dist/core/capabilities/project-context.d.ts +33 -2
  66. package/dist/core/capabilities/project-context.js +87 -4
  67. package/dist/core/capabilities/replace-node.d.ts +91 -0
  68. package/dist/core/capabilities/replace-node.js +423 -0
  69. package/dist/core/capabilities/runtime-control.d.ts +20 -4
  70. package/dist/core/capabilities/runtime-control.js +67 -2
  71. package/dist/core/capabilities/scene-batch.d.ts +63 -0
  72. package/dist/core/capabilities/scene-batch.js +394 -0
  73. package/dist/core/capabilities/scene-readback.d.ts +36 -0
  74. package/dist/core/capabilities/scene-readback.js +54 -0
  75. package/dist/core/capabilities/seeing/args.d.ts +348 -0
  76. package/dist/core/capabilities/seeing/args.js +178 -0
  77. package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
  78. package/dist/core/capabilities/seeing/candidates.js +282 -0
  79. package/dist/core/capabilities/seeing/math.d.ts +134 -0
  80. package/dist/core/capabilities/seeing/math.js +269 -0
  81. package/dist/core/capabilities/seeing/probe.d.ts +62 -0
  82. package/dist/core/capabilities/seeing/probe.js +188 -0
  83. package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
  84. package/dist/core/capabilities/seeing/scoring.js +639 -0
  85. package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
  86. package/dist/core/capabilities/seeing/seeing.js +1341 -0
  87. package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
  88. package/dist/core/capabilities/seeing/shot-store.js +204 -0
  89. package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
  90. package/dist/core/capabilities/surface-snap-mesh.js +425 -0
  91. package/dist/core/capabilities/surface-snap.d.ts +77 -0
  92. package/dist/core/capabilities/surface-snap.js +461 -0
  93. package/dist/core/capabilities/tool-dispatch.js +231 -72
  94. package/dist/core/capabilities/tscn.d.ts +85 -0
  95. package/dist/core/capabilities/tscn.js +391 -0
  96. package/dist/core/capabilities/world-snapshot.d.ts +58 -0
  97. package/dist/core/capabilities/world-snapshot.js +150 -0
  98. package/dist/core/capability-skew.d.ts +5 -5
  99. package/dist/core/capability-skew.js +14 -6
  100. package/dist/core/headless/worker-engine-client.d.ts +1 -1
  101. package/dist/core/headless/worker-engine-client.js +2 -2
  102. package/dist/core/library-read.d.ts +37 -5
  103. package/dist/core/library-read.js +264 -33
  104. package/dist/core/library-search.d.ts +2 -2
  105. package/dist/core/skills-registry.d.ts +9 -1
  106. package/dist/core/skills-registry.js +25 -2
  107. package/dist/installer/agent-config.d.ts +9 -4
  108. package/dist/installer/agent-config.js +194 -392
  109. package/dist/installer/agent-table.d.ts +119 -0
  110. package/dist/installer/agent-table.js +619 -0
  111. package/dist/installer/setup.js +4 -2
  112. package/dist/installer/skill-locations.d.ts +8 -17
  113. package/dist/installer/skill-locations.js +22 -101
  114. package/dist/installer/version-check.d.ts +5 -1
  115. package/dist/installer/version-check.js +38 -28
  116. package/dist/mcp/server.js +9 -3
  117. package/dist/mcp/tools/asset-tools.js +21 -3
  118. package/dist/mcp/tools/audit-tools.d.ts +23 -0
  119. package/dist/mcp/tools/audit-tools.js +60 -0
  120. package/dist/mcp/tools/debug-tools.js +3 -1
  121. package/dist/mcp/tools/file-tools.js +17 -4
  122. package/dist/mcp/tools/generate-tools.js +74 -66
  123. package/dist/mcp/tools/library-tools.js +2 -0
  124. package/dist/mcp/tools/perception-tools.js +6 -19
  125. package/dist/mcp/tools/placement-tools.d.ts +24 -0
  126. package/dist/mcp/tools/placement-tools.js +91 -0
  127. package/dist/mcp/tools/project-tools.js +18 -15
  128. package/dist/mcp/tools/scene-tools.js +71 -83
  129. package/dist/mcp/tools/seeing-tools.d.ts +24 -0
  130. package/dist/mcp/tools/seeing-tools.js +64 -0
  131. package/dist/mcp/tools/spatial-tools.js +17 -9
  132. package/dist/mcp/tools/visual-tools.js +59 -7
  133. package/gemini-extension.json +26 -4
  134. package/integrations/README.md +48 -19
  135. package/integrations/amp/README.md +12 -0
  136. package/integrations/amp/manifest-target.json +3 -0
  137. package/integrations/antigravity/README.md +12 -0
  138. package/integrations/antigravity/manifest-target.json +3 -0
  139. package/integrations/claude-desktop/README.md +12 -0
  140. package/integrations/claude-desktop/manifest-target.json +3 -0
  141. package/integrations/cline/README.md +8 -9
  142. package/integrations/cline-cli/README.md +12 -0
  143. package/integrations/cline-cli/manifest-target.json +3 -0
  144. package/integrations/copilot-jetbrains/README.md +12 -0
  145. package/integrations/copilot-jetbrains/manifest-target.json +3 -0
  146. package/integrations/crush/README.md +12 -0
  147. package/integrations/crush/manifest-target.json +3 -0
  148. package/integrations/github-copilot/README.md +8 -8
  149. package/integrations/goose/README.md +12 -0
  150. package/integrations/goose/manifest-target.json +3 -0
  151. package/integrations/grok-build/README.md +12 -0
  152. package/integrations/grok-build/manifest-target.json +3 -0
  153. package/integrations/hermes/README.md +12 -0
  154. package/integrations/hermes/manifest-target.json +3 -0
  155. package/integrations/junie/README.md +12 -0
  156. package/integrations/junie/manifest-target.json +3 -0
  157. package/integrations/kilo-code/README.md +6 -10
  158. package/integrations/kimi-code/README.md +12 -0
  159. package/integrations/kimi-code/manifest-target.json +3 -0
  160. package/integrations/kiro/README.md +12 -0
  161. package/integrations/kiro/manifest-target.json +3 -0
  162. package/integrations/lm-studio/README.md +8 -8
  163. package/integrations/mistral-vibe/README.md +12 -0
  164. package/integrations/mistral-vibe/manifest-target.json +3 -0
  165. package/integrations/opencode/README.md +9 -12
  166. package/integrations/qoder/README.md +12 -0
  167. package/integrations/qoder/manifest-target.json +3 -0
  168. package/integrations/qwen-code/README.md +12 -0
  169. package/integrations/qwen-code/manifest-target.json +3 -0
  170. package/integrations/roo-code/README.md +8 -7
  171. package/integrations/rovo-dev/README.md +12 -0
  172. package/integrations/rovo-dev/manifest-target.json +3 -0
  173. package/integrations/trae/README.md +12 -0
  174. package/integrations/trae/manifest-target.json +3 -0
  175. package/integrations/visual-studio/README.md +12 -0
  176. package/integrations/visual-studio/manifest-target.json +3 -0
  177. package/integrations/vscode-copilot/README.md +9 -9
  178. package/integrations/warp/README.md +12 -0
  179. package/integrations/warp/manifest-target.json +3 -0
  180. package/integrations/windsurf/README.md +8 -9
  181. package/integrations/zed/README.md +12 -0
  182. package/integrations/zed/manifest-target.json +3 -0
  183. package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
  184. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  185. package/library/references/summer-folder/resource.yaml +1 -1
  186. package/library/references/summer-folder/summer-folder.md +1 -1
  187. package/library/skills/3d-lighting/SKILL.md +1 -1
  188. package/library/skills/README.md +264 -0
  189. package/library/skills/adaptive-music/SKILL.md +1 -1
  190. package/library/skills/agent-playtesting/SKILL.md +1 -1
  191. package/library/skills/ai-characters-in-games/SKILL.md +220 -0
  192. package/library/skills/ai-characters-in-games/resource.yaml +30 -0
  193. package/library/skills/ambient-bed/SKILL.md +1 -1
  194. package/library/skills/animated-loop/SKILL.md +1 -1
  195. package/library/skills/animation-tree/SKILL.md +1 -1
  196. package/library/skills/art-direction/SKILL.md +1 -1
  197. package/library/skills/asset-strategy/SKILL.md +2 -1
  198. package/library/skills/asset-strategy/resource.yaml +2 -1
  199. package/library/skills/audio-direction/SKILL.md +1 -1
  200. package/library/skills/auto-fire-targeting/SKILL.md +1 -1
  201. package/library/skills/brainstorm-game/SKILL.md +1 -1
  202. package/library/skills/brainstorming/SKILL.md +1 -1
  203. package/library/skills/browse-templates/SKILL.md +3 -3
  204. package/library/skills/browse-templates/resource.yaml +1 -1
  205. package/library/skills/camera-rigs/SKILL.md +128 -0
  206. package/library/skills/camera-rigs/resource.yaml +29 -0
  207. package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
  208. package/library/skills/character-animation-wiring/SKILL.md +1 -1
  209. package/library/skills/character-model/SKILL.md +1 -1
  210. package/library/skills/character-movement/SKILL.md +326 -0
  211. package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
  212. package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
  213. package/library/skills/character-movement/references/movement-3d.md +136 -0
  214. package/library/skills/character-movement/references/platformer-feel.md +83 -0
  215. package/library/skills/character-movement/resource.yaml +40 -0
  216. package/library/skills/character-portrait/SKILL.md +1 -1
  217. package/library/skills/cinematic-cutscene/SKILL.md +1 -1
  218. package/library/skills/combat-basics/SKILL.md +159 -0
  219. package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
  220. package/library/skills/combat-basics/resource.yaml +34 -0
  221. package/library/skills/concept-art/SKILL.md +1 -1
  222. package/library/skills/create-asset-sheet/SKILL.md +1 -1
  223. package/library/skills/debug/SKILL.md +1 -1
  224. package/library/skills/debugging-game-feel/SKILL.md +1 -1
  225. package/library/skills/design-level/SKILL.md +1 -1
  226. package/library/skills/design-mechanic/SKILL.md +1 -1
  227. package/library/skills/design-npc/SKILL.md +1 -1
  228. package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
  229. package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
  230. package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
  231. package/library/skills/environment-kit/SKILL.md +1 -1
  232. package/library/skills/export-and-ship/SKILL.md +1 -1
  233. package/library/skills/fabricating-assets/SKILL.md +1 -1
  234. package/library/skills/facial-and-lipsync/SKILL.md +1 -1
  235. package/library/skills/fps-controller/SKILL.md +9 -7
  236. package/library/skills/fps-controller/resource.yaml +4 -1
  237. package/library/skills/game-feel/SKILL.md +1 -1
  238. package/library/skills/gameskill/SKILL.md +1 -1
  239. package/library/skills/gdscript-patterns/SKILL.md +1 -1
  240. package/library/skills/generate-motion/SKILL.md +9 -9
  241. package/library/skills/generate-motion/resource.yaml +2 -1
  242. package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
  243. package/library/skills/headless-scripting/SKILL.md +1 -1
  244. package/library/skills/in-game-purchases/SKILL.md +254 -0
  245. package/library/skills/in-game-purchases/resource.yaml +29 -0
  246. package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
  247. package/library/skills/investigating-bugs/SKILL.md +1 -1
  248. package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
  249. package/library/skills/make-game/SKILL.md +25 -3
  250. package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
  251. package/library/skills/make-game/references/2d-platformer.md +117 -0
  252. package/library/skills/make-game/references/2d-rpg-content.md +234 -0
  253. package/library/skills/make-game/references/2d-rpg.md +241 -0
  254. package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
  255. package/library/skills/make-game/references/2d-survivors.md +237 -0
  256. package/library/skills/make-game/references/3d-fps-systems.md +216 -0
  257. package/library/skills/make-game/references/3d-fps.md +177 -0
  258. package/library/skills/make-game/references/3d-horror-systems.md +186 -0
  259. package/library/skills/make-game/references/3d-horror.md +201 -0
  260. package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
  261. package/library/skills/make-game/references/3d-third-person.md +215 -0
  262. package/library/skills/make-game/resource.yaml +6 -1
  263. package/library/skills/multiplayer/SKILL.md +150 -0
  264. package/library/skills/multiplayer/resource.yaml +33 -0
  265. package/library/skills/multiplayer-movement/SKILL.md +510 -0
  266. package/library/skills/multiplayer-movement/files.md +501 -0
  267. package/library/skills/multiplayer-movement/resource.yaml +30 -0
  268. package/library/skills/multiplayer-project/SKILL.md +521 -0
  269. package/library/skills/multiplayer-project/files.md +448 -0
  270. package/library/skills/multiplayer-project/resource.yaml +36 -0
  271. package/library/skills/multiplayer-publish/SKILL.md +229 -0
  272. package/library/skills/multiplayer-publish/export-errors.md +89 -0
  273. package/library/skills/multiplayer-publish/resource.yaml +28 -0
  274. package/library/skills/multiplayer-state/SKILL.md +438 -0
  275. package/library/skills/multiplayer-state/files.md +592 -0
  276. package/library/skills/multiplayer-state/resource.yaml +35 -0
  277. package/library/skills/multiplayer-testing/SKILL.md +236 -0
  278. package/library/skills/multiplayer-testing/resource.yaml +39 -0
  279. package/library/skills/music-track/SKILL.md +1 -1
  280. package/library/skills/navigate-summer/SKILL.md +1 -1
  281. package/library/skills/new-project/SKILL.md +1 -1
  282. package/library/skills/organic-model/SKILL.md +1 -1
  283. package/library/skills/pixel-art/SKILL.md +1 -1
  284. package/library/skills/play/SKILL.md +1 -1
  285. package/library/skills/playtesting-a-feature/SKILL.md +1 -1
  286. package/library/skills/procedural-animation/SKILL.md +2 -2
  287. package/library/skills/procedural-animation/resource.yaml +1 -1
  288. package/library/skills/prop-model/SKILL.md +1 -1
  289. package/library/skills/psx-retro-rendering/SKILL.md +1 -1
  290. package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
  291. package/library/skills/remote-deploy/SKILL.md +1 -1
  292. package/library/skills/retarget/SKILL.md +1 -1
  293. package/library/skills/running-in-the-cloud/SKILL.md +1 -1
  294. package/library/skills/save-load/SKILL.md +156 -0
  295. package/library/skills/save-load/resource.yaml +27 -0
  296. package/library/skills/scene-composition/SKILL.md +1 -1
  297. package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
  298. package/library/skills/scene-scripting/SKILL.md +1 -1
  299. package/library/skills/scene-to-level/SKILL.md +1 -1
  300. package/library/skills/skill-create/SKILL.md +1 -1
  301. package/library/skills/skill-improve/SKILL.md +1 -1
  302. package/library/skills/skill-test/SKILL.md +1 -1
  303. package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
  304. package/library/skills/skybox-panorama/SKILL.md +1 -1
  305. package/library/skills/sound-effect/SKILL.md +1 -1
  306. package/library/skills/spatial-placement/SKILL.md +134 -3
  307. package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
  308. package/library/skills/spatial-placement/resource.yaml +16 -2
  309. package/library/skills/sprite-sheet/SKILL.md +1 -1
  310. package/library/skills/summer-analytics/SKILL.md +147 -0
  311. package/library/skills/summer-analytics/resource.yaml +28 -0
  312. package/library/skills/summer-friends/SKILL.md +226 -0
  313. package/library/skills/summer-friends/resource.yaml +28 -0
  314. package/library/skills/summer-leaderboards/SKILL.md +231 -0
  315. package/library/skills/summer-leaderboards/resource.yaml +30 -0
  316. package/library/skills/summer-match-results/SKILL.md +286 -0
  317. package/library/skills/summer-match-results/resource.yaml +32 -0
  318. package/library/skills/summer-matchmaking/SKILL.md +301 -0
  319. package/library/skills/summer-matchmaking/resource.yaml +32 -0
  320. package/library/skills/summer-parties/SKILL.md +253 -0
  321. package/library/skills/summer-parties/resource.yaml +30 -0
  322. package/library/skills/summer-player-data/SKILL.md +322 -0
  323. package/library/skills/summer-player-data/resource.yaml +30 -0
  324. package/library/skills/summer-store/SKILL.md +271 -0
  325. package/library/skills/summer-store/resource.yaml +29 -0
  326. package/library/skills/summer-world-chat/SKILL.md +195 -0
  327. package/library/skills/summer-world-chat/resource.yaml +30 -0
  328. package/library/skills/summer-world-saves/SKILL.md +284 -0
  329. package/library/skills/summer-world-saves/resource.yaml +29 -0
  330. package/library/skills/text-to-motion/SKILL.md +99 -0
  331. package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
  332. package/library/skills/text-to-motion/resource.yaml +41 -0
  333. package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
  334. package/library/skills/tileable-texture/SKILL.md +1 -1
  335. package/library/skills/trailer-shot/SKILL.md +1 -1
  336. package/library/skills/tune-performance/SKILL.md +1 -1
  337. package/library/skills/ui-basics/SKILL.md +10 -1
  338. package/library/skills/ui-graphics/SKILL.md +1 -1
  339. package/library/skills/use-widget-asset/SKILL.md +1 -1
  340. package/library/skills/using-summer/SKILL.md +2 -2
  341. package/library/skills/using-summer/resource.yaml +2 -2
  342. package/library/skills/vehicle-model/SKILL.md +1 -1
  343. package/library/skills/verification-before-completion/SKILL.md +1 -1
  344. package/library/skills/verifying-scenes/SKILL.md +76 -2
  345. package/library/skills/verifying-scenes/resource.yaml +13 -2
  346. package/library/skills/vfx-dissolve/SKILL.md +1 -1
  347. package/library/skills/vfx-fire/SKILL.md +1 -1
  348. package/library/skills/vfx-hit-spark/SKILL.md +1 -1
  349. package/library/skills/vfx-lightning/SKILL.md +1 -1
  350. package/library/skills/vfx-magic-glow/SKILL.md +1 -1
  351. package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
  352. package/library/skills/vfx-smoke/SKILL.md +1 -1
  353. package/library/skills/vfx-water-ripple/SKILL.md +1 -1
  354. package/library/skills/voice-line/SKILL.md +1 -1
  355. package/library/skills/web-and-touch-input/SKILL.md +38 -0
  356. package/library/skills/web-and-touch-input/resource.yaml +28 -0
  357. package/library/skills/web-compatibility-look/SKILL.md +62 -0
  358. package/library/skills/web-compatibility-look/resource.yaml +30 -0
  359. package/library/skills/world-building-3d/SKILL.md +8 -1
  360. package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
  361. package/library/skills/world-building-3d/resource.yaml +2 -1
  362. package/library/skills/writing-plans/SKILL.md +1 -1
  363. package/library/skills/writing-skills/SKILL.md +1 -1
  364. package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
  365. package/library/tools/attach-to-surface/resource.yaml +142 -0
  366. package/library/tools/batch/resource.yaml +9 -3
  367. package/library/tools/connect-ports/resource.yaml +120 -0
  368. package/library/tools/connect-signal/resource.yaml +3 -2
  369. package/library/tools/debug-views/resource.yaml +94 -0
  370. package/library/tools/frame-nodes/resource.yaml +104 -0
  371. package/library/tools/frame-shot/resource.yaml +134 -0
  372. package/library/tools/generate-motion/resource.yaml +44 -9
  373. package/library/tools/get-board/resource.yaml +50 -0
  374. package/library/tools/get-project-context/resource.yaml +13 -2
  375. package/library/tools/grep/resource.yaml +77 -0
  376. package/library/tools/inspect-asset/resource.yaml +71 -0
  377. package/library/tools/inspect-node/resource.yaml +9 -2
  378. package/library/tools/inspect-resource/resource.yaml +16 -5
  379. package/library/tools/instantiate-scene/resource.yaml +27 -2
  380. package/library/tools/measure/resource.yaml +96 -0
  381. package/library/tools/place-adjacent/resource.yaml +113 -0
  382. package/library/tools/play/resource.yaml +15 -2
  383. package/library/tools/raycast/resource.yaml +101 -0
  384. package/library/tools/read-file/resource.yaml +36 -2
  385. package/library/tools/read-library/resource.yaml +2 -2
  386. package/library/tools/repeat-along/resource.yaml +129 -0
  387. package/library/tools/replace-node/resource.yaml +11 -5
  388. package/library/tools/scene-audit/resource.yaml +144 -0
  389. package/library/tools/screenshot/resource.yaml +13 -2
  390. package/library/tools/search-assets/resource.yaml +18 -0
  391. package/library/tools/shot-sheet/resource.yaml +110 -0
  392. package/library/tools/snap-to-surface/resource.yaml +4 -1
  393. package/library/tools/world-snapshot/resource.yaml +24 -3
  394. package/library/tools/zoom/resource.yaml +105 -0
  395. package/package.json +3 -3
  396. package/registry/generated/aliases.json +17 -3
  397. package/registry/generated/counts.json +3 -3
  398. package/registry/generated/gemini-extension.json +26 -4
  399. package/registry/generated/index.json +2154 -469
  400. package/registry/generated/marketplace.claude.json +2 -2
  401. package/registry/generated/plugin.claude.json +27 -5
  402. package/registry/generated/plugin.codex.json +28 -6
  403. package/registry/generated/plugin.cursor.json +27 -5
  404. package/registry/generated/plugin.factory.json +27 -5
  405. package/registry/generated/skills-index.md +264 -0
  406. package/registry/generated/skills-registry.json +901 -206
  407. package/registry/generated/templates-registry.json +2 -2
  408. package/library/skills/host-authoritative-state/SKILL.md +0 -332
  409. package/library/skills/host-authoritative-state/resource.yaml +0 -28
  410. package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
  411. package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
  412. package/library/skills/setup-multiplayer/SKILL.md +0 -336
  413. package/library/skills/setup-multiplayer/resource.yaml +0 -28
@@ -0,0 +1,1096 @@
1
+ /**
2
+ * The measurement probe behind the placement tools. ONE GDScript, sent through
3
+ * the existing RunSceneScript op (summer_run_script's op: in-process, on the
4
+ * live editor's main thread, no child editor is spawned), with a JSON argument
5
+ * block selecting the command. It only READS: it never adds, moves, or frees a
6
+ * node of an open scene. Mutations are sent afterwards as ordinary SetProp /
7
+ * SnapToSurface / InstantiateScene ops so they get the scene-target, undo and
8
+ * save contract every other scene tool has.
9
+ *
10
+ * Commands:
11
+ * inspect_asset load a .tscn/.glb/.gltf/Mesh, instantiate it OFF the scene
12
+ * tree, measure (AABB, meshes, planar faces, open boundary
13
+ * loops, Marker3D anchors, collision shapes), free it.
14
+ * bounds visible-geometry extents of nodes along three directions
15
+ * (world axes or a node's local axes), plus transforms.
16
+ * raycast physics ray (active scene only) and a visual-AABB ray.
17
+ * ports resolve Marker3D ports (optionally with the subject's
18
+ * other markers), or return every open loop of the node in
19
+ * its own frame for placement.ts to order and pick from.
20
+ * blockers what a node overlaps where it stands and what it would
21
+ * touch first moving along a direction.
22
+ * multi run several of the scene commands above in one call.
23
+ *
24
+ * "Scene space" = the scene root's frame, accumulated by hand from
25
+ * Node3D.transform (the same walk GetWorldSnapshot uses), so it works for an
26
+ * open scene tab that is not the active one. Physics needs the active tab:
27
+ * only that scene's bodies are in the editor's physics space.
28
+ */
29
+ export const PLACEMENT_SCRIPT_BUDGET_SECONDS = 20;
30
+ /** Placeholder for the argument literal; appears exactly once in the source. */
31
+ export const PLACEMENT_ARGS_TOKEN = "__SUMMER_PLACEMENT_ARGS__";
32
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
33
+ /**
34
+ * Encode the arguments as base64 of their UTF-8 JSON. No caller text is ever
35
+ * interpolated into the GDScript: node paths and port names can come from
36
+ * scene content, so they are data the script decodes (Marshalls.base64_to_utf8
37
+ * + JSON.parse_string), never source it compiles. The base64 alphabet cannot
38
+ * close the string literal or start a statement.
39
+ */
40
+ export function encodeScriptArgs(args) {
41
+ const encoded = Buffer.from(JSON.stringify(args), "utf8").toString("base64");
42
+ if (!BASE64.test(encoded))
43
+ throw new Error("placement probe arguments did not encode to base64");
44
+ return encoded;
45
+ }
46
+ export function buildPlacementScript(args) {
47
+ // split/join, not String.replace: a replacement string would interpret $&,
48
+ // $`, $' and $1 patterns. The literal here is base64 only, but the splice
49
+ // stays literal regardless of what it carries.
50
+ return PLACEMENT_PROBE_SOURCE.split(PLACEMENT_ARGS_TOKEN).join(`"${encodeScriptArgs(args)}"`);
51
+ }
52
+ /** The probe source with its placeholder, for tests. */
53
+ export function placementProbeTemplate() {
54
+ return PLACEMENT_PROBE_SOURCE;
55
+ }
56
+ // Tabs are significant: GDScript indentation. String.raw keeps every backslash
57
+ // literal; the source never contains a template placeholder.
58
+ const PLACEMENT_PROBE_SOURCE = String.raw `@tool
59
+ extends RefCounted
60
+
61
+ const ARGS_B64 = __SUMMER_PLACEMENT_ARGS__
62
+ const MAX_GEOMS = 20000
63
+
64
+
65
+ func run(_ctx):
66
+ var args = JSON.parse_string(Marshalls.base64_to_utf8(ARGS_B64))
67
+ if typeof(args) != TYPE_DICTIONARY:
68
+ return _fail("bad_args", "placement probe arguments did not parse")
69
+ var cmd = String(args.get("cmd", ""))
70
+ if cmd == "inspect_asset":
71
+ return _cmd_inspect_asset(args)
72
+ var scene = _open_scene(String(args.get("scene_path", "")))
73
+ if scene.has("fail"):
74
+ return scene["fail"]
75
+ if cmd == "multi":
76
+ var steps = []
77
+ for step in args.get("steps", []):
78
+ var r = _dispatch(scene["root"], scene["active"], step)
79
+ steps.append(r)
80
+ if not r.get("ok", false):
81
+ return r
82
+ return {"ok": true, "steps": steps}
83
+ return _dispatch(scene["root"], scene["active"], args)
84
+
85
+
86
+ func _dispatch(root, active, args):
87
+ var cmd = String(args.get("cmd", ""))
88
+ if cmd == "bounds":
89
+ return _cmd_bounds(root, active, args)
90
+ if cmd == "raycast":
91
+ return _cmd_raycast(root, active, args)
92
+ if cmd == "ports":
93
+ return _cmd_ports(root, args)
94
+ if cmd == "blockers":
95
+ return _cmd_blockers(root, active, args)
96
+ return _fail("bad_args", "unknown placement probe command: " + cmd)
97
+
98
+
99
+ # ---------------------------------------------------------------- helpers
100
+
101
+ func _fail(reason, message):
102
+ return {"ok": false, "failure_reason": reason, "error": message}
103
+
104
+
105
+ func _r(x):
106
+ return snappedf(float(x), 0.001)
107
+
108
+
109
+ func _v3(v):
110
+ return [snappedf(v.x, 0.001), snappedf(v.y, 0.001), snappedf(v.z, 0.001)]
111
+
112
+
113
+ func _d3(v):
114
+ return [snappedf(v.x, 0.0001), snappedf(v.y, 0.0001), snappedf(v.z, 0.0001)]
115
+
116
+
117
+ func _raw3(v):
118
+ return [v.x, v.y, v.z]
119
+
120
+
121
+ func _xf12(t):
122
+ return [t.basis.x.x, t.basis.x.y, t.basis.x.z, t.basis.y.x, t.basis.y.y, t.basis.y.z, t.basis.z.x, t.basis.z.y, t.basis.z.z, t.origin.x, t.origin.y, t.origin.z]
123
+
124
+
125
+ func _vec(a):
126
+ return Vector3(float(a[0]), float(a[1]), float(a[2]))
127
+
128
+
129
+ func _open_scene(scene_path):
130
+ var wanted = scene_path.simplify_path()
131
+ var edited = EditorInterface.get_edited_scene_root()
132
+ for r in EditorInterface.get_open_scene_roots():
133
+ if r != null and String(r.scene_file_path).simplify_path() == wanted:
134
+ return {"root": r, "active": r == edited and r.is_inside_tree()}
135
+ return {"fail": _fail("scene_not_open", "Scene " + wanted + " is not open in the editor. Open it with summer_open_scene, or run any scene mutation with this scenePath (that opens it), then retry.")}
136
+
137
+
138
+ func _node(root, path):
139
+ var p = String(path).strip_edges()
140
+ if p == "" or p == "." or p == "./":
141
+ return root
142
+ if p.begins_with("./"):
143
+ p = p.substr(2)
144
+ var n = root.get_node_or_null(NodePath(p))
145
+ if n == null and p.begins_with(String(root.name) + "/"):
146
+ n = root.get_node_or_null(NodePath(p.substr(String(root.name).length() + 1)))
147
+ return n
148
+
149
+
150
+ func _rel(root, n):
151
+ if n == root:
152
+ return "."
153
+ return String(root.get_path_to(n))
154
+
155
+
156
+ # Scene-space transform: Node3D.transform accumulated up to and including root.
157
+ func _xf(node, root):
158
+ var t = Transform3D.IDENTITY
159
+ var cur = node
160
+ while cur != null:
161
+ if cur is Node3D:
162
+ t = cur.transform * t
163
+ if cur.top_level and cur != root:
164
+ break
165
+ if cur == root:
166
+ break
167
+ cur = cur.get_parent()
168
+ return t
169
+
170
+
171
+ func _visible(node, root):
172
+ var cur = node
173
+ while cur != null:
174
+ if cur is Node3D and not cur.visible:
175
+ return false
176
+ if cur == root:
177
+ break
178
+ cur = cur.get_parent()
179
+ return true
180
+
181
+
182
+ # Visible GeometryInstance3D descendants of start (inclusive) with their
183
+ # scene-space transform and local AABB: the bounds definition AlignDistribute3D
184
+ # uses. exclude holds subtree roots to skip.
185
+ func _geoms(start, root, exclude):
186
+ var base_xf = Transform3D.IDENTITY
187
+ var base_vis = true
188
+ if start != root and start.get_parent() != null:
189
+ base_xf = _xf(start.get_parent(), root)
190
+ base_vis = _visible(start.get_parent(), root)
191
+ var out = []
192
+ var stack = [[start, base_xf, base_vis]]
193
+ var visited = 0
194
+ while stack.size() > 0:
195
+ visited += 1
196
+ if visited > MAX_GEOMS:
197
+ break
198
+ var item = stack.pop_back()
199
+ var n = item[0]
200
+ if exclude.has(n):
201
+ continue
202
+ var t = item[1]
203
+ var vis = item[2]
204
+ if n is Node3D:
205
+ if n.top_level and n != root:
206
+ t = n.transform
207
+ else:
208
+ t = t * n.transform
209
+ vis = vis and n.visible
210
+ if vis and n is GeometryInstance3D:
211
+ var bb = n.get_aabb()
212
+ if bb.size != Vector3.ZERO:
213
+ out.append([n, t, bb])
214
+ for c in n.get_children():
215
+ stack.append([c, t, vis])
216
+ return out
217
+
218
+
219
+ func _corners(bb, t):
220
+ var out = []
221
+ var p = bb.position
222
+ var s = bb.size
223
+ for i in range(8):
224
+ var c = Vector3(p.x + (s.x if (i & 1) != 0 else 0.0), p.y + (s.y if (i & 2) != 0 else 0.0), p.z + (s.z if (i & 4) != 0 else 0.0))
225
+ out.append(t * c)
226
+ return out
227
+
228
+
229
+ # [min, max] of the geometry corners projected on each direction.
230
+ func _intervals(geoms, dirs):
231
+ var lo = []
232
+ var hi = []
233
+ for d in dirs:
234
+ lo.append(INF)
235
+ hi.append(-INF)
236
+ for g in geoms:
237
+ for c in _corners(g[2], g[1]):
238
+ for k in range(dirs.size()):
239
+ var v = c.dot(dirs[k])
240
+ if v < lo[k]:
241
+ lo[k] = v
242
+ if v > hi[k]:
243
+ hi[k] = v
244
+ var out = []
245
+ for k in range(dirs.size()):
246
+ out.append([lo[k], hi[k]])
247
+ return out
248
+
249
+
250
+ func _dirs(root, args):
251
+ var space_node = String(args.get("space_node", ""))
252
+ if space_node == "":
253
+ return {"dirs": [Vector3.RIGHT, Vector3.UP, Vector3.BACK], "space": "world"}
254
+ var n = _node(root, space_node)
255
+ if n == null:
256
+ return {"fail": _fail("node_not_found", "space node not found: " + space_node)}
257
+ if not (n is Node3D):
258
+ return {"fail": _fail("not_node3d", "space node is not a Node3D: " + space_node)}
259
+ var b = _xf(n, root).basis.orthonormalized()
260
+ return {"dirs": [b.x.normalized(), b.y.normalized(), b.z.normalized()], "space": "local"}
261
+
262
+
263
+ # ---------------------------------------------------------------- bounds
264
+
265
+ func _cmd_bounds(root, active, args):
266
+ var d = _dirs(root, args)
267
+ if d.has("fail"):
268
+ return d["fail"]
269
+ var dirs = d["dirs"]
270
+ var nodes = []
271
+ var excluded_subjects = []
272
+ for spec in args.get("nodes", []):
273
+ var path = String(spec.get("path", ""))
274
+ var n = _node(root, path)
275
+ if n == null:
276
+ return _fail("node_not_found", "node not found in scene: " + path)
277
+ if not (n is Node3D):
278
+ return _fail("not_node3d", "node is not a Node3D: " + path)
279
+ var exclude = []
280
+ for ex in spec.get("exclude", []):
281
+ var en = _node(root, String(ex))
282
+ if en != null and n.is_ancestor_of(en):
283
+ exclude.append(en)
284
+ excluded_subjects.append(String(ex))
285
+ var geoms = _geoms(n, root, exclude)
286
+ var rec = {"path": path, "resolved": _rel(root, n), "geometry_count": geoms.size(), "xform": _xf12(_xf(n, root))}
287
+ if n != root and n.get_parent() != null:
288
+ rec["parent_xform"] = _xf12(_xf(n.get_parent(), root))
289
+ else:
290
+ rec["parent_xform"] = _xf12(Transform3D.IDENTITY)
291
+ rec["position"] = _raw3(n.position)
292
+ rec["transform_str"] = var_to_str(n.transform)
293
+ if geoms.size() > 0:
294
+ rec["intervals"] = _intervals(geoms, dirs)
295
+ var reach = 0.0
296
+ var o = _xf(n, root).origin
297
+ for g in geoms:
298
+ for c in _corners(g[2], g[1]):
299
+ reach = maxf(reach, (c - o).length())
300
+ rec["reach"] = reach
301
+ nodes.append(rec)
302
+ var dir_out = []
303
+ for v in dirs:
304
+ dir_out.append(_raw3(v))
305
+ return {"ok": true, "space": d["space"], "dirs": dir_out, "nodes": nodes, "scene_active": active, "excluded": excluded_subjects}
306
+
307
+
308
+ # ---------------------------------------------------------------- raycast
309
+
310
+ func _phys_ray(root, active, origin, dir, dist, mask, areas, exclude_nodes, accept_under):
311
+ if not active:
312
+ return {"available": false, "reason": "scene_not_active"}
313
+ var vp = root.get_viewport()
314
+ var world = vp.find_world_3d() if vp != null else null
315
+ if world == null:
316
+ return {"available": false, "reason": "no_world_3d"}
317
+ var space = world.direct_space_state
318
+ if space == null:
319
+ return {"available": false, "reason": "no_space_state"}
320
+ var ex: Array[RID] = []
321
+ for en in exclude_nodes:
322
+ var stack = [en]
323
+ while stack.size() > 0:
324
+ var n = stack.pop_back()
325
+ if n is CollisionObject3D:
326
+ ex.append(n.get_rid())
327
+ for c in n.get_children():
328
+ stack.append(c)
329
+ var skipped = 0
330
+ for _attempt in range(9):
331
+ var q = PhysicsRayQueryParameters3D.create(origin, origin + dir * dist, mask, ex)
332
+ q.collide_with_areas = areas
333
+ q.collide_with_bodies = true
334
+ var hit = space.intersect_ray(q)
335
+ if hit.is_empty():
336
+ return {"available": true, "hit": false, "skipped": skipped}
337
+ var col = hit.get("collider")
338
+ if accept_under != null and col is Node and not (col == accept_under or accept_under.is_ancestor_of(col)):
339
+ ex.append(hit.get("rid"))
340
+ skipped += 1
341
+ continue
342
+ var path = ""
343
+ if col is Node:
344
+ path = _rel(root, col) if (col == root or root.is_ancestor_of(col)) else String(col.get_path())
345
+ return {"available": true, "hit": true, "path": path, "point": hit.get("position"), "normal": hit.get("normal"), "skipped": skipped}
346
+ return {"available": true, "hit": false, "skipped": skipped, "gave_up": true}
347
+
348
+
349
+ func _vis_ray(geoms, origin, dir, dist, accept_under):
350
+ var best_t = INF
351
+ var best = null
352
+ var best_axis = -1
353
+ var inside = 0
354
+ for g in geoms:
355
+ if accept_under != null and not (g[0] == accept_under or accept_under.is_ancestor_of(g[0])):
356
+ continue
357
+ var cs = _corners(g[2], g[1])
358
+ var mn = cs[0]
359
+ var mx = cs[0]
360
+ for c in cs:
361
+ mn = Vector3(minf(mn.x, c.x), minf(mn.y, c.y), minf(mn.z, c.z))
362
+ mx = Vector3(maxf(mx.x, c.x), maxf(mx.y, c.y), maxf(mx.z, c.z))
363
+ var tmin = -INF
364
+ var tmax = INF
365
+ var axis = -1
366
+ var miss = false
367
+ for k in range(3):
368
+ var o = origin[k]
369
+ var dk = dir[k]
370
+ if absf(dk) < 0.000000001:
371
+ if o < mn[k] or o > mx[k]:
372
+ miss = true
373
+ break
374
+ else:
375
+ var t1 = (mn[k] - o) / dk
376
+ var t2 = (mx[k] - o) / dk
377
+ if t1 > t2:
378
+ var tmp = t1
379
+ t1 = t2
380
+ t2 = tmp
381
+ if t1 > tmin:
382
+ tmin = t1
383
+ axis = k
384
+ if t2 < tmax:
385
+ tmax = t2
386
+ if miss or axis < 0 or tmin > tmax or tmax < 0.0:
387
+ continue
388
+ if tmin < 0.0:
389
+ inside += 1
390
+ continue
391
+ if tmin <= dist and tmin < best_t:
392
+ best_t = tmin
393
+ best = g[0]
394
+ best_axis = axis
395
+ if best == null:
396
+ return {"hit": false, "inside": inside}
397
+ var nrm = Vector3.ZERO
398
+ nrm[best_axis] = -signf(dir[best_axis])
399
+ return {"hit": true, "node": best, "t": best_t, "point": origin + dir * best_t, "normal": nrm, "inside": inside}
400
+
401
+
402
+ func _cmd_raycast(root, active, args):
403
+ var origin = _vec(args.get("origin", [0, 0, 0]))
404
+ var dir = _vec(args.get("direction", [0, -1, 0]))
405
+ if dir.length() < 0.000001:
406
+ return _fail("bad_args", "direction must be non-zero")
407
+ dir = dir.normalized()
408
+ var dist = float(args.get("max_distance", 100.0))
409
+ var mask = int(args.get("collision_mask", 4294967295))
410
+ var areas = bool(args.get("collide_with_areas", false))
411
+ var mode = String(args.get("evidence", "auto"))
412
+ var exclude_nodes = []
413
+ for p in args.get("exclude", []):
414
+ var en = _node(root, String(p))
415
+ if en == null:
416
+ return _fail("node_not_found", "exclude node not found: " + String(p))
417
+ exclude_nodes.append(en)
418
+ var accept_under = null
419
+ var surface = String(args.get("surface", ""))
420
+ if surface != "":
421
+ accept_under = _node(root, surface)
422
+ if accept_under == null:
423
+ return _fail("node_not_found", "surface node not found: " + surface)
424
+ # A ray from a subject toward a surface node: start at the subject origin,
425
+ # aim at the nearest point of the surface's visible bounds.
426
+ var from_subject = String(args.get("from_subject", ""))
427
+ if from_subject != "":
428
+ var sn = _node(root, from_subject)
429
+ if sn == null:
430
+ return _fail("node_not_found", "subject not found: " + from_subject)
431
+ if accept_under == null:
432
+ return _fail("bad_args", "from_subject needs a surface node")
433
+ origin = _xf(sn, root).origin
434
+ var sg = _geoms(accept_under, root, [])
435
+ if sg.size() == 0:
436
+ return _fail("surface_has_no_visual_bounds", "surface has no visible geometry to aim at: " + surface)
437
+ var iv = _intervals(sg, [Vector3.RIGHT, Vector3.UP, Vector3.BACK])
438
+ var closest = Vector3(clampf(origin.x, iv[0][0], iv[0][1]), clampf(origin.y, iv[1][0], iv[1][1]), clampf(origin.z, iv[2][0], iv[2][1]))
439
+ if (closest - origin).length() < 0.0001:
440
+ return _fail("subject_inside_surface_bounds", "the subject origin is inside the surface's bounds, so no direction toward it is defined; pass an explicit ray")
441
+ dir = (closest - origin).normalized()
442
+ exclude_nodes.append(sn)
443
+ var out = {"ok": true, "origin": _raw3(origin), "direction": _raw3(dir), "scene_active": active}
444
+ var phys = {"available": false, "reason": "not_requested"}
445
+ if mode != "visual_aabb":
446
+ phys = _phys_ray(root, active, origin, dir, dist, mask, areas, exclude_nodes, accept_under)
447
+ var geoms = _geoms(root, root, exclude_nodes)
448
+ var vis = _vis_ray(geoms, origin, dir, dist, accept_under)
449
+ out["physics_available"] = phys.get("available", false)
450
+ if not phys.get("available", false):
451
+ out["physics_unavailable_reason"] = phys.get("reason", "")
452
+ if phys.get("hit", false):
453
+ out["physics"] = {"path": phys["path"], "point": _raw3(phys["point"]), "normal": _raw3(phys["normal"]), "distance": (phys["point"] - origin).length(), "skipped": phys.get("skipped", 0)}
454
+ if vis.get("hit", false):
455
+ out["visual"] = {"path": _rel(root, vis["node"]), "point": _raw3(vis["point"]), "normal": _raw3(vis["normal"]), "distance": vis["t"]}
456
+ out["visual_origin_inside"] = vis.get("inside", 0)
457
+ if geoms.size() >= MAX_GEOMS:
458
+ out["visual_truncated"] = true
459
+ return out
460
+
461
+
462
+ # ---------------------------------------------------------------- meshes
463
+
464
+ # Triangles of the given meshes (each [MeshInstance3D, frame transform, aabb])
465
+ # in one frame: area-binned planar faces and open boundary loops.
466
+ func _analyze(meshes, budget, want_planes, want_loops):
467
+ var planes = {}
468
+ var loops = []
469
+ var analyzed = 0
470
+ var truncated = false
471
+ var open_chains = 0
472
+ for mi in range(meshes.size()):
473
+ if truncated:
474
+ break
475
+ var m = meshes[mi]
476
+ var mesh = m[0].mesh
477
+ var t = m[1]
478
+ var flip = t.basis.determinant() < 0.0
479
+ var weld = {}
480
+ var pos = PackedVector3Array()
481
+ var edges = {}
482
+ for s in range(mesh.get_surface_count()):
483
+ if truncated:
484
+ break
485
+ if not _is_tris(mesh, s):
486
+ continue
487
+ var culls = _culls_back(m[0], s) if want_planes else true
488
+ var arr = mesh.surface_get_arrays(s)
489
+ var verts = arr[Mesh.ARRAY_VERTEX]
490
+ if verts == null or verts.size() == 0:
491
+ continue
492
+ if verts.size() > budget * 3:
493
+ truncated = true
494
+ break
495
+ var idx = arr[Mesh.ARRAY_INDEX]
496
+ var has_idx = idx != null and idx.size() > 0
497
+ var count = idx.size() if has_idx else verts.size()
498
+ var ids = PackedInt32Array()
499
+ ids.resize(verts.size())
500
+ var wv = PackedVector3Array()
501
+ wv.resize(verts.size())
502
+ for i in range(verts.size()):
503
+ var p = t * verts[i]
504
+ wv[i] = p
505
+ var key = Vector3i(roundi(p.x * 10000.0), roundi(p.y * 10000.0), roundi(p.z * 10000.0))
506
+ var id = weld.get(key, -1)
507
+ if id < 0:
508
+ id = pos.size()
509
+ weld[key] = id
510
+ pos.append(p)
511
+ ids[i] = id
512
+ var k = 0
513
+ while k + 2 < count:
514
+ if analyzed >= budget:
515
+ truncated = true
516
+ break
517
+ analyzed += 1
518
+ var i0 = idx[k] if has_idx else k
519
+ var i1 = idx[k + 1] if has_idx else k + 1
520
+ var i2 = idx[k + 2] if has_idx else k + 2
521
+ k += 3
522
+ if want_planes:
523
+ var a = wv[i0]
524
+ var cr = (a - wv[i2]).cross(a - wv[i1])
525
+ if flip:
526
+ cr = -cr
527
+ var l = cr.length()
528
+ if l > 0.0000000001:
529
+ var nn = cr / l
530
+ var area = l * 0.5
531
+ var ca = area if culls else 0.0
532
+ var off = nn.dot(a)
533
+ var pk = Vector4i(roundi(nn.x * 40.0), roundi(nn.y * 40.0), roundi(nn.z * 40.0), roundi(off * 100.0))
534
+ var acc = planes.get(pk)
535
+ if acc == null:
536
+ planes[pk] = [area, nn * area, off * area, 1, ca]
537
+ else:
538
+ acc[0] += area
539
+ acc[1] += nn * area
540
+ acc[2] += off * area
541
+ acc[3] += 1
542
+ acc[4] += ca
543
+ if want_loops:
544
+ _edge(edges, ids[i0], ids[i1])
545
+ _edge(edges, ids[i1], ids[i2])
546
+ _edge(edges, ids[i2], ids[i0])
547
+ if want_loops and edges.size() > 0:
548
+ var mc = t * m[2].get_center()
549
+ var found = _loops(edges, pos)
550
+ open_chains += found["open"]
551
+ for lp in found["loops"]:
552
+ var rec = _loop_record(lp, pos, mc)
553
+ rec["mesh"] = mi
554
+ loops.append(rec)
555
+ var plist = []
556
+ for pk in planes:
557
+ var acc = planes[pk]
558
+ plist.append({"area": acc[0], "normal": (acc[1] / acc[0]).normalized(), "offset": acc[2] / acc[0], "tris": acc[3], "cull_back": acc[4] / acc[0]})
559
+ plist.sort_custom(func(a, b): return a["area"] > b["area"])
560
+ # Loops keep their discovery order; the caller (placement.ts orderLoops)
561
+ # gives them their stable order and ids from direction and position.
562
+ return {"planes": plist, "loops": loops, "analyzed": analyzed, "truncated": truncated, "open_chains": open_chains}
563
+
564
+
565
+ # Whether surface s of a MeshInstance3D is drawn with back-face culling, so its
566
+ # faces are invisible from behind. No material means the default material,
567
+ # which culls back faces.
568
+ func _culls_back(mi, s):
569
+ var mat = mi.get_active_material(s)
570
+ if mat == null:
571
+ return true
572
+ if mat is BaseMaterial3D:
573
+ return mat.cull_mode == BaseMaterial3D.CULL_BACK
574
+ if mat is ShaderMaterial:
575
+ if mat.shader == null:
576
+ return true
577
+ var code = String(mat.shader.code)
578
+ return not (code.contains("cull_disabled") or code.contains("cull_front"))
579
+ return true
580
+
581
+
582
+ func _edge(edges, a, b):
583
+ if a == b:
584
+ return
585
+ var k = _ek(a, b)
586
+ edges[k] = edges.get(k, 0) + 1
587
+
588
+
589
+ func _ek(a, b):
590
+ return Vector2i(a, b) if a < b else Vector2i(b, a)
591
+
592
+
593
+ func _loops(edges, pos):
594
+ var adj = {}
595
+ var boundary = 0
596
+ for k in edges:
597
+ if edges[k] != 1:
598
+ continue
599
+ boundary += 1
600
+ if boundary > 200000:
601
+ break
602
+ var a = k.x
603
+ var b = k.y
604
+ if not adj.has(a):
605
+ adj[a] = []
606
+ if not adj.has(b):
607
+ adj[b] = []
608
+ adj[a].append(b)
609
+ adj[b].append(a)
610
+ var used = {}
611
+ var loops = []
612
+ var open = 0
613
+ for start in adj:
614
+ if loops.size() >= 256:
615
+ break
616
+ for nb in adj[start]:
617
+ var ek = _ek(start, nb)
618
+ if used.has(ek):
619
+ continue
620
+ used[ek] = true
621
+ var loop = [start]
622
+ var cur = nb
623
+ var closed = false
624
+ var guard = 0
625
+ while guard < 200000:
626
+ guard += 1
627
+ if cur == start:
628
+ closed = true
629
+ break
630
+ loop.append(cur)
631
+ var nxt = -1
632
+ for cand in adj[cur]:
633
+ var ck = _ek(cur, cand)
634
+ if not used.has(ck):
635
+ used[ck] = true
636
+ nxt = cand
637
+ break
638
+ if nxt < 0:
639
+ break
640
+ cur = nxt
641
+ if closed and loop.size() >= 3:
642
+ loops.append(loop)
643
+ else:
644
+ open += 1
645
+ return {"loops": loops, "open": open}
646
+
647
+
648
+ func _loop_record(loop, pos, mesh_center):
649
+ var c = Vector3.ZERO
650
+ for id in loop:
651
+ c += pos[id]
652
+ c /= float(loop.size())
653
+ var n = Vector3.ZERO
654
+ for i in range(loop.size()):
655
+ var p = pos[loop[i]] - c
656
+ var q = pos[loop[(i + 1) % loop.size()]] - c
657
+ n += p.cross(q)
658
+ var amb = false
659
+ if n.length() < 0.000000000001:
660
+ amb = true
661
+ n = Vector3.ZERO
662
+ else:
663
+ n = n.normalized()
664
+ var r = 0.0
665
+ var dev = 0.0
666
+ for id in loop:
667
+ var d = pos[id] - c
668
+ r += d.length()
669
+ dev = maxf(dev, absf(d.dot(n)))
670
+ r /= float(loop.size())
671
+ var side = n.dot(c - mesh_center)
672
+ if absf(side) < 0.00001:
673
+ amb = true
674
+ elif side < 0.0:
675
+ n = -n
676
+ return {"center": c, "direction": n, "radius": r, "vertices": loop.size(), "max_dev": dev, "ambiguous": amb}
677
+
678
+
679
+ # One loop as data for placement.ts: full precision, in the frame of the
680
+ # meshes' top node (its own transform excluded).
681
+ func _loop_raw(lp):
682
+ var rec = {"mesh": lp["mesh"], "center": _raw3(lp["center"]), "direction": _raw3(lp["direction"]), "radius": lp["radius"], "vertices": lp["vertices"], "max_dev": lp["max_dev"]}
683
+ if lp["ambiguous"]:
684
+ rec["direction_ambiguous"] = true
685
+ return rec
686
+
687
+
688
+ # Meshes, markers and shapes under top, in top's own frame (top's transform excluded).
689
+ func _collect(top):
690
+ var meshes = []
691
+ var geoms = []
692
+ var markers = []
693
+ var shapes = []
694
+ var stack = [[top, Transform3D.IDENTITY, true, true]]
695
+ while stack.size() > 0:
696
+ var it = stack.pop_back()
697
+ var n = it[0]
698
+ var t = it[1]
699
+ var vis = it[2]
700
+ if n is Node3D:
701
+ if not it[3]:
702
+ t = t * n.transform
703
+ vis = vis and n.visible
704
+ if n is GeometryInstance3D:
705
+ var bb = n.get_aabb()
706
+ if bb.size != Vector3.ZERO:
707
+ if vis:
708
+ geoms.append([n, t, bb])
709
+ if n is MeshInstance3D and n.mesh != null:
710
+ meshes.append([n, t, bb, vis])
711
+ elif n is Marker3D:
712
+ markers.append([n, t])
713
+ elif n is CollisionShape3D:
714
+ shapes.append([n, t])
715
+ for c in n.get_children():
716
+ stack.append([c, t, vis, false])
717
+ var by_path = func(a, b): return String(top.get_path_to(a[0])) < String(top.get_path_to(b[0]))
718
+ meshes.sort_custom(by_path)
719
+ markers.sort_custom(by_path)
720
+ shapes.sort_custom(by_path)
721
+ return {"meshes": meshes, "geoms": geoms, "markers": markers, "shapes": shapes}
722
+
723
+
724
+ func _visible_meshes(col):
725
+ var out = []
726
+ for m in col["meshes"]:
727
+ if m[3]:
728
+ out.append(m)
729
+ return out
730
+
731
+
732
+ # Whether surface s is a triangle list. Only ArrayMesh binds
733
+ # surface_get_primitive_type; the built-in primitive meshes (PlaneMesh,
734
+ # BoxMesh, CylinderMesh, ...) are always triangles.
735
+ func _is_tris(mesh, s):
736
+ if mesh is ArrayMesh:
737
+ return mesh.surface_get_primitive_type(s) == Mesh.PRIMITIVE_TRIANGLES
738
+ return mesh is PrimitiveMesh
739
+
740
+
741
+ func _mesh_tris(mesh):
742
+ var n = 0
743
+ for s in range(mesh.get_surface_count()):
744
+ if not _is_tris(mesh, s):
745
+ continue
746
+ if mesh is ArrayMesh:
747
+ var il = mesh.surface_get_array_index_len(s)
748
+ n += floori(float(il if il > 0 else mesh.surface_get_array_len(s)) / 3.0)
749
+ else:
750
+ var arr = mesh.surface_get_arrays(s)
751
+ var idx = arr[Mesh.ARRAY_INDEX]
752
+ var verts = arr[Mesh.ARRAY_VERTEX]
753
+ n += floori(float(idx.size() if idx != null and idx.size() > 0 else (verts.size() if verts != null else 0)) / 3.0)
754
+ return n
755
+
756
+
757
+ func _shape_aabb(sh):
758
+ if sh is BoxShape3D:
759
+ return AABB(-sh.size * 0.5, sh.size)
760
+ if sh is SphereShape3D:
761
+ return AABB(Vector3.ONE * -sh.radius, Vector3.ONE * sh.radius * 2.0)
762
+ if sh is CapsuleShape3D or sh is CylinderShape3D:
763
+ return AABB(Vector3(-sh.radius, -sh.height * 0.5, -sh.radius), Vector3(sh.radius * 2.0, sh.height, sh.radius * 2.0))
764
+ var pts = PackedVector3Array()
765
+ if sh is ConcavePolygonShape3D:
766
+ pts = sh.get_faces()
767
+ elif sh is ConvexPolygonShape3D:
768
+ pts = sh.points
769
+ if pts.size() > 0:
770
+ var bb = AABB(pts[0], Vector3.ZERO)
771
+ for p in pts:
772
+ bb = bb.expand(p)
773
+ return bb
774
+ if sh != null:
775
+ var dm = sh.get_debug_mesh()
776
+ if dm != null:
777
+ return dm.get_aabb()
778
+ return null
779
+
780
+
781
+ func _bounds_of(geoms):
782
+ if geoms.size() == 0:
783
+ return null
784
+ var iv = _intervals(geoms, [Vector3.RIGHT, Vector3.UP, Vector3.BACK])
785
+ return AABB(Vector3(iv[0][0], iv[1][0], iv[2][0]), Vector3(iv[0][1] - iv[0][0], iv[1][1] - iv[1][0], iv[2][1] - iv[2][0]))
786
+
787
+
788
+ func _frac_label(f):
789
+ if f < -0.02 or f > 1.02:
790
+ return "outside"
791
+ if f < 0.02:
792
+ return "min"
793
+ if f > 0.98:
794
+ return "max"
795
+ if absf(f - 0.5) < 0.02:
796
+ return "center"
797
+ return "inside"
798
+
799
+
800
+ # ---------------------------------------------------------------- inspect_asset
801
+
802
+ func _cmd_inspect_asset(args):
803
+ var path = String(args.get("path", ""))
804
+ if not ResourceLoader.exists(path):
805
+ return _fail("asset_not_found", "No resource at " + path)
806
+ var res = ResourceLoader.load(path)
807
+ if res == null:
808
+ return _fail("asset_load_failed", "ResourceLoader could not load " + path)
809
+ var inst = null
810
+ if res is PackedScene:
811
+ if not res.can_instantiate():
812
+ return _fail("asset_not_instantiable", path + " cannot be instantiated")
813
+ inst = res.instantiate()
814
+ elif res is Mesh:
815
+ inst = MeshInstance3D.new()
816
+ inst.mesh = res
817
+ else:
818
+ return _fail("unsupported_resource", path + " is a " + res.get_class() + "; pass a scene (.tscn/.scn/.glb/.gltf) or a Mesh resource")
819
+ if inst == null:
820
+ return _fail("asset_instantiate_failed", "Could not instantiate " + path)
821
+ var out = _describe(inst, args)
822
+ inst.free()
823
+ out["path"] = path
824
+ return out
825
+
826
+
827
+ func _describe(inst, args):
828
+ var budget = int(args.get("max_triangles", 60000))
829
+ var col = _collect(inst)
830
+ var out = {"ok": true, "frame": "asset_root", "root_class": inst.get_class(), "root_name": String(inst.name)}
831
+ if inst is Node3D:
832
+ out["root_transform_identity"] = inst.transform.is_equal_approx(Transform3D.IDENTITY)
833
+ var bb = _bounds_of(col["geoms"])
834
+ if bb == null:
835
+ out["aabb"] = null
836
+ out["origin"] = null
837
+ else:
838
+ out["aabb"] = {"min": _v3(bb.position), "max": _v3(bb.end), "size": _v3(bb.size)}
839
+ var f = []
840
+ var labels = []
841
+ var names = ["x", "y", "z"]
842
+ for k in range(3):
843
+ var fk = 0.5 if bb.size[k] < 0.000001 else (0.0 - bb.position[k]) / bb.size[k]
844
+ f.append(snappedf(fk, 0.001))
845
+ labels.append(names[k] + ":" + _frac_label(fk))
846
+ out["origin"] = {"fraction": f, "label": " ".join(labels)}
847
+ var meshes_out = []
848
+ var total = 0
849
+ for m in col["meshes"]:
850
+ var tris = _mesh_tris(m[0].mesh)
851
+ total += tris
852
+ var mb = _bounds_of([[m[0], m[1], m[2]]])
853
+ var rec = {"path": String(inst.get_path_to(m[0])), "tris": tris, "min": _v3(mb.position), "max": _v3(mb.end)}
854
+ if not m[3]:
855
+ rec["hidden"] = true
856
+ meshes_out.append(rec)
857
+ out["mesh_count"] = meshes_out.size()
858
+ out["meshes"] = meshes_out
859
+ out["triangles"] = total
860
+ var an = _analyze(_visible_meshes(col), budget, true, true)
861
+ var planes_out = []
862
+ for p in an["planes"]:
863
+ if planes_out.size() >= 16:
864
+ break
865
+ planes_out.append({"normal": _d3(p["normal"]), "offset": _r(p["offset"]), "area": snappedf(p["area"], 0.000001), "tris": p["tris"], "cull_back": snappedf(p["cull_back"], 0.01)})
866
+ out["planes"] = planes_out
867
+ var loops_out = []
868
+ for lp in an["loops"]:
869
+ loops_out.append(_loop_raw(lp))
870
+ out["open_loop_count"] = loops_out.size()
871
+ out["open_loops"] = loops_out
872
+ out["open_chains"] = an["open_chains"]
873
+ var anchors = []
874
+ for mk in col["markers"]:
875
+ var t = mk[1]
876
+ anchors.append({"name": String(mk[0].name), "path": String(inst.get_path_to(mk[0])), "position": _v3(t.origin), "forward": _d3((-t.basis.z).normalized()), "up": _d3(t.basis.y.normalized())})
877
+ out["anchor_count"] = anchors.size()
878
+ out["anchors"] = anchors
879
+ var shapes = []
880
+ for sh in col["shapes"]:
881
+ var node = sh[0]
882
+ var t = sh[1]
883
+ var shape = node.shape
884
+ var rec = {"path": String(inst.get_path_to(node)), "shape": shape.get_class() if shape != null else "none", "center": _v3(t.origin)}
885
+ if node.disabled:
886
+ rec["disabled"] = true
887
+ if shape is BoxShape3D:
888
+ rec["size"] = _v3(shape.size)
889
+ elif shape is SphereShape3D:
890
+ rec["radius"] = _r(shape.radius)
891
+ elif shape is CapsuleShape3D or shape is CylinderShape3D:
892
+ rec["radius"] = _r(shape.radius)
893
+ rec["height"] = _r(shape.height)
894
+ elif shape is ConcavePolygonShape3D:
895
+ rec["faces"] = floori(shape.get_faces().size() / 3.0)
896
+ elif shape is ConvexPolygonShape3D:
897
+ rec["points"] = shape.points.size()
898
+ var sbb = _shape_aabb(shape)
899
+ if sbb != null:
900
+ var b2 = _bounds_of([[node, t, sbb]])
901
+ rec["min"] = _v3(b2.position)
902
+ rec["max"] = _v3(b2.end)
903
+ shapes.append(rec)
904
+ out["collision_count"] = shapes.size()
905
+ out["collision"] = shapes
906
+ out["analysis"] = {"triangles_analyzed": an["analyzed"], "triangle_budget": budget, "truncated": an["truncated"], "weld_m": 0.0001, "plane_bin": {"normal_step": 0.025, "offset_m": 0.01}}
907
+ return out
908
+
909
+
910
+ # ---------------------------------------------------------------- ports
911
+
912
+ func _resolve_port(root, node, port, budget, want_others):
913
+ var nt = _xf(node, root)
914
+ if typeof(port) == TYPE_STRING:
915
+ var mk = node.get_node_or_null(NodePath(String(port)))
916
+ if mk == null:
917
+ mk = node.find_child(String(port), true, false)
918
+ if mk == null:
919
+ return {"fail": _fail("port_not_found", "no node named '" + String(port) + "' under " + _rel(root, node))}
920
+ if not (mk is Node3D):
921
+ return {"fail": _fail("port_not_node3d", "port '" + String(port) + "' is not a Node3D")}
922
+ var mt = _xf(mk, root)
923
+ var mrec = {"kind": "marker", "name": String(port), "path": _rel(root, mk), "position": mt.origin, "direction": (-mt.basis.z).normalized()}
924
+ if want_others:
925
+ var mothers = []
926
+ var mtotal = 0
927
+ for item in _collect(node)["markers"]:
928
+ if item[0] == mk:
929
+ continue
930
+ mtotal += 1
931
+ if mothers.size() >= 16:
932
+ continue
933
+ var it = _xf(item[0], root)
934
+ mothers.append({"kind": "marker", "name": String(item[0].name), "path": _rel(root, item[0]), "position": _raw3(it.origin), "direction": _raw3((-it.basis.z).normalized())})
935
+ mrec["others"] = mothers
936
+ mrec["others_total"] = mtotal
937
+ return mrec
938
+ # An open-loop port: every loop of the node in its own frame, plus the
939
+ # node's scene transform. placement.ts orders them (stable ids from
940
+ # direction and position, never radius) and picks the one asked for.
941
+ var col = _collect(node)
942
+ var an = _analyze(_visible_meshes(col), budget, false, true)
943
+ var loops = []
944
+ for lp in an["loops"]:
945
+ loops.append(_loop_raw(lp))
946
+ var out = {"kind": "open_loops", "loops": loops, "node_xform": _xf12(nt)}
947
+ if an["truncated"]:
948
+ out["analysis_truncated"] = true
949
+ return out
950
+
951
+
952
+ func _cmd_ports(root, args):
953
+ var budget = int(args.get("max_triangles", 60000))
954
+ var sub = _node(root, String(args.get("subject", "")))
955
+ if sub == null:
956
+ return _fail("node_not_found", "subject not found: " + String(args.get("subject", "")))
957
+ var tgt = _node(root, String(args.get("target", "")))
958
+ if tgt == null:
959
+ return _fail("node_not_found", "target not found: " + String(args.get("target", "")))
960
+ if not (sub is Node3D) or not (tgt is Node3D):
961
+ return _fail("not_node3d", "subject and target must be Node3D")
962
+ if sub == tgt or sub.is_ancestor_of(tgt):
963
+ return _fail("target_inside_subject", "the target must not be the subject or inside it")
964
+ var sp = _resolve_port(root, sub, args.get("subject_port"), budget, bool(args.get("other_ports", false)))
965
+ if sp.has("fail"):
966
+ return sp["fail"]
967
+ var tp = _resolve_port(root, tgt, args.get("target_port"), budget, false)
968
+ if tp.has("fail"):
969
+ return tp["fail"]
970
+ for p in [sp, tp]:
971
+ if p.has("position"):
972
+ p["position"] = _raw3(p["position"])
973
+ p["direction"] = _raw3(p["direction"])
974
+ var parent_xf = Transform3D.IDENTITY
975
+ if sub != root and sub.get_parent() != null:
976
+ parent_xf = _xf(sub.get_parent(), root)
977
+ return {"ok": true, "subject_port": sp, "target_port": tp, "xform": _xf12(_xf(sub, root)), "parent_xform": _xf12(parent_xf)}
978
+
979
+
980
+ # ---------------------------------------------------------------- blockers
981
+
982
+ # What holds a node where it stands: the nodes it overlaps now, and the first
983
+ # node it would touch moving along dir within dist. Physics (its enabled
984
+ # collision shapes against bodies) when it has shapes and the scene is the
985
+ # active tab, else visible-mesh AABBs (SnapToSurface's own fallback).
986
+ func _cmd_blockers(root, active, args):
987
+ var path = String(args.get("path", ""))
988
+ var n = _node(root, path)
989
+ if n == null:
990
+ return _fail("node_not_found", "node not found in scene: " + path)
991
+ if not (n is Node3D):
992
+ return _fail("not_node3d", "node is not a Node3D: " + path)
993
+ var dir = _vec(args.get("direction", [0, -1, 0]))
994
+ if dir.length() < 0.000001:
995
+ return _fail("bad_args", "direction must be non-zero")
996
+ dir = dir.normalized()
997
+ var dist = float(args.get("max_distance", 1.0))
998
+ var mask = int(args.get("collision_mask", 4294967295))
999
+ var shapes = []
1000
+ var ex: Array[RID] = []
1001
+ var stack = [n]
1002
+ while stack.size() > 0:
1003
+ var cur = stack.pop_back()
1004
+ if cur is CollisionObject3D:
1005
+ ex.append(cur.get_rid())
1006
+ if cur is CollisionShape3D and not cur.disabled and cur.shape != null:
1007
+ shapes.append(cur)
1008
+ for c in cur.get_children():
1009
+ stack.append(c)
1010
+ var space = null
1011
+ if active and shapes.size() > 0 and n.is_inside_tree():
1012
+ var vp = root.get_viewport()
1013
+ var world = vp.find_world_3d() if vp != null else null
1014
+ if world != null:
1015
+ space = world.direct_space_state
1016
+ var overlaps = []
1017
+ var best = INF
1018
+ var best_path = ""
1019
+ if space != null:
1020
+ for cs in shapes:
1021
+ var q = PhysicsShapeQueryParameters3D.new()
1022
+ q.shape = cs.shape
1023
+ q.transform = cs.global_transform
1024
+ q.collision_mask = mask
1025
+ q.exclude = ex
1026
+ for hit in space.intersect_shape(q, 8):
1027
+ var col = hit.get("collider")
1028
+ if col is Node:
1029
+ var hp = _rel(root, col) if (col == root or root.is_ancestor_of(col)) else String(col.get_path())
1030
+ if not overlaps.has(hp):
1031
+ overlaps.append(hp)
1032
+ q.motion = dir * dist
1033
+ var frac = space.cast_motion(q)
1034
+ if frac.size() == 2 and frac[1] < 1.0 and frac[0] * dist < best:
1035
+ var q2 = PhysicsShapeQueryParameters3D.new()
1036
+ q2.shape = cs.shape
1037
+ q2.transform = cs.global_transform.translated(dir * (dist * frac[1]))
1038
+ q2.collision_mask = mask
1039
+ q2.exclude = ex
1040
+ var info = space.get_rest_info(q2)
1041
+ if not info.is_empty():
1042
+ var obj = instance_from_id(int(info.get("collider_id", 0)))
1043
+ if obj is Node:
1044
+ best = frac[0] * dist
1045
+ best_path = _rel(root, obj) if (obj == root or root.is_ancestor_of(obj)) else String(obj.get_path())
1046
+ var pout = {"ok": true, "evidence": "physics", "overlaps": overlaps, "first_contact": null}
1047
+ if best_path != "":
1048
+ pout["first_contact"] = {"path": best_path, "distance": best}
1049
+ return pout
1050
+ var mine = _bounds_of(_geoms(n, root, []))
1051
+ if mine == null:
1052
+ return _fail("no_bounds", "the node has no enabled collision shape in the active scene and no visible geometry")
1053
+ for g in _geoms(root, root, [n]):
1054
+ var ob = _bounds_of([g])
1055
+ if mine.intersects(ob):
1056
+ var vp_path = _rel(root, g[0])
1057
+ if overlaps.size() < 8 and not overlaps.has(vp_path):
1058
+ overlaps.append(vp_path)
1059
+ continue
1060
+ var t = _box_sweep(mine, ob, dir, dist)
1061
+ if t >= 0.0 and t < best:
1062
+ best = t
1063
+ best_path = _rel(root, g[0])
1064
+ var vout = {"ok": true, "evidence": "visual_aabb", "overlaps": overlaps, "first_contact": null}
1065
+ if shapes.size() > 0:
1066
+ vout["physics_unavailable_reason"] = "scene_not_active" if not active else "no_space_state"
1067
+ if best_path != "":
1068
+ vout["first_contact"] = {"path": best_path, "distance": best}
1069
+ return vout
1070
+
1071
+
1072
+ # Travel t in [0, dist] at which box a, moved along unit dir, first touches
1073
+ # box b; -1.0 when it does not within dist.
1074
+ func _box_sweep(a, b, dir, dist):
1075
+ var lo = b.position - a.size
1076
+ var hi = b.end
1077
+ var o = a.position
1078
+ var tmin = 0.0
1079
+ var tmax = dist
1080
+ for k in range(3):
1081
+ if absf(dir[k]) < 0.000000001:
1082
+ if o[k] <= lo[k] or o[k] >= hi[k]:
1083
+ return -1.0
1084
+ else:
1085
+ var t1 = (lo[k] - o[k]) / dir[k]
1086
+ var t2 = (hi[k] - o[k]) / dir[k]
1087
+ if t1 > t2:
1088
+ var tmp = t1
1089
+ t1 = t2
1090
+ t2 = tmp
1091
+ tmin = maxf(tmin, t1)
1092
+ tmax = minf(tmax, t2)
1093
+ if tmin > tmax:
1094
+ return -1.0
1095
+ return tmin
1096
+ `;