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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +27 -5
- package/.codex-plugin/plugin.json +28 -6
- package/.cursor-plugin/plugin.json +27 -5
- package/.factory-plugin/plugin.json +27 -5
- package/.opencode/plugins/summer.js +1 -1
- package/CHANGELOG.md +102 -0
- package/README.md +18 -3
- package/assets/audit/scene_audit.gd +4136 -0
- package/assets/seeing/seeing_probe.gd +1873 -0
- package/dist/cli/commands/skills.js +20 -2
- package/dist/core/api-client.d.ts +9 -1
- package/dist/core/api-client.js +21 -4
- package/dist/core/capabilities/agent-playbook.js +46 -0
- package/dist/core/capabilities/audit/accept.d.ts +55 -0
- package/dist/core/capabilities/audit/accept.js +147 -0
- package/dist/core/capabilities/audit/args.d.ts +95 -0
- package/dist/core/capabilities/audit/args.js +89 -0
- package/dist/core/capabilities/audit/audit.d.ts +56 -0
- package/dist/core/capabilities/audit/audit.js +287 -0
- package/dist/core/capabilities/audit/frame.d.ts +14 -0
- package/dist/core/capabilities/audit/frame.js +35 -0
- package/dist/core/capabilities/audit/gaps.d.ts +96 -0
- package/dist/core/capabilities/audit/gaps.js +257 -0
- package/dist/core/capabilities/audit/judge.d.ts +162 -0
- package/dist/core/capabilities/audit/judge.js +1134 -0
- package/dist/core/capabilities/audit/math.d.ts +110 -0
- package/dist/core/capabilities/audit/math.js +263 -0
- package/dist/core/capabilities/audit/summary.d.ts +74 -0
- package/dist/core/capabilities/audit/summary.js +212 -0
- package/dist/core/capabilities/board.d.ts +31 -0
- package/dist/core/capabilities/board.js +43 -0
- package/dist/core/capabilities/camera-view.d.ts +4 -2
- package/dist/core/capabilities/camera-view.js +6 -3
- package/dist/core/capabilities/connect-signal.d.ts +85 -0
- package/dist/core/capabilities/connect-signal.js +285 -0
- package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
- package/dist/core/capabilities/engine-fallbacks.js +5 -0
- package/dist/core/capabilities/engine-ops.d.ts +5 -1
- package/dist/core/capabilities/engine-ops.js +0 -3
- package/dist/core/capabilities/engine-receipt.d.ts +12 -0
- package/dist/core/capabilities/engine-receipt.js +26 -0
- package/dist/core/capabilities/file-read.d.ts +64 -0
- package/dist/core/capabilities/file-read.js +325 -0
- package/dist/core/capabilities/game-task-plan.js +4 -2
- package/dist/core/capabilities/grep.d.ts +60 -0
- package/dist/core/capabilities/grep.js +190 -0
- package/dist/core/capabilities/inspect-node.d.ts +56 -0
- package/dist/core/capabilities/inspect-node.js +225 -0
- package/dist/core/capabilities/inspect-resource.d.ts +61 -0
- package/dist/core/capabilities/inspect-resource.js +376 -0
- package/dist/core/capabilities/math3d.d.ts +28 -0
- package/dist/core/capabilities/math3d.js +88 -0
- package/dist/core/capabilities/motion-generation.d.ts +62 -0
- package/dist/core/capabilities/motion-generation.js +138 -0
- package/dist/core/capabilities/placement-batch.d.ts +102 -0
- package/dist/core/capabilities/placement-batch.js +483 -0
- package/dist/core/capabilities/placement-math.d.ts +70 -0
- package/dist/core/capabilities/placement-math.js +209 -0
- package/dist/core/capabilities/placement-script.d.ts +42 -0
- package/dist/core/capabilities/placement-script.js +1096 -0
- package/dist/core/capabilities/placement.d.ts +485 -0
- package/dist/core/capabilities/placement.js +1731 -0
- package/dist/core/capabilities/project-context.d.ts +33 -2
- package/dist/core/capabilities/project-context.js +87 -4
- package/dist/core/capabilities/replace-node.d.ts +91 -0
- package/dist/core/capabilities/replace-node.js +423 -0
- package/dist/core/capabilities/runtime-control.d.ts +18 -2
- package/dist/core/capabilities/runtime-control.js +67 -2
- package/dist/core/capabilities/scene-batch.d.ts +63 -0
- package/dist/core/capabilities/scene-batch.js +394 -0
- package/dist/core/capabilities/scene-readback.d.ts +36 -0
- package/dist/core/capabilities/scene-readback.js +54 -0
- package/dist/core/capabilities/seeing/args.d.ts +348 -0
- package/dist/core/capabilities/seeing/args.js +178 -0
- package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
- package/dist/core/capabilities/seeing/candidates.js +282 -0
- package/dist/core/capabilities/seeing/math.d.ts +134 -0
- package/dist/core/capabilities/seeing/math.js +269 -0
- package/dist/core/capabilities/seeing/probe.d.ts +62 -0
- package/dist/core/capabilities/seeing/probe.js +188 -0
- package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
- package/dist/core/capabilities/seeing/scoring.js +639 -0
- package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
- package/dist/core/capabilities/seeing/seeing.js +1341 -0
- package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
- package/dist/core/capabilities/seeing/shot-store.js +204 -0
- package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
- package/dist/core/capabilities/surface-snap-mesh.js +425 -0
- package/dist/core/capabilities/surface-snap.d.ts +77 -0
- package/dist/core/capabilities/surface-snap.js +461 -0
- package/dist/core/capabilities/tool-dispatch.js +231 -72
- package/dist/core/capabilities/tscn.d.ts +85 -0
- package/dist/core/capabilities/tscn.js +391 -0
- package/dist/core/capabilities/world-snapshot.d.ts +58 -0
- package/dist/core/capabilities/world-snapshot.js +150 -0
- package/dist/core/capability-skew.d.ts +5 -5
- package/dist/core/capability-skew.js +14 -6
- package/dist/core/headless/worker-engine-client.d.ts +1 -1
- package/dist/core/headless/worker-engine-client.js +2 -2
- package/dist/core/library-read.d.ts +37 -5
- package/dist/core/library-read.js +264 -33
- package/dist/core/library-search.d.ts +2 -2
- package/dist/installer/agent-config.js +5 -3
- package/dist/installer/agent-table.d.ts +16 -0
- package/dist/installer/agent-table.js +52 -17
- package/dist/mcp/server.js +9 -3
- package/dist/mcp/tools/asset-tools.js +21 -3
- package/dist/mcp/tools/audit-tools.d.ts +23 -0
- package/dist/mcp/tools/audit-tools.js +60 -0
- package/dist/mcp/tools/debug-tools.js +3 -1
- package/dist/mcp/tools/file-tools.js +17 -4
- package/dist/mcp/tools/generate-tools.js +74 -66
- package/dist/mcp/tools/library-tools.js +2 -0
- package/dist/mcp/tools/perception-tools.js +6 -19
- package/dist/mcp/tools/placement-tools.d.ts +24 -0
- package/dist/mcp/tools/placement-tools.js +91 -0
- package/dist/mcp/tools/project-tools.js +18 -15
- package/dist/mcp/tools/scene-tools.js +71 -83
- package/dist/mcp/tools/seeing-tools.d.ts +24 -0
- package/dist/mcp/tools/seeing-tools.js +64 -0
- package/dist/mcp/tools/spatial-tools.js +17 -9
- package/dist/mcp/tools/visual-tools.js +59 -7
- package/gemini-extension.json +26 -4
- package/integrations/amp/README.md +1 -1
- package/integrations/crush/README.md +1 -1
- package/integrations/grok-build/README.md +1 -1
- package/integrations/hermes/README.md +1 -1
- package/integrations/kimi-code/README.md +1 -1
- package/integrations/mistral-vibe/README.md +1 -1
- package/integrations/opencode/README.md +1 -1
- package/integrations/rovo-dev/README.md +1 -1
- package/integrations/vscode-copilot/README.md +1 -1
- package/integrations/warp/README.md +1 -1
- package/integrations/windsurf/README.md +1 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/references/summer-folder/resource.yaml +1 -1
- package/library/references/summer-folder/summer-folder.md +1 -1
- package/library/skills/3d-lighting/SKILL.md +1 -1
- package/library/skills/README.md +36 -14
- package/library/skills/adaptive-music/SKILL.md +1 -1
- package/library/skills/agent-playtesting/SKILL.md +1 -1
- package/library/skills/ai-characters-in-games/SKILL.md +220 -0
- package/library/skills/ai-characters-in-games/resource.yaml +30 -0
- package/library/skills/ambient-bed/SKILL.md +1 -1
- package/library/skills/animated-loop/SKILL.md +1 -1
- package/library/skills/animation-tree/SKILL.md +1 -1
- package/library/skills/art-direction/SKILL.md +1 -1
- package/library/skills/asset-strategy/SKILL.md +2 -1
- package/library/skills/asset-strategy/resource.yaml +2 -1
- package/library/skills/audio-direction/SKILL.md +1 -1
- package/library/skills/auto-fire-targeting/SKILL.md +1 -1
- package/library/skills/brainstorm-game/SKILL.md +1 -1
- package/library/skills/brainstorming/SKILL.md +1 -1
- package/library/skills/browse-templates/SKILL.md +3 -3
- package/library/skills/browse-templates/resource.yaml +1 -1
- package/library/skills/camera-rigs/SKILL.md +128 -0
- package/library/skills/camera-rigs/resource.yaml +29 -0
- package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
- package/library/skills/character-animation-wiring/SKILL.md +1 -1
- package/library/skills/character-model/SKILL.md +1 -1
- package/library/skills/character-movement/SKILL.md +326 -0
- package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
- package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
- package/library/skills/character-movement/references/movement-3d.md +136 -0
- package/library/skills/character-movement/references/platformer-feel.md +83 -0
- package/library/skills/character-movement/resource.yaml +40 -0
- package/library/skills/character-portrait/SKILL.md +1 -1
- package/library/skills/cinematic-cutscene/SKILL.md +1 -1
- package/library/skills/combat-basics/SKILL.md +159 -0
- package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
- package/library/skills/combat-basics/resource.yaml +34 -0
- package/library/skills/concept-art/SKILL.md +1 -1
- package/library/skills/create-asset-sheet/SKILL.md +1 -1
- package/library/skills/debug/SKILL.md +1 -1
- package/library/skills/debugging-game-feel/SKILL.md +1 -1
- package/library/skills/design-level/SKILL.md +1 -1
- package/library/skills/design-mechanic/SKILL.md +1 -1
- package/library/skills/design-npc/SKILL.md +1 -1
- package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
- package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
- package/library/skills/environment-kit/SKILL.md +1 -1
- package/library/skills/export-and-ship/SKILL.md +1 -1
- package/library/skills/fabricating-assets/SKILL.md +1 -1
- package/library/skills/facial-and-lipsync/SKILL.md +1 -1
- package/library/skills/fps-controller/SKILL.md +9 -7
- package/library/skills/fps-controller/resource.yaml +4 -1
- package/library/skills/game-feel/SKILL.md +1 -1
- package/library/skills/gameskill/SKILL.md +1 -1
- package/library/skills/gdscript-patterns/SKILL.md +1 -1
- package/library/skills/generate-motion/SKILL.md +9 -9
- package/library/skills/generate-motion/resource.yaml +2 -1
- package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
- package/library/skills/headless-scripting/SKILL.md +1 -1
- package/library/skills/in-game-purchases/SKILL.md +254 -0
- package/library/skills/in-game-purchases/resource.yaml +29 -0
- package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
- package/library/skills/investigating-bugs/SKILL.md +1 -1
- package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
- package/library/skills/make-game/SKILL.md +25 -3
- package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
- package/library/skills/make-game/references/2d-platformer.md +117 -0
- package/library/skills/make-game/references/2d-rpg-content.md +234 -0
- package/library/skills/make-game/references/2d-rpg.md +241 -0
- package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
- package/library/skills/make-game/references/2d-survivors.md +237 -0
- package/library/skills/make-game/references/3d-fps-systems.md +216 -0
- package/library/skills/make-game/references/3d-fps.md +177 -0
- package/library/skills/make-game/references/3d-horror-systems.md +186 -0
- package/library/skills/make-game/references/3d-horror.md +201 -0
- package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
- package/library/skills/make-game/references/3d-third-person.md +215 -0
- package/library/skills/make-game/resource.yaml +6 -1
- package/library/skills/multiplayer/SKILL.md +150 -0
- package/library/skills/multiplayer/resource.yaml +33 -0
- package/library/skills/multiplayer-movement/SKILL.md +510 -0
- package/library/skills/multiplayer-movement/files.md +501 -0
- package/library/skills/multiplayer-movement/resource.yaml +30 -0
- package/library/skills/multiplayer-project/SKILL.md +521 -0
- package/library/skills/multiplayer-project/files.md +448 -0
- package/library/skills/multiplayer-project/resource.yaml +36 -0
- package/library/skills/multiplayer-publish/SKILL.md +229 -0
- package/library/skills/multiplayer-publish/export-errors.md +89 -0
- package/library/skills/multiplayer-publish/resource.yaml +28 -0
- package/library/skills/multiplayer-state/SKILL.md +438 -0
- package/library/skills/multiplayer-state/files.md +592 -0
- package/library/skills/multiplayer-state/resource.yaml +35 -0
- package/library/skills/multiplayer-testing/SKILL.md +236 -0
- package/library/skills/multiplayer-testing/resource.yaml +39 -0
- package/library/skills/music-track/SKILL.md +1 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/new-project/SKILL.md +1 -1
- package/library/skills/organic-model/SKILL.md +1 -1
- package/library/skills/pixel-art/SKILL.md +1 -1
- package/library/skills/play/SKILL.md +1 -1
- package/library/skills/playtesting-a-feature/SKILL.md +1 -1
- package/library/skills/procedural-animation/SKILL.md +2 -2
- package/library/skills/procedural-animation/resource.yaml +1 -1
- package/library/skills/prop-model/SKILL.md +1 -1
- package/library/skills/psx-retro-rendering/SKILL.md +1 -1
- package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
- package/library/skills/remote-deploy/SKILL.md +1 -1
- package/library/skills/retarget/SKILL.md +1 -1
- package/library/skills/running-in-the-cloud/SKILL.md +1 -1
- package/library/skills/save-load/SKILL.md +156 -0
- package/library/skills/save-load/resource.yaml +27 -0
- package/library/skills/scene-composition/SKILL.md +1 -1
- package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
- package/library/skills/scene-scripting/SKILL.md +1 -1
- package/library/skills/scene-to-level/SKILL.md +1 -1
- package/library/skills/skill-create/SKILL.md +1 -1
- package/library/skills/skill-improve/SKILL.md +1 -1
- package/library/skills/skill-test/SKILL.md +1 -1
- package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
- package/library/skills/skybox-panorama/SKILL.md +1 -1
- package/library/skills/sound-effect/SKILL.md +1 -1
- package/library/skills/spatial-placement/SKILL.md +134 -3
- package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
- package/library/skills/spatial-placement/resource.yaml +16 -2
- package/library/skills/sprite-sheet/SKILL.md +1 -1
- package/library/skills/summer-analytics/SKILL.md +147 -0
- package/library/skills/summer-analytics/resource.yaml +28 -0
- package/library/skills/summer-friends/SKILL.md +226 -0
- package/library/skills/summer-friends/resource.yaml +28 -0
- package/library/skills/summer-leaderboards/SKILL.md +231 -0
- package/library/skills/summer-leaderboards/resource.yaml +30 -0
- package/library/skills/summer-match-results/SKILL.md +286 -0
- package/library/skills/summer-match-results/resource.yaml +32 -0
- package/library/skills/summer-matchmaking/SKILL.md +301 -0
- package/library/skills/summer-matchmaking/resource.yaml +32 -0
- package/library/skills/summer-parties/SKILL.md +253 -0
- package/library/skills/summer-parties/resource.yaml +30 -0
- package/library/skills/summer-player-data/SKILL.md +322 -0
- package/library/skills/summer-player-data/resource.yaml +30 -0
- package/library/skills/summer-store/SKILL.md +271 -0
- package/library/skills/summer-store/resource.yaml +29 -0
- package/library/skills/summer-world-chat/SKILL.md +195 -0
- package/library/skills/summer-world-chat/resource.yaml +30 -0
- package/library/skills/summer-world-saves/SKILL.md +284 -0
- package/library/skills/summer-world-saves/resource.yaml +29 -0
- package/library/skills/text-to-motion/SKILL.md +99 -0
- package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
- package/library/skills/text-to-motion/resource.yaml +41 -0
- package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
- package/library/skills/tileable-texture/SKILL.md +1 -1
- package/library/skills/trailer-shot/SKILL.md +1 -1
- package/library/skills/tune-performance/SKILL.md +1 -1
- package/library/skills/ui-basics/SKILL.md +10 -1
- package/library/skills/ui-graphics/SKILL.md +1 -1
- package/library/skills/use-widget-asset/SKILL.md +1 -1
- package/library/skills/using-summer/SKILL.md +2 -2
- package/library/skills/using-summer/resource.yaml +2 -2
- package/library/skills/vehicle-model/SKILL.md +1 -1
- package/library/skills/verification-before-completion/SKILL.md +1 -1
- package/library/skills/verifying-scenes/SKILL.md +76 -2
- package/library/skills/verifying-scenes/resource.yaml +13 -2
- package/library/skills/vfx-dissolve/SKILL.md +1 -1
- package/library/skills/vfx-fire/SKILL.md +1 -1
- package/library/skills/vfx-hit-spark/SKILL.md +1 -1
- package/library/skills/vfx-lightning/SKILL.md +1 -1
- package/library/skills/vfx-magic-glow/SKILL.md +1 -1
- package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
- package/library/skills/vfx-smoke/SKILL.md +1 -1
- package/library/skills/vfx-water-ripple/SKILL.md +1 -1
- package/library/skills/voice-line/SKILL.md +1 -1
- package/library/skills/web-and-touch-input/SKILL.md +38 -0
- package/library/skills/web-and-touch-input/resource.yaml +28 -0
- package/library/skills/web-compatibility-look/SKILL.md +62 -0
- package/library/skills/web-compatibility-look/resource.yaml +30 -0
- package/library/skills/world-building-3d/SKILL.md +8 -1
- package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
- package/library/skills/world-building-3d/resource.yaml +2 -1
- package/library/skills/writing-plans/SKILL.md +1 -1
- package/library/skills/writing-skills/SKILL.md +1 -1
- package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
- package/library/tools/attach-to-surface/resource.yaml +142 -0
- package/library/tools/batch/resource.yaml +9 -3
- package/library/tools/connect-ports/resource.yaml +120 -0
- package/library/tools/connect-signal/resource.yaml +3 -2
- package/library/tools/debug-views/resource.yaml +94 -0
- package/library/tools/frame-nodes/resource.yaml +104 -0
- package/library/tools/frame-shot/resource.yaml +134 -0
- package/library/tools/generate-motion/resource.yaml +44 -9
- package/library/tools/get-board/resource.yaml +50 -0
- package/library/tools/get-project-context/resource.yaml +13 -2
- package/library/tools/grep/resource.yaml +77 -0
- package/library/tools/inspect-asset/resource.yaml +71 -0
- package/library/tools/inspect-node/resource.yaml +9 -2
- package/library/tools/inspect-resource/resource.yaml +16 -5
- package/library/tools/instantiate-scene/resource.yaml +27 -2
- package/library/tools/measure/resource.yaml +96 -0
- package/library/tools/place-adjacent/resource.yaml +113 -0
- package/library/tools/play/resource.yaml +15 -2
- package/library/tools/raycast/resource.yaml +101 -0
- package/library/tools/read-file/resource.yaml +36 -2
- package/library/tools/read-library/resource.yaml +2 -2
- package/library/tools/repeat-along/resource.yaml +129 -0
- package/library/tools/replace-node/resource.yaml +11 -5
- package/library/tools/scene-audit/resource.yaml +144 -0
- package/library/tools/screenshot/resource.yaml +13 -2
- package/library/tools/search-assets/resource.yaml +18 -0
- package/library/tools/shot-sheet/resource.yaml +110 -0
- package/library/tools/snap-to-surface/resource.yaml +4 -1
- package/library/tools/world-snapshot/resource.yaml +24 -3
- package/library/tools/zoom/resource.yaml +105 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +17 -3
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +26 -4
- package/registry/generated/index.json +2154 -469
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +27 -5
- package/registry/generated/plugin.codex.json +28 -6
- package/registry/generated/plugin.cursor.json +27 -5
- package/registry/generated/plugin.factory.json +27 -5
- package/registry/generated/skills-index.md +36 -14
- package/registry/generated/skills-registry.json +421 -124
- package/registry/generated/templates-registry.json +2 -2
- package/library/skills/host-authoritative-state/SKILL.md +0 -332
- package/library/skills/host-authoritative-state/resource.yaml +0 -28
- package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
- package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
- package/library/skills/setup-multiplayer/SKILL.md +0 -336
- 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
|
+
`;
|