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.
- 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 +120 -0
- package/README.md +22 -7
- package/assets/audit/scene_audit.gd +4136 -0
- package/assets/seeing/seeing_probe.gd +1873 -0
- package/dist/cli/commands/setup.js +6 -19
- package/dist/cli/commands/skills.js +57 -32
- 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 +20 -4
- 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/core/skills-registry.d.ts +9 -1
- package/dist/core/skills-registry.js +25 -2
- package/dist/installer/agent-config.d.ts +9 -4
- package/dist/installer/agent-config.js +194 -392
- package/dist/installer/agent-table.d.ts +119 -0
- package/dist/installer/agent-table.js +619 -0
- package/dist/installer/setup.js +4 -2
- package/dist/installer/skill-locations.d.ts +8 -17
- package/dist/installer/skill-locations.js +22 -101
- package/dist/installer/version-check.d.ts +5 -1
- package/dist/installer/version-check.js +38 -28
- 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/README.md +48 -19
- package/integrations/amp/README.md +12 -0
- package/integrations/amp/manifest-target.json +3 -0
- package/integrations/antigravity/README.md +12 -0
- package/integrations/antigravity/manifest-target.json +3 -0
- package/integrations/claude-desktop/README.md +12 -0
- package/integrations/claude-desktop/manifest-target.json +3 -0
- package/integrations/cline/README.md +8 -9
- package/integrations/cline-cli/README.md +12 -0
- package/integrations/cline-cli/manifest-target.json +3 -0
- package/integrations/copilot-jetbrains/README.md +12 -0
- package/integrations/copilot-jetbrains/manifest-target.json +3 -0
- package/integrations/crush/README.md +12 -0
- package/integrations/crush/manifest-target.json +3 -0
- package/integrations/github-copilot/README.md +8 -8
- package/integrations/goose/README.md +12 -0
- package/integrations/goose/manifest-target.json +3 -0
- package/integrations/grok-build/README.md +12 -0
- package/integrations/grok-build/manifest-target.json +3 -0
- package/integrations/hermes/README.md +12 -0
- package/integrations/hermes/manifest-target.json +3 -0
- package/integrations/junie/README.md +12 -0
- package/integrations/junie/manifest-target.json +3 -0
- package/integrations/kilo-code/README.md +6 -10
- package/integrations/kimi-code/README.md +12 -0
- package/integrations/kimi-code/manifest-target.json +3 -0
- package/integrations/kiro/README.md +12 -0
- package/integrations/kiro/manifest-target.json +3 -0
- package/integrations/lm-studio/README.md +8 -8
- package/integrations/mistral-vibe/README.md +12 -0
- package/integrations/mistral-vibe/manifest-target.json +3 -0
- package/integrations/opencode/README.md +9 -12
- package/integrations/qoder/README.md +12 -0
- package/integrations/qoder/manifest-target.json +3 -0
- package/integrations/qwen-code/README.md +12 -0
- package/integrations/qwen-code/manifest-target.json +3 -0
- package/integrations/roo-code/README.md +8 -7
- package/integrations/rovo-dev/README.md +12 -0
- package/integrations/rovo-dev/manifest-target.json +3 -0
- package/integrations/trae/README.md +12 -0
- package/integrations/trae/manifest-target.json +3 -0
- package/integrations/visual-studio/README.md +12 -0
- package/integrations/visual-studio/manifest-target.json +3 -0
- package/integrations/vscode-copilot/README.md +9 -9
- package/integrations/warp/README.md +12 -0
- package/integrations/warp/manifest-target.json +3 -0
- package/integrations/windsurf/README.md +8 -9
- package/integrations/zed/README.md +12 -0
- package/integrations/zed/manifest-target.json +3 -0
- 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 +264 -0
- 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 +3 -3
- 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 +264 -0
- package/registry/generated/skills-registry.json +901 -206
- 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,4136 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
## Summer scene-audit kernel (summer_scene_audit).
|
|
4
|
+
##
|
|
5
|
+
## The MCP tool writes a throwaway wrapper scene into a private per-call OS
|
|
6
|
+
## temp directory: the wrapper instances the target scene as "Subject" and
|
|
7
|
+
## carries this script as a built-in @tool script. The wrapper is rendered with
|
|
8
|
+
## the engine's ScenePreview op, which instantiates it under an offscreen
|
|
9
|
+
## SubViewport with its own World3D, so this node runs inside a private COPY of
|
|
10
|
+
## the SAVED scene. Nothing here touches the edited scene, the open tab, the
|
|
11
|
+
## undo history or any project file. The only file written is result.json in
|
|
12
|
+
## the wrapper's own temp directory.
|
|
13
|
+
##
|
|
14
|
+
## Arguments arrive as DATA only: config.json next to the wrapper, found
|
|
15
|
+
## through this built-in script's own resource path and parsed as JSON.
|
|
16
|
+
##
|
|
17
|
+
## This kernel MEASURES; it does not judge. It walks every node, groups the
|
|
18
|
+
## geometry into instances, gives each one a role from its geometry and
|
|
19
|
+
## engine data alone (never its name or any metadata: see _classify_shapes),
|
|
20
|
+
## builds one private physics space from the VISIBLE meshes (one-sided like
|
|
21
|
+
## the renderer: a ray only hits a triangle from its front, so back faces are
|
|
22
|
+
## as see-through as on screen; shapes are cached per mesh resource and
|
|
23
|
+
## shared by every unscaled instance), casts capped ray grids and
|
|
24
|
+
## per-instance probes, and writes raw evidence. The TypeScript side
|
|
25
|
+
## (core/capabilities/audit/) clusters, applies thresholds, assigns
|
|
26
|
+
## severities, sorts, pages and frames.
|
|
27
|
+
|
|
28
|
+
const SUBJECT_NAME := "Subject"
|
|
29
|
+
|
|
30
|
+
const L_WALL := 1
|
|
31
|
+
const L_STRUCT := 2
|
|
32
|
+
const L_FLOOR := 4
|
|
33
|
+
const L_UNDERLAY := 8
|
|
34
|
+
const L_PROP := 16
|
|
35
|
+
const L_INSERT := 64
|
|
36
|
+
const ROLE_LAYER := {"wall": L_WALL, "struct": L_STRUCT, "floor": L_FLOOR, "underlay": L_UNDERLAY, "prop": L_PROP, "insert": L_INSERT}
|
|
37
|
+
const MASK_FACADE := L_WALL | L_STRUCT | L_INSERT
|
|
38
|
+
# What covers an opening: walls, structure and inserts (a window, door or
|
|
39
|
+
# shutter in its opening). Props never close a hole.
|
|
40
|
+
const MASK_HOLE := MASK_FACADE
|
|
41
|
+
const MASK_SOLID := L_WALL | L_STRUCT | L_INSERT | L_FLOOR | L_UNDERLAY | L_PROP
|
|
42
|
+
|
|
43
|
+
# Roles (_classify_shapes, _classify_rest), in metres. A slab: upright, at most SLAB_THICK
|
|
44
|
+
# thick, at least SLAB_MIN across both ways, its top faces covering at least
|
|
45
|
+
# SLAB_COVER of its footprint. A sheet: upright, at least SHEET_H tall and
|
|
46
|
+
# SHEET_W wide, at most SHEET_THICK thick. A facade member touches a wall face
|
|
47
|
+
# within MEMBER_TOUCH; a band run is at most BAND_MAX_H tall and BAND_MAX_D
|
|
48
|
+
# deep, and at least BAND_MIN_LEN and BAND_ASPECT times its height long.
|
|
49
|
+
# PROP_MAX: the longest side of a prop.
|
|
50
|
+
const SLAB_THICK := 0.6
|
|
51
|
+
const SLAB_MIN := 1.5
|
|
52
|
+
const SLAB_COVER := 0.5
|
|
53
|
+
const SHEET_H := 1.5
|
|
54
|
+
const SHEET_W := 1.0
|
|
55
|
+
const SHEET_THICK := 0.6
|
|
56
|
+
const MEMBER_TOUCH := 0.03
|
|
57
|
+
const MEMBER_REACH := 0.8
|
|
58
|
+
const BAND_MAX_H := 1.2
|
|
59
|
+
const BAND_MAX_D := 0.6
|
|
60
|
+
const BAND_MIN_LEN := 1.0
|
|
61
|
+
const BAND_ASPECT := 3.0
|
|
62
|
+
const PROP_MAX := 3.5
|
|
63
|
+
# floor_gap strips: edge step along a tile edge, and how far out a wall bounds
|
|
64
|
+
# the strip between that edge and the wall (walkable area enclosed by walls).
|
|
65
|
+
const STRIP_STEP := 0.25
|
|
66
|
+
const STRIP_REACH := 1.0
|
|
67
|
+
# Usual share of a check's editor time (the budget's weights).
|
|
68
|
+
const STAGE_WEIGHT := {"through_hole": 4.0, "floor_gap": 2.0, "floating_sunken": 1.0, "interpenetration": 2.0, "orientation": 1.0, "uv_stretch": 1.0, "z_fight_geometry": 3.0, "gap_setup": 0.3, "exposed_edge": 3.0, "open_fixture_end": 0.3, "depth_step": 2.0, "lights": 0.3, "resource": 0.5, "poses": 1.0}
|
|
69
|
+
# The gap detectors' stages (budget tier 2, run after every other check).
|
|
70
|
+
const GAP_STAGES := ["gap_setup", "open_fixture_end", "exposed_edge", "depth_step"]
|
|
71
|
+
# z_fight geometry pass: planar face groups per mesh resource.
|
|
72
|
+
# Triangles under ZF_MIN_TRI m2 are not bucketed (relief, props); a group
|
|
73
|
+
# needs ZF_MIN_AREA m2 (the smallest overlap reported); at most
|
|
74
|
+
# ZF_MAX_GROUPS per mesh, largest first. Normals bin at 1/30 per component,
|
|
75
|
+
# plane offsets at ZF_MAX_GAP (the widest gap a candidate may have).
|
|
76
|
+
const ZF_MIN_TRI := 0.0005
|
|
77
|
+
const ZF_MIN_AREA := 0.01
|
|
78
|
+
const ZF_MAX_GROUPS := 96
|
|
79
|
+
const ZF_MAX_GAP := 0.025
|
|
80
|
+
const ZF_DEPTH_STEPS := 16777216.0
|
|
81
|
+
# Shader code that offsets depth or moves the vertex along its normal, and
|
|
82
|
+
# shader code that makes a surface see-through (alpha-tested or blended).
|
|
83
|
+
const DEPTH_OFFSET_RE := "(\\bDEPTH\\s*=|VERTEX\\s*[-+]?=[^;\\n]*NORMAL|NORMAL\\s*\\*[^;\\n]*VERTEX)"
|
|
84
|
+
const ALPHA_CODE_RE := "(\\bALPHA\\s*=[^=]|\\bALPHA_SCISSOR_THRESHOLD\\b|\\bALPHA_HASH_SCALE\\b|\\bdiscard\\b|render_mode[^;]*\\bblend_(add|sub|mul|premul_alpha)\\b)"
|
|
85
|
+
|
|
86
|
+
var _cfg: Dictionary = {}
|
|
87
|
+
var _out_dir := ""
|
|
88
|
+
var _result: Dictionary = {}
|
|
89
|
+
var _subject: Node = null
|
|
90
|
+
var _checks: Dictionary = {}
|
|
91
|
+
var _report_root: Node = null
|
|
92
|
+
var _want_poses := false
|
|
93
|
+
|
|
94
|
+
var _t0 := 0
|
|
95
|
+
var _ms: Dictionary = {}
|
|
96
|
+
|
|
97
|
+
var _nodes := 0
|
|
98
|
+
var _hidden := 0
|
|
99
|
+
var _multimesh := 0
|
|
100
|
+
var _screen_space := 0
|
|
101
|
+
var _inst: Array = []
|
|
102
|
+
var _lights: Array = []
|
|
103
|
+
var _cameras: Array = []
|
|
104
|
+
var _players: Array = []
|
|
105
|
+
var _empty_meshes: Array = []
|
|
106
|
+
var _shader_no_code: Array = []
|
|
107
|
+
|
|
108
|
+
# The role counts by the evidence that set them.
|
|
109
|
+
var _role_stats: Dictionary = {}
|
|
110
|
+
|
|
111
|
+
var _mesh_info: Dictionary = {}
|
|
112
|
+
var _space := RID()
|
|
113
|
+
var _state: PhysicsDirectSpaceState3D = null
|
|
114
|
+
var _bodies: Array = []
|
|
115
|
+
var _shapes: Array = []
|
|
116
|
+
var _hulls: Dictionary = {}
|
|
117
|
+
var _body_inst: Dictionary = {}
|
|
118
|
+
var _q: PhysicsRayQueryParameters3D = null
|
|
119
|
+
var _ex: Array[RID] = []
|
|
120
|
+
var _ex_set := false
|
|
121
|
+
var _rays := 0
|
|
122
|
+
var _floor_gap_count := 0
|
|
123
|
+
var _floor_gap_void := false
|
|
124
|
+
var _floor_gap_covered := false
|
|
125
|
+
var _floor_recs: Array = []
|
|
126
|
+
|
|
127
|
+
# Editor-time budget (budget_ms): each check gets a share of what is left,
|
|
128
|
+
# weighted by its usual cost; time a fast check leaves unused passes on to
|
|
129
|
+
# the next ones. A check past its share stops and is reported partial:
|
|
130
|
+
# _tally[stage] = [units done, units planned].
|
|
131
|
+
var _budget_us := 0
|
|
132
|
+
var _check_end := 0
|
|
133
|
+
var _pending: Array = []
|
|
134
|
+
var _tally: Dictionary = {}
|
|
135
|
+
|
|
136
|
+
var _re_offset: RegEx = null
|
|
137
|
+
var _re_alpha: RegEx = null
|
|
138
|
+
|
|
139
|
+
# z_fight geometry: finalized face groups per mesh (instance id), viewpoints
|
|
140
|
+
# [[position, kind]], the main camera's near / far, floors for eye points.
|
|
141
|
+
var _zf_groups: Dictionary = {}
|
|
142
|
+
var _views: Array = []
|
|
143
|
+
var _near := 0.05
|
|
144
|
+
var _far := 4000.0
|
|
145
|
+
var _walk: Array = []
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
func _ready() -> void:
|
|
149
|
+
_result = {"ok": false, "stage": "ready", "warnings": [], "errors": []}
|
|
150
|
+
var script_path := String((get_script() as Script).resource_path)
|
|
151
|
+
var wrapper_path := script_path.get_slice("::", 0)
|
|
152
|
+
_out_dir = wrapper_path.get_base_dir()
|
|
153
|
+
if wrapper_path == "" or not wrapper_path.ends_with("wrapper.tscn"):
|
|
154
|
+
push_error("Summer scene audit: cannot locate its wrapper scene (script path '%s')." % script_path)
|
|
155
|
+
return
|
|
156
|
+
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(_out_dir.path_join("config.json")))
|
|
157
|
+
if typeof(parsed) != TYPE_DICTIONARY:
|
|
158
|
+
_fail("config_unreadable", "Could not read the audit config.")
|
|
159
|
+
return
|
|
160
|
+
_cfg = parsed
|
|
161
|
+
_result["renderer"] = RenderingServer.get_current_rendering_method()
|
|
162
|
+
_subject = get_parent().get_node_or_null(SUBJECT_NAME)
|
|
163
|
+
if _subject == null:
|
|
164
|
+
_fail("no_subject", "The wrapper scene has no Subject instance (the target scene failed to instantiate).")
|
|
165
|
+
return
|
|
166
|
+
get_viewport().disable_3d = true
|
|
167
|
+
_result["stage"] = "configured"
|
|
168
|
+
_write_result()
|
|
169
|
+
# Deferred so CSG roots and @tool scripts that settle with call_deferred
|
|
170
|
+
# have run first. ScenePreview iterates the main loop, which flushes this.
|
|
171
|
+
call_deferred("_run")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
func _exit_tree() -> void:
|
|
175
|
+
_free_physics()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Plumbing
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
func _fail(reason: String, message: String) -> void:
|
|
183
|
+
_result["ok"] = false
|
|
184
|
+
_result["failure_reason"] = reason
|
|
185
|
+
(_result["errors"] as Array).append(message)
|
|
186
|
+
_write_result()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
func _warn(message: String) -> void:
|
|
190
|
+
var w: Array = _result["warnings"]
|
|
191
|
+
if w.size() < 24 and not w.has(message):
|
|
192
|
+
w.append(message)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
func _write_result() -> void:
|
|
196
|
+
var f := FileAccess.open(_out_dir.path_join("result.json"), FileAccess.WRITE)
|
|
197
|
+
if f == null:
|
|
198
|
+
return
|
|
199
|
+
f.store_string(JSON.stringify(_result))
|
|
200
|
+
f.close()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
func _lap(stage: String, since: int) -> int:
|
|
204
|
+
var now := Time.get_ticks_usec()
|
|
205
|
+
_ms[stage] = snappedf(float(now - since) / 1000.0, 0.1)
|
|
206
|
+
return now
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# Start a check: its deadline is its weighted share of the budget left.
|
|
210
|
+
func _begin(stage: String) -> void:
|
|
211
|
+
# Two tiers: the gap detectors run last and share what the other checks
|
|
212
|
+
# leave, so adding them never shortens another check's share.
|
|
213
|
+
var tier2 := GAP_STAGES.has(stage)
|
|
214
|
+
var w_all := 0.0
|
|
215
|
+
for s in _pending:
|
|
216
|
+
if GAP_STAGES.has(s) == tier2:
|
|
217
|
+
w_all += float(STAGE_WEIGHT.get(s, 1.0))
|
|
218
|
+
var w := float(STAGE_WEIGHT.get(stage, 1.0))
|
|
219
|
+
_pending.erase(stage)
|
|
220
|
+
if _budget_us <= 0:
|
|
221
|
+
_check_end = 0
|
|
222
|
+
return
|
|
223
|
+
var now := Time.get_ticks_usec()
|
|
224
|
+
var left := maxi(0, _budget_us - (now - _t0))
|
|
225
|
+
var share := int(float(left) * w / maxf(w_all, w))
|
|
226
|
+
# Every check gets a little time, even when setup used up the budget.
|
|
227
|
+
_check_end = now + maxi(share, int(float(_budget_us) / 30.0))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# The running check is past its share of the budget.
|
|
231
|
+
func _over() -> bool:
|
|
232
|
+
return _check_end > 0 and Time.get_ticks_usec() > _check_end
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
func _count(stage: String, done: int, total: int) -> void:
|
|
236
|
+
var tl: Array = _tally.get(stage, [0, 0])
|
|
237
|
+
_tally[stage] = [int(tl[0]) + done, int(tl[1]) + total]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
static func _a3(v: Vector3, step := 0.001) -> Array:
|
|
241
|
+
return [snappedf(v.x, step), snappedf(v.y, step), snappedf(v.z, step)]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
static func _b9(b: Basis) -> Array:
|
|
245
|
+
return [snappedf(b.x.x, 0.0001), snappedf(b.x.y, 0.0001), snappedf(b.x.z, 0.0001), snappedf(b.y.x, 0.0001), snappedf(b.y.y, 0.0001), snappedf(b.y.z, 0.0001), snappedf(b.z.x, 0.0001), snappedf(b.z.y, 0.0001), snappedf(b.z.z, 0.0001)]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
func _rel(node: Node) -> String:
|
|
249
|
+
if node == _subject:
|
|
250
|
+
return "."
|
|
251
|
+
return String(_subject.get_path_to(node))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
func _in_root(node: Node) -> bool:
|
|
255
|
+
return _report_root == null or node == _report_root or _report_root.is_ancestor_of(node)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
func _resolve(path: String) -> Node:
|
|
259
|
+
var p := path.strip_edges()
|
|
260
|
+
if p == "" or p == ".":
|
|
261
|
+
return _subject
|
|
262
|
+
var n := _subject.get_node_or_null(NodePath(p))
|
|
263
|
+
var root_name := String(_subject.name)
|
|
264
|
+
if n == null and p.begins_with(root_name + "/"):
|
|
265
|
+
n = _subject.get_node_or_null(NodePath(p.substr(root_name.length() + 1)))
|
|
266
|
+
return n
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Run
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
func _run() -> void:
|
|
274
|
+
_t0 = Time.get_ticks_usec()
|
|
275
|
+
var t := _t0
|
|
276
|
+
_result["stage"] = "collect"
|
|
277
|
+
for c in (_cfg.get("checks", []) as Array):
|
|
278
|
+
_checks[String(c)] = true
|
|
279
|
+
_want_poses = bool(_cfg.get("poses", false))
|
|
280
|
+
_budget_us = int(float(_cfg.get("budget_ms", 0)) * 1000.0)
|
|
281
|
+
var root_path := String(_cfg.get("root", ""))
|
|
282
|
+
if root_path != "" and root_path != ".":
|
|
283
|
+
_report_root = _resolve(root_path)
|
|
284
|
+
if _report_root == null:
|
|
285
|
+
_fail("node_not_found", "root node not found in the scene: " + root_path)
|
|
286
|
+
return
|
|
287
|
+
_re_offset = RegEx.create_from_string(DEPTH_OFFSET_RE)
|
|
288
|
+
_re_alpha = RegEx.create_from_string(ALPHA_CODE_RE)
|
|
289
|
+
|
|
290
|
+
_collect()
|
|
291
|
+
t = _lap("collect", t)
|
|
292
|
+
# Roles need the meshes' face areas; see-through cards (dressing) are
|
|
293
|
+
# decided from their materials first and skip the mesh pass.
|
|
294
|
+
for rec in _inst:
|
|
295
|
+
if _see_through(rec):
|
|
296
|
+
rec["role"] = "dressing"
|
|
297
|
+
rec["why"] = "see_through"
|
|
298
|
+
_mesh_pass()
|
|
299
|
+
t = _lap("mesh_pass", t)
|
|
300
|
+
var sheets := _classify_shapes()
|
|
301
|
+
t = _lap("roles", t)
|
|
302
|
+
|
|
303
|
+
var need_physics := false
|
|
304
|
+
for c in ["through_hole", "floor_gap", "floating", "sunken", "interpenetration", "orientation", "uv_stretch", "z_fight", "exposed_edge", "open_fixture_end", "depth_step"]:
|
|
305
|
+
if _checks.has(c):
|
|
306
|
+
need_physics = true
|
|
307
|
+
if need_physics:
|
|
308
|
+
_build_space()
|
|
309
|
+
t = _lap("physics_build", t)
|
|
310
|
+
if _state == null:
|
|
311
|
+
_fail("physics_unavailable", "Could not create a private physics space for the audit.")
|
|
312
|
+
return
|
|
313
|
+
_q = PhysicsRayQueryParameters3D.new()
|
|
314
|
+
_q.collide_with_areas = false
|
|
315
|
+
_q.collide_with_bodies = true
|
|
316
|
+
_classify_rest(sheets)
|
|
317
|
+
var t_roles := Time.get_ticks_usec()
|
|
318
|
+
_ms["roles"] = snappedf(float(_ms.get("roles", 0.0)) + float(t_roles - t) / 1000.0, 0.1)
|
|
319
|
+
t = t_roles
|
|
320
|
+
|
|
321
|
+
_result["stage"] = "checks"
|
|
322
|
+
var run_th := _checks.has("through_hole") or _checks.has("z_fight")
|
|
323
|
+
var run_fl := _checks.has("floor_gap") or _checks.has("z_fight")
|
|
324
|
+
var run_su := _checks.has("floating") or _checks.has("sunken")
|
|
325
|
+
var run_po := _want_poses and _state != null
|
|
326
|
+
var run_zg := _checks.has("z_fight") and _state != null
|
|
327
|
+
var run_ee := _checks.has("exposed_edge") and _state != null
|
|
328
|
+
var run_fe := _checks.has("open_fixture_end") and _state != null
|
|
329
|
+
var run_ds := _checks.has("depth_step") and _state != null
|
|
330
|
+
var run_gk := run_ee or run_fe or run_ds
|
|
331
|
+
for stage_on in [["through_hole", run_th], ["floor_gap", run_fl], ["floating_sunken", run_su], ["interpenetration", _checks.has("interpenetration")], ["orientation", _checks.has("orientation")], ["uv_stretch", _checks.has("uv_stretch")], ["z_fight_geometry", run_zg], ["lights", _checks.has("lights")], ["resource", _checks.has("resource")], ["poses", run_po], ["gap_setup", run_gk], ["open_fixture_end", run_fe], ["exposed_edge", run_ee], ["depth_step", run_ds]]:
|
|
332
|
+
if bool(stage_on[1]):
|
|
333
|
+
_pending.append(String(stage_on[0]))
|
|
334
|
+
if run_th:
|
|
335
|
+
_begin("through_hole")
|
|
336
|
+
_result["lines"] = _scan_facades()
|
|
337
|
+
t = _lap("through_hole", t)
|
|
338
|
+
if run_fl:
|
|
339
|
+
_begin("floor_gap")
|
|
340
|
+
_result["floors"] = _scan_floors()
|
|
341
|
+
t = _lap("floor_gap", t)
|
|
342
|
+
if run_su:
|
|
343
|
+
_begin("floating_sunken")
|
|
344
|
+
_result["support"] = _scan_support()
|
|
345
|
+
t = _lap("floating_sunken", t)
|
|
346
|
+
if _checks.has("interpenetration"):
|
|
347
|
+
_begin("interpenetration")
|
|
348
|
+
_result["overlaps"] = _scan_overlaps()
|
|
349
|
+
t = _lap("interpenetration", t)
|
|
350
|
+
if _checks.has("orientation"):
|
|
351
|
+
_begin("orientation")
|
|
352
|
+
_result["long_props"] = _scan_long_props()
|
|
353
|
+
t = _lap("orientation", t)
|
|
354
|
+
if _checks.has("uv_stretch"):
|
|
355
|
+
_begin("uv_stretch")
|
|
356
|
+
_result["uv"] = _scan_uv()
|
|
357
|
+
t = _lap("uv_stretch", t)
|
|
358
|
+
if run_zg:
|
|
359
|
+
_begin("z_fight_geometry")
|
|
360
|
+
_result["zfight_geo"] = _scan_zfight_geometry()
|
|
361
|
+
t = _lap("z_fight_geometry", t)
|
|
362
|
+
if _checks.has("lights"):
|
|
363
|
+
_begin("lights")
|
|
364
|
+
_result["lights"] = _scan_lights()
|
|
365
|
+
t = _lap("lights", t)
|
|
366
|
+
if _checks.has("resource"):
|
|
367
|
+
_begin("resource")
|
|
368
|
+
_result["resources"] = _scan_resources()
|
|
369
|
+
t = _lap("resource", t)
|
|
370
|
+
if run_po:
|
|
371
|
+
_begin("poses")
|
|
372
|
+
_scan_clearances()
|
|
373
|
+
t = _lap("poses", t)
|
|
374
|
+
if run_gk:
|
|
375
|
+
_begin("gap_setup")
|
|
376
|
+
_gk_setup()
|
|
377
|
+
_result["gk"] = {"eyes": _gk_eyes.size(), "indoor_cells": _gk_indoor.size(), "floor_y": snappedf(_gk_floor_y, 0.001), "lines": _gk_lines_rows(), "doors": _gk_doors()}
|
|
378
|
+
t = _lap("gap_setup", t)
|
|
379
|
+
if run_fe:
|
|
380
|
+
_begin("open_fixture_end")
|
|
381
|
+
_result["fixture_ends"] = _scan_fixture_ends()
|
|
382
|
+
t = _lap("open_fixture_end", t)
|
|
383
|
+
if run_ee:
|
|
384
|
+
_begin("exposed_edge")
|
|
385
|
+
_result["exposed"] = _scan_exposed_edges()
|
|
386
|
+
t = _lap("exposed_edge", t)
|
|
387
|
+
if run_ds:
|
|
388
|
+
_begin("depth_step")
|
|
389
|
+
_result["depth_steps"] = _scan_depth_steps()
|
|
390
|
+
t = _lap("depth_step", t)
|
|
391
|
+
# Checks a budget stopped early: [units done, units planned].
|
|
392
|
+
var partial: Dictionary = {}
|
|
393
|
+
for stage in _tally:
|
|
394
|
+
var tl: Array = _tally[stage]
|
|
395
|
+
if int(tl[0]) < int(tl[1]):
|
|
396
|
+
partial[stage] = [int(tl[0]), int(tl[1])]
|
|
397
|
+
_result["partial"] = partial
|
|
398
|
+
_result["instances"] = _instance_rows()
|
|
399
|
+
t = _lap("emit", t)
|
|
400
|
+
_ms["total"] = snappedf(float(Time.get_ticks_usec() - _t0) / 1000.0, 0.1)
|
|
401
|
+
_result["ms"] = _ms
|
|
402
|
+
_result["stats"] = {
|
|
403
|
+
"nodes": _nodes, "hidden_skipped": _hidden, "instances": _inst.size(), "mesh_instances": _count_meshes(),
|
|
404
|
+
"unique_meshes": _mesh_info.size(), "tris_unique": _tris_unique(), "bodies": _bodies.size(), "lights": _lights.size(),
|
|
405
|
+
"multimesh_skipped": _multimesh, "screen_space_skipped": _screen_space, "rays": _rays,
|
|
406
|
+
}
|
|
407
|
+
_result["roles"] = _role_stats
|
|
408
|
+
# Look items the agent accepted (written by the MCP side, read-only here).
|
|
409
|
+
var accept_path := "res://.summer/audit-accept.json"
|
|
410
|
+
if FileAccess.file_exists(accept_path):
|
|
411
|
+
var acc: Variant = JSON.parse_string(FileAccess.get_file_as_string(accept_path))
|
|
412
|
+
if typeof(acc) == TYPE_DICTIONARY:
|
|
413
|
+
_result["accept"] = acc
|
|
414
|
+
_result["ok"] = true
|
|
415
|
+
_result["stage"] = "done"
|
|
416
|
+
_write_result()
|
|
417
|
+
_free_physics()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
func _count_meshes() -> int:
|
|
421
|
+
var n := 0
|
|
422
|
+
for rec in _inst:
|
|
423
|
+
n += (rec["meshes"] as Array).size()
|
|
424
|
+
return n
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
func _tris_unique() -> int:
|
|
428
|
+
var n := 0
|
|
429
|
+
for k in _mesh_info:
|
|
430
|
+
n += int((_mesh_info[k] as Dictionary).get("tris", 0))
|
|
431
|
+
return n
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ---------------------------------------------------------------------------
|
|
435
|
+
# Collect: every node -> kit instances (outermost instanced scenes) and loose
|
|
436
|
+
# geometry, lights, empty meshes.
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
func _collect() -> void:
|
|
440
|
+
var loose: Dictionary = {}
|
|
441
|
+
var stack: Array = [[_subject, -1]]
|
|
442
|
+
while not stack.is_empty():
|
|
443
|
+
var item: Array = stack.pop_back()
|
|
444
|
+
var node: Node = item[0]
|
|
445
|
+
var owner_i: int = item[1]
|
|
446
|
+
_nodes += 1
|
|
447
|
+
if node is Node3D and not (node as Node3D).visible:
|
|
448
|
+
_hidden += 1
|
|
449
|
+
continue
|
|
450
|
+
if node != _subject and owner_i < 0 and node.scene_file_path != "":
|
|
451
|
+
owner_i = _new_inst(node, node.scene_file_path)
|
|
452
|
+
if node is Camera3D:
|
|
453
|
+
_cameras.append(node)
|
|
454
|
+
elif node is CharacterBody3D and _players.size() < 16:
|
|
455
|
+
_players.append(node)
|
|
456
|
+
if node is Light3D:
|
|
457
|
+
_lights.append(node)
|
|
458
|
+
elif node is MeshInstance3D:
|
|
459
|
+
var mi := node as MeshInstance3D
|
|
460
|
+
# Screen-space effects (a post-process quad, never culled) are not
|
|
461
|
+
# scene geometry: they sit "at the origin" and "inside" whatever is there.
|
|
462
|
+
if mi.extra_cull_margin >= 1000.0 or mi.custom_aabb.size.length() > 1000.0:
|
|
463
|
+
_screen_space += 1
|
|
464
|
+
continue
|
|
465
|
+
if mi.mesh == null or mi.mesh.get_surface_count() == 0:
|
|
466
|
+
if _empty_meshes.size() < 64:
|
|
467
|
+
_empty_meshes.append([_rel(mi), owner_i])
|
|
468
|
+
else:
|
|
469
|
+
var oi := owner_i if owner_i >= 0 else _loose_owner(mi, loose)
|
|
470
|
+
(_inst[oi]["meshes"] as Array).append([mi, mi.mesh, mi.global_transform])
|
|
471
|
+
elif node is CSGShape3D:
|
|
472
|
+
var csg := node as CSGShape3D
|
|
473
|
+
if csg.is_root_shape():
|
|
474
|
+
var meshes: Array = csg.get_meshes()
|
|
475
|
+
if meshes.size() >= 2 and meshes[1] is Mesh:
|
|
476
|
+
var oi2 := owner_i if owner_i >= 0 else _loose_owner(csg, loose)
|
|
477
|
+
(_inst[oi2]["meshes"] as Array).append([csg, meshes[1], csg.global_transform * Transform3D(meshes[0])])
|
|
478
|
+
continue
|
|
479
|
+
elif node is MultiMeshInstance3D:
|
|
480
|
+
_multimesh += 1
|
|
481
|
+
for child in node.get_children():
|
|
482
|
+
stack.push_back([child, owner_i])
|
|
483
|
+
var kept: Array = []
|
|
484
|
+
for rec in _inst:
|
|
485
|
+
if (rec["meshes"] as Array).is_empty():
|
|
486
|
+
continue
|
|
487
|
+
rec["i"] = kept.size()
|
|
488
|
+
kept.append(rec)
|
|
489
|
+
_inst = kept
|
|
490
|
+
for rec in _inst:
|
|
491
|
+
_bounds(rec)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
func _new_inst(node: Node, scene: String) -> int:
|
|
495
|
+
var xf := Transform3D.IDENTITY
|
|
496
|
+
if node is Node3D:
|
|
497
|
+
xf = (node as Node3D).global_transform
|
|
498
|
+
var path := _rel(node)
|
|
499
|
+
_inst.append({
|
|
500
|
+
"i": _inst.size(), "node": node, "path": path, "scene": scene, "piece": scene.get_file().get_basename(),
|
|
501
|
+
"name": String(node.name), "xf": xf, "meshes": [], "bodies": [], "role": "", "in": _in_root(node),
|
|
502
|
+
"top": path.get_slice("/", 0),
|
|
503
|
+
})
|
|
504
|
+
return _inst.size() - 1
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
func _loose_owner(node: Node3D, loose: Dictionary) -> int:
|
|
508
|
+
var key: Node = node
|
|
509
|
+
var parent := node.get_parent()
|
|
510
|
+
if parent is CollisionObject3D and parent != _subject:
|
|
511
|
+
key = parent
|
|
512
|
+
if loose.has(key):
|
|
513
|
+
return int(loose[key])
|
|
514
|
+
var i := _new_inst(key, "")
|
|
515
|
+
_inst[i]["piece"] = String(key.name)
|
|
516
|
+
loose[key] = i
|
|
517
|
+
return i
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
static func _corners(box: AABB) -> Array:
|
|
521
|
+
var p := box.position
|
|
522
|
+
var s := box.size
|
|
523
|
+
return [p, p + Vector3(s.x, 0, 0), p + Vector3(0, s.y, 0), p + Vector3(0, 0, s.z), p + Vector3(s.x, s.y, 0), p + Vector3(s.x, 0, s.z), p + Vector3(0, s.y, s.z), p + s]
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
func _bounds(rec: Dictionary) -> void:
|
|
527
|
+
var xf: Transform3D = rec["xf"]
|
|
528
|
+
var inv := xf.affine_inverse()
|
|
529
|
+
var la := AABB()
|
|
530
|
+
var wa := AABB()
|
|
531
|
+
var first := true
|
|
532
|
+
for m in (rec["meshes"] as Array):
|
|
533
|
+
var mesh: Mesh = m[1]
|
|
534
|
+
var mxf: Transform3D = m[2]
|
|
535
|
+
var mb := mesh.get_aabb()
|
|
536
|
+
var lx := inv * mxf
|
|
537
|
+
for c in _corners(mb):
|
|
538
|
+
var lp: Vector3 = lx * c
|
|
539
|
+
var wp: Vector3 = mxf * c
|
|
540
|
+
if first:
|
|
541
|
+
la = AABB(lp, Vector3.ZERO)
|
|
542
|
+
wa = AABB(wp, Vector3.ZERO)
|
|
543
|
+
first = false
|
|
544
|
+
else:
|
|
545
|
+
la = la.expand(lp)
|
|
546
|
+
wa = wa.expand(wp)
|
|
547
|
+
rec["laabb"] = la
|
|
548
|
+
rec["waabb"] = wa
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
# Roles, from geometry and engine data only: never a node or scene name, never
|
|
553
|
+
# metadata. In this order:
|
|
554
|
+
# - dressing: every surface the piece draws is see-through (an alpha-tested or
|
|
555
|
+
# alpha-blended material: foliage cards, overlay sheets). Drawn, not solid:
|
|
556
|
+
# it stays out of the physics space.
|
|
557
|
+
# - flat (upright, at most SLAB_THICK thick, at least SLAB_MIN across both
|
|
558
|
+
# ways): a slab when its upward faces cover SLAB_COVER of its footprint,
|
|
559
|
+
# else ground cover (struct: scattered pebbles, a grate). A slab is a floor;
|
|
560
|
+
# a roof (struct) when it is raised 2 m or more above the lowest slab and a
|
|
561
|
+
# wall stands under it (rising from at least 1 m below its underside to
|
|
562
|
+
# within 1.5 m of it, or up to 1.5 m above it: a parapet, within 10 cm of
|
|
563
|
+
# its footprint); the underlay when it is 8x the median floor and below the
|
|
564
|
+
# floor level.
|
|
565
|
+
# - sheet (upright, at least SHEET_H tall and SHEET_W wide, at most
|
|
566
|
+
# SHEET_THICK thick): a wall facing along its thin axis, to the side with
|
|
567
|
+
# more outward face area.
|
|
568
|
+
# - insert: a piece inside a larger wall's rectangle (80% of its own) that
|
|
569
|
+
# reaches at least 3 cm through the wall's front plane where the wall
|
|
570
|
+
# itself is open (rays through its rectangle miss the wall): a window, door
|
|
571
|
+
# or shutter in its opening. A piece pushed into a solid wall is not one.
|
|
572
|
+
# (Checks that build no physics space skip the opening test.)
|
|
573
|
+
# - facade member (struct): walls on one plane form a facade line; a piece
|
|
574
|
+
# whose bounds touch a wall of the line (crossing or within MEMBER_TOUCH of
|
|
575
|
+
# its slab, overlapping it along the line and in height) and stick out at
|
|
576
|
+
# most MEMBER_REACH is
|
|
577
|
+
# - a band: a run of such pieces at one height (bottoms and tops within
|
|
578
|
+
# 5 cm, end to end within 5 cm), at most BAND_MAX_H tall and BAND_MAX_D
|
|
579
|
+
# deep, together at least BAND_MIN_LEN and BAND_ASPECT x its height long;
|
|
580
|
+
# - a pier: at least BAND_MIN_LEN tall and BAND_ASPECT x taller than wide,
|
|
581
|
+
# at most 1 m wide;
|
|
582
|
+
# - a joint: a piece touching a band at that band's height (bottom within
|
|
583
|
+
# 10 cm, top within 15 cm) and sticking out at most 30 cm further: a
|
|
584
|
+
# corner or end block.
|
|
585
|
+
# - anything else: a prop up to PROP_MAX on its longest side, else struct.
|
|
586
|
+
# rec.why keeps the evidence that set each role; result.roles counts them.
|
|
587
|
+
# ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
# Every surface the instance draws uses a see-through material.
|
|
590
|
+
func _see_through(rec: Dictionary) -> bool:
|
|
591
|
+
var any := false
|
|
592
|
+
for m in (rec["meshes"] as Array):
|
|
593
|
+
var node: Node = m[0]
|
|
594
|
+
var mesh: Mesh = m[1]
|
|
595
|
+
for s in mesh.get_surface_count():
|
|
596
|
+
any = true
|
|
597
|
+
if not _mat_see_through(_surface_mat(node, mesh, s)):
|
|
598
|
+
return false
|
|
599
|
+
return any
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# Alpha-tested or alpha-blended: BaseMaterial3D transparency (alpha, scissor,
|
|
603
|
+
# hash, depth pre-pass), or a shader that writes ALPHA, discards or blends.
|
|
604
|
+
func _mat_see_through(mat: Variant) -> bool:
|
|
605
|
+
if mat is BaseMaterial3D:
|
|
606
|
+
return (mat as BaseMaterial3D).transparency != BaseMaterial3D.TRANSPARENCY_DISABLED
|
|
607
|
+
if mat is ShaderMaterial:
|
|
608
|
+
var sh: Shader = (mat as ShaderMaterial).shader
|
|
609
|
+
return sh != null and _re_alpha.search(sh.code) != null
|
|
610
|
+
return false
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# Size in the instance's own scaled frame when that frame stands upright
|
|
614
|
+
# (local +Y along world +Y or -Y, so local x and z are horizontal), else the
|
|
615
|
+
# world bounds.
|
|
616
|
+
func _shape_of(rec: Dictionary) -> void:
|
|
617
|
+
var xf: Transform3D = rec["xf"]
|
|
618
|
+
var la: AABB = rec["laabb"]
|
|
619
|
+
var b := xf.basis
|
|
620
|
+
var sc := Vector3(b.x.length(), b.y.length(), b.z.length())
|
|
621
|
+
var up := sc.y > 0.000001 and absf(b.y.y / sc.y) >= 0.98
|
|
622
|
+
rec["upright"] = up
|
|
623
|
+
rec["dims"] = Vector3(la.size.x * sc.x, la.size.y * sc.y, la.size.z * sc.z) if up else (rec["waabb"] as AABB).size
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# Outward face area per instance axis [+x, -x, +y, -y, +z, -z] (m2), from
|
|
627
|
+
# each mesh's cached areas mapped through the mesh's transform.
|
|
628
|
+
func _inst_axis_area(rec: Dictionary) -> PackedFloat32Array:
|
|
629
|
+
var out := PackedFloat32Array([0, 0, 0, 0, 0, 0])
|
|
630
|
+
var inv := (rec["xf"] as Transform3D).affine_inverse()
|
|
631
|
+
for m in (rec["meshes"] as Array):
|
|
632
|
+
var info: Dictionary = _mesh_info.get((m[1] as Mesh).get_instance_id(), {})
|
|
633
|
+
var areas: PackedFloat32Array = info.get("axis_area", PackedFloat32Array())
|
|
634
|
+
if areas.size() < 6:
|
|
635
|
+
continue
|
|
636
|
+
var mb: Basis = (m[2] as Transform3D).basis
|
|
637
|
+
var rel: Basis = (inv * (m[2] as Transform3D)).basis
|
|
638
|
+
var cols := [mb.x.length(), mb.y.length(), mb.z.length()]
|
|
639
|
+
for a in 3:
|
|
640
|
+
var f: float = float(cols[0]) * float(cols[1]) * float(cols[2]) / maxf(float(cols[a]), 0.000001)
|
|
641
|
+
for sgn in 2:
|
|
642
|
+
var e := Vector3.ZERO
|
|
643
|
+
e[a] = 1.0 if sgn == 0 else -1.0
|
|
644
|
+
var d := rel * e
|
|
645
|
+
var k := 0 if absf(d.x) >= absf(d.y) and absf(d.x) >= absf(d.z) else (1 if absf(d.y) >= absf(d.z) else 2)
|
|
646
|
+
out[k * 2 + (0 if d[k] > 0.0 else 1)] += areas[a * 2 + sgn] * f
|
|
647
|
+
return out
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
func _role(rec: Dictionary, role: String, why: String) -> void:
|
|
651
|
+
rec["role"] = role
|
|
652
|
+
rec["why"] = why
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# First pass, before the physics space exists: slabs, roofs, the underlay and
|
|
656
|
+
# sheets. Returns the sheets.
|
|
657
|
+
func _classify_shapes() -> Array:
|
|
658
|
+
var sheets: Array = []
|
|
659
|
+
for rec in _inst:
|
|
660
|
+
_shape_of(rec)
|
|
661
|
+
rec["front"] = Vector3.ZERO
|
|
662
|
+
if rec["role"] == "dressing":
|
|
663
|
+
continue
|
|
664
|
+
var d: Vector3 = rec["dims"]
|
|
665
|
+
if bool(rec["upright"]) and d.y <= SLAB_THICK and d.x >= SLAB_MIN and d.z >= SLAB_MIN:
|
|
666
|
+
var ar := _inst_axis_area(rec)
|
|
667
|
+
var upk := 2 if (rec["xf"] as Transform3D).basis.y.y > 0.0 else 3
|
|
668
|
+
if ar[upk] >= SLAB_COVER * d.x * d.z:
|
|
669
|
+
_role(rec, "floor", "slab")
|
|
670
|
+
else:
|
|
671
|
+
# Flat but open (scattered pebbles, leaves, a grate): ground
|
|
672
|
+
# cover, part of the ground, not a floor and not a prop.
|
|
673
|
+
_role(rec, "struct", "flat")
|
|
674
|
+
continue
|
|
675
|
+
if bool(rec["upright"]) and d.y >= SHEET_H and maxf(d.x, d.z) >= SHEET_W and minf(d.x, d.z) <= SHEET_THICK:
|
|
676
|
+
_role(rec, "wall", "sheet")
|
|
677
|
+
_sheet_front(rec)
|
|
678
|
+
sheets.append(rec)
|
|
679
|
+
var ground := INF
|
|
680
|
+
for rec in _inst:
|
|
681
|
+
if rec["role"] == "floor":
|
|
682
|
+
ground = minf(ground, (rec["waabb"] as AABB).end.y)
|
|
683
|
+
for rec in _inst:
|
|
684
|
+
if rec["role"] == "floor" and _walls_under(rec, sheets, ground):
|
|
685
|
+
_role(rec, "struct", "roof")
|
|
686
|
+
_resolve_underlays()
|
|
687
|
+
return sheets
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# Second pass (the physics space, when there is one, holds every solid piece
|
|
691
|
+
# on a provisional layer): inserts, facade lines, facade members, the rest by
|
|
692
|
+
# size; then every body gets its role's layer.
|
|
693
|
+
func _classify_rest(sheets: Array) -> void:
|
|
694
|
+
_find_inserts(sheets)
|
|
695
|
+
_find_members()
|
|
696
|
+
for rec in _inst:
|
|
697
|
+
if rec["role"] == "":
|
|
698
|
+
var wa: AABB = rec["waabb"]
|
|
699
|
+
_role(rec, "struct" if maxf(wa.size.x, maxf(wa.size.y, wa.size.z)) > PROP_MAX else "prop", "size")
|
|
700
|
+
for rec in _inst:
|
|
701
|
+
var k := String(rec["role"]) + ":" + String(rec.get("why", ""))
|
|
702
|
+
_role_stats[k] = int(_role_stats.get(k, 0)) + 1
|
|
703
|
+
_layers()
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
# Every body on its role's layer (pieces without a role yet: the prop layer).
|
|
707
|
+
func _layers() -> void:
|
|
708
|
+
if _state == null:
|
|
709
|
+
return
|
|
710
|
+
for rec in _inst:
|
|
711
|
+
var role := String(rec["role"])
|
|
712
|
+
var layer := int(ROLE_LAYER.get(role, L_PROP if role == "" else 0))
|
|
713
|
+
for b in (rec["bodies"] as Array):
|
|
714
|
+
PhysicsServer3D.body_set_collision_layer(b, layer)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
# The local axis nearest a local direction, as a unit vector.
|
|
718
|
+
static func _axis_snap(v: Vector3) -> Vector3:
|
|
719
|
+
var k := 0 if absf(v.x) >= absf(v.y) and absf(v.x) >= absf(v.z) else (1 if absf(v.y) >= absf(v.z) else 2)
|
|
720
|
+
var out := Vector3.ZERO
|
|
721
|
+
out[k] = 1.0 if v[k] >= 0.0 else -1.0
|
|
722
|
+
return out
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# A sheet faces along its thin axis, to the side with more outward area.
|
|
726
|
+
func _sheet_front(rec: Dictionary) -> void:
|
|
727
|
+
var d: Vector3 = rec["dims"]
|
|
728
|
+
var axis := 2 if d.z <= d.x else 0
|
|
729
|
+
var ar := _inst_axis_area(rec)
|
|
730
|
+
var v := Vector3.ZERO
|
|
731
|
+
v[axis] = 1.0 if ar[axis * 2] >= ar[axis * 2 + 1] else -1.0
|
|
732
|
+
rec["front"] = v
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
# A roof: the slab is raised at least 2 m above the lowest slab (the ground)
|
|
736
|
+
# and walls stand under it: one rises from at least 1 m below its underside to
|
|
737
|
+
# within 1.5 m of it (or up to 1.5 m above: a parapet), within 10 cm of its
|
|
738
|
+
# footprint.
|
|
739
|
+
func _walls_under(rec: Dictionary, sheets: Array, ground: float) -> bool:
|
|
740
|
+
var wa: AABB = rec["waabb"]
|
|
741
|
+
var bottom := wa.position.y
|
|
742
|
+
if bottom < ground + 2.0:
|
|
743
|
+
return false
|
|
744
|
+
var foot := AABB(Vector3(wa.position.x - 0.1, -1.0e6, wa.position.z - 0.1), Vector3(wa.size.x + 0.2, 2.0e6, wa.size.z + 0.2))
|
|
745
|
+
for w in sheets:
|
|
746
|
+
var ww: AABB = w["waabb"]
|
|
747
|
+
if ww.end.y < bottom - 1.5 or ww.end.y > wa.end.y + 1.5 or ww.position.y > bottom - 1.0:
|
|
748
|
+
continue
|
|
749
|
+
if foot.intersects(ww):
|
|
750
|
+
return true
|
|
751
|
+
return false
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# Inserts: see the section comment. Without a physics space (only checks
|
|
755
|
+
# that need none) the opening test is skipped.
|
|
756
|
+
func _find_inserts(sheets: Array) -> void:
|
|
757
|
+
for w in sheets:
|
|
758
|
+
var pl := _wall_plane(w)
|
|
759
|
+
if pl.is_empty():
|
|
760
|
+
continue
|
|
761
|
+
w["plane"] = pl
|
|
762
|
+
for rec in _inst:
|
|
763
|
+
var role := String(rec["role"])
|
|
764
|
+
if role == "dressing" or role == "floor" or role == "underlay" or (role == "struct" and String(rec.get("why", "")) == "roof"):
|
|
765
|
+
continue
|
|
766
|
+
var pa: AABB = rec["waabb"]
|
|
767
|
+
for w in sheets:
|
|
768
|
+
if w == rec or not w.has("plane") or String(w["role"]) != "wall":
|
|
769
|
+
continue
|
|
770
|
+
if not pa.intersects((w["waabb"] as AABB).grow(0.05)):
|
|
771
|
+
continue
|
|
772
|
+
var pl: Dictionary = w["plane"]
|
|
773
|
+
var n: Vector3 = pl["n"]
|
|
774
|
+
var tax := Vector3(n.z, 0, -n.x)
|
|
775
|
+
var wr := _rect(w, tax)
|
|
776
|
+
var r := _rect(rec, tax)
|
|
777
|
+
var area: float = (r[1] - r[0]) * (r[3] - r[2])
|
|
778
|
+
var warea: float = (wr[1] - wr[0]) * (wr[3] - wr[2])
|
|
779
|
+
if area <= 0.0 or area > 0.9 * warea:
|
|
780
|
+
continue
|
|
781
|
+
var ot: float = minf(r[1], wr[1]) - maxf(r[0], wr[0])
|
|
782
|
+
var oy: float = minf(r[3], wr[3]) - maxf(r[2], wr[2])
|
|
783
|
+
if ot <= 0.0 or oy <= 0.0 or ot * oy < 0.8 * area:
|
|
784
|
+
continue
|
|
785
|
+
var nr := _span(rec, n)
|
|
786
|
+
var df := float(pl["d"])
|
|
787
|
+
if nr[0] > df - 0.03 or nr[1] > df + MEMBER_REACH:
|
|
788
|
+
continue
|
|
789
|
+
if _state != null and not _open_at(w, rec, n, tax, r, df - float(pl["thick"])):
|
|
790
|
+
continue
|
|
791
|
+
_role(rec, "insert", "in_opening")
|
|
792
|
+
rec["front"] = _axis_snap((rec["xf"] as Transform3D).basis.inverse() * n)
|
|
793
|
+
rec["host"] = int(w["i"])
|
|
794
|
+
break
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# The extent of an instance's bounds along a world direction: [min, max].
|
|
798
|
+
func _span(rec: Dictionary, dir: Vector3) -> Array:
|
|
799
|
+
var la: AABB = rec["laabb"]
|
|
800
|
+
var xf: Transform3D = rec["xf"]
|
|
801
|
+
var lo := INF
|
|
802
|
+
var hi := -INF
|
|
803
|
+
for c in _corners(la):
|
|
804
|
+
var v := dir.dot(xf * c)
|
|
805
|
+
lo = minf(lo, v)
|
|
806
|
+
hi = maxf(hi, v)
|
|
807
|
+
return [lo, hi]
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# Rays through the piece's rectangle (its centre and a quarter in from each
|
|
811
|
+
# side along the wall) miss the wall itself at two of the three points: the
|
|
812
|
+
# wall is open there.
|
|
813
|
+
func _open_at(w: Dictionary, rec: Dictionary, n: Vector3, tax: Vector3, r: Array, back: float) -> bool:
|
|
814
|
+
var probe_layer := 1 << 19
|
|
815
|
+
for b in (w["bodies"] as Array):
|
|
816
|
+
PhysicsServer3D.body_set_collision_layer(b, probe_layer)
|
|
817
|
+
var front := float((w["plane"] as Dictionary)["d"])
|
|
818
|
+
var yc: float = (float(r[2]) + float(r[3])) * 0.5
|
|
819
|
+
var open := 0
|
|
820
|
+
for f in [0.5, 0.25, 0.75]:
|
|
821
|
+
var t: float = float(r[0]) + (float(r[1]) - float(r[0])) * float(f)
|
|
822
|
+
var p := tax * t + Vector3(0, yc, 0)
|
|
823
|
+
var hit := _ray(p + n * (front + 0.05), p + n * (back - 0.05), probe_layer, [], true)
|
|
824
|
+
if hit.is_empty():
|
|
825
|
+
open += 1
|
|
826
|
+
for b in (w["bodies"] as Array):
|
|
827
|
+
PhysicsServer3D.body_set_collision_layer(b, L_WALL)
|
|
828
|
+
return open >= 2
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
# Facade lines: walls whose fronts share a plane (normals within 2 deg, planes
|
|
832
|
+
# within 20 cm). [{n, d, thick, walls}]
|
|
833
|
+
func _facade_lines() -> Array:
|
|
834
|
+
var lines: Array = []
|
|
835
|
+
for rec in _inst:
|
|
836
|
+
if rec["role"] != "wall":
|
|
837
|
+
continue
|
|
838
|
+
var pl := _wall_plane(rec)
|
|
839
|
+
if pl.is_empty():
|
|
840
|
+
continue
|
|
841
|
+
var n: Vector3 = pl["n"]
|
|
842
|
+
var placed := false
|
|
843
|
+
for line in lines:
|
|
844
|
+
var ln: Vector3 = line["n"]
|
|
845
|
+
if ln.dot(n) > 0.9994 and absf(float(pl["d"]) - float(line["d"])) < 0.2:
|
|
846
|
+
(line["walls"] as Array).append(rec)
|
|
847
|
+
line["thick"] = maxf(float(line["thick"]), float(pl["thick"]))
|
|
848
|
+
placed = true
|
|
849
|
+
break
|
|
850
|
+
if not placed:
|
|
851
|
+
lines.append({"n": n, "d": float(pl["d"]), "thick": float(pl["thick"]), "walls": [rec]})
|
|
852
|
+
return lines
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
# Facade members (see the section comment): pieces whose bounds touch a
|
|
856
|
+
# facade line's wall.
|
|
857
|
+
func _find_members() -> void:
|
|
858
|
+
var lines := _facade_lines()
|
|
859
|
+
for line in lines:
|
|
860
|
+
var n: Vector3 = line["n"]
|
|
861
|
+
var tax := Vector3(n.z, 0, -n.x)
|
|
862
|
+
line["t"] = tax
|
|
863
|
+
var rects: Array = []
|
|
864
|
+
var zone := AABB()
|
|
865
|
+
for w in (line["walls"] as Array):
|
|
866
|
+
rects.append(_rect(w, tax))
|
|
867
|
+
zone = (w["waabb"] as AABB) if zone.size == Vector3.ZERO else zone.merge(w["waabb"] as AABB)
|
|
868
|
+
line["rects"] = rects
|
|
869
|
+
line["zone"] = zone.grow(MEMBER_REACH)
|
|
870
|
+
var cand: Array = []
|
|
871
|
+
for rec in _inst:
|
|
872
|
+
if rec["role"] != "" or not bool(rec["upright"]):
|
|
873
|
+
continue
|
|
874
|
+
var d: Vector3 = rec["dims"]
|
|
875
|
+
var long := maxf(d.x, d.z)
|
|
876
|
+
var short := minf(d.x, d.z)
|
|
877
|
+
var pier := d.y >= BAND_MIN_LEN and d.y >= BAND_ASPECT * long and long <= 1.0
|
|
878
|
+
var band := d.y <= BAND_MAX_H and short <= BAND_MAX_D and long >= d.y
|
|
879
|
+
if not pier and not band:
|
|
880
|
+
continue
|
|
881
|
+
var c := _touch_line(rec, lines)
|
|
882
|
+
if c.is_empty():
|
|
883
|
+
continue
|
|
884
|
+
var n: Vector3 = c[0]
|
|
885
|
+
if pier:
|
|
886
|
+
_member(rec, "pier", n)
|
|
887
|
+
continue
|
|
888
|
+
var tax := Vector3(n.z, 0, -n.x)
|
|
889
|
+
var wa: AABB = rec["waabb"]
|
|
890
|
+
var r := _span(rec, tax)
|
|
891
|
+
cand.append([rec, n, float(c[1]), float(r[0]), float(r[1]), wa.position.y, wa.end.y])
|
|
892
|
+
# Band runs: pieces on one surface (normals within 2 deg, planes within
|
|
893
|
+
# 10 cm) at one height laid end to end.
|
|
894
|
+
var used: Dictionary = {}
|
|
895
|
+
for k in cand.size():
|
|
896
|
+
if used.has(k):
|
|
897
|
+
continue
|
|
898
|
+
var n: Vector3 = cand[k][1]
|
|
899
|
+
var run: Array = [k]
|
|
900
|
+
used[k] = true
|
|
901
|
+
var lo: float = cand[k][3]
|
|
902
|
+
var hi: float = cand[k][4]
|
|
903
|
+
var y0: float = cand[k][5]
|
|
904
|
+
var y1: float = cand[k][6]
|
|
905
|
+
var grew := true
|
|
906
|
+
while grew:
|
|
907
|
+
grew = false
|
|
908
|
+
for j in cand.size():
|
|
909
|
+
if used.has(j):
|
|
910
|
+
continue
|
|
911
|
+
var cj: Array = cand[j]
|
|
912
|
+
if (cj[1] as Vector3).dot(n) < 0.9994 or absf(float(cj[2]) - float(cand[k][2])) > 0.1:
|
|
913
|
+
continue
|
|
914
|
+
if absf(float(cj[5]) - y0) > 0.05 or absf(float(cj[6]) - y1) > 0.05:
|
|
915
|
+
continue
|
|
916
|
+
if float(cj[3]) > hi + 0.05 or float(cj[4]) < lo - 0.05:
|
|
917
|
+
continue
|
|
918
|
+
run.append(j)
|
|
919
|
+
used[j] = true
|
|
920
|
+
lo = minf(lo, float(cj[3]))
|
|
921
|
+
hi = maxf(hi, float(cj[4]))
|
|
922
|
+
grew = true
|
|
923
|
+
if hi - lo < maxf(BAND_MIN_LEN, BAND_ASPECT * (y1 - y0)):
|
|
924
|
+
continue
|
|
925
|
+
for j in run:
|
|
926
|
+
_member(cand[j][0], "band", cand[j][1])
|
|
927
|
+
# Joints: touch a band at its height, stick out at most 30 cm further.
|
|
928
|
+
var bands: Array = []
|
|
929
|
+
for rec in _inst:
|
|
930
|
+
if rec["role"] == "struct" and String(rec.get("why", "")) == "band":
|
|
931
|
+
bands.append(rec)
|
|
932
|
+
for rec in _inst:
|
|
933
|
+
if rec["role"] != "":
|
|
934
|
+
continue
|
|
935
|
+
var pa: AABB = rec["waabb"]
|
|
936
|
+
for b in bands:
|
|
937
|
+
var ba: AABB = b["waabb"]
|
|
938
|
+
if not pa.intersects(ba.grow(MEMBER_TOUCH)):
|
|
939
|
+
continue
|
|
940
|
+
if absf(pa.position.y - ba.position.y) > 0.1 or absf(pa.end.y - ba.end.y) > 0.15:
|
|
941
|
+
continue
|
|
942
|
+
var n: Vector3 = b["n_out"]
|
|
943
|
+
if float(_span(rec, n)[1]) > float(_span(b, n)[1]) + 0.3:
|
|
944
|
+
continue
|
|
945
|
+
_member(rec, "joint", n)
|
|
946
|
+
break
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
func _member(rec: Dictionary, why: String, n: Vector3) -> void:
|
|
950
|
+
_role(rec, "struct", why)
|
|
951
|
+
rec["n_out"] = n
|
|
952
|
+
rec["front"] = _axis_snap((rec["xf"] as Transform3D).basis.inverse() * n)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
# [the line's front, the line's plane] when the piece's bounds touch one of
|
|
956
|
+
# the line's walls: crossing or within MEMBER_TOUCH of the wall's slab,
|
|
957
|
+
# overlapping it along the line and in height (5 cm slack), sticking out at
|
|
958
|
+
# most MEMBER_REACH. [] otherwise.
|
|
959
|
+
func _touch_line(rec: Dictionary, lines: Array) -> Array:
|
|
960
|
+
var wa: AABB = rec["waabb"]
|
|
961
|
+
for line in lines:
|
|
962
|
+
if not (line["zone"] as AABB).intersects(wa):
|
|
963
|
+
continue
|
|
964
|
+
var n: Vector3 = line["n"]
|
|
965
|
+
var d := float(line["d"])
|
|
966
|
+
var nr := _span(rec, n)
|
|
967
|
+
if nr[1] < d - float(line["thick"]) - MEMBER_TOUCH or nr[0] > d + MEMBER_TOUCH or nr[1] > d + MEMBER_REACH:
|
|
968
|
+
continue
|
|
969
|
+
var r := _rect(rec, line["t"] as Vector3)
|
|
970
|
+
for wr in (line["rects"] as Array):
|
|
971
|
+
if r[0] <= float(wr[1]) + 0.05 and r[1] >= float(wr[0]) - 0.05 and r[2] <= float(wr[3]) + 0.05 and r[3] >= float(wr[2]) - 0.05:
|
|
972
|
+
return [n, d]
|
|
973
|
+
return []
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
func _resolve_underlays() -> void:
|
|
977
|
+
var areas: Array = []
|
|
978
|
+
for rec in _inst:
|
|
979
|
+
if rec["role"] == "floor":
|
|
980
|
+
var wa: AABB = rec["waabb"]
|
|
981
|
+
areas.append(wa.size.x * wa.size.z)
|
|
982
|
+
if areas.size() < 2:
|
|
983
|
+
return
|
|
984
|
+
areas.sort()
|
|
985
|
+
var median: float = areas[areas.size() / 2]
|
|
986
|
+
var tops: Array = []
|
|
987
|
+
for rec in _inst:
|
|
988
|
+
if rec["role"] == "floor":
|
|
989
|
+
tops.append((rec["waabb"] as AABB).end.y)
|
|
990
|
+
tops.sort()
|
|
991
|
+
var median_top: float = tops[tops.size() / 2]
|
|
992
|
+
for rec in _inst:
|
|
993
|
+
if rec["role"] != "floor":
|
|
994
|
+
continue
|
|
995
|
+
var wa: AABB = rec["waabb"]
|
|
996
|
+
if wa.size.x * wa.size.z >= 8.0 * median and wa.end.y < median_top - 0.005:
|
|
997
|
+
_role(rec, "underlay", "below_floors")
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
# ---------------------------------------------------------------------------
|
|
1001
|
+
# One pass per mesh RESOURCE (cached by instance id): physics faces in index
|
|
1002
|
+
# order (double-sided surfaces get both windings), outward face area per
|
|
1003
|
+
# local axis (roles), and UV-stretch candidate triangles.
|
|
1004
|
+
# ---------------------------------------------------------------------------
|
|
1005
|
+
|
|
1006
|
+
func _mesh_pass() -> void:
|
|
1007
|
+
var want_uv := _checks.has("uv_stretch")
|
|
1008
|
+
var want_groups := _checks.has("z_fight")
|
|
1009
|
+
for rec in _inst:
|
|
1010
|
+
if rec["role"] == "dressing":
|
|
1011
|
+
continue
|
|
1012
|
+
for m in (rec["meshes"] as Array):
|
|
1013
|
+
var mesh: Mesh = m[1]
|
|
1014
|
+
var key := mesh.get_instance_id()
|
|
1015
|
+
if not _mesh_info.has(key):
|
|
1016
|
+
_mesh_info[key] = _analyze_mesh(mesh, want_uv, want_groups)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
func _double_sided(mat: Material) -> bool:
|
|
1020
|
+
if mat is BaseMaterial3D:
|
|
1021
|
+
return (mat as BaseMaterial3D).cull_mode == BaseMaterial3D.CULL_DISABLED
|
|
1022
|
+
if mat is ShaderMaterial:
|
|
1023
|
+
var sh: Shader = (mat as ShaderMaterial).shader
|
|
1024
|
+
if sh == null:
|
|
1025
|
+
return false
|
|
1026
|
+
return sh.code.contains("cull_disabled")
|
|
1027
|
+
return false
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
func _analyze_mesh(mesh: Mesh, want_uv: bool, want_groups := false) -> Dictionary:
|
|
1031
|
+
var faces := PackedVector3Array()
|
|
1032
|
+
var axis_area := PackedFloat32Array([0, 0, 0, 0, 0, 0])
|
|
1033
|
+
# z_fight: per surface, triangles bucketed by (normal, plane offset):
|
|
1034
|
+
# [surface, material, double-sided, {Vector4i -> [area, normal * area, offset * area, [vertices]]}].
|
|
1035
|
+
var zf_raw: Array = []
|
|
1036
|
+
var uv_cands: Array = []
|
|
1037
|
+
var tris := 0
|
|
1038
|
+
var no_material := 0
|
|
1039
|
+
for s in mesh.get_surface_count():
|
|
1040
|
+
# Only ArrayMesh exposes the primitive; PrimitiveMesh surfaces are triangles.
|
|
1041
|
+
if mesh is ArrayMesh and (mesh as ArrayMesh).surface_get_primitive_type(s) != Mesh.PRIMITIVE_TRIANGLES:
|
|
1042
|
+
continue
|
|
1043
|
+
var arrays := mesh.surface_get_arrays(s)
|
|
1044
|
+
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
1045
|
+
if verts.is_empty():
|
|
1046
|
+
continue
|
|
1047
|
+
var mat := mesh.surface_get_material(s)
|
|
1048
|
+
if mat == null:
|
|
1049
|
+
no_material += 1
|
|
1050
|
+
var double := mat != null and _double_sided(mat)
|
|
1051
|
+
var sbins: Dictionary = {}
|
|
1052
|
+
if want_groups:
|
|
1053
|
+
zf_raw.append([s, mat, double, sbins])
|
|
1054
|
+
var idx: PackedInt32Array
|
|
1055
|
+
if arrays[Mesh.ARRAY_INDEX] != null:
|
|
1056
|
+
idx = arrays[Mesh.ARRAY_INDEX]
|
|
1057
|
+
else:
|
|
1058
|
+
idx = PackedInt32Array()
|
|
1059
|
+
idx.resize(verts.size())
|
|
1060
|
+
for k in verts.size():
|
|
1061
|
+
idx[k] = k
|
|
1062
|
+
var uvs := PackedVector2Array()
|
|
1063
|
+
if want_uv and arrays[Mesh.ARRAY_TEX_UV] != null:
|
|
1064
|
+
uvs = arrays[Mesh.ARRAY_TEX_UV]
|
|
1065
|
+
var has_uv := uvs.size() == verts.size()
|
|
1066
|
+
var n := idx.size() - idx.size() % 3
|
|
1067
|
+
var base := faces.size()
|
|
1068
|
+
faces.resize(base + (n * 2 if double else n))
|
|
1069
|
+
var w := base
|
|
1070
|
+
var j := 0
|
|
1071
|
+
while j < n:
|
|
1072
|
+
var i0 := idx[j]
|
|
1073
|
+
var i1 := idx[j + 1]
|
|
1074
|
+
var i2 := idx[j + 2]
|
|
1075
|
+
var p0: Vector3 = verts[i0]
|
|
1076
|
+
var p1: Vector3 = verts[i1]
|
|
1077
|
+
var p2: Vector3 = verts[i2]
|
|
1078
|
+
faces[w] = p0
|
|
1079
|
+
faces[w + 1] = p1
|
|
1080
|
+
faces[w + 2] = p2
|
|
1081
|
+
w += 3
|
|
1082
|
+
if double:
|
|
1083
|
+
faces[w] = p0
|
|
1084
|
+
faces[w + 1] = p2
|
|
1085
|
+
faces[w + 2] = p1
|
|
1086
|
+
w += 3
|
|
1087
|
+
# Plane(p0, p1, p2) normal: the side physics and the renderer call front.
|
|
1088
|
+
var cr: Vector3 = (p0 - p2).cross(p0 - p1)
|
|
1089
|
+
var a2 := cr.length()
|
|
1090
|
+
if a2 > 0.000001:
|
|
1091
|
+
var nn := cr / a2
|
|
1092
|
+
var area := a2 * 0.5
|
|
1093
|
+
if absf(nn.x) > 0.7:
|
|
1094
|
+
axis_area[0 if nn.x > 0 else 1] += area
|
|
1095
|
+
if absf(nn.y) > 0.7:
|
|
1096
|
+
axis_area[2 if nn.y > 0 else 3] += area
|
|
1097
|
+
if absf(nn.z) > 0.7:
|
|
1098
|
+
axis_area[4 if nn.z > 0 else 5] += area
|
|
1099
|
+
if want_groups and area >= ZF_MIN_TRI:
|
|
1100
|
+
var dd := nn.dot(p0)
|
|
1101
|
+
var bk := Vector4i(roundi(nn.x * 30.0), roundi(nn.y * 30.0), roundi(nn.z * 30.0), roundi(dd / 0.004))
|
|
1102
|
+
var bg: Array = sbins.get(bk, [])
|
|
1103
|
+
if bg.is_empty():
|
|
1104
|
+
bg = [0.0, Vector3.ZERO, 0.0, []]
|
|
1105
|
+
sbins[bk] = bg
|
|
1106
|
+
bg[0] = float(bg[0]) + area
|
|
1107
|
+
bg[1] = (bg[1] as Vector3) + nn * area
|
|
1108
|
+
bg[2] = float(bg[2]) + dd * area
|
|
1109
|
+
(bg[3] as Array).append_array([p0, p1, p2])
|
|
1110
|
+
if has_uv and area >= 0.015:
|
|
1111
|
+
var t0: Vector2 = uvs[i0]
|
|
1112
|
+
var u1: Vector2 = uvs[i1] - t0
|
|
1113
|
+
var u2: Vector2 = uvs[i2] - t0
|
|
1114
|
+
var e1 := p1 - p0
|
|
1115
|
+
var e2 := p2 - p0
|
|
1116
|
+
var det := u1.x * u2.y - u2.x * u1.y
|
|
1117
|
+
var ratio := 1000.0
|
|
1118
|
+
if absf(det) > 1e-12:
|
|
1119
|
+
var inv := 1.0 / det
|
|
1120
|
+
var du := (e1 * u2.y - e2 * u1.y) * inv
|
|
1121
|
+
var dv := (e2 * u1.x - e1 * u2.x) * inv
|
|
1122
|
+
var aa := du.dot(du)
|
|
1123
|
+
var bb := dv.dot(dv)
|
|
1124
|
+
var ab := du.dot(dv)
|
|
1125
|
+
var disc := sqrt(maxf(0.0, (aa - bb) * (aa - bb) + 4.0 * ab * ab))
|
|
1126
|
+
var s2 := (aa + bb - disc) * 0.5
|
|
1127
|
+
var s1 := (aa + bb + disc) * 0.5
|
|
1128
|
+
ratio = 1000.0 if s2 <= 1e-14 else sqrt(s1 / s2)
|
|
1129
|
+
if ratio > 4.0:
|
|
1130
|
+
uv_cands.append([area, ratio, p0, p1, p2, uvs[i0], uvs[i1], uvs[i2], nn])
|
|
1131
|
+
j += 3
|
|
1132
|
+
tris += n / 3
|
|
1133
|
+
uv_cands.sort_custom(func(a, b): return float(a[0]) > float(b[0]))
|
|
1134
|
+
if uv_cands.size() > 6:
|
|
1135
|
+
uv_cands.resize(6)
|
|
1136
|
+
return {"faces": faces, "axis_area": axis_area, "zf_raw": zf_raw, "uv": uv_cands, "tris": tris, "no_material": no_material, "shape": RID(), "name": _mesh_name(mesh)}
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
func _mesh_name(mesh: Mesh) -> String:
|
|
1140
|
+
var p := String(mesh.resource_path)
|
|
1141
|
+
if p != "":
|
|
1142
|
+
return p.get_file()
|
|
1143
|
+
return String(mesh.resource_name) if String(mesh.resource_name) != "" else "mesh"
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
# ---------------------------------------------------------------------------
|
|
1147
|
+
# Physics space from the visible meshes (dressing excluded).
|
|
1148
|
+
# ---------------------------------------------------------------------------
|
|
1149
|
+
|
|
1150
|
+
static func _is_identity(xf: Transform3D) -> bool:
|
|
1151
|
+
return xf.origin.length() < 0.001 and (xf.basis.x - Vector3(1, 0, 0)).length() < 0.0001 and (xf.basis.y - Vector3(0, 1, 0)).length() < 0.0001 and (xf.basis.z - Vector3(0, 0, 1)).length() < 0.0001
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
static func _is_rigid(b: Basis) -> bool:
|
|
1155
|
+
return absf(b.x.length() - 1.0) < 0.001 and absf(b.y.length() - 1.0) < 0.001 and absf(b.z.length() - 1.0) < 0.001 \
|
|
1156
|
+
and absf(b.x.dot(b.y)) < 0.001 and absf(b.x.dot(b.z)) < 0.001 and absf(b.y.dot(b.z)) < 0.001
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
func _build_space() -> void:
|
|
1160
|
+
_space = PhysicsServer3D.space_create()
|
|
1161
|
+
PhysicsServer3D.space_set_active(_space, true)
|
|
1162
|
+
for rec in _inst:
|
|
1163
|
+
# Every solid piece; pieces without a role yet sit on the prop layer
|
|
1164
|
+
# until _classify_rest gives each body its role's layer.
|
|
1165
|
+
if rec["role"] == "dressing":
|
|
1166
|
+
continue
|
|
1167
|
+
var layer := int(ROLE_LAYER.get(String(rec["role"]), L_PROP))
|
|
1168
|
+
for m in (rec["meshes"] as Array):
|
|
1169
|
+
var mesh: Mesh = m[1]
|
|
1170
|
+
var xf: Transform3D = m[2]
|
|
1171
|
+
var info: Dictionary = _mesh_info.get(mesh.get_instance_id(), {})
|
|
1172
|
+
var faces: PackedVector3Array = info.get("faces", PackedVector3Array())
|
|
1173
|
+
if faces.is_empty():
|
|
1174
|
+
continue
|
|
1175
|
+
var shape := RID()
|
|
1176
|
+
var body_xf := xf
|
|
1177
|
+
if _is_rigid(xf.basis):
|
|
1178
|
+
shape = info["shape"]
|
|
1179
|
+
if not shape.is_valid():
|
|
1180
|
+
shape = PhysicsServer3D.concave_polygon_shape_create()
|
|
1181
|
+
PhysicsServer3D.shape_set_data(shape, {"faces": faces, "backface_collision": true})
|
|
1182
|
+
info["shape"] = shape
|
|
1183
|
+
_shapes.append(shape)
|
|
1184
|
+
else:
|
|
1185
|
+
var baked := faces.duplicate()
|
|
1186
|
+
for k in baked.size():
|
|
1187
|
+
baked[k] = xf * baked[k]
|
|
1188
|
+
shape = PhysicsServer3D.concave_polygon_shape_create()
|
|
1189
|
+
PhysicsServer3D.shape_set_data(shape, {"faces": baked, "backface_collision": true})
|
|
1190
|
+
_shapes.append(shape)
|
|
1191
|
+
body_xf = Transform3D.IDENTITY
|
|
1192
|
+
var body := PhysicsServer3D.body_create()
|
|
1193
|
+
PhysicsServer3D.body_set_mode(body, PhysicsServer3D.BODY_MODE_STATIC)
|
|
1194
|
+
PhysicsServer3D.body_add_shape(body, shape)
|
|
1195
|
+
PhysicsServer3D.body_set_collision_layer(body, layer)
|
|
1196
|
+
PhysicsServer3D.body_set_collision_mask(body, 0)
|
|
1197
|
+
PhysicsServer3D.body_set_space(body, _space)
|
|
1198
|
+
PhysicsServer3D.body_set_state(body, PhysicsServer3D.BODY_STATE_TRANSFORM, body_xf)
|
|
1199
|
+
_bodies.append(body)
|
|
1200
|
+
_body_inst[body.get_id()] = int(rec["i"])
|
|
1201
|
+
(rec["bodies"] as Array).append(body)
|
|
1202
|
+
_state = PhysicsServer3D.space_get_direct_state(_space)
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
func _free_physics() -> void:
|
|
1206
|
+
for body in _bodies:
|
|
1207
|
+
PhysicsServer3D.free_rid(body)
|
|
1208
|
+
_bodies.clear()
|
|
1209
|
+
for shape in _shapes:
|
|
1210
|
+
PhysicsServer3D.free_rid(shape)
|
|
1211
|
+
_shapes.clear()
|
|
1212
|
+
_hulls.clear()
|
|
1213
|
+
if _space.is_valid():
|
|
1214
|
+
PhysicsServer3D.free_rid(_space)
|
|
1215
|
+
_space = RID()
|
|
1216
|
+
_state = null
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
# One-sided (renderer-like) ray unless `two_sided`. Returns {} on a miss.
|
|
1220
|
+
func _ray(from: Vector3, to: Vector3, mask: int, exclude: Array = [], two_sided := false) -> Dictionary:
|
|
1221
|
+
_rays += 1
|
|
1222
|
+
_q.from = from
|
|
1223
|
+
_q.to = to
|
|
1224
|
+
_q.collision_mask = mask
|
|
1225
|
+
_q.hit_back_faces = two_sided
|
|
1226
|
+
# Re-set the exclude list only when it changes (most rays exclude nothing,
|
|
1227
|
+
# and setting it costs more than the ray).
|
|
1228
|
+
if not exclude.is_empty():
|
|
1229
|
+
_ex.clear()
|
|
1230
|
+
for r in exclude:
|
|
1231
|
+
_ex.append(r)
|
|
1232
|
+
_q.exclude = _ex
|
|
1233
|
+
_ex_set = true
|
|
1234
|
+
elif _ex_set:
|
|
1235
|
+
_ex.clear()
|
|
1236
|
+
_q.exclude = _ex
|
|
1237
|
+
_ex_set = false
|
|
1238
|
+
return _state.intersect_ray(_q)
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
func _hit_inst(hit: Dictionary) -> int:
|
|
1242
|
+
if hit.is_empty():
|
|
1243
|
+
return -1
|
|
1244
|
+
return int(_body_inst.get((hit["rid"] as RID).get_id(), -1))
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
# Clear distance along dir from p (two-sided: any surface, front or back).
|
|
1248
|
+
func _clear(p: Vector3, dir: Vector3, max_len: float, exclude: Array = []) -> float:
|
|
1249
|
+
var hit := _ray(p, p + dir * max_len, MASK_SOLID, exclude, true)
|
|
1250
|
+
if hit.is_empty():
|
|
1251
|
+
return max_len
|
|
1252
|
+
return (hit["position"] as Vector3).distance_to(p)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
# ---------------------------------------------------------------------------
|
|
1256
|
+
# through_hole (+ wall z-fight samples): facade lines -> capped ray grid
|
|
1257
|
+
# ---------------------------------------------------------------------------
|
|
1258
|
+
|
|
1259
|
+
func _wall_plane(rec: Dictionary) -> Dictionary:
|
|
1260
|
+
var lf: Vector3 = rec["front"]
|
|
1261
|
+
if lf == Vector3.ZERO:
|
|
1262
|
+
return {}
|
|
1263
|
+
var xf: Transform3D = rec["xf"]
|
|
1264
|
+
var wn: Vector3 = xf.basis * lf
|
|
1265
|
+
var h := Vector3(wn.x, 0, wn.z)
|
|
1266
|
+
if h.length() < 0.7 * wn.length() or h.length() < 0.000001:
|
|
1267
|
+
return {}
|
|
1268
|
+
var n := h.normalized()
|
|
1269
|
+
var la: AABB = rec["laabb"]
|
|
1270
|
+
var c := la.get_center()
|
|
1271
|
+
var half := absf(lf.dot(la.size)) * 0.5
|
|
1272
|
+
var wf: Vector3 = xf * (c + lf * half)
|
|
1273
|
+
var wb: Vector3 = xf * (c - lf * half)
|
|
1274
|
+
return {"n": n, "d": n.dot(wf), "thick": maxf(0.0, n.dot(wf - wb))}
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
func _rect(rec: Dictionary, tax: Vector3) -> Array:
|
|
1278
|
+
var la: AABB = rec["laabb"]
|
|
1279
|
+
var xf: Transform3D = rec["xf"]
|
|
1280
|
+
var t0 := INF
|
|
1281
|
+
var t1 := -INF
|
|
1282
|
+
var y0 := INF
|
|
1283
|
+
var y1 := -INF
|
|
1284
|
+
for c in _corners(la):
|
|
1285
|
+
var w: Vector3 = xf * c
|
|
1286
|
+
var t := tax.dot(w)
|
|
1287
|
+
t0 = minf(t0, t)
|
|
1288
|
+
t1 = maxf(t1, t)
|
|
1289
|
+
y0 = minf(y0, w.y)
|
|
1290
|
+
y1 = maxf(y1, w.y)
|
|
1291
|
+
return [t0, t1, y0, y1]
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
func _scan_facades() -> Array:
|
|
1295
|
+
var lines: Array = _facade_lines()
|
|
1296
|
+
# Rects, union area, spacing.
|
|
1297
|
+
var total_area := 0.0
|
|
1298
|
+
for line in lines:
|
|
1299
|
+
var n: Vector3 = line["n"]
|
|
1300
|
+
var tax := Vector3(n.z, 0, -n.x)
|
|
1301
|
+
line["t"] = tax
|
|
1302
|
+
var rects: Array = []
|
|
1303
|
+
for rec in (line["walls"] as Array):
|
|
1304
|
+
var r := _rect(rec, tax)
|
|
1305
|
+
rects.append([int(rec["i"]), r[0], r[1], r[2], r[3]])
|
|
1306
|
+
total_area += (r[1] - r[0]) * (r[3] - r[2])
|
|
1307
|
+
line["rects"] = rects
|
|
1308
|
+
var ins: Array = []
|
|
1309
|
+
for rec in _inst:
|
|
1310
|
+
if rec["role"] != "insert":
|
|
1311
|
+
continue
|
|
1312
|
+
var pl := _wall_plane(rec)
|
|
1313
|
+
if pl.is_empty() or (pl["n"] as Vector3).dot(n) < 0.996 or absf(float(pl["d"]) - float(line["d"])) > 0.6:
|
|
1314
|
+
continue
|
|
1315
|
+
var r := _rect(rec, tax)
|
|
1316
|
+
if _overlaps_any(rects, r):
|
|
1317
|
+
ins.append([int(rec["i"]), r[0], r[1], r[2], r[3]])
|
|
1318
|
+
line["inserts"] = ins
|
|
1319
|
+
var cap := int(_cfg.get("max_hole_rays", 24000))
|
|
1320
|
+
var s := clampf(sqrt(maxf(total_area, 1.0) / float(maxi(cap, 1000))), 0.15, 0.4)
|
|
1321
|
+
# Opposite-facing lines, for "reached the far side of the building".
|
|
1322
|
+
var out: Array = []
|
|
1323
|
+
for li in lines.size():
|
|
1324
|
+
var line: Dictionary = lines[li]
|
|
1325
|
+
out.append(_scan_line(line, lines, s))
|
|
1326
|
+
return out
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
static func _overlaps_any(rects: Array, r: Array) -> bool:
|
|
1330
|
+
for q in rects:
|
|
1331
|
+
if r[0] < q[2] and r[1] > q[1] and r[2] < q[4] and r[3] > q[3]:
|
|
1332
|
+
return true
|
|
1333
|
+
return false
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
# How far a ray that passed this facade must still travel to have reached the
|
|
1337
|
+
# far side of the building: up to just before the nearest opposite-facing
|
|
1338
|
+
# facade line behind it (its inside is the far side; what is on it, a shutter
|
|
1339
|
+
# back or culled wall backs, is what the hole shows), else 4 m of open space.
|
|
1340
|
+
func _far_distance(line: Dictionary, lines: Array, p: Vector3) -> float:
|
|
1341
|
+
var n: Vector3 = line["n"]
|
|
1342
|
+
var best_s := INF
|
|
1343
|
+
var stop := 4.0
|
|
1344
|
+
for other in lines:
|
|
1345
|
+
var on: Vector3 = other["n"]
|
|
1346
|
+
if on.dot(n) > -0.99:
|
|
1347
|
+
continue
|
|
1348
|
+
var s := float(other["d"]) - on.dot(p)
|
|
1349
|
+
# on ~ -n: the plane on.X = d lies at distance s behind p along -n.
|
|
1350
|
+
if s <= 0.3 or s >= best_s or s > 40.0:
|
|
1351
|
+
continue
|
|
1352
|
+
var tax: Vector3 = other["t"]
|
|
1353
|
+
var t := tax.dot(p)
|
|
1354
|
+
var covered := false
|
|
1355
|
+
for r in (other["rects"] as Array):
|
|
1356
|
+
if t >= float(r[1]) - 1.0 and t <= float(r[2]) + 1.0 and p.y >= float(r[3]) - 0.5 and p.y <= float(r[4]) + 0.5:
|
|
1357
|
+
covered = true
|
|
1358
|
+
break
|
|
1359
|
+
if covered:
|
|
1360
|
+
best_s = s
|
|
1361
|
+
stop = maxf(0.3, s - float(other["thick"]) - 0.15)
|
|
1362
|
+
return stop
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
func _scan_line(line: Dictionary, lines: Array, s: float) -> Dictionary:
|
|
1366
|
+
var n: Vector3 = line["n"]
|
|
1367
|
+
var tax: Vector3 = line["t"]
|
|
1368
|
+
var d := float(line["d"])
|
|
1369
|
+
var rects: Array = line["rects"]
|
|
1370
|
+
var inserts: Array = line["inserts"]
|
|
1371
|
+
var slab := clampf(float(line["thick"]) + 0.25, 0.35, 1.0)
|
|
1372
|
+
var in_scope := false
|
|
1373
|
+
for rec in (line["walls"] as Array):
|
|
1374
|
+
if bool(rec["in"]):
|
|
1375
|
+
in_scope = true
|
|
1376
|
+
var samples: Array = [] # [t, y, kind]
|
|
1377
|
+
if in_scope:
|
|
1378
|
+
# Grid over the union of the piece rects (cell centers, 1 cm inside).
|
|
1379
|
+
var seen: Dictionary = {}
|
|
1380
|
+
for r in rects:
|
|
1381
|
+
var i0 := int(floor((float(r[1]) + 0.01) / s))
|
|
1382
|
+
var i1 := int(floor((float(r[2]) - 0.01) / s))
|
|
1383
|
+
var j0 := int(floor((float(r[3]) + 0.01) / s))
|
|
1384
|
+
var j1 := int(floor((float(r[4]) - 0.01) / s))
|
|
1385
|
+
for i in range(i0, i1 + 1):
|
|
1386
|
+
var tc := (i + 0.5) * s
|
|
1387
|
+
if tc < float(r[1]) + 0.01 or tc > float(r[2]) - 0.01:
|
|
1388
|
+
continue
|
|
1389
|
+
for j in range(j0, j1 + 1):
|
|
1390
|
+
var yc := (j + 0.5) * s
|
|
1391
|
+
if yc < float(r[3]) + 0.01 or yc > float(r[4]) - 0.01:
|
|
1392
|
+
continue
|
|
1393
|
+
var key := Vector2i(i, j)
|
|
1394
|
+
if seen.has(key):
|
|
1395
|
+
continue
|
|
1396
|
+
seen[key] = true
|
|
1397
|
+
samples.append([tc, yc, 0])
|
|
1398
|
+
# Seams: AABB gaps between neighbours in the line (5 mm .. 1.5 m).
|
|
1399
|
+
for a in rects:
|
|
1400
|
+
for b in rects:
|
|
1401
|
+
if a == b:
|
|
1402
|
+
continue
|
|
1403
|
+
var gap := float(b[1]) - float(a[2])
|
|
1404
|
+
if gap <= 0.005 or gap > 1.5:
|
|
1405
|
+
continue
|
|
1406
|
+
var ylo := maxf(float(a[3]), float(b[3]))
|
|
1407
|
+
var yhi := minf(float(a[4]), float(b[4]))
|
|
1408
|
+
if yhi - ylo < 0.3:
|
|
1409
|
+
continue
|
|
1410
|
+
var tm := (float(a[2]) + float(b[1])) * 0.5
|
|
1411
|
+
var y := ylo + 0.05
|
|
1412
|
+
while y < yhi - 0.04:
|
|
1413
|
+
samples.append([tm, y, 1])
|
|
1414
|
+
y += 0.1
|
|
1415
|
+
# Insert borders: just outside each insert's rect, inside a host rect.
|
|
1416
|
+
for r in inserts:
|
|
1417
|
+
var t0 := float(r[1]) - 0.04
|
|
1418
|
+
var t1 := float(r[2]) + 0.04
|
|
1419
|
+
var y0 := float(r[3]) - 0.04
|
|
1420
|
+
var y1 := float(r[4]) + 0.04
|
|
1421
|
+
var y := float(r[3]) + 0.05
|
|
1422
|
+
while y < float(r[4]):
|
|
1423
|
+
for t in [t0, t1]:
|
|
1424
|
+
if _inside_rects(rects, t, y):
|
|
1425
|
+
samples.append([t, y, 2])
|
|
1426
|
+
y += 0.1
|
|
1427
|
+
var t := float(r[1]) + 0.05
|
|
1428
|
+
while t < float(r[2]):
|
|
1429
|
+
for yy in [y0, y1]:
|
|
1430
|
+
if _inside_rects(rects, t, yy):
|
|
1431
|
+
samples.append([t, yy, 2])
|
|
1432
|
+
t += 0.1
|
|
1433
|
+
var through: Array = []
|
|
1434
|
+
var zf: Dictionary = {}
|
|
1435
|
+
var blocked := 0
|
|
1436
|
+
var backed := 0
|
|
1437
|
+
var want_zf := _checks.has("z_fight")
|
|
1438
|
+
var want_holes := _checks.has("through_hole")
|
|
1439
|
+
var done := 0
|
|
1440
|
+
var stopped := false
|
|
1441
|
+
for smp in samples:
|
|
1442
|
+
if _over():
|
|
1443
|
+
stopped = true
|
|
1444
|
+
break
|
|
1445
|
+
done += 1
|
|
1446
|
+
var t := float(smp[0])
|
|
1447
|
+
var y := float(smp[1])
|
|
1448
|
+
var p := tax * t + n * d + Vector3(0, y, 0)
|
|
1449
|
+
var o := p + n * 0.6
|
|
1450
|
+
var e := p - n * slab
|
|
1451
|
+
var hit := _ray(o, e, MASK_HOLE)
|
|
1452
|
+
if not hit.is_empty():
|
|
1453
|
+
blocked += 1
|
|
1454
|
+
if want_zf and int(smp[2]) == 0:
|
|
1455
|
+
_zfight_probe(o, e, hit, MASK_HOLE, zf)
|
|
1456
|
+
continue
|
|
1457
|
+
if not want_holes:
|
|
1458
|
+
continue
|
|
1459
|
+
var far := _far_distance(line, lines, e)
|
|
1460
|
+
var hit2 := _ray(e, e - n * far, MASK_HOLE | L_FLOOR)
|
|
1461
|
+
if not hit2.is_empty():
|
|
1462
|
+
backed += 1
|
|
1463
|
+
continue
|
|
1464
|
+
var row := [snappedf(t, 0.001), snappedf(y, 0.001), int(smp[2]), snappedf(far, 0.01)]
|
|
1465
|
+
# Extent of the opening around this ray (3 cm steps, up to one cell).
|
|
1466
|
+
if through.size() < 400:
|
|
1467
|
+
row.append(snappedf(t - _extent(line, lines, tax, n, d, slab, t, y, -1.0, 0.0, s), 0.001))
|
|
1468
|
+
row.append(snappedf(t + _extent(line, lines, tax, n, d, slab, t, y, 1.0, 0.0, s), 0.001))
|
|
1469
|
+
row.append(snappedf(y - _extent(line, lines, tax, n, d, slab, t, y, 0.0, -1.0, s), 0.001))
|
|
1470
|
+
row.append(snappedf(y + _extent(line, lines, tax, n, d, slab, t, y, 0.0, 1.0, s), 0.001))
|
|
1471
|
+
else:
|
|
1472
|
+
row.append_array([row[0], row[0], row[1], row[1]])
|
|
1473
|
+
if _want_poses:
|
|
1474
|
+
row.append(snappedf(_clear(p + n * 0.05, n, 12.0), 0.01))
|
|
1475
|
+
through.append(row)
|
|
1476
|
+
if through.size() >= 4000:
|
|
1477
|
+
_warn("A facade line had more than 4000 see-through rays; the rest were not recorded.")
|
|
1478
|
+
break
|
|
1479
|
+
_count("through_hole", done if stopped else samples.size(), samples.size())
|
|
1480
|
+
var rect_rows: Array = []
|
|
1481
|
+
for r in rects:
|
|
1482
|
+
rect_rows.append([r[0], snappedf(r[1], 0.001), snappedf(r[2], 0.001), snappedf(r[3], 0.001), snappedf(r[4], 0.001)])
|
|
1483
|
+
var ins_rows: Array = []
|
|
1484
|
+
for r in inserts:
|
|
1485
|
+
ins_rows.append([r[0], snappedf(r[1], 0.001), snappedf(r[2], 0.001), snappedf(r[3], 0.001), snappedf(r[4], 0.001)])
|
|
1486
|
+
return {
|
|
1487
|
+
"n": _a3(n, 0.0001), "t": _a3(tax, 0.0001), "d": snappedf(d, 0.001), "slab": snappedf(slab, 0.01), "spacing": snappedf(s, 0.001),
|
|
1488
|
+
"in": in_scope, "rays": samples.size(), "blocked": blocked, "backed": backed, "through": through,
|
|
1489
|
+
"pieces": rect_rows, "inserts": ins_rows, "zfight": zf.values(),
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
# A ray at (t, y) passes the facade slab AND reaches the far side.
|
|
1494
|
+
func _passes(line: Dictionary, lines: Array, tax: Vector3, n: Vector3, d: float, slab: float, t: float, y: float) -> bool:
|
|
1495
|
+
var p := tax * t + n * d + Vector3(0, y, 0)
|
|
1496
|
+
var e := p - n * slab
|
|
1497
|
+
if not _ray(p + n * 0.6, e, MASK_HOLE).is_empty():
|
|
1498
|
+
return false
|
|
1499
|
+
return _ray(e, e - n * _far_distance(line, lines, e), MASK_HOLE | L_FLOOR).is_empty()
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
# How far the opening extends from (t, y) along (dt, dy), in 3 cm steps.
|
|
1503
|
+
func _extent(line: Dictionary, lines: Array, tax: Vector3, n: Vector3, d: float, slab: float, t: float, y: float, dt: float, dy: float, limit: float) -> float:
|
|
1504
|
+
var step := 0.03
|
|
1505
|
+
var k := 1
|
|
1506
|
+
while step * k <= limit + 0.0001:
|
|
1507
|
+
if not _passes(line, lines, tax, n, d, slab, t + dt * step * k, y + dy * step * k):
|
|
1508
|
+
return step * (k - 1) + step * 0.5
|
|
1509
|
+
k += 1
|
|
1510
|
+
return limit
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
static func _inside_rects(rects: Array, t: float, y: float) -> bool:
|
|
1514
|
+
for r in rects:
|
|
1515
|
+
if t > float(r[1]) and t < float(r[2]) and y > float(r[3]) and y < float(r[4]):
|
|
1516
|
+
return true
|
|
1517
|
+
return false
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
# Second visible surface within 3 mm of the first, parallel, on a DIFFERENT
|
|
1521
|
+
# instance: coplanar overlapping faces (z-fighting).
|
|
1522
|
+
func _zfight_probe(o: Vector3, e: Vector3, hit: Dictionary, mask: int, acc: Dictionary) -> void:
|
|
1523
|
+
var a := _hit_inst(hit)
|
|
1524
|
+
if a < 0:
|
|
1525
|
+
return
|
|
1526
|
+
var p1: Vector3 = hit["position"]
|
|
1527
|
+
var n1: Vector3 = hit["normal"]
|
|
1528
|
+
var dir := (e - o).normalized()
|
|
1529
|
+
var start := p1 - dir * 0.01
|
|
1530
|
+
var hit2 := _ray(start, p1 + dir * 0.01, mask, [hit["rid"]])
|
|
1531
|
+
if hit2.is_empty():
|
|
1532
|
+
return
|
|
1533
|
+
var b := _hit_inst(hit2)
|
|
1534
|
+
if b < 0 or b == a:
|
|
1535
|
+
return
|
|
1536
|
+
var p2: Vector3 = hit2["position"]
|
|
1537
|
+
if absf(dir.dot(p2 - p1)) > 0.003 or (hit2["normal"] as Vector3).dot(n1) < 0.999:
|
|
1538
|
+
return
|
|
1539
|
+
var key := str(mini(a, b)) + ":" + str(maxi(a, b))
|
|
1540
|
+
if not acc.has(key):
|
|
1541
|
+
acc[key] = [mini(a, b), maxi(a, b), 0, _a3(p1), _a3(n1, 0.0001), _a3(p1), _a3(p1), (snappedf(_clear(p1 + n1 * 0.05, n1, 8.0), 0.01) if _want_poses else 0.0)]
|
|
1542
|
+
var row: Array = acc[key]
|
|
1543
|
+
row[2] = int(row[2]) + 1
|
|
1544
|
+
var lo: Array = row[5]
|
|
1545
|
+
var hi: Array = row[6]
|
|
1546
|
+
row[5] = [minf(lo[0], p1.x), minf(lo[1], p1.y), minf(lo[2], p1.z)]
|
|
1547
|
+
row[6] = [maxf(hi[0], p1.x), maxf(hi[1], p1.y), maxf(hi[2], p1.z)]
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
# ---------------------------------------------------------------------------
|
|
1551
|
+
# floor_gap (+ floor z-fight samples): down rays over the floor tiles
|
|
1552
|
+
# ---------------------------------------------------------------------------
|
|
1553
|
+
|
|
1554
|
+
func _scan_floors() -> Dictionary:
|
|
1555
|
+
var floors: Array = []
|
|
1556
|
+
var underlays: Array = []
|
|
1557
|
+
var area := 0.0
|
|
1558
|
+
for rec in _inst:
|
|
1559
|
+
if rec["role"] == "floor":
|
|
1560
|
+
floors.append(rec)
|
|
1561
|
+
var wa: AABB = rec["waabb"]
|
|
1562
|
+
area += wa.size.x * wa.size.z
|
|
1563
|
+
elif rec["role"] == "underlay":
|
|
1564
|
+
underlays.append(int(rec["i"]))
|
|
1565
|
+
if floors.is_empty():
|
|
1566
|
+
return {"floors": 0, "underlays": underlays, "gaps": [], "rays": 0}
|
|
1567
|
+
var cap := int(_cfg.get("max_floor_rays", 16000))
|
|
1568
|
+
var cell := clampf(sqrt(area / float(maxi(cap, 1000))), 0.25, 0.6)
|
|
1569
|
+
# Occupancy: cell -> owning floor (oriented footprint from the local AABB).
|
|
1570
|
+
var occ: Dictionary = {}
|
|
1571
|
+
var top_of: Dictionary = {}
|
|
1572
|
+
for rec in floors:
|
|
1573
|
+
var la: AABB = rec["laabb"]
|
|
1574
|
+
var xf: Transform3D = rec["xf"]
|
|
1575
|
+
var inv := xf.affine_inverse()
|
|
1576
|
+
var wa: AABB = rec["waabb"]
|
|
1577
|
+
var i0 := int(floor(wa.position.x / cell))
|
|
1578
|
+
var i1 := int(floor(wa.end.x / cell))
|
|
1579
|
+
var k0 := int(floor(wa.position.z / cell))
|
|
1580
|
+
var k1 := int(floor(wa.end.z / cell))
|
|
1581
|
+
for i in range(i0, i1 + 1):
|
|
1582
|
+
for k in range(k0, k1 + 1):
|
|
1583
|
+
var wp := Vector3((i + 0.5) * cell, wa.get_center().y, (k + 0.5) * cell)
|
|
1584
|
+
var lp: Vector3 = inv * wp
|
|
1585
|
+
if lp.x < la.position.x + 0.005 or lp.x > la.end.x - 0.005 or lp.z < la.position.z + 0.005 or lp.z > la.end.z - 0.005:
|
|
1586
|
+
continue
|
|
1587
|
+
var key := Vector2i(i, k)
|
|
1588
|
+
if not occ.has(key):
|
|
1589
|
+
occ[key] = int(rec["i"])
|
|
1590
|
+
top_of[key] = wa.end.y
|
|
1591
|
+
else:
|
|
1592
|
+
top_of[key] = maxf(float(top_of[key]), wa.end.y)
|
|
1593
|
+
var reach := int(ceil(0.5 / cell))
|
|
1594
|
+
var gaps: Array = []
|
|
1595
|
+
var zf: Dictionary = {}
|
|
1596
|
+
var rays := 0
|
|
1597
|
+
var want_gaps := _checks.has("floor_gap")
|
|
1598
|
+
var want_zf := _checks.has("z_fight")
|
|
1599
|
+
var interior := 0
|
|
1600
|
+
var per_owner: Dictionary = {}
|
|
1601
|
+
var keys_done := 0
|
|
1602
|
+
var stopped := false
|
|
1603
|
+
for key in occ:
|
|
1604
|
+
if _over():
|
|
1605
|
+
stopped = true
|
|
1606
|
+
break
|
|
1607
|
+
keys_done += 1
|
|
1608
|
+
var owner_i := int(occ[key])
|
|
1609
|
+
if not bool((_inst[owner_i] as Dictionary)["in"]):
|
|
1610
|
+
continue
|
|
1611
|
+
var c: Vector2i = key
|
|
1612
|
+
if not (occ.has(c + Vector2i(reach, 0)) and occ.has(c - Vector2i(reach, 0)) and occ.has(c + Vector2i(0, reach)) and occ.has(c - Vector2i(0, reach))):
|
|
1613
|
+
continue
|
|
1614
|
+
interior += 1
|
|
1615
|
+
var x := (c.x + 0.5) * cell
|
|
1616
|
+
var z := (c.y + 0.5) * cell
|
|
1617
|
+
var before := _floor_gap_count
|
|
1618
|
+
_floor_sample(x, z, float(top_of[key]), owner_i, gaps, zf, want_gaps, want_zf, 0, cell, cell)
|
|
1619
|
+
rays += 1
|
|
1620
|
+
# Per tile: cells, holes (the ray fell past the tile), of which void,
|
|
1621
|
+
# and cells where an underlay plane covers the tile's own low surface.
|
|
1622
|
+
if not per_owner.has(owner_i):
|
|
1623
|
+
per_owner[owner_i] = [owner_i, 0, 0, 0, 0]
|
|
1624
|
+
var po: Array = per_owner[owner_i]
|
|
1625
|
+
po[1] = int(po[1]) + 1
|
|
1626
|
+
if _floor_gap_count > before:
|
|
1627
|
+
if _floor_gap_covered:
|
|
1628
|
+
po[4] = int(po[4]) + 1
|
|
1629
|
+
else:
|
|
1630
|
+
po[2] = int(po[2]) + 1
|
|
1631
|
+
if _floor_gap_void:
|
|
1632
|
+
po[3] = int(po[3]) + 1
|
|
1633
|
+
_count("floor_gap", keys_done if stopped else occ.size(), occ.size())
|
|
1634
|
+
# Seams between neighbouring tiles (world AABB gaps of 2 mm .. 1 m).
|
|
1635
|
+
var seams := 0
|
|
1636
|
+
if want_gaps:
|
|
1637
|
+
var seam_done := 0
|
|
1638
|
+
for a in floors:
|
|
1639
|
+
if _over():
|
|
1640
|
+
break
|
|
1641
|
+
seam_done += 1
|
|
1642
|
+
for b in floors:
|
|
1643
|
+
if a == b or not (bool(a["in"]) or bool(b["in"])):
|
|
1644
|
+
continue
|
|
1645
|
+
var wa: AABB = a["waabb"]
|
|
1646
|
+
var wb: AABB = b["waabb"]
|
|
1647
|
+
for ax in [0, 2]:
|
|
1648
|
+
var axis: int = ax
|
|
1649
|
+
var other: int = 2 - axis
|
|
1650
|
+
var gap: float = wb.position[axis] - wa.end[axis]
|
|
1651
|
+
if gap <= 0.002 or gap > 1.0:
|
|
1652
|
+
continue
|
|
1653
|
+
var lo: float = maxf(wa.position[other], wb.position[other])
|
|
1654
|
+
var hi: float = minf(wa.end[other], wb.end[other])
|
|
1655
|
+
if hi - lo < 0.3:
|
|
1656
|
+
continue
|
|
1657
|
+
var mid: float = (wa.end[axis] + wb.position[axis]) * 0.5
|
|
1658
|
+
# Each seam ray stands for 10 cm of the seam, the seam's width across.
|
|
1659
|
+
var ssx: float = gap if axis == 0 else 0.1
|
|
1660
|
+
var ssz: float = 0.1 if axis == 0 else gap
|
|
1661
|
+
var u: float = lo + 0.05
|
|
1662
|
+
while u < hi - 0.04:
|
|
1663
|
+
var x: float = mid if axis == 0 else u
|
|
1664
|
+
var z: float = u if axis == 0 else mid
|
|
1665
|
+
_floor_sample(x, z, maxf(wa.end.y, wb.end.y), int(a["i"]), gaps, zf, true, false, 1, ssx, ssz)
|
|
1666
|
+
seams += 1
|
|
1667
|
+
u += 0.1
|
|
1668
|
+
_count("floor_gap", seam_done, floors.size())
|
|
1669
|
+
var strips := 0
|
|
1670
|
+
if want_gaps:
|
|
1671
|
+
strips = _scan_strips(floors, gaps, zf)
|
|
1672
|
+
return {"floors": floors.size(), "underlays": underlays, "cell": snappedf(cell, 0.001), "rays": rays + seams + strips, "interior_cells": interior, "seam_rays": seams, "strip_rays": strips, "gaps": gaps, "gap_rays": _floor_gap_count, "per_owner": per_owner.values(), "zfight": zf.values()}
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
# Bare strips OUTSIDE the tile footprints: a tile row that stops short of a
|
|
1676
|
+
# wall (a back wall a few decimetres past the last tile) leaves the underlay
|
|
1677
|
+
# showing at the wall base, where no grid cell or seam ray looks. Walk each
|
|
1678
|
+
# in-scope tile's footprint edges; where a horizontal ray finds a wall within
|
|
1679
|
+
# STRIP_REACH outside the edge, step down rays out from the edge to the wall
|
|
1680
|
+
# base, stopping at the first floor (the next tile). An edge with no wall in
|
|
1681
|
+
# reach is an open edge, not walkable area enclosed by walls: left alone.
|
|
1682
|
+
func _scan_strips(floors: Array, gaps: Array, zf: Dictionary) -> int:
|
|
1683
|
+
var rays := 0
|
|
1684
|
+
var done := 0
|
|
1685
|
+
for rec in floors:
|
|
1686
|
+
if _over():
|
|
1687
|
+
break
|
|
1688
|
+
done += 1
|
|
1689
|
+
if not bool(rec["in"]):
|
|
1690
|
+
continue
|
|
1691
|
+
var la: AABB = rec["laabb"]
|
|
1692
|
+
var xf: Transform3D = rec["xf"]
|
|
1693
|
+
var top := (rec["waabb"] as AABB).end.y
|
|
1694
|
+
var owner_i := int(rec["i"])
|
|
1695
|
+
var x0 := la.position.x
|
|
1696
|
+
var x1 := la.end.x
|
|
1697
|
+
var z0 := la.position.z
|
|
1698
|
+
var z1 := la.end.z
|
|
1699
|
+
var ly := la.get_center().y
|
|
1700
|
+
# The four footprint edges in local XZ: [start, end, outward direction].
|
|
1701
|
+
var edges := [
|
|
1702
|
+
[Vector3(x0, ly, z0), Vector3(x0, ly, z1), Vector3(-1, 0, 0)],
|
|
1703
|
+
[Vector3(x1, ly, z0), Vector3(x1, ly, z1), Vector3(1, 0, 0)],
|
|
1704
|
+
[Vector3(x0, ly, z0), Vector3(x1, ly, z0), Vector3(0, 0, -1)],
|
|
1705
|
+
[Vector3(x0, ly, z1), Vector3(x1, ly, z1), Vector3(0, 0, 1)],
|
|
1706
|
+
]
|
|
1707
|
+
for edge in edges:
|
|
1708
|
+
var a: Vector3 = xf * (edge[0] as Vector3)
|
|
1709
|
+
var b: Vector3 = xf * (edge[1] as Vector3)
|
|
1710
|
+
var out: Vector3 = xf.basis * (edge[2] as Vector3)
|
|
1711
|
+
out.y = 0
|
|
1712
|
+
if out.length() < 0.2:
|
|
1713
|
+
continue
|
|
1714
|
+
out = out.normalized()
|
|
1715
|
+
var along := Vector3(b.x - a.x, 0, b.z - a.z)
|
|
1716
|
+
var edge_len := along.length()
|
|
1717
|
+
if edge_len < STRIP_STEP:
|
|
1718
|
+
continue
|
|
1719
|
+
along = along / edge_len
|
|
1720
|
+
var n := int(floor(edge_len / STRIP_STEP))
|
|
1721
|
+
var step := edge_len / float(n)
|
|
1722
|
+
for k in n:
|
|
1723
|
+
var p := Vector3(a.x, top, a.z) + along * (step * (float(k) + 0.5))
|
|
1724
|
+
var h := p + Vector3(0, 0.3, 0)
|
|
1725
|
+
rays += 1
|
|
1726
|
+
var wall := _ray(h, h + out * STRIP_REACH, MASK_FACADE, [], true)
|
|
1727
|
+
if wall.is_empty():
|
|
1728
|
+
continue
|
|
1729
|
+
var wp: Vector3 = wall["position"]
|
|
1730
|
+
var span := Vector3(wp.x - p.x, 0, wp.z - p.z).length() - 0.02
|
|
1731
|
+
if span < 0.03:
|
|
1732
|
+
continue
|
|
1733
|
+
var wi := _hit_inst(wall)
|
|
1734
|
+
var m := maxi(1, int(ceil(span / 0.1)))
|
|
1735
|
+
var ds := span / float(m)
|
|
1736
|
+
var ssx := absf(out.x) * ds + absf(along.x) * step
|
|
1737
|
+
var ssz := absf(out.z) * ds + absf(along.z) * step
|
|
1738
|
+
for j in m:
|
|
1739
|
+
var q := p + out * (ds * (float(j) + 0.5))
|
|
1740
|
+
rays += 1
|
|
1741
|
+
if _floor_sample(q.x, q.z, top, owner_i, gaps, zf, true, false, 2, ssx, ssz, wi, ds * step) == 0:
|
|
1742
|
+
break
|
|
1743
|
+
_count("floor_gap", done, floors.size())
|
|
1744
|
+
return rays
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
# One down ray at (x, z). Returns 0 when the floor (or a wall base over it)
|
|
1748
|
+
# is there, 1 when a gap row was recorded.
|
|
1749
|
+
# kind: 0 grid cell, 1 seam between tiles, 2 bare strip between a tile edge
|
|
1750
|
+
# and a wall. sx / sz: the footprint the sample stands for (axis-aligned
|
|
1751
|
+
# bounds); area: its area (default sx * sz). Gap row:
|
|
1752
|
+
# [x, z, top, owner, first hit, first hit y, clearance, kind, sx, sz, area,
|
|
1753
|
+
# floor y under an underlay, that floor, the wall a strip runs along].
|
|
1754
|
+
func _floor_sample(x: float, z: float, top: float, owner_i: int, gaps: Array, zf: Dictionary, want_gaps: bool, want_zf: bool, kind := 0, sx := 0.0, sz := 0.0, wall_i := -1, area := -1.0) -> int:
|
|
1755
|
+
var o := Vector3(x, top + 0.6, z)
|
|
1756
|
+
var e := Vector3(x, top - 1.5, z)
|
|
1757
|
+
var hit := _ray(o, e, L_FLOOR | L_UNDERLAY | MASK_FACADE)
|
|
1758
|
+
var hi := _hit_inst(hit)
|
|
1759
|
+
var role := "" if hi < 0 else String((_inst[hi] as Dictionary)["role"])
|
|
1760
|
+
if not hit.is_empty() and role != "underlay":
|
|
1761
|
+
if want_zf and role == "floor":
|
|
1762
|
+
_zfight_probe(o, e, hit, L_FLOOR, zf)
|
|
1763
|
+
return 0
|
|
1764
|
+
if not want_gaps:
|
|
1765
|
+
return 0
|
|
1766
|
+
# Missed the floor: under a wall or kerb (inside it) does not show.
|
|
1767
|
+
var cover := _ray(o, Vector3(x, top - 0.02, z), MASK_FACADE, [], true)
|
|
1768
|
+
if not cover.is_empty():
|
|
1769
|
+
return 0
|
|
1770
|
+
# The first surface is an underlay. A floor surface right under it means
|
|
1771
|
+
# the underlay plane sits ABOVE the floor's own low surface (a drain
|
|
1772
|
+
# channel, a dip in the slab) and paints over it: not a hole in the tile.
|
|
1773
|
+
var below: Variant = null
|
|
1774
|
+
var below_i := -1
|
|
1775
|
+
if not hit.is_empty():
|
|
1776
|
+
var hy := (hit["position"] as Vector3).y
|
|
1777
|
+
var under := _ray(Vector3(x, hy - 0.001, z), Vector3(x, hy - 0.5, z), L_FLOOR)
|
|
1778
|
+
if not under.is_empty():
|
|
1779
|
+
below = snappedf((under["position"] as Vector3).y, 0.001)
|
|
1780
|
+
below_i = _hit_inst(under)
|
|
1781
|
+
_floor_gap_count += 1
|
|
1782
|
+
_floor_gap_void = hit.is_empty()
|
|
1783
|
+
_floor_gap_covered = below != null
|
|
1784
|
+
if gaps.size() >= 6000:
|
|
1785
|
+
return 1
|
|
1786
|
+
var clear: Variant = null
|
|
1787
|
+
if _want_poses:
|
|
1788
|
+
clear = snappedf(_clear(Vector3(x, top + 0.05, z), Vector3(0.3, 1.0, 0.25).normalized(), 8.0), 0.01)
|
|
1789
|
+
var a := area if area >= 0.0 else sx * sz
|
|
1790
|
+
gaps.append([snappedf(x, 0.001), snappedf(z, 0.001), snappedf(top, 0.001), owner_i, hi, (snappedf((hit["position"] as Vector3).y, 0.001) if not hit.is_empty() else null), clear, kind, snappedf(sx, 0.001), snappedf(sz, 0.001), snappedf(a, 0.0001), below, below_i, wall_i])
|
|
1791
|
+
return 1
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
# ---------------------------------------------------------------------------
|
|
1795
|
+
# floating / sunken: support under each prop's footprint
|
|
1796
|
+
# ---------------------------------------------------------------------------
|
|
1797
|
+
|
|
1798
|
+
func _scan_support() -> Array:
|
|
1799
|
+
var out: Array = []
|
|
1800
|
+
var want_above := _checks.has("sunken")
|
|
1801
|
+
_floor_recs.clear()
|
|
1802
|
+
for f in _inst:
|
|
1803
|
+
if f["role"] == "floor":
|
|
1804
|
+
_floor_recs.append(f)
|
|
1805
|
+
var done := 0
|
|
1806
|
+
var total := 0
|
|
1807
|
+
for rec in _inst:
|
|
1808
|
+
if not bool(rec["in"]):
|
|
1809
|
+
continue
|
|
1810
|
+
var role := String(rec["role"])
|
|
1811
|
+
if role != "prop" and role != "dressing":
|
|
1812
|
+
continue
|
|
1813
|
+
total += 1
|
|
1814
|
+
if _over():
|
|
1815
|
+
continue
|
|
1816
|
+
done += 1
|
|
1817
|
+
var la: AABB = rec["laabb"]
|
|
1818
|
+
var wa: AABB = rec["waabb"]
|
|
1819
|
+
var xf: Transform3D = rec["xf"]
|
|
1820
|
+
var ymin := wa.position.y
|
|
1821
|
+
var oy := ymin + clampf(0.5 * wa.size.y, 0.1, 1.0)
|
|
1822
|
+
var c := la.get_center()
|
|
1823
|
+
var hx := la.size.x * 0.35
|
|
1824
|
+
var hz := la.size.z * 0.35
|
|
1825
|
+
var pts := [c, c + Vector3(hx, 0, hz), c + Vector3(-hx, 0, hz), c + Vector3(hx, 0, -hz), c + Vector3(-hx, 0, -hz)]
|
|
1826
|
+
var hits: Array = []
|
|
1827
|
+
var above: Array = []
|
|
1828
|
+
var ex: Array = rec["bodies"]
|
|
1829
|
+
var top := -INF
|
|
1830
|
+
for lp in pts:
|
|
1831
|
+
var w: Vector3 = xf * lp
|
|
1832
|
+
var hit := _ray(Vector3(w.x, oy, w.z), Vector3(w.x, ymin - 1.0, w.z), MASK_SOLID, ex)
|
|
1833
|
+
if hit.is_empty():
|
|
1834
|
+
hits.append(null)
|
|
1835
|
+
else:
|
|
1836
|
+
var hi := _hit_inst(hit)
|
|
1837
|
+
var hy := (hit["position"] as Vector3).y
|
|
1838
|
+
top = maxf(top, hy)
|
|
1839
|
+
hits.append([snappedf(hy, 0.0001), hi])
|
|
1840
|
+
# The first surface from ABOVE, down through the footprint: what the
|
|
1841
|
+
# prop stands in. A prop buried deeper than the support ray's start
|
|
1842
|
+
# (a bottle stood upright with its origin on the floor) has the
|
|
1843
|
+
# floor's top above that start, where the ray from oy never looks.
|
|
1844
|
+
if want_above:
|
|
1845
|
+
var ah := _ray(Vector3(w.x, wa.end.y + 0.02, w.z), Vector3(w.x, ymin - 0.5, w.z), MASK_SOLID, ex)
|
|
1846
|
+
if ah.is_empty():
|
|
1847
|
+
above.append(null)
|
|
1848
|
+
else:
|
|
1849
|
+
above.append([snappedf((ah["position"] as Vector3).y, 0.0001), _hit_inst(ah)])
|
|
1850
|
+
# Held by a wall? Only asked when it would otherwise float clearly.
|
|
1851
|
+
var touch: Variant = null
|
|
1852
|
+
if top == -INF or ymin - top > 0.3:
|
|
1853
|
+
var wc: Vector3 = xf * c
|
|
1854
|
+
for ldir in [Vector3(1, 0, 0), Vector3(-1, 0, 0), Vector3(0, 0, 1), Vector3(0, 0, -1)]:
|
|
1855
|
+
var wd: Vector3 = xf.basis * ldir
|
|
1856
|
+
wd.y = 0
|
|
1857
|
+
if wd.length() < 0.2:
|
|
1858
|
+
continue
|
|
1859
|
+
wd = wd.normalized()
|
|
1860
|
+
var ext := absf((ldir as Vector3).dot(la.size)) * 0.5
|
|
1861
|
+
var hit := _ray(wc, wc + wd * (ext + 0.3), MASK_FACADE, ex)
|
|
1862
|
+
if not hit.is_empty():
|
|
1863
|
+
var gap := (hit["position"] as Vector3).distance_to(wc) - ext
|
|
1864
|
+
if touch == null or gap < float(touch):
|
|
1865
|
+
touch = snappedf(gap, 0.001)
|
|
1866
|
+
# The floor level around it (top of the floor tiles whose footprint holds
|
|
1867
|
+
# its centre): a prop at that level over an underlay sits over a hole.
|
|
1868
|
+
var floor_top: Variant = null
|
|
1869
|
+
var wcen: Vector3 = wa.get_center()
|
|
1870
|
+
for f in _floor_recs:
|
|
1871
|
+
var fa: AABB = f["waabb"]
|
|
1872
|
+
if wcen.x >= fa.position.x and wcen.x <= fa.end.x and wcen.z >= fa.position.z and wcen.z <= fa.end.z:
|
|
1873
|
+
floor_top = fa.end.y if floor_top == null else maxf(float(floor_top), fa.end.y)
|
|
1874
|
+
out.append([int(rec["i"]), snappedf(ymin, 0.0001), snappedf(oy, 0.001), hits, touch, (snappedf(float(floor_top), 0.001) if floor_top != null else null), above])
|
|
1875
|
+
_count("floating_sunken", done, total)
|
|
1876
|
+
return out
|
|
1877
|
+
|
|
1878
|
+
|
|
1879
|
+
# ---------------------------------------------------------------------------
|
|
1880
|
+
# interpenetration: each prop's per-mesh convex hulls against props, walls,
|
|
1881
|
+
# structure and inserts (floors excluded: that is support).
|
|
1882
|
+
# ---------------------------------------------------------------------------
|
|
1883
|
+
|
|
1884
|
+
func _hull_for(mesh: Mesh) -> Shape3D:
|
|
1885
|
+
var key := mesh.get_instance_id()
|
|
1886
|
+
if _hulls.has(key):
|
|
1887
|
+
return _hulls[key]
|
|
1888
|
+
var hull: Shape3D = mesh.create_convex_shape(true, false)
|
|
1889
|
+
_hulls[key] = hull
|
|
1890
|
+
return hull
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
func _scan_overlaps() -> Array:
|
|
1894
|
+
var out: Array = []
|
|
1895
|
+
var seen: Dictionary = {}
|
|
1896
|
+
var q := PhysicsShapeQueryParameters3D.new()
|
|
1897
|
+
q.collide_with_areas = false
|
|
1898
|
+
q.margin = 0.0
|
|
1899
|
+
var mask := L_PROP | L_WALL | L_STRUCT | L_INSERT
|
|
1900
|
+
q.collision_mask = mask
|
|
1901
|
+
var done := 0
|
|
1902
|
+
var total := 0
|
|
1903
|
+
for rec in _inst:
|
|
1904
|
+
if rec["role"] != "prop" or not bool(rec["in"]):
|
|
1905
|
+
continue
|
|
1906
|
+
total += 1
|
|
1907
|
+
if _over():
|
|
1908
|
+
continue
|
|
1909
|
+
done += 1
|
|
1910
|
+
var own: Array = rec["bodies"]
|
|
1911
|
+
var depth_by: Dictionary = {}
|
|
1912
|
+
var point_by: Dictionary = {}
|
|
1913
|
+
for m in (rec["meshes"] as Array):
|
|
1914
|
+
var mesh: Mesh = m[1]
|
|
1915
|
+
var mxf: Transform3D = m[2]
|
|
1916
|
+
if not _is_rigid(mxf.basis):
|
|
1917
|
+
continue
|
|
1918
|
+
var hull := _hull_for(mesh)
|
|
1919
|
+
if hull == null:
|
|
1920
|
+
continue
|
|
1921
|
+
q.shape_rid = hull.get_rid()
|
|
1922
|
+
q.transform = mxf
|
|
1923
|
+
var ex: Array[RID] = []
|
|
1924
|
+
for r in own:
|
|
1925
|
+
ex.append(r)
|
|
1926
|
+
q.exclude = ex
|
|
1927
|
+
var touching := _state.intersect_shape(q, 32)
|
|
1928
|
+
if touching.is_empty():
|
|
1929
|
+
continue
|
|
1930
|
+
var by_inst: Dictionary = {}
|
|
1931
|
+
for t in touching:
|
|
1932
|
+
var bi := int(_body_inst.get((t["rid"] as RID).get_id(), -1))
|
|
1933
|
+
if bi < 0 or bi == int(rec["i"]):
|
|
1934
|
+
continue
|
|
1935
|
+
if not by_inst.has(bi):
|
|
1936
|
+
by_inst[bi] = []
|
|
1937
|
+
(by_inst[bi] as Array).append(t["rid"])
|
|
1938
|
+
for bi in by_inst:
|
|
1939
|
+
var ex2: Array[RID] = []
|
|
1940
|
+
for r in own:
|
|
1941
|
+
ex2.append(r)
|
|
1942
|
+
for t in touching:
|
|
1943
|
+
if not (by_inst[bi] as Array).has(t["rid"]):
|
|
1944
|
+
ex2.append(t["rid"])
|
|
1945
|
+
q.exclude = ex2
|
|
1946
|
+
var pairs := _state.collide_shape(q, 32)
|
|
1947
|
+
var depth := 0.0
|
|
1948
|
+
var at := Vector3.ZERO
|
|
1949
|
+
var k := 0
|
|
1950
|
+
while k + 1 < pairs.size():
|
|
1951
|
+
var dd := (pairs[k] as Vector3).distance_to(pairs[k + 1])
|
|
1952
|
+
if dd > depth:
|
|
1953
|
+
depth = dd
|
|
1954
|
+
at = (pairs[k] + pairs[k + 1]) * 0.5
|
|
1955
|
+
k += 2
|
|
1956
|
+
if depth > float(depth_by.get(bi, 0.0)):
|
|
1957
|
+
depth_by[bi] = depth
|
|
1958
|
+
point_by[bi] = at
|
|
1959
|
+
for bi in depth_by:
|
|
1960
|
+
var depth := float(depth_by[bi])
|
|
1961
|
+
if depth < 0.01:
|
|
1962
|
+
continue
|
|
1963
|
+
var a := int(rec["i"])
|
|
1964
|
+
var key := str(mini(a, bi)) + ":" + str(maxi(a, bi))
|
|
1965
|
+
if seen.has(key):
|
|
1966
|
+
if depth > float((out[int(seen[key])] as Array)[2]):
|
|
1967
|
+
(out[int(seen[key])] as Array)[2] = snappedf(depth, 0.001)
|
|
1968
|
+
continue
|
|
1969
|
+
seen[key] = out.size()
|
|
1970
|
+
out.append([a, int(bi), snappedf(depth, 0.001), _a3(point_by[bi])])
|
|
1971
|
+
_count("interpenetration", done, total)
|
|
1972
|
+
return out
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
# ---------------------------------------------------------------------------
|
|
1976
|
+
# orientation of long props near a wall
|
|
1977
|
+
# ---------------------------------------------------------------------------
|
|
1978
|
+
|
|
1979
|
+
func _scan_long_props() -> Array:
|
|
1980
|
+
var out: Array = []
|
|
1981
|
+
var done := 0
|
|
1982
|
+
var total := 0
|
|
1983
|
+
for rec in _inst:
|
|
1984
|
+
if rec["role"] != "prop" or not bool(rec["in"]):
|
|
1985
|
+
continue
|
|
1986
|
+
total += 1
|
|
1987
|
+
if _over():
|
|
1988
|
+
continue
|
|
1989
|
+
done += 1
|
|
1990
|
+
var la: AABB = rec["laabb"]
|
|
1991
|
+
var xf: Transform3D = rec["xf"]
|
|
1992
|
+
var sx := la.size.x
|
|
1993
|
+
var sz := la.size.z
|
|
1994
|
+
var length := maxf(sx, sz)
|
|
1995
|
+
var depth := minf(sx, sz)
|
|
1996
|
+
if length < 0.6 or length < 1.8 * maxf(depth, 0.001):
|
|
1997
|
+
continue
|
|
1998
|
+
var lax := Vector3(1, 0, 0) if sx >= sz else Vector3(0, 0, 1)
|
|
1999
|
+
var wl: Vector3 = xf.basis * lax
|
|
2000
|
+
wl.y = 0
|
|
2001
|
+
if wl.length() < 0.2:
|
|
2002
|
+
continue
|
|
2003
|
+
wl = wl.normalized()
|
|
2004
|
+
var wx: Vector3 = (xf.basis * Vector3(1, 0, 0))
|
|
2005
|
+
var wz: Vector3 = (xf.basis * Vector3(0, 0, 1))
|
|
2006
|
+
wx.y = 0
|
|
2007
|
+
wz.y = 0
|
|
2008
|
+
var c := la.get_center()
|
|
2009
|
+
var wc: Vector3 = xf * c
|
|
2010
|
+
var wa: AABB = rec["waabb"]
|
|
2011
|
+
wc.y = wa.position.y + minf(0.5, wa.size.y * 0.5)
|
|
2012
|
+
var ex: Array = rec["bodies"]
|
|
2013
|
+
var best: Array = []
|
|
2014
|
+
for k in 8:
|
|
2015
|
+
var ang := TAU * float(k) / 8.0
|
|
2016
|
+
var dir := Vector3(sin(ang), 0, cos(ang))
|
|
2017
|
+
var ext := absf(dir.dot(wx.normalized())) * sx * 0.5 + absf(dir.dot(wz.normalized())) * sz * 0.5
|
|
2018
|
+
var hit := _ray(wc, wc + dir * (ext + 1.4), MASK_FACADE, ex)
|
|
2019
|
+
if hit.is_empty():
|
|
2020
|
+
continue
|
|
2021
|
+
var nrm: Vector3 = hit["normal"]
|
|
2022
|
+
if absf(nrm.y) > 0.35:
|
|
2023
|
+
continue
|
|
2024
|
+
var clearance := (hit["position"] as Vector3).distance_to(wc) - ext
|
|
2025
|
+
if best.is_empty() or clearance < float(best[0]):
|
|
2026
|
+
var hn := Vector3(nrm.x, 0, nrm.z).normalized()
|
|
2027
|
+
best = [snappedf(clearance, 0.001), _hit_inst(hit), _a3(hn, 0.0001), _a3(dir, 0.0001)]
|
|
2028
|
+
if best.is_empty():
|
|
2029
|
+
continue
|
|
2030
|
+
out.append([int(rec["i"]), _a3(wl, 0.0001), snappedf(length, 0.001), snappedf(depth, 0.001), best])
|
|
2031
|
+
_count("orientation", done, total)
|
|
2032
|
+
return out
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
# ---------------------------------------------------------------------------
|
|
2036
|
+
# uv_stretch: candidate triangles per mesh, visibility per instance
|
|
2037
|
+
# ---------------------------------------------------------------------------
|
|
2038
|
+
|
|
2039
|
+
func _scan_uv() -> Array:
|
|
2040
|
+
var users: Dictionary = {}
|
|
2041
|
+
for rec in _inst:
|
|
2042
|
+
if not bool(rec["in"]):
|
|
2043
|
+
continue
|
|
2044
|
+
var role := String(rec["role"])
|
|
2045
|
+
if role == "dressing" or role == "underlay":
|
|
2046
|
+
continue
|
|
2047
|
+
for m in (rec["meshes"] as Array):
|
|
2048
|
+
var key := (m[1] as Mesh).get_instance_id()
|
|
2049
|
+
var info: Dictionary = _mesh_info.get(key, {})
|
|
2050
|
+
if (info.get("uv", []) as Array).is_empty():
|
|
2051
|
+
continue
|
|
2052
|
+
if not users.has(key):
|
|
2053
|
+
users[key] = []
|
|
2054
|
+
if (users[key] as Array).size() < 64:
|
|
2055
|
+
(users[key] as Array).append([rec, m[2]])
|
|
2056
|
+
var out: Array = []
|
|
2057
|
+
var done := 0
|
|
2058
|
+
for key in users:
|
|
2059
|
+
if _over():
|
|
2060
|
+
break
|
|
2061
|
+
done += 1
|
|
2062
|
+
var info: Dictionary = _mesh_info[key]
|
|
2063
|
+
var cands: Array = info["uv"]
|
|
2064
|
+
var tris: Array = []
|
|
2065
|
+
for cnd in cands:
|
|
2066
|
+
var t0: Vector2 = cnd[5]
|
|
2067
|
+
var t1: Vector2 = cnd[6]
|
|
2068
|
+
var t2: Vector2 = cnd[7]
|
|
2069
|
+
tris.append([_a3(cnd[2], 0.0001), _a3(cnd[3], 0.0001), _a3(cnd[4], 0.0001), [snappedf(t0.x, 0.00001), snappedf(t0.y, 0.00001)], [snappedf(t1.x, 0.00001), snappedf(t1.y, 0.00001)], [snappedf(t2.x, 0.00001), snappedf(t2.y, 0.00001)]])
|
|
2070
|
+
var shown: Array = []
|
|
2071
|
+
for pair in (users[key] as Array):
|
|
2072
|
+
var rec: Dictionary = pair[0]
|
|
2073
|
+
var mxf: Transform3D = pair[1]
|
|
2074
|
+
var front: Vector3 = rec.get("front", Vector3.ZERO)
|
|
2075
|
+
var wfront := Vector3.ZERO
|
|
2076
|
+
if front != Vector3.ZERO:
|
|
2077
|
+
wfront = ((rec["xf"] as Transform3D).basis * front).normalized()
|
|
2078
|
+
# Per candidate triangle: visible (the view ray lands on it), or
|
|
2079
|
+
# covered (an insert sits within 0.3 m in front of it: a door, a
|
|
2080
|
+
# shutter). Covered somewhere + visible here = exposed here.
|
|
2081
|
+
var mask_bits := 0
|
|
2082
|
+
var covered_bits := 0
|
|
2083
|
+
var viewer_out: Variant = null
|
|
2084
|
+
var center_out: Variant = null
|
|
2085
|
+
for ti in cands.size():
|
|
2086
|
+
var cnd: Array = cands[ti]
|
|
2087
|
+
var cw: Vector3 = mxf * ((cnd[2] + cnd[3] + cnd[4]) / 3.0)
|
|
2088
|
+
var nw: Vector3 = (mxf.basis * (cnd[8] as Vector3)).normalized()
|
|
2089
|
+
var viewer := cw + nw * 1.2 + wfront * 0.8
|
|
2090
|
+
var dir := (cw - viewer).normalized()
|
|
2091
|
+
var hit := _ray(viewer, cw + dir * 0.02, MASK_SOLID)
|
|
2092
|
+
if hit.is_empty():
|
|
2093
|
+
continue
|
|
2094
|
+
var hp: Vector3 = hit["position"]
|
|
2095
|
+
var hi := _hit_inst(hit)
|
|
2096
|
+
if hi == int(rec["i"]):
|
|
2097
|
+
if hp.distance_to(cw) <= 0.05:
|
|
2098
|
+
mask_bits |= 1 << ti
|
|
2099
|
+
if viewer_out == null:
|
|
2100
|
+
viewer_out = _a3(viewer)
|
|
2101
|
+
center_out = _a3(cw)
|
|
2102
|
+
elif hi >= 0 and hp.distance_to(cw) <= 0.3 and String((_inst[hi] as Dictionary)["role"]) == "insert":
|
|
2103
|
+
covered_bits |= 1 << ti
|
|
2104
|
+
if mask_bits != 0 or covered_bits != 0:
|
|
2105
|
+
shown.append([int(rec["i"]), mask_bits, viewer_out, center_out, covered_bits])
|
|
2106
|
+
out.append({"mesh": String(info["name"]), "tris": tris, "users": (users[key] as Array).size(), "shown": shown})
|
|
2107
|
+
_count("uv_stretch", done, users.size())
|
|
2108
|
+
return out
|
|
2109
|
+
|
|
2110
|
+
|
|
2111
|
+
# ---------------------------------------------------------------------------
|
|
2112
|
+
# z_fight geometry: coplanar overlapping faces anywhere, not only where the
|
|
2113
|
+
# facade and floor ray grids sample. Per mesh resource (cached): planar face
|
|
2114
|
+
# groups (normal, offset, 2D hull, own triangles). Between instances: plane
|
|
2115
|
+
# bins (canonical normal + offset) as the broadphase, then parallel normals
|
|
2116
|
+
# (|dot| > 0.999; opposite only when both materials are double-sided), plane
|
|
2117
|
+
# gap under the depth-precision tolerance, hull overlap clipped in the plane
|
|
2118
|
+
# and verified with samples against both groups' OWN triangles. Within one
|
|
2119
|
+
# mesh: the same test between groups of different surfaces. Measures only;
|
|
2120
|
+
# the judge applies the tolerance and the severity.
|
|
2121
|
+
# ---------------------------------------------------------------------------
|
|
2122
|
+
|
|
2123
|
+
func _scan_zfight_geometry() -> Dictionary:
|
|
2124
|
+
_zf_setup_views()
|
|
2125
|
+
var units_done := 0
|
|
2126
|
+
var units_total := 0
|
|
2127
|
+
# Face groups per mesh resource, and which instances use each mesh.
|
|
2128
|
+
var users: Dictionary = {}
|
|
2129
|
+
var mesh_of: Dictionary = {}
|
|
2130
|
+
var meshes: Array = []
|
|
2131
|
+
for rec in _inst:
|
|
2132
|
+
for m in (rec["meshes"] as Array):
|
|
2133
|
+
var mid := (m[1] as Mesh).get_instance_id()
|
|
2134
|
+
if not users.has(mid):
|
|
2135
|
+
users[mid] = []
|
|
2136
|
+
mesh_of[mid] = m[1]
|
|
2137
|
+
meshes.append([mid, m[1]])
|
|
2138
|
+
(users[mid] as Array).append([int(rec["i"]), m[0], m[2]])
|
|
2139
|
+
var stopped := false
|
|
2140
|
+
for km in meshes:
|
|
2141
|
+
units_total += 1
|
|
2142
|
+
if stopped or _over():
|
|
2143
|
+
stopped = true
|
|
2144
|
+
continue
|
|
2145
|
+
units_done += 1
|
|
2146
|
+
var fid: int = km[0]
|
|
2147
|
+
if not _mesh_info.has(fid):
|
|
2148
|
+
# Dressing (decals, overlay cards) skips the mesh pass.
|
|
2149
|
+
_mesh_info[fid] = _analyze_mesh(km[1] as Mesh, false, true)
|
|
2150
|
+
_zf_groups[fid] = _finalize_groups(_mesh_info[fid] as Dictionary)
|
|
2151
|
+
# Within one mesh: groups of different surfaces on one plane.
|
|
2152
|
+
var in_mesh: Array = []
|
|
2153
|
+
for km2 in meshes:
|
|
2154
|
+
var wid: int = km2[0]
|
|
2155
|
+
if not _zf_groups.has(wid):
|
|
2156
|
+
continue
|
|
2157
|
+
units_total += 1
|
|
2158
|
+
if stopped or _over():
|
|
2159
|
+
stopped = true
|
|
2160
|
+
continue
|
|
2161
|
+
units_done += 1
|
|
2162
|
+
var row := _zf_within(wid, String((_mesh_info[wid] as Dictionary).get("name", "mesh")), users[wid] as Array)
|
|
2163
|
+
if not row.is_empty() and in_mesh.size() < 64:
|
|
2164
|
+
in_mesh.append(row)
|
|
2165
|
+
# Between instances: world plane records in canonical bins.
|
|
2166
|
+
var recs: Array = []
|
|
2167
|
+
var bins: Dictionary = {}
|
|
2168
|
+
for gid in _zf_groups:
|
|
2169
|
+
for use in (users[gid] as Array):
|
|
2170
|
+
var rec_i: int = use[0]
|
|
2171
|
+
var node: Node = use[1]
|
|
2172
|
+
var xf: Transform3D = use[2]
|
|
2173
|
+
var nb := xf.basis.inverse().transposed()
|
|
2174
|
+
var groups: Array = _zf_groups[gid]
|
|
2175
|
+
for gi in groups.size():
|
|
2176
|
+
var g: Dictionary = groups[gi]
|
|
2177
|
+
var nw := (nb * (g["n"] as Vector3)).normalized()
|
|
2178
|
+
var ow: Vector3 = xf * (g["o"] as Vector3)
|
|
2179
|
+
var dw := nw.dot(ow)
|
|
2180
|
+
var cn := nw
|
|
2181
|
+
var cd := dw
|
|
2182
|
+
if cn.x < -0.0001 or (absf(cn.x) <= 0.0001 and (cn.y < -0.0001 or (absf(cn.y) <= 0.0001 and cn.z < 0.0))):
|
|
2183
|
+
cn = -cn
|
|
2184
|
+
cd = -cd
|
|
2185
|
+
var mat := _surface_mat(node, mesh_of[gid] as Mesh, int(g["s"]))
|
|
2186
|
+
var r := {"i": rec_i, "node": node, "xf": xf, "g": g, "n": nw, "o": ow, "box": _group_box(g, xf), "mat": mat, "double": mat != null and _double_sided(mat)}
|
|
2187
|
+
var pkey := Vector4i(roundi(cn.x * 30.0), roundi(cn.y * 30.0), roundi(cn.z * 30.0), roundi(cd / ZF_MAX_GAP))
|
|
2188
|
+
if not bins.has(pkey):
|
|
2189
|
+
bins[pkey] = []
|
|
2190
|
+
(bins[pkey] as Array).append(recs.size())
|
|
2191
|
+
recs.append(r)
|
|
2192
|
+
var pairs: Dictionary = {}
|
|
2193
|
+
for bin_key in bins:
|
|
2194
|
+
units_total += 1
|
|
2195
|
+
if stopped or _over():
|
|
2196
|
+
stopped = true
|
|
2197
|
+
continue
|
|
2198
|
+
units_done += 1
|
|
2199
|
+
var here: Array = bins[bin_key]
|
|
2200
|
+
var k4: Vector4i = bin_key
|
|
2201
|
+
for dq in [0, 1]:
|
|
2202
|
+
var there: Array = bins.get(Vector4i(k4.x, k4.y, k4.z, k4.w + int(dq)), [])
|
|
2203
|
+
for ia in here.size():
|
|
2204
|
+
var jb0 := ia + 1 if int(dq) == 0 else 0
|
|
2205
|
+
for jb in range(jb0, there.size()):
|
|
2206
|
+
_zf_pair(recs[int(here[ia])] as Dictionary, recs[int(there[jb])] as Dictionary, pairs)
|
|
2207
|
+
_count("z_fight_geometry", units_done, units_total)
|
|
2208
|
+
var rows: Array = pairs.values()
|
|
2209
|
+
rows.sort_custom(func(a, b): return float(a[2]) > float(b[2]))
|
|
2210
|
+
if rows.size() > 300:
|
|
2211
|
+
rows.resize(300)
|
|
2212
|
+
return {"near": snappedf(_near, 0.0001), "far": snappedf(_far, 0.01), "views": _views.size(), "groups": recs.size(), "pairs": rows, "in_mesh": in_mesh}
|
|
2213
|
+
|
|
2214
|
+
|
|
2215
|
+
# The main camera's near / far (the current Camera3D, else the first; 0.05 /
|
|
2216
|
+
# 4000 without one), and the viewpoints: scene cameras, camera bookmarks
|
|
2217
|
+
# (res://.summer/camera_bookmarks.json, read-only) and the floors for eye
|
|
2218
|
+
# points.
|
|
2219
|
+
func _zf_setup_views() -> void:
|
|
2220
|
+
var main: Camera3D = null
|
|
2221
|
+
for c in _cameras:
|
|
2222
|
+
var cam := c as Camera3D
|
|
2223
|
+
if main == null or cam.current:
|
|
2224
|
+
main = cam
|
|
2225
|
+
_views.append([cam.global_position, "camera " + String(cam.name)])
|
|
2226
|
+
if main != null and main.projection == Camera3D.PROJECTION_PERSPECTIVE:
|
|
2227
|
+
_near = main.near
|
|
2228
|
+
_far = main.far
|
|
2229
|
+
var path := "res://.summer/camera_bookmarks.json"
|
|
2230
|
+
if FileAccess.file_exists(path):
|
|
2231
|
+
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
|
|
2232
|
+
if typeof(parsed) == TYPE_DICTIONARY:
|
|
2233
|
+
var re := RegEx.create_from_string("Vector3\\(\\s*([-+0-9.eE]+)\\s*,\\s*([-+0-9.eE]+)\\s*,\\s*([-+0-9.eE]+)\\s*\\)")
|
|
2234
|
+
for name in (parsed as Dictionary):
|
|
2235
|
+
var bm: Variant = (parsed as Dictionary)[name]
|
|
2236
|
+
if typeof(bm) != TYPE_DICTIONARY or _views.size() >= 64:
|
|
2237
|
+
continue
|
|
2238
|
+
var m := re.search(String((bm as Dictionary).get("position", "")))
|
|
2239
|
+
if m != null:
|
|
2240
|
+
_views.append([Vector3(float(m.get_string(1)), float(m.get_string(2)), float(m.get_string(3))), "bookmark " + String(name)])
|
|
2241
|
+
for rec in _inst:
|
|
2242
|
+
if rec["role"] == "floor":
|
|
2243
|
+
_walk.append(rec)
|
|
2244
|
+
if _walk.is_empty():
|
|
2245
|
+
for rec in _inst:
|
|
2246
|
+
if rec["role"] == "underlay":
|
|
2247
|
+
_walk.append(rec)
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
# The nearest viewpoint to p: a camera, a bookmark, or eye height (1.6 m)
|
|
2251
|
+
# over the nearest floor. [distance, position, kind], or [] without any.
|
|
2252
|
+
func _zf_view(p: Vector3) -> Array:
|
|
2253
|
+
var best: Array = []
|
|
2254
|
+
for vw in _views:
|
|
2255
|
+
var vp: Vector3 = vw[0]
|
|
2256
|
+
if best.is_empty() or vp.distance_to(p) < float(best[0]):
|
|
2257
|
+
best = [vp.distance_to(p), vp, String(vw[1])]
|
|
2258
|
+
for f in _walk:
|
|
2259
|
+
var fa: AABB = f["waabb"]
|
|
2260
|
+
var ep := Vector3(clampf(p.x, fa.position.x, fa.end.x), fa.end.y + 1.6, clampf(p.z, fa.position.z, fa.end.z))
|
|
2261
|
+
if best.is_empty() or ep.distance_to(p) < float(best[0]):
|
|
2262
|
+
best = [ep.distance_to(p), ep, "walkable area"]
|
|
2263
|
+
return best
|
|
2264
|
+
|
|
2265
|
+
|
|
2266
|
+
# Twice the 24-bit depth step at `dist` for the main camera's near / far
|
|
2267
|
+
# (Compatibility / WebGL2), never under 0.1 mm (float noise in transforms).
|
|
2268
|
+
func _zf_tol(dist: float) -> float:
|
|
2269
|
+
var n := maxf(0.0001, _near)
|
|
2270
|
+
var f := maxf(n * 1.0001, _far)
|
|
2271
|
+
return maxf(0.0001, 2.0 * dist * dist * (f - n) / (f * n * ZF_DEPTH_STEPS))
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
# p is seen from vp: a one-sided ray (like the renderer) reaches it.
|
|
2275
|
+
func _zf_seen(vp: Vector3, p: Vector3) -> bool:
|
|
2276
|
+
var dir := p - vp
|
|
2277
|
+
var dist := dir.length()
|
|
2278
|
+
if dist < 0.01:
|
|
2279
|
+
return true
|
|
2280
|
+
var hit := _ray(vp, p + dir / dist * 0.05, MASK_SOLID)
|
|
2281
|
+
return hit.is_empty() or (hit["position"] as Vector3).distance_to(p) <= 0.15
|
|
2282
|
+
|
|
2283
|
+
|
|
2284
|
+
func _finalize_groups(info: Dictionary) -> Array:
|
|
2285
|
+
var cands: Array = []
|
|
2286
|
+
for sr in (info.get("zf_raw", []) as Array):
|
|
2287
|
+
var sb: Dictionary = sr[3]
|
|
2288
|
+
for bk in sb:
|
|
2289
|
+
var bg: Array = sb[bk]
|
|
2290
|
+
if float(bg[0]) >= ZF_MIN_AREA:
|
|
2291
|
+
cands.append([float(bg[0]), int(sr[0]), sr[1], bool(sr[2]), bg])
|
|
2292
|
+
cands.sort_custom(func(a, b): return float(a[0]) > float(b[0]))
|
|
2293
|
+
if cands.size() > ZF_MAX_GROUPS:
|
|
2294
|
+
cands.resize(ZF_MAX_GROUPS)
|
|
2295
|
+
var out: Array = []
|
|
2296
|
+
for cnd in cands:
|
|
2297
|
+
var bg: Array = cnd[4]
|
|
2298
|
+
var area := float(cnd[0])
|
|
2299
|
+
var n := (bg[1] as Vector3).normalized()
|
|
2300
|
+
var o := n * (float(bg[2]) / area)
|
|
2301
|
+
var u := n.cross(Vector3.UP)
|
|
2302
|
+
if u.length() < 0.1:
|
|
2303
|
+
u = n.cross(Vector3.RIGHT)
|
|
2304
|
+
u = u.normalized()
|
|
2305
|
+
var v := n.cross(u).normalized()
|
|
2306
|
+
var verts: Array = bg[3]
|
|
2307
|
+
var tri := PackedVector2Array()
|
|
2308
|
+
tri.resize(verts.size())
|
|
2309
|
+
for k in verts.size():
|
|
2310
|
+
var p: Vector3 = verts[k]
|
|
2311
|
+
tri[k] = Vector2((p - o).dot(u), (p - o).dot(v))
|
|
2312
|
+
var hull := Geometry2D.convex_hull(tri)
|
|
2313
|
+
if hull.size() >= 2 and hull[0] == hull[hull.size() - 1]:
|
|
2314
|
+
hull.remove_at(hull.size() - 1)
|
|
2315
|
+
if hull.size() < 3:
|
|
2316
|
+
continue
|
|
2317
|
+
var lo := hull[0]
|
|
2318
|
+
var hi := hull[0]
|
|
2319
|
+
for q in hull:
|
|
2320
|
+
lo = Vector2(minf(lo.x, q.x), minf(lo.y, q.y))
|
|
2321
|
+
hi = Vector2(maxf(hi.x, q.x), maxf(hi.y, q.y))
|
|
2322
|
+
# Triangles bucketed on a 2D grid for point-in-face tests.
|
|
2323
|
+
var cell := maxf(0.05, sqrt(area / 64.0))
|
|
2324
|
+
var grid: Dictionary = {}
|
|
2325
|
+
var t := 0
|
|
2326
|
+
while t + 2 < tri.size():
|
|
2327
|
+
var a: Vector2 = tri[t]
|
|
2328
|
+
var b: Vector2 = tri[t + 1]
|
|
2329
|
+
var c: Vector2 = tri[t + 2]
|
|
2330
|
+
for gx in range(floori(minf(a.x, minf(b.x, c.x)) / cell), floori(maxf(a.x, maxf(b.x, c.x)) / cell) + 1):
|
|
2331
|
+
for gy in range(floori(minf(a.y, minf(b.y, c.y)) / cell), floori(maxf(a.y, maxf(b.y, c.y)) / cell) + 1):
|
|
2332
|
+
var gk := Vector2i(gx, gy)
|
|
2333
|
+
if not grid.has(gk):
|
|
2334
|
+
grid[gk] = []
|
|
2335
|
+
(grid[gk] as Array).append(t)
|
|
2336
|
+
t += 3
|
|
2337
|
+
out.append({"s": int(cnd[1]), "mat": cnd[2], "double": bool(cnd[3]), "area": area, "n": n, "o": o, "u": u, "v": v, "tri": tri, "hull": hull, "lo": lo, "hi": hi, "cell": cell, "grid": grid})
|
|
2338
|
+
return out
|
|
2339
|
+
|
|
2340
|
+
|
|
2341
|
+
# World bounds of a face group (its 2D bounds' corners on its plane).
|
|
2342
|
+
func _group_box(g: Dictionary, xf: Transform3D) -> AABB:
|
|
2343
|
+
var o: Vector3 = g["o"]
|
|
2344
|
+
var u: Vector3 = g["u"]
|
|
2345
|
+
var v: Vector3 = g["v"]
|
|
2346
|
+
var lo: Vector2 = g["lo"]
|
|
2347
|
+
var hi: Vector2 = g["hi"]
|
|
2348
|
+
var box := AABB(xf * (o + u * lo.x + v * lo.y), Vector3.ZERO)
|
|
2349
|
+
box = box.expand(xf * (o + u * hi.x + v * lo.y))
|
|
2350
|
+
box = box.expand(xf * (o + u * lo.x + v * hi.y))
|
|
2351
|
+
box = box.expand(xf * (o + u * hi.x + v * hi.y))
|
|
2352
|
+
return box
|
|
2353
|
+
|
|
2354
|
+
|
|
2355
|
+
func _in_group(g: Dictionary, local: Vector3) -> bool:
|
|
2356
|
+
var o: Vector3 = g["o"]
|
|
2357
|
+
var q := Vector2((local - o).dot(g["u"] as Vector3), (local - o).dot(g["v"] as Vector3))
|
|
2358
|
+
var cell := float(g["cell"])
|
|
2359
|
+
var list: Variant = (g["grid"] as Dictionary).get(Vector2i(floori(q.x / cell), floori(q.y / cell)), null)
|
|
2360
|
+
if list == null:
|
|
2361
|
+
return false
|
|
2362
|
+
var tri: PackedVector2Array = g["tri"]
|
|
2363
|
+
for t in (list as Array):
|
|
2364
|
+
var b := int(t)
|
|
2365
|
+
if Geometry2D.point_is_inside_triangle(q, tri[b], tri[b + 1], tri[b + 2]):
|
|
2366
|
+
return true
|
|
2367
|
+
return false
|
|
2368
|
+
|
|
2369
|
+
|
|
2370
|
+
static func _poly_area(poly: PackedVector2Array) -> float:
|
|
2371
|
+
var a := 0.0
|
|
2372
|
+
for k in poly.size():
|
|
2373
|
+
var p: Vector2 = poly[k]
|
|
2374
|
+
var q: Vector2 = poly[(k + 1) % poly.size()]
|
|
2375
|
+
a += p.x * q.y - q.x * p.y
|
|
2376
|
+
return absf(a) * 0.5
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
# Overlap of group ga (instance transform xa) and gb (xb) on about one
|
|
2380
|
+
# plane: both hulls clipped in ga's world plane, then a grid of up to ~120
|
|
2381
|
+
# samples inside the clip, each tested against BOTH groups' own triangles (a
|
|
2382
|
+
# wall face's window opening is not wall). [overlap m2, world centre] or [].
|
|
2383
|
+
func _group_overlap(ga: Dictionary, xa: Transform3D, gb: Dictionary, xb: Transform3D) -> Array:
|
|
2384
|
+
var oa: Vector3 = xa * (ga["o"] as Vector3)
|
|
2385
|
+
var na := (xa.basis.inverse().transposed() * (ga["n"] as Vector3)).normalized()
|
|
2386
|
+
var ua: Vector3 = xa.basis * (ga["u"] as Vector3)
|
|
2387
|
+
ua = (ua - na * ua.dot(na)).normalized()
|
|
2388
|
+
var va := na.cross(ua)
|
|
2389
|
+
var polys := Geometry2D.intersect_polygons(_hull_in(ga, xa, oa, ua, va), _hull_in(gb, xb, oa, ua, va))
|
|
2390
|
+
var inv_a := xa.affine_inverse()
|
|
2391
|
+
var inv_b := xb.affine_inverse()
|
|
2392
|
+
var overlap := 0.0
|
|
2393
|
+
var centre := Vector3.ZERO
|
|
2394
|
+
var weight := 0.0
|
|
2395
|
+
for poly in polys:
|
|
2396
|
+
var pa := _poly_area(poly)
|
|
2397
|
+
if pa < ZF_MIN_AREA * 0.25:
|
|
2398
|
+
continue
|
|
2399
|
+
var lo: Vector2 = poly[0]
|
|
2400
|
+
var hi: Vector2 = poly[0]
|
|
2401
|
+
for q in poly:
|
|
2402
|
+
lo = Vector2(minf(lo.x, q.x), minf(lo.y, q.y))
|
|
2403
|
+
hi = Vector2(maxf(hi.x, q.x), maxf(hi.y, q.y))
|
|
2404
|
+
var step := maxf(0.01, sqrt(pa / 100.0))
|
|
2405
|
+
var inside := 0
|
|
2406
|
+
var both := 0
|
|
2407
|
+
var cx := 0.0
|
|
2408
|
+
var cy := 0.0
|
|
2409
|
+
var y := lo.y + step * 0.5
|
|
2410
|
+
while y < hi.y and inside < 160:
|
|
2411
|
+
var x := lo.x + step * 0.5
|
|
2412
|
+
while x < hi.x and inside < 160:
|
|
2413
|
+
var q := Vector2(x, y)
|
|
2414
|
+
if Geometry2D.is_point_in_polygon(q, poly):
|
|
2415
|
+
inside += 1
|
|
2416
|
+
var w := oa + ua * x + va * y
|
|
2417
|
+
if _in_group(ga, inv_a * w) and _in_group(gb, inv_b * w):
|
|
2418
|
+
both += 1
|
|
2419
|
+
cx += x
|
|
2420
|
+
cy += y
|
|
2421
|
+
x += step
|
|
2422
|
+
y += step
|
|
2423
|
+
if inside == 0 or both == 0:
|
|
2424
|
+
continue
|
|
2425
|
+
var ov := pa * float(both) / float(inside)
|
|
2426
|
+
overlap += ov
|
|
2427
|
+
centre += (oa + ua * (cx / float(both)) + va * (cy / float(both))) * ov
|
|
2428
|
+
weight += ov
|
|
2429
|
+
if overlap < ZF_MIN_AREA or weight <= 0.0:
|
|
2430
|
+
return []
|
|
2431
|
+
return [overlap, centre / weight]
|
|
2432
|
+
|
|
2433
|
+
|
|
2434
|
+
func _hull_in(g: Dictionary, xf: Transform3D, o: Vector3, u: Vector3, v: Vector3) -> PackedVector2Array:
|
|
2435
|
+
var go: Vector3 = g["o"]
|
|
2436
|
+
var gu: Vector3 = g["u"]
|
|
2437
|
+
var gv: Vector3 = g["v"]
|
|
2438
|
+
var out := PackedVector2Array()
|
|
2439
|
+
for q in (g["hull"] as PackedVector2Array):
|
|
2440
|
+
var w: Vector3 = xf * (go + gu * q.x + gv * q.y)
|
|
2441
|
+
out.append(Vector2((w - o).dot(u), (w - o).dot(v)))
|
|
2442
|
+
return out
|
|
2443
|
+
|
|
2444
|
+
|
|
2445
|
+
# Parallel enough and close enough to be candidates (gap is the caller's).
|
|
2446
|
+
func _zf_parallel(na: Vector3, nb: Vector3, double_a: bool, double_b: bool) -> int:
|
|
2447
|
+
var dot := na.dot(nb)
|
|
2448
|
+
if dot > 0.999:
|
|
2449
|
+
return 1
|
|
2450
|
+
if dot < -0.999 and double_a and double_b:
|
|
2451
|
+
return -1
|
|
2452
|
+
return 0
|
|
2453
|
+
|
|
2454
|
+
|
|
2455
|
+
func _zf_pair(ra: Dictionary, rb: Dictionary, pairs: Dictionary) -> void:
|
|
2456
|
+
var a: int = ra["i"]
|
|
2457
|
+
var b: int = rb["i"]
|
|
2458
|
+
if ra["node"] == rb["node"]:
|
|
2459
|
+
return
|
|
2460
|
+
if not (bool((_inst[a] as Dictionary)["in"]) or bool((_inst[b] as Dictionary)["in"])):
|
|
2461
|
+
return
|
|
2462
|
+
var box_a: AABB = ra["box"]
|
|
2463
|
+
var box_b: AABB = rb["box"]
|
|
2464
|
+
if not box_a.grow(0.03).intersects(box_b):
|
|
2465
|
+
return
|
|
2466
|
+
var na: Vector3 = ra["n"]
|
|
2467
|
+
var facing := _zf_parallel(na, rb["n"] as Vector3, bool(ra["double"]), bool(rb["double"]))
|
|
2468
|
+
if facing == 0:
|
|
2469
|
+
return
|
|
2470
|
+
var gap := absf(na.dot((rb["o"] as Vector3) - (ra["o"] as Vector3)))
|
|
2471
|
+
if gap > ZF_MAX_GAP:
|
|
2472
|
+
return
|
|
2473
|
+
var mid := box_a.grow(0.03).intersection(box_b).get_center()
|
|
2474
|
+
var view := _zf_view(mid)
|
|
2475
|
+
var dist := 30.0 if view.is_empty() else float(view[0])
|
|
2476
|
+
if gap > minf(ZF_MAX_GAP, 1.5 * _zf_tol(dist)):
|
|
2477
|
+
return
|
|
2478
|
+
var ov := _group_overlap(ra["g"] as Dictionary, ra["xf"] as Transform3D, rb["g"] as Dictionary, rb["xf"] as Transform3D)
|
|
2479
|
+
if ov.is_empty():
|
|
2480
|
+
return
|
|
2481
|
+
var centre: Vector3 = ov[1]
|
|
2482
|
+
view = _zf_view(centre)
|
|
2483
|
+
var seen := false
|
|
2484
|
+
if not view.is_empty():
|
|
2485
|
+
seen = _zf_seen(view[1] as Vector3, centre)
|
|
2486
|
+
var key := str(mini(a, b)) + ":" + str(maxi(a, b))
|
|
2487
|
+
var prev: Variant = pairs.get(key, null)
|
|
2488
|
+
var area := float(ov[0])
|
|
2489
|
+
if prev != null:
|
|
2490
|
+
var pr: Array = prev
|
|
2491
|
+
pr[14] = snappedf(float(pr[14]) + area, 0.001)
|
|
2492
|
+
pr[15] = int(pr[15]) + 1
|
|
2493
|
+
if area <= float(pr[2]):
|
|
2494
|
+
return
|
|
2495
|
+
var ga: Dictionary = ra["g"]
|
|
2496
|
+
var gb: Dictionary = rb["g"]
|
|
2497
|
+
var row := [a, b, snappedf(area, 0.001), snappedf(gap, 0.00001), _a3(centre), _a3(na, 0.0001), snappedf(30.0 if view.is_empty() else float(view[0]), 0.01), ("typical view distance" if view.is_empty() else String(view[2])), seen, _zf_flags(ra["mat"]), _zf_flags(rb["mat"]), _mat_name(ra["mat"], int(ga["s"])), _mat_name(rb["mat"], int(gb["s"])), facing < 0, snappedf(area, 0.001), 1, _rel(ra["node"] as Node), _rel(rb["node"] as Node)]
|
|
2498
|
+
if prev != null:
|
|
2499
|
+
row[14] = (prev as Array)[14]
|
|
2500
|
+
row[15] = (prev as Array)[15]
|
|
2501
|
+
pairs[key] = row
|
|
2502
|
+
|
|
2503
|
+
|
|
2504
|
+
# Within one mesh: groups of DIFFERENT surfaces on one plane (an overlay
|
|
2505
|
+
# layer with no offset). Once per mesh resource, with up to 4 instances that
|
|
2506
|
+
# show it (the tolerance and visibility are theirs). [] when clean.
|
|
2507
|
+
func _zf_within(key: int, mesh_name: String, uses: Array) -> Array:
|
|
2508
|
+
var groups: Array = _zf_groups[key]
|
|
2509
|
+
var best: Array = []
|
|
2510
|
+
for i in groups.size():
|
|
2511
|
+
var ga: Dictionary = groups[i]
|
|
2512
|
+
for j in range(i + 1, groups.size()):
|
|
2513
|
+
var gb: Dictionary = groups[j]
|
|
2514
|
+
if int(ga["s"]) == int(gb["s"]):
|
|
2515
|
+
continue
|
|
2516
|
+
var facing := _zf_parallel(ga["n"] as Vector3, gb["n"] as Vector3, bool(ga["double"]), bool(gb["double"]))
|
|
2517
|
+
if facing == 0:
|
|
2518
|
+
continue
|
|
2519
|
+
var gap := absf((ga["n"] as Vector3).dot((gb["o"] as Vector3) - (ga["o"] as Vector3)))
|
|
2520
|
+
if gap > ZF_MAX_GAP:
|
|
2521
|
+
continue
|
|
2522
|
+
var ov := _group_overlap(ga, Transform3D.IDENTITY, gb, Transform3D.IDENTITY)
|
|
2523
|
+
if ov.is_empty() or (not best.is_empty() and float(ov[0]) <= float(best[0])):
|
|
2524
|
+
continue
|
|
2525
|
+
best = [float(ov[0]), gap, ov[1], ga, gb, facing]
|
|
2526
|
+
if best.is_empty():
|
|
2527
|
+
return []
|
|
2528
|
+
var shows: Array = []
|
|
2529
|
+
var view_d := -1.0
|
|
2530
|
+
var view_k := "typical view distance"
|
|
2531
|
+
var seen := false
|
|
2532
|
+
var centre_w := Vector3.ZERO
|
|
2533
|
+
var normal_w := Vector3.ZERO
|
|
2534
|
+
var ga2: Dictionary = best[3]
|
|
2535
|
+
var gb2: Dictionary = best[4]
|
|
2536
|
+
for use in uses:
|
|
2537
|
+
var rec_i: int = use[0]
|
|
2538
|
+
if not bool((_inst[rec_i] as Dictionary)["in"]) or shows.size() >= 4:
|
|
2539
|
+
continue
|
|
2540
|
+
var xf: Transform3D = use[2]
|
|
2541
|
+
var c: Vector3 = xf * (best[2] as Vector3)
|
|
2542
|
+
var view := _zf_view(c)
|
|
2543
|
+
var dist := 30.0 if view.is_empty() else float(view[0])
|
|
2544
|
+
if float(best[1]) > minf(ZF_MAX_GAP, 1.5 * _zf_tol(dist)):
|
|
2545
|
+
continue
|
|
2546
|
+
shows.append(rec_i)
|
|
2547
|
+
if shows.size() == 1:
|
|
2548
|
+
centre_w = c
|
|
2549
|
+
normal_w = (xf.basis.inverse().transposed() * (ga2["n"] as Vector3)).normalized()
|
|
2550
|
+
if dist > view_d:
|
|
2551
|
+
view_d = dist
|
|
2552
|
+
view_k = "typical view distance" if view.is_empty() else String(view[2])
|
|
2553
|
+
if not seen and not view.is_empty():
|
|
2554
|
+
seen = _zf_seen(view[1] as Vector3, c)
|
|
2555
|
+
if shows.is_empty():
|
|
2556
|
+
return []
|
|
2557
|
+
return [mesh_name, int(ga2["s"]), int(gb2["s"]), _mat_name(ga2["mat"], int(ga2["s"])), _mat_name(gb2["mat"], int(gb2["s"])), snappedf(float(best[0]), 0.001), snappedf(float(best[1]), 0.00001), shows, uses.size(), _a3(centre_w), _a3(normal_w, 0.0001), snappedf(view_d, 0.01), view_k, seen, _zf_flags(ga2["mat"]), _zf_flags(gb2["mat"]), int(best[5]) < 0]
|
|
2558
|
+
|
|
2559
|
+
|
|
2560
|
+
# The material a surface renders with on this node: override, surface
|
|
2561
|
+
# override, then the mesh's own.
|
|
2562
|
+
func _surface_mat(node: Node, mesh: Mesh, s: int) -> Material:
|
|
2563
|
+
if node is GeometryInstance3D and (node as GeometryInstance3D).material_override != null:
|
|
2564
|
+
return (node as GeometryInstance3D).material_override
|
|
2565
|
+
if node is MeshInstance3D and s < (node as MeshInstance3D).get_surface_override_material_count():
|
|
2566
|
+
var ov := (node as MeshInstance3D).get_surface_override_material(s)
|
|
2567
|
+
if ov != null:
|
|
2568
|
+
return ov
|
|
2569
|
+
if mesh != null and s < mesh.get_surface_count():
|
|
2570
|
+
return mesh.surface_get_material(s)
|
|
2571
|
+
return null
|
|
2572
|
+
|
|
2573
|
+
|
|
2574
|
+
static func _mat_name(mat: Variant, s: int) -> String:
|
|
2575
|
+
if mat is Material:
|
|
2576
|
+
var m := mat as Material
|
|
2577
|
+
if String(m.resource_name) != "":
|
|
2578
|
+
return String(m.resource_name)
|
|
2579
|
+
if String(m.resource_path) != "":
|
|
2580
|
+
return String(m.resource_path).get_file()
|
|
2581
|
+
return "surface %d" % s
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
# Why a coplanar pair may be intentional (the judge demotes it to look), from
|
|
2585
|
+
# the material alone: render_priority, a material or shader that offsets
|
|
2586
|
+
# depth or the vertex along the normal, a see-through (alpha) material drawn
|
|
2587
|
+
# after the opaque pass like decals and overlays, or no depth test / write.
|
|
2588
|
+
func _zf_flags(mat: Variant) -> Array:
|
|
2589
|
+
var out: Array = []
|
|
2590
|
+
if mat is Material:
|
|
2591
|
+
var m := mat as Material
|
|
2592
|
+
if m.render_priority != 0:
|
|
2593
|
+
out.append("render_priority %d" % m.render_priority)
|
|
2594
|
+
if m is BaseMaterial3D:
|
|
2595
|
+
var bm := m as BaseMaterial3D
|
|
2596
|
+
if bm.grow and absf(bm.grow_amount) > 0.0:
|
|
2597
|
+
out.append("material grows along the normal")
|
|
2598
|
+
if bm.no_depth_test or bm.depth_draw_mode == BaseMaterial3D.DEPTH_DRAW_DISABLED:
|
|
2599
|
+
out.append("material skips the depth test or depth writes")
|
|
2600
|
+
if m is ShaderMaterial and (m as ShaderMaterial).shader != null:
|
|
2601
|
+
var code := (m as ShaderMaterial).shader.code
|
|
2602
|
+
if _re_offset.search(code) != null:
|
|
2603
|
+
out.append("shader offsets depth or the vertex along the normal")
|
|
2604
|
+
if code.contains("depth_test_disabled") or code.contains("depth_draw_never"):
|
|
2605
|
+
out.append("shader skips the depth test or depth writes")
|
|
2606
|
+
if _mat_see_through(m):
|
|
2607
|
+
out.append("see-through material (alpha): drawn after opaque surfaces, like a decal or overlay")
|
|
2608
|
+
return out
|
|
2609
|
+
|
|
2610
|
+
|
|
2611
|
+
# ---------------------------------------------------------------------------
|
|
2612
|
+
# Gap detectors: exposed_edge (A), open_fixture_end and depth_step (C). They
|
|
2613
|
+
# share one setup (_gk_setup): walkable eye points, indoor cells and
|
|
2614
|
+
# axis-aligned facade lines grouped into buildings. Which pieces they look at
|
|
2615
|
+
# comes from the roles (geometry only): walls, and the facade members
|
|
2616
|
+
# (bands, piers, joints) for edges and band levels; any piece whose mesh has
|
|
2617
|
+
# two or more open rims of one size for open ends. Everything here MEASURES;
|
|
2618
|
+
# gaps.ts thresholds, confirms (A by C) and grades.
|
|
2619
|
+
#
|
|
2620
|
+
# Sight lines are two-sided rays (any face blocks).
|
|
2621
|
+
# A point is "visible from walkable space" when an eye point (1.7 m over an
|
|
2622
|
+
# open-sky floor cell, flood-filled from the scene's cameras and characters
|
|
2623
|
+
# when it has any) sees it within 30 m without the sight line crossing an
|
|
2624
|
+
# indoor cell (a cell under a roof-sized piece: looking through a building).
|
|
2625
|
+
# ---------------------------------------------------------------------------
|
|
2626
|
+
|
|
2627
|
+
const GK_EYE_H := 1.7
|
|
2628
|
+
const GK_EYE_STEP := 1.5
|
|
2629
|
+
const GK_EYE_CELL := 8.0
|
|
2630
|
+
const GK_IN_CELL := 0.5
|
|
2631
|
+
const GK_VIS_CELL := 0.5
|
|
2632
|
+
const GK_EDGE_SPACING := 0.25
|
|
2633
|
+
const GK_WELD := 10000.0
|
|
2634
|
+
# depth_step: wall rows every C_WALL_ROW m; runs merge across one row step.
|
|
2635
|
+
const C_WALL_ROW := 1.25
|
|
2636
|
+
const C_MERGE_Y := 1.3
|
|
2637
|
+
|
|
2638
|
+
var _gk_ready := false
|
|
2639
|
+
var _gk_eyes := PackedVector3Array()
|
|
2640
|
+
var _gk_eye_grid: Dictionary = {}
|
|
2641
|
+
var _gk_ring: Array = []
|
|
2642
|
+
var _gk_indoor: Dictionary = {}
|
|
2643
|
+
var _gk_lines: Array = []
|
|
2644
|
+
var _gk_vis_cache: Dictionary = {}
|
|
2645
|
+
var _gk_sphere := RID()
|
|
2646
|
+
var _gk_sphere_q: PhysicsShapeQueryParameters3D = null
|
|
2647
|
+
var _gk_stats: Dictionary = {}
|
|
2648
|
+
var _gk_floor_y := 0.0
|
|
2649
|
+
|
|
2650
|
+
|
|
2651
|
+
func _gk_setup() -> void:
|
|
2652
|
+
if _gk_ready:
|
|
2653
|
+
return
|
|
2654
|
+
_gk_ready = true
|
|
2655
|
+
var b := _gk_struct_bounds()
|
|
2656
|
+
if b.size == Vector3.ZERO:
|
|
2657
|
+
_gk_stats = {"eyes": 0, "lines": 0}
|
|
2658
|
+
return
|
|
2659
|
+
_gk_build_eyes(b)
|
|
2660
|
+
_gk_build_indoor(b)
|
|
2661
|
+
_gk_build_lines()
|
|
2662
|
+
_gk_stats = {"eyes": _gk_eyes.size(), "indoor_cells": _gk_indoor.size(), "lines": _gk_lines.size()}
|
|
2663
|
+
|
|
2664
|
+
|
|
2665
|
+
func _gk_struct_bounds() -> AABB:
|
|
2666
|
+
var b := AABB()
|
|
2667
|
+
var first := true
|
|
2668
|
+
var tops: Array = []
|
|
2669
|
+
for rec in _inst:
|
|
2670
|
+
var role := String(rec["role"])
|
|
2671
|
+
if role == "underlay" or role == "dressing" or role == "":
|
|
2672
|
+
continue
|
|
2673
|
+
var wa: AABB = rec["waabb"]
|
|
2674
|
+
if wa.size.x > 120.0 or wa.size.z > 120.0:
|
|
2675
|
+
continue
|
|
2676
|
+
if role == "floor":
|
|
2677
|
+
tops.append(wa.end.y)
|
|
2678
|
+
b = wa if first else b.merge(wa)
|
|
2679
|
+
first = false
|
|
2680
|
+
if not tops.is_empty():
|
|
2681
|
+
tops.sort()
|
|
2682
|
+
_gk_floor_y = float(tops[tops.size() / 2])
|
|
2683
|
+
elif not first:
|
|
2684
|
+
_gk_floor_y = b.position.y
|
|
2685
|
+
return b
|
|
2686
|
+
|
|
2687
|
+
|
|
2688
|
+
func _gk_build_eyes(b: AABB) -> void:
|
|
2689
|
+
var floors := false
|
|
2690
|
+
for rec in _inst:
|
|
2691
|
+
if rec["role"] == "floor":
|
|
2692
|
+
floors = true
|
|
2693
|
+
break
|
|
2694
|
+
var top := b.end.y + 5.0
|
|
2695
|
+
var bot := b.position.y - 5.0
|
|
2696
|
+
var x0 := b.position.x - 8.0
|
|
2697
|
+
var z0 := b.position.z - 8.0
|
|
2698
|
+
var nx := int((b.size.x + 16.0) / GK_EYE_STEP) + 1
|
|
2699
|
+
var nz := int((b.size.z + 16.0) / GK_EYE_STEP) + 1
|
|
2700
|
+
var raw: Dictionary = {}
|
|
2701
|
+
for i in nx:
|
|
2702
|
+
for k in nz:
|
|
2703
|
+
var x := x0 + i * GK_EYE_STEP
|
|
2704
|
+
var z := z0 + k * GK_EYE_STEP
|
|
2705
|
+
# The first surface from above (two-sided): a floor with open sky
|
|
2706
|
+
# over it; under a roof or a balcony the roof is hit first.
|
|
2707
|
+
var hit := _ray(Vector3(x, top, z), Vector3(x, bot, z), MASK_SOLID, [], true)
|
|
2708
|
+
if hit.is_empty() or absf((hit["normal"] as Vector3).y) <= 0.7:
|
|
2709
|
+
continue
|
|
2710
|
+
var hp: Vector3 = hit["position"]
|
|
2711
|
+
if floors:
|
|
2712
|
+
var hi := _hit_inst(hit)
|
|
2713
|
+
if hi < 0 or String((_inst[hi] as Dictionary)["role"]) != "floor":
|
|
2714
|
+
continue
|
|
2715
|
+
elif hp.y > _gk_floor_y + 2.0:
|
|
2716
|
+
continue
|
|
2717
|
+
raw[Vector2i(i, k)] = hp + Vector3(0, GK_EYE_H, 0)
|
|
2718
|
+
# Walkable: reachable from the scene's cameras and characters through
|
|
2719
|
+
# 4-neighbour steps not blocked at 0.5 and 1.2 m (without any, every
|
|
2720
|
+
# open-sky floor cell counts).
|
|
2721
|
+
var seeds: Array = []
|
|
2722
|
+
for c in _cameras:
|
|
2723
|
+
seeds.append((c as Node3D).global_position)
|
|
2724
|
+
for ch in _players:
|
|
2725
|
+
seeds.append((ch as Node3D).global_position)
|
|
2726
|
+
var keep: Dictionary = {}
|
|
2727
|
+
if seeds.is_empty():
|
|
2728
|
+
for key in raw:
|
|
2729
|
+
keep[key] = true
|
|
2730
|
+
else:
|
|
2731
|
+
var queue: Array = []
|
|
2732
|
+
for sd in seeds:
|
|
2733
|
+
var best: Variant = null
|
|
2734
|
+
var bd := 3.0
|
|
2735
|
+
var sp: Vector3 = sd
|
|
2736
|
+
for key in raw:
|
|
2737
|
+
var e: Vector3 = raw[key]
|
|
2738
|
+
var dd := Vector2(e.x - sp.x, e.z - sp.z).length()
|
|
2739
|
+
if dd < bd:
|
|
2740
|
+
bd = dd
|
|
2741
|
+
best = key
|
|
2742
|
+
if best != null and not keep.has(best):
|
|
2743
|
+
keep[best] = true
|
|
2744
|
+
queue.append(best)
|
|
2745
|
+
if queue.is_empty():
|
|
2746
|
+
for key in raw:
|
|
2747
|
+
keep[key] = true
|
|
2748
|
+
while not queue.is_empty():
|
|
2749
|
+
var kk: Vector2i = queue.pop_back()
|
|
2750
|
+
var e1: Vector3 = raw[kk]
|
|
2751
|
+
for off in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
|
|
2752
|
+
var k2: Vector2i = kk + off
|
|
2753
|
+
if keep.has(k2) or not raw.has(k2):
|
|
2754
|
+
continue
|
|
2755
|
+
var e2: Vector3 = raw[k2]
|
|
2756
|
+
if absf(e2.y - e1.y) > 0.5:
|
|
2757
|
+
continue
|
|
2758
|
+
var lo := Vector3(0, 0.5 - GK_EYE_H, 0)
|
|
2759
|
+
var mid := Vector3(0, 1.2 - GK_EYE_H, 0)
|
|
2760
|
+
if not _ray(e1 + lo, e2 + lo, MASK_SOLID, [], true).is_empty() or not _ray(e1 + mid, e2 + mid, MASK_SOLID, [], true).is_empty():
|
|
2761
|
+
continue
|
|
2762
|
+
keep[k2] = true
|
|
2763
|
+
queue.append(k2)
|
|
2764
|
+
for key in keep:
|
|
2765
|
+
var e: Vector3 = raw[key]
|
|
2766
|
+
var cell := Vector2i(floori(e.x / GK_EYE_CELL), floori(e.z / GK_EYE_CELL))
|
|
2767
|
+
if not _gk_eye_grid.has(cell):
|
|
2768
|
+
_gk_eye_grid[cell] = PackedInt32Array()
|
|
2769
|
+
var arr: PackedInt32Array = _gk_eye_grid[cell]
|
|
2770
|
+
arr.append(_gk_eyes.size())
|
|
2771
|
+
_gk_eye_grid[cell] = arr
|
|
2772
|
+
_gk_eyes.append(e)
|
|
2773
|
+
var r := int(ceil(32.0 / GK_EYE_CELL))
|
|
2774
|
+
for dx in range(-r, r + 1):
|
|
2775
|
+
for dz in range(-r, r + 1):
|
|
2776
|
+
_gk_ring.append(Vector2i(dx, dz))
|
|
2777
|
+
_gk_ring.sort_custom(func(a, c): return (a as Vector2i).length_squared() < (c as Vector2i).length_squared())
|
|
2778
|
+
|
|
2779
|
+
|
|
2780
|
+
# Indoor cells: 0.5 m cells whose up-ray from 0.5 m over the floor level hits a
|
|
2781
|
+
# roof-sized piece (2.5 m or more in x and z). A sight line through one looks
|
|
2782
|
+
# through a building (a hole, a window without glass), not from walkable space.
|
|
2783
|
+
func _gk_build_indoor(b: AABB) -> void:
|
|
2784
|
+
var y := _gk_floor_y + 0.5
|
|
2785
|
+
var x := b.position.x - 2.0
|
|
2786
|
+
while x <= b.end.x + 2.0:
|
|
2787
|
+
var z := b.position.z - 2.0
|
|
2788
|
+
while z <= b.end.z + 2.0:
|
|
2789
|
+
var hit := _ray(Vector3(x, y, z), Vector3(x, y + 60.0, z), MASK_SOLID, [], true)
|
|
2790
|
+
if not hit.is_empty():
|
|
2791
|
+
var hi := _hit_inst(hit)
|
|
2792
|
+
if hi >= 0:
|
|
2793
|
+
var ab: AABB = (_inst[hi] as Dictionary)["waabb"]
|
|
2794
|
+
if ab.size.x >= 2.5 and ab.size.z >= 2.5:
|
|
2795
|
+
_gk_indoor[Vector2i(floori(x / GK_IN_CELL), floori(z / GK_IN_CELL))] = true
|
|
2796
|
+
z += GK_IN_CELL
|
|
2797
|
+
x += GK_IN_CELL
|
|
2798
|
+
|
|
2799
|
+
|
|
2800
|
+
func _gk_crosses_indoor(e: Vector3, p: Vector3) -> bool:
|
|
2801
|
+
var v := Vector2(p.x - e.x, p.z - e.z)
|
|
2802
|
+
var L := v.length()
|
|
2803
|
+
if L < 0.7:
|
|
2804
|
+
return false
|
|
2805
|
+
var u := v / L
|
|
2806
|
+
var t := 0.25
|
|
2807
|
+
while t < L - 0.6:
|
|
2808
|
+
var q := Vector2(e.x, e.z) + u * t
|
|
2809
|
+
if _gk_indoor.has(Vector2i(floori(q.x / GK_IN_CELL), floori(q.y / GK_IN_CELL))):
|
|
2810
|
+
return true
|
|
2811
|
+
t += 0.45
|
|
2812
|
+
return false
|
|
2813
|
+
|
|
2814
|
+
|
|
2815
|
+
# Eye points that see p (at most max_eyes, at most max_tests sight lines,
|
|
2816
|
+
# nearest cells first). facing != ZERO: only eyes on that side of p.
|
|
2817
|
+
func _gk_visible_eyes(p: Vector3, max_eyes: int, max_tests: int, facing := Vector3.ZERO, facing_min := 0.2) -> PackedVector3Array:
|
|
2818
|
+
var out := PackedVector3Array()
|
|
2819
|
+
if _gk_eyes.is_empty():
|
|
2820
|
+
return out
|
|
2821
|
+
var c0 := Vector2i(floori(p.x / GK_EYE_CELL), floori(p.z / GK_EYE_CELL))
|
|
2822
|
+
var tests := 0
|
|
2823
|
+
for off in _gk_ring:
|
|
2824
|
+
var c: Vector2i = c0 + (off as Vector2i)
|
|
2825
|
+
if not _gk_eye_grid.has(c):
|
|
2826
|
+
continue
|
|
2827
|
+
var cell_eyes: PackedInt32Array = _gk_eye_grid[c]
|
|
2828
|
+
var stride := maxi(1, cell_eyes.size() / 4)
|
|
2829
|
+
var j := 0
|
|
2830
|
+
while j < cell_eyes.size():
|
|
2831
|
+
var e: Vector3 = _gk_eyes[cell_eyes[j]]
|
|
2832
|
+
j += stride
|
|
2833
|
+
var dv := p - e
|
|
2834
|
+
var dist := dv.length()
|
|
2835
|
+
if dist > 30.0 or dist < 0.3:
|
|
2836
|
+
continue
|
|
2837
|
+
if facing != Vector3.ZERO and (-dv).dot(facing) < facing_min * dist:
|
|
2838
|
+
continue
|
|
2839
|
+
tests += 1
|
|
2840
|
+
if _ray(e, p - dv / dist * 0.01, MASK_SOLID, [], true).is_empty() and not _gk_crosses_indoor(e, p):
|
|
2841
|
+
out.append(e)
|
|
2842
|
+
if out.size() >= max_eyes:
|
|
2843
|
+
return out
|
|
2844
|
+
if tests >= max_tests:
|
|
2845
|
+
return out
|
|
2846
|
+
return out
|
|
2847
|
+
|
|
2848
|
+
|
|
2849
|
+
# Cached per 50 cm cell (exposed_edge tests thousands of edge samples).
|
|
2850
|
+
func _gk_visible_cached(p: Vector3) -> PackedVector3Array:
|
|
2851
|
+
var key := Vector3i(floori(p.x / GK_VIS_CELL), floori(p.y / GK_VIS_CELL), floori(p.z / GK_VIS_CELL))
|
|
2852
|
+
if _gk_vis_cache.has(key):
|
|
2853
|
+
return _gk_vis_cache[key]
|
|
2854
|
+
var ve := _gk_visible_eyes(p, 3, 12)
|
|
2855
|
+
_gk_vis_cache[key] = ve
|
|
2856
|
+
return ve
|
|
2857
|
+
|
|
2858
|
+
|
|
2859
|
+
# Facade point visible: 15 cm off the facade on either side of its thin axis.
|
|
2860
|
+
func _gk_point_visible(p: Vector3, th: int) -> bool:
|
|
2861
|
+
for sgn in [1.0, -1.0]:
|
|
2862
|
+
var q := p
|
|
2863
|
+
q[th] += 0.15 * float(sgn)
|
|
2864
|
+
if not _gk_visible_eyes(q, 1, 60).is_empty():
|
|
2865
|
+
return true
|
|
2866
|
+
return false
|
|
2867
|
+
|
|
2868
|
+
|
|
2869
|
+
# Facade lines: wall-like pieces (1-40 m tall, at least 0.8 m wide
|
|
2870
|
+
# along one horizontal axis and at most 0.6 m along the other), buildings =
|
|
2871
|
+
# pieces whose bounds touch within 0.15 m, lines = one building's pieces on
|
|
2872
|
+
# one axis within 0.35 m of one plane, merged along with gaps up to 1 m;
|
|
2873
|
+
# sides = the sides with walkable eyes 0.3-12 m in front.
|
|
2874
|
+
func _gk_build_lines() -> void:
|
|
2875
|
+
var cands: Array = []
|
|
2876
|
+
for rec in _inst:
|
|
2877
|
+
var role := String(rec["role"])
|
|
2878
|
+
if role != "wall" and role != "struct":
|
|
2879
|
+
continue
|
|
2880
|
+
var ab: AABB = rec["waabb"]
|
|
2881
|
+
if ab.size.y < 1.0 or ab.size.y > 40.0:
|
|
2882
|
+
continue
|
|
2883
|
+
var ax := -1
|
|
2884
|
+
if ab.size.x >= 0.8 and ab.size.z <= 0.6:
|
|
2885
|
+
ax = 0
|
|
2886
|
+
elif ab.size.z >= 0.8 and ab.size.x <= 0.6:
|
|
2887
|
+
ax = 2
|
|
2888
|
+
if ax < 0:
|
|
2889
|
+
continue
|
|
2890
|
+
var th := 2 - ax
|
|
2891
|
+
cands.append({"i": int(rec["i"]), "ax": ax, "c": ab.get_center()[th], "lo": ab.position[ax], "hi": ab.end[ax], "y0": ab.position.y, "y1": ab.end.y, "tmin": ab.position[th], "tmax": ab.end[th], "ab": ab})
|
|
2892
|
+
# Buildings: union-find over touching bounds, through a 4 m grid.
|
|
2893
|
+
var par := PackedInt32Array()
|
|
2894
|
+
par.resize(cands.size())
|
|
2895
|
+
for i in cands.size():
|
|
2896
|
+
par[i] = i
|
|
2897
|
+
var grid: Dictionary = {}
|
|
2898
|
+
for i in cands.size():
|
|
2899
|
+
var a: AABB = (cands[i]["ab"] as AABB).grow(0.15)
|
|
2900
|
+
for gx in range(floori(a.position.x / 4.0), floori(a.end.x / 4.0) + 1):
|
|
2901
|
+
for gz in range(floori(a.position.z / 4.0), floori(a.end.z / 4.0) + 1):
|
|
2902
|
+
var gk := Vector2i(gx, gz)
|
|
2903
|
+
if not grid.has(gk):
|
|
2904
|
+
grid[gk] = []
|
|
2905
|
+
for j in (grid[gk] as Array):
|
|
2906
|
+
if a.intersects(cands[j]["ab"] as AABB):
|
|
2907
|
+
var ri := _gk_find(par, i)
|
|
2908
|
+
var rj := _gk_find(par, int(j))
|
|
2909
|
+
if ri != rj:
|
|
2910
|
+
par[ri] = rj
|
|
2911
|
+
(grid[gk] as Array).append(i)
|
|
2912
|
+
for i in cands.size():
|
|
2913
|
+
cands[i]["b"] = _gk_find(par, i)
|
|
2914
|
+
cands.sort_custom(func(a, c): return int(a["ax"]) < int(c["ax"]) or (int(a["ax"]) == int(c["ax"]) and float(a["c"]) < float(c["c"])))
|
|
2915
|
+
var groups: Array = []
|
|
2916
|
+
for c in cands:
|
|
2917
|
+
var placed := false
|
|
2918
|
+
for g in groups:
|
|
2919
|
+
if int(g["b"]) == int(c["b"]) and int(g["ax"]) == int(c["ax"]) and absf(float(g["c"]) - float(c["c"])) <= 0.35:
|
|
2920
|
+
(g["items"] as Array).append(c)
|
|
2921
|
+
placed = true
|
|
2922
|
+
break
|
|
2923
|
+
if not placed:
|
|
2924
|
+
groups.append({"b": c["b"], "ax": c["ax"], "c": c["c"], "items": [c]})
|
|
2925
|
+
for g in groups:
|
|
2926
|
+
var items: Array = g["items"]
|
|
2927
|
+
items.sort_custom(func(a, c): return float(a["lo"]) < float(c["lo"]))
|
|
2928
|
+
var cur: Variant = null
|
|
2929
|
+
for it in items:
|
|
2930
|
+
if cur != null and float(it["lo"]) <= float(cur["hi"]) + 1.0:
|
|
2931
|
+
cur["hi"] = maxf(float(cur["hi"]), float(it["hi"]))
|
|
2932
|
+
cur["y0"] = minf(float(cur["y0"]), float(it["y0"]))
|
|
2933
|
+
cur["y1"] = maxf(float(cur["y1"]), float(it["y1"]))
|
|
2934
|
+
cur["tmin"] = minf(float(cur["tmin"]), float(it["tmin"]))
|
|
2935
|
+
cur["tmax"] = maxf(float(cur["tmax"]), float(it["tmax"]))
|
|
2936
|
+
(cur["pieces"] as Array).append(it["i"])
|
|
2937
|
+
else:
|
|
2938
|
+
if cur != null:
|
|
2939
|
+
_gk_lines.append(cur)
|
|
2940
|
+
cur = {"b": g["b"], "ax": g["ax"], "c": g["c"], "lo": it["lo"], "hi": it["hi"], "y0": it["y0"], "y1": it["y1"], "tmin": it["tmin"], "tmax": it["tmax"], "pieces": [it["i"]]}
|
|
2941
|
+
if cur != null:
|
|
2942
|
+
_gk_lines.append(cur)
|
|
2943
|
+
for ln in _gk_lines:
|
|
2944
|
+
var sides: Array = []
|
|
2945
|
+
var ax: int = ln["ax"]
|
|
2946
|
+
var th := 2 - ax
|
|
2947
|
+
for s in [1, -1]:
|
|
2948
|
+
var face: float = float(ln["tmax"]) if s == 1 else float(ln["tmin"])
|
|
2949
|
+
for e in _gk_eyes:
|
|
2950
|
+
var off: float = (e[th] - face) * float(s)
|
|
2951
|
+
if off > 0.3 and off < 12.0 and e[ax] > float(ln["lo"]) - 2.0 and e[ax] < float(ln["hi"]) + 2.0:
|
|
2952
|
+
sides.append(s)
|
|
2953
|
+
break
|
|
2954
|
+
ln["sides"] = sides
|
|
2955
|
+
|
|
2956
|
+
|
|
2957
|
+
static func _gk_find(par: PackedInt32Array, i: int) -> int:
|
|
2958
|
+
while par[i] != i:
|
|
2959
|
+
par[i] = par[par[i]]
|
|
2960
|
+
i = par[i]
|
|
2961
|
+
return i
|
|
2962
|
+
|
|
2963
|
+
|
|
2964
|
+
func _gk_line_of(ab: AABB, margin: float) -> int:
|
|
2965
|
+
var best := -1
|
|
2966
|
+
var bd := INF
|
|
2967
|
+
for li in _gk_lines.size():
|
|
2968
|
+
var ln: Dictionary = _gk_lines[li]
|
|
2969
|
+
var ax: int = ln["ax"]
|
|
2970
|
+
var th := 2 - ax
|
|
2971
|
+
if ab.end[ax] < float(ln["lo"]) - margin or ab.position[ax] > float(ln["hi"]) + margin:
|
|
2972
|
+
continue
|
|
2973
|
+
if ab.end.y < float(ln["y0"]) - margin or ab.position.y > float(ln["y1"]) + margin:
|
|
2974
|
+
continue
|
|
2975
|
+
if ab.end[th] < float(ln["tmin"]) - margin or ab.position[th] > float(ln["tmax"]) + margin:
|
|
2976
|
+
continue
|
|
2977
|
+
var d := absf(ab.get_center()[th] - float(ln["c"]))
|
|
2978
|
+
if d < bd:
|
|
2979
|
+
bd = d
|
|
2980
|
+
best = li
|
|
2981
|
+
return best
|
|
2982
|
+
|
|
2983
|
+
|
|
2984
|
+
# Door-like openings: inserts at least 1.8 m tall whose bottom is within 30 cm
|
|
2985
|
+
# of the floor level. [[lo, hi]] world bounds (depth_step skips the recess a
|
|
2986
|
+
# door leaves).
|
|
2987
|
+
func _gk_doors() -> Array:
|
|
2988
|
+
var out: Array = []
|
|
2989
|
+
for rec in _inst:
|
|
2990
|
+
if rec["role"] != "insert":
|
|
2991
|
+
continue
|
|
2992
|
+
var ab: AABB = rec["waabb"]
|
|
2993
|
+
if ab.size.y >= 1.8 and absf(ab.position.y - _gk_floor_y) <= 0.3 and out.size() < 200:
|
|
2994
|
+
out.append([_a3(ab.position), _a3(ab.end)])
|
|
2995
|
+
return out
|
|
2996
|
+
|
|
2997
|
+
|
|
2998
|
+
func _gk_lines_rows() -> Array:
|
|
2999
|
+
var out: Array = []
|
|
3000
|
+
for ln in _gk_lines:
|
|
3001
|
+
out.append({"b": int(ln["b"]), "ax": int(ln["ax"]), "c": snappedf(float(ln["c"]), 0.001), "lo": snappedf(float(ln["lo"]), 0.001), "hi": snappedf(float(ln["hi"]), 0.001), "y0": snappedf(float(ln["y0"]), 0.001), "y1": snappedf(float(ln["y1"]), 0.001), "tmin": snappedf(float(ln["tmin"]), 0.001), "tmax": snappedf(float(ln["tmax"]), 0.001), "sides": ln["sides"], "pieces": ln["pieces"]})
|
|
3002
|
+
return out
|
|
3003
|
+
|
|
3004
|
+
|
|
3005
|
+
# ---------------------------------------------------------------------------
|
|
3006
|
+
# exposed_edge (A): open outline edges of walls and facade members (bands,
|
|
3007
|
+
# piers, joints) that nothing covers within 4-7 mm and that walkable space
|
|
3008
|
+
# sees, with what shows behind them. Per mesh resource (cached): welded
|
|
3009
|
+
# boundary edges of the opaque surfaces, sampled every 25 cm, only those on
|
|
3010
|
+
# the mesh's own bounds (openings inside a module are by design).
|
|
3011
|
+
# ---------------------------------------------------------------------------
|
|
3012
|
+
|
|
3013
|
+
# Raw boundary edges of a mesh's opaque surfaces: [a, b, n, d] per edge,
|
|
3014
|
+
# mesh-local (n: the edge's sheet normal, d: outward from its triangle).
|
|
3015
|
+
func _gk_boundary(mesh: Mesh, info: Dictionary) -> PackedVector3Array:
|
|
3016
|
+
if info.has("gk_boundary"):
|
|
3017
|
+
return info["gk_boundary"]
|
|
3018
|
+
var out := PackedVector3Array()
|
|
3019
|
+
var weld: Dictionary = {}
|
|
3020
|
+
var pos := PackedVector3Array()
|
|
3021
|
+
var cnt: Dictionary = {}
|
|
3022
|
+
var third: Dictionary = {}
|
|
3023
|
+
for s in mesh.get_surface_count():
|
|
3024
|
+
if mesh is ArrayMesh and (mesh as ArrayMesh).surface_get_primitive_type(s) != Mesh.PRIMITIVE_TRIANGLES:
|
|
3025
|
+
continue
|
|
3026
|
+
var mat := mesh.surface_get_material(s)
|
|
3027
|
+
if mat is BaseMaterial3D and (mat as BaseMaterial3D).transparency != BaseMaterial3D.TRANSPARENCY_DISABLED:
|
|
3028
|
+
continue
|
|
3029
|
+
var arrays := mesh.surface_get_arrays(s)
|
|
3030
|
+
var v: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
3031
|
+
if v.is_empty():
|
|
3032
|
+
continue
|
|
3033
|
+
var idx := PackedInt32Array()
|
|
3034
|
+
if arrays[Mesh.ARRAY_INDEX] != null:
|
|
3035
|
+
idx = arrays[Mesh.ARRAY_INDEX]
|
|
3036
|
+
var n := idx.size() if idx.size() > 0 else v.size()
|
|
3037
|
+
n -= n % 3
|
|
3038
|
+
var remap := PackedInt32Array()
|
|
3039
|
+
remap.resize(v.size())
|
|
3040
|
+
for i in v.size():
|
|
3041
|
+
var k := Vector3i((v[i] * GK_WELD).round())
|
|
3042
|
+
var id: int = weld.get(k, -1)
|
|
3043
|
+
if id == -1:
|
|
3044
|
+
id = pos.size()
|
|
3045
|
+
weld[k] = id
|
|
3046
|
+
pos.append(v[i])
|
|
3047
|
+
remap[i] = id
|
|
3048
|
+
var t := 0
|
|
3049
|
+
while t < n:
|
|
3050
|
+
var ia := remap[idx[t] if idx.size() > 0 else t]
|
|
3051
|
+
var ib := remap[idx[t + 1] if idx.size() > 0 else t + 1]
|
|
3052
|
+
var ic := remap[idx[t + 2] if idx.size() > 0 else t + 2]
|
|
3053
|
+
t += 3
|
|
3054
|
+
if ia == ib or ib == ic or ia == ic:
|
|
3055
|
+
continue
|
|
3056
|
+
for e in [[ia, ib, ic], [ib, ic, ia], [ic, ia, ib]]:
|
|
3057
|
+
var key := mini(int(e[0]), int(e[1])) * 4194304 + maxi(int(e[0]), int(e[1]))
|
|
3058
|
+
cnt[key] = int(cnt.get(key, 0)) + 1
|
|
3059
|
+
if not third.has(key):
|
|
3060
|
+
third[key] = int(e[2])
|
|
3061
|
+
for key in cnt:
|
|
3062
|
+
if int(cnt[key]) != 1:
|
|
3063
|
+
continue
|
|
3064
|
+
var a: Vector3 = pos[int(key) / 4194304]
|
|
3065
|
+
var b: Vector3 = pos[int(key) % 4194304]
|
|
3066
|
+
var c: Vector3 = pos[int(third[key])]
|
|
3067
|
+
var e := b - a
|
|
3068
|
+
var L := e.length()
|
|
3069
|
+
if L < 0.0005:
|
|
3070
|
+
continue
|
|
3071
|
+
e /= L
|
|
3072
|
+
var w := c - a
|
|
3073
|
+
var inward := w - e * w.dot(e)
|
|
3074
|
+
if inward.length() < 1e-7:
|
|
3075
|
+
continue
|
|
3076
|
+
inward = inward.normalized()
|
|
3077
|
+
out.append_array([a, b, e.cross(inward).normalized(), -inward])
|
|
3078
|
+
info["gk_boundary"] = out
|
|
3079
|
+
return out
|
|
3080
|
+
|
|
3081
|
+
|
|
3082
|
+
static func _gk_on_aabb(p: Vector3, b: AABB, tol: float) -> bool:
|
|
3083
|
+
for k in 3:
|
|
3084
|
+
if absf(p[k] - b.position[k]) <= tol or absf(p[k] - b.end[k]) <= tol:
|
|
3085
|
+
return true
|
|
3086
|
+
return false
|
|
3087
|
+
|
|
3088
|
+
|
|
3089
|
+
# Outline samples of a mesh: [p, n, d, (length, 0, 0)] every 25 cm along each
|
|
3090
|
+
# boundary edge that lies on the mesh's bounds (2 cm).
|
|
3091
|
+
func _gk_outline(mesh: Mesh, info: Dictionary) -> PackedVector3Array:
|
|
3092
|
+
if info.has("gk_outline"):
|
|
3093
|
+
return info["gk_outline"]
|
|
3094
|
+
var bnd := _gk_boundary(mesh, info)
|
|
3095
|
+
var out := PackedVector3Array()
|
|
3096
|
+
var maab := mesh.get_aabb()
|
|
3097
|
+
for k in range(0, bnd.size(), 4):
|
|
3098
|
+
var a: Vector3 = bnd[k]
|
|
3099
|
+
var b: Vector3 = bnd[k + 1]
|
|
3100
|
+
var L := a.distance_to(b)
|
|
3101
|
+
var ns := maxi(1, ceili(L / GK_EDGE_SPACING))
|
|
3102
|
+
for s in ns:
|
|
3103
|
+
var p := a.lerp(b, (float(s) + 0.5) / float(ns))
|
|
3104
|
+
if not _gk_on_aabb(p, maab, 0.02):
|
|
3105
|
+
continue
|
|
3106
|
+
out.append_array([p, bnd[k + 2], bnd[k + 3], Vector3(L / float(ns), 0, 0)])
|
|
3107
|
+
info["gk_outline"] = out
|
|
3108
|
+
return out
|
|
3109
|
+
|
|
3110
|
+
|
|
3111
|
+
# Covered: another surface continues the edge's sheet within 4 or 7 mm (a
|
|
3112
|
+
# ray across the sheet's plane just beyond the edge), or a face meets or
|
|
3113
|
+
# abuts the edge (rays along d from 3 mm inside to 7 mm beyond, 2 mm to
|
|
3114
|
+
# either side of the sheet: a side wall turning the corner, a neighbour's
|
|
3115
|
+
# end face). Any face counts, the piece's own too.
|
|
3116
|
+
# (The query is set up once per stage: every gap ray is two-sided over all
|
|
3117
|
+
# solid layers and excludes nothing, so only the end points change.)
|
|
3118
|
+
func _gk_covered(p: Vector3, n: Vector3, d: Vector3) -> bool:
|
|
3119
|
+
var q := p + d * 0.004
|
|
3120
|
+
var n6 := n * 0.006
|
|
3121
|
+
_rays += 1
|
|
3122
|
+
_q.from = q - n6
|
|
3123
|
+
_q.to = q + n6
|
|
3124
|
+
if not _state.intersect_ray(_q).is_empty():
|
|
3125
|
+
return true
|
|
3126
|
+
var n2 := n * 0.002
|
|
3127
|
+
_rays += 2
|
|
3128
|
+
_q.from = p + n2 - d * 0.003
|
|
3129
|
+
_q.to = p + n2 + d * 0.007
|
|
3130
|
+
if not _state.intersect_ray(_q).is_empty():
|
|
3131
|
+
return true
|
|
3132
|
+
_q.from = p - n2 - d * 0.003
|
|
3133
|
+
_q.to = p - n2 + d * 0.007
|
|
3134
|
+
if not _state.intersect_ray(_q).is_empty():
|
|
3135
|
+
return true
|
|
3136
|
+
q = p + d * 0.007
|
|
3137
|
+
_rays += 1
|
|
3138
|
+
_q.from = q - n6
|
|
3139
|
+
_q.to = q + n6
|
|
3140
|
+
return not _state.intersect_ray(_q).is_empty()
|
|
3141
|
+
|
|
3142
|
+
|
|
3143
|
+
# Every gap-detector ray: two-sided, all solid layers, nothing excluded.
|
|
3144
|
+
func _gk_query() -> void:
|
|
3145
|
+
_q.collision_mask = MASK_SOLID
|
|
3146
|
+
_q.hit_back_faces = true
|
|
3147
|
+
if _ex_set:
|
|
3148
|
+
_ex.clear()
|
|
3149
|
+
_q.exclude = _ex
|
|
3150
|
+
_ex_set = false
|
|
3151
|
+
|
|
3152
|
+
|
|
3153
|
+
# A coplanar continuation a little further out: a seam (within 4 cm) or a
|
|
3154
|
+
# bridged gap (within 35 cm). Returns the width, -1 without one.
|
|
3155
|
+
func _gk_seam_width(p: Vector3, n: Vector3, d: Vector3) -> float:
|
|
3156
|
+
for w in [0.008, 0.012, 0.018, 0.025, 0.035, 0.05, 0.075, 0.1, 0.14, 0.19, 0.25, 0.35]:
|
|
3157
|
+
var q := p + d * float(w)
|
|
3158
|
+
if not _ray(q - n * 0.006, q + n * 0.006, MASK_SOLID, [], true).is_empty():
|
|
3159
|
+
return float(w)
|
|
3160
|
+
return -1.0
|
|
3161
|
+
|
|
3162
|
+
|
|
3163
|
+
# What lies just beyond an open edge at p (outward d) seen from eye e: the
|
|
3164
|
+
# reveal depth (along the sheet normal n when it is within 0.6 m), 50 when
|
|
3165
|
+
# the reveal is the ground, 99 when nothing is within 2 m, -1 when blocked in
|
|
3166
|
+
# front. [depth, hit instance].
|
|
3167
|
+
func _gk_beyond(e: Vector3, p: Vector3, d: Vector3, n: Vector3) -> Array:
|
|
3168
|
+
var v := (p - e).normalized()
|
|
3169
|
+
var dp := d - v * d.dot(v)
|
|
3170
|
+
if dp.length() < 0.2:
|
|
3171
|
+
dp = d
|
|
3172
|
+
var b := p + dp.normalized() * 0.02
|
|
3173
|
+
var dist := (p - e).length()
|
|
3174
|
+
var dir := (b - e).normalized()
|
|
3175
|
+
var hit := _ray(e, e + dir * (dist + 2.0), MASK_SOLID, [], true)
|
|
3176
|
+
if hit.is_empty():
|
|
3177
|
+
return [99.0, -1]
|
|
3178
|
+
var hp: Vector3 = hit["position"]
|
|
3179
|
+
var t := (hp - e).length()
|
|
3180
|
+
var hi := _hit_inst(hit)
|
|
3181
|
+
if t < dist - 0.005:
|
|
3182
|
+
return [-1.0, hi]
|
|
3183
|
+
if absf((hit["normal"] as Vector3).y) > 0.7 and hp.y < _gk_floor_y + 0.35 and t - dist <= 0.6:
|
|
3184
|
+
return [50.0, hi]
|
|
3185
|
+
if t - dist <= 0.6:
|
|
3186
|
+
return [maxf(absf((hp - p).dot(n)), 0.0), hi]
|
|
3187
|
+
return [t - dist, hi]
|
|
3188
|
+
|
|
3189
|
+
|
|
3190
|
+
func _gk_sheet(ab: AABB) -> bool:
|
|
3191
|
+
var dims := [ab.size.x, ab.size.y, ab.size.z]
|
|
3192
|
+
dims.sort()
|
|
3193
|
+
var hmax := maxf(ab.size.x, ab.size.z)
|
|
3194
|
+
return (ab.size.y >= 1.0 and hmax >= 0.8) or (hmax >= 0.8 and ab.size.y <= 1.2 and float(dims[0]) <= 0.6) or (ab.size.y >= 1.5 and hmax <= 0.8)
|
|
3195
|
+
|
|
3196
|
+
|
|
3197
|
+
func _scan_exposed_edges() -> Dictionary:
|
|
3198
|
+
_gk_setup()
|
|
3199
|
+
var pts := PackedVector3Array()
|
|
3200
|
+
var lens := PackedFloat32Array()
|
|
3201
|
+
var steps := PackedFloat32Array()
|
|
3202
|
+
var owners := PackedInt32Array()
|
|
3203
|
+
var eyes_at := PackedVector3Array()
|
|
3204
|
+
var hits := PackedInt32Array()
|
|
3205
|
+
var total := 0
|
|
3206
|
+
var done := 0
|
|
3207
|
+
var samples := 0
|
|
3208
|
+
var uncovered := 0
|
|
3209
|
+
var inside := 0
|
|
3210
|
+
# Band pieces first (they warn), then piers and joints, then walls: a
|
|
3211
|
+
# budget stop leaves the walls. Facade pieces only: walls and the facade
|
|
3212
|
+
# members the roles found (a crate leaning on a wall is not one).
|
|
3213
|
+
var order: Array = []
|
|
3214
|
+
for rec in _inst:
|
|
3215
|
+
var role := String(rec["role"])
|
|
3216
|
+
var why := String(rec.get("why", ""))
|
|
3217
|
+
if not bool(rec["in"]):
|
|
3218
|
+
continue
|
|
3219
|
+
if role == "wall":
|
|
3220
|
+
order.append([2, rec])
|
|
3221
|
+
elif role == "struct" and (why == "band" or why == "pier" or why == "joint"):
|
|
3222
|
+
order.append([0 if why == "band" else 1, rec])
|
|
3223
|
+
order.sort_custom(func(a, b): return int(a[0]) < int(b[0]))
|
|
3224
|
+
for item in order:
|
|
3225
|
+
var rec: Dictionary = item[1]
|
|
3226
|
+
var ab: AABB = rec["waabb"]
|
|
3227
|
+
if ab.size.y < 0.25 and ab.end.y < _gk_floor_y + 0.5 and maxf(ab.size.x, ab.size.z) > 1.5:
|
|
3228
|
+
continue
|
|
3229
|
+
if ab.size.length() < 0.15 or not _gk_sheet(ab):
|
|
3230
|
+
continue
|
|
3231
|
+
var xf: Transform3D = rec["xf"]
|
|
3232
|
+
if xf.basis.y.normalized().y < 0.98:
|
|
3233
|
+
continue
|
|
3234
|
+
var li := _gk_line_of(ab, 0.8)
|
|
3235
|
+
if li < 0 or ((_gk_lines[li] as Dictionary)["sides"] as Array).is_empty():
|
|
3236
|
+
continue
|
|
3237
|
+
total += 1
|
|
3238
|
+
if _over():
|
|
3239
|
+
continue
|
|
3240
|
+
done += 1
|
|
3241
|
+
# A facade seen from one side only: samples behind its back face, away
|
|
3242
|
+
# from the line's ends (where a side street may see them), are inside.
|
|
3243
|
+
var ln: Dictionary = _gk_lines[li]
|
|
3244
|
+
var lsides: Array = ln["sides"]
|
|
3245
|
+
var lax: int = ln["ax"]
|
|
3246
|
+
var lth := 2 - lax
|
|
3247
|
+
var back := INF
|
|
3248
|
+
if lsides.size() == 1:
|
|
3249
|
+
back = float(ln["tmin"]) - 0.02 if int(lsides[0]) == 1 else -(float(ln["tmax"]) + 0.02)
|
|
3250
|
+
for m in (rec["meshes"] as Array):
|
|
3251
|
+
var mesh: Mesh = m[1]
|
|
3252
|
+
var info: Dictionary = _mesh_info.get(mesh.get_instance_id(), {})
|
|
3253
|
+
if info.is_empty():
|
|
3254
|
+
continue
|
|
3255
|
+
var ol := _gk_outline(mesh, info)
|
|
3256
|
+
var mxf: Transform3D = m[2]
|
|
3257
|
+
var bas := mxf.basis
|
|
3258
|
+
var sc := bas.get_scale().x
|
|
3259
|
+
for k in range(0, ol.size(), 4):
|
|
3260
|
+
samples += 1
|
|
3261
|
+
var p: Vector3 = mxf * ol[k]
|
|
3262
|
+
if back < INF and p[lax] > float(ln["lo"]) + 0.5 and p[lax] < float(ln["hi"]) - 0.5:
|
|
3263
|
+
var depth_behind: float = (back - p[lth]) if int(lsides[0]) == 1 else (p[lth] + back)
|
|
3264
|
+
if depth_behind > 0.0:
|
|
3265
|
+
inside += 1
|
|
3266
|
+
continue
|
|
3267
|
+
var n: Vector3 = (bas * ol[k + 1]).normalized()
|
|
3268
|
+
var d: Vector3 = (bas * ol[k + 2]).normalized()
|
|
3269
|
+
_gk_query()
|
|
3270
|
+
if _gk_covered(p, n, d):
|
|
3271
|
+
continue
|
|
3272
|
+
uncovered += 1
|
|
3273
|
+
var ve := _gk_visible_cached(p)
|
|
3274
|
+
if ve.is_empty():
|
|
3275
|
+
continue
|
|
3276
|
+
var best := [-1.0, Vector3.ZERO, -1]
|
|
3277
|
+
for e in ve:
|
|
3278
|
+
var bs := _gk_beyond(e, p, d, n)
|
|
3279
|
+
var st: float = bs[0]
|
|
3280
|
+
if st >= 0.02 and st <= 0.6:
|
|
3281
|
+
best = [st, e, bs[1]]
|
|
3282
|
+
break
|
|
3283
|
+
if st > float(best[0]):
|
|
3284
|
+
best = [st, e, bs[1]]
|
|
3285
|
+
if float(best[0]) < 0.0:
|
|
3286
|
+
continue
|
|
3287
|
+
var sw := _gk_seam_width(p, n, d)
|
|
3288
|
+
pts.append(p)
|
|
3289
|
+
lens.append(ol[k + 3].x * sc)
|
|
3290
|
+
steps.append((-2.0 if sw <= 0.04 else -3.0) if sw > 0.0 else float(best[0]))
|
|
3291
|
+
owners.append(int(rec["i"]))
|
|
3292
|
+
eyes_at.append(best[1])
|
|
3293
|
+
hits.append(int(best[2]))
|
|
3294
|
+
_count("exposed_edge", done, total)
|
|
3295
|
+
var flags := _gk_flags(pts, lens, steps, owners, eyes_at, hits)
|
|
3296
|
+
return {"flags": flags, "stats": {"pieces": total, "samples": samples, "inside": inside, "uncovered": uncovered, "exposed": pts.size(), "vis_cells": _gk_vis_cache.size()}}
|
|
3297
|
+
|
|
3298
|
+
|
|
3299
|
+
# Exposed samples -> clusters (25 cm links, one piece each), longest first:
|
|
3300
|
+
# [owner, class, length, samples, centre, min, max, deepest reveal, gap,
|
|
3301
|
+
# seam, bridge, sky, far, an eye that sees it, what the reveal hit].
|
|
3302
|
+
func _gk_flags(pts: PackedVector3Array, lens: PackedFloat32Array, steps: PackedFloat32Array, owners: PackedInt32Array, eyes_at: PackedVector3Array, hits: PackedInt32Array) -> Array:
|
|
3303
|
+
var par := PackedInt32Array()
|
|
3304
|
+
par.resize(pts.size())
|
|
3305
|
+
for i in pts.size():
|
|
3306
|
+
par[i] = i
|
|
3307
|
+
var grid: Dictionary = {}
|
|
3308
|
+
for i in pts.size():
|
|
3309
|
+
var c := Vector3i((pts[i] / 0.25).floor())
|
|
3310
|
+
if not grid.has(c):
|
|
3311
|
+
grid[c] = PackedInt32Array()
|
|
3312
|
+
var arr: PackedInt32Array = grid[c]
|
|
3313
|
+
arr.append(i)
|
|
3314
|
+
grid[c] = arr
|
|
3315
|
+
for c in grid:
|
|
3316
|
+
for dx in [-1, 0, 1]:
|
|
3317
|
+
for dy in [-1, 0, 1]:
|
|
3318
|
+
for dz in [-1, 0, 1]:
|
|
3319
|
+
var c2: Vector3i = (c as Vector3i) + Vector3i(dx, dy, dz)
|
|
3320
|
+
if not grid.has(c2):
|
|
3321
|
+
continue
|
|
3322
|
+
for i in (grid[c] as PackedInt32Array):
|
|
3323
|
+
for j in (grid[c2] as PackedInt32Array):
|
|
3324
|
+
if j <= i or owners[i] != owners[j]:
|
|
3325
|
+
continue
|
|
3326
|
+
if pts[i].distance_to(pts[j]) <= 0.25:
|
|
3327
|
+
var ri := _gk_find(par, i)
|
|
3328
|
+
var rj := _gk_find(par, j)
|
|
3329
|
+
if ri != rj:
|
|
3330
|
+
par[ri] = rj
|
|
3331
|
+
var groups: Dictionary = {}
|
|
3332
|
+
for i in pts.size():
|
|
3333
|
+
var g := _gk_find(par, i)
|
|
3334
|
+
if not groups.has(g):
|
|
3335
|
+
groups[g] = []
|
|
3336
|
+
(groups[g] as Array).append(i)
|
|
3337
|
+
var flags: Array = []
|
|
3338
|
+
for g in groups:
|
|
3339
|
+
var ids: Array = groups[g]
|
|
3340
|
+
var L := 0.0
|
|
3341
|
+
var c := Vector3.ZERO
|
|
3342
|
+
var bmin: Vector3 = pts[ids[0]]
|
|
3343
|
+
var bmax: Vector3 = pts[ids[0]]
|
|
3344
|
+
var ngap := 0
|
|
3345
|
+
var nseam := 0
|
|
3346
|
+
var nbridge := 0
|
|
3347
|
+
var nsky := 0
|
|
3348
|
+
var nfar := 0
|
|
3349
|
+
var smax := 0.0
|
|
3350
|
+
for i in ids:
|
|
3351
|
+
L += lens[i]
|
|
3352
|
+
c += pts[i]
|
|
3353
|
+
bmin = bmin.min(pts[i])
|
|
3354
|
+
bmax = bmax.max(pts[i])
|
|
3355
|
+
var st: float = steps[i]
|
|
3356
|
+
if st == -2.0:
|
|
3357
|
+
nseam += 1
|
|
3358
|
+
elif st == -3.0:
|
|
3359
|
+
nbridge += 1
|
|
3360
|
+
elif st >= 0.02 and st <= 0.6:
|
|
3361
|
+
ngap += 1
|
|
3362
|
+
smax = maxf(smax, st)
|
|
3363
|
+
elif st >= 98.0:
|
|
3364
|
+
nsky += 1
|
|
3365
|
+
else:
|
|
3366
|
+
nfar += 1
|
|
3367
|
+
if L < 0.05:
|
|
3368
|
+
continue
|
|
3369
|
+
c /= float(ids.size())
|
|
3370
|
+
var cls := "mixed"
|
|
3371
|
+
if nseam * 2 >= ids.size():
|
|
3372
|
+
cls = "seam"
|
|
3373
|
+
elif nbridge * 2 >= ids.size():
|
|
3374
|
+
cls = "bridge"
|
|
3375
|
+
elif (ngap + nbridge + nseam) * 2 >= ids.size():
|
|
3376
|
+
cls = "gap"
|
|
3377
|
+
elif nsky * 2 >= ids.size():
|
|
3378
|
+
cls = "sky"
|
|
3379
|
+
elif nfar * 2 >= ids.size():
|
|
3380
|
+
cls = "far"
|
|
3381
|
+
var mid: int = ids[ids.size() / 2]
|
|
3382
|
+
flags.append([owners[ids[0]], cls, snappedf(L, 0.01), ids.size(), _a3(c), _a3(bmin), _a3(bmax), snappedf(smax, 0.001), ngap, nseam, nbridge, nsky, nfar, _a3(eyes_at[mid], 0.01), hits[mid]])
|
|
3383
|
+
flags.sort_custom(func(a, b): return float(a[2]) > float(b[2]))
|
|
3384
|
+
if flags.size() > 400:
|
|
3385
|
+
flags.resize(400)
|
|
3386
|
+
return flags
|
|
3387
|
+
|
|
3388
|
+
|
|
3389
|
+
# ---------------------------------------------------------------------------
|
|
3390
|
+
# open_fixture_end: the open ends of run pieces (pipe, duct or gutter
|
|
3391
|
+
# sections, elbows, tees: a piece whose mesh has two or more open rims of one
|
|
3392
|
+
# size, the rims being open boundary loops on the mesh's bounds, cached per
|
|
3393
|
+
# mesh) that nothing covers within a sleeve tolerance and that walkable space
|
|
3394
|
+
# sees into. A piece with one rim, or rims of different sizes (a cap, a
|
|
3395
|
+
# funnel, an outlet), is open by design. Meshes over FE_MAX_TRIS triangles
|
|
3396
|
+
# are not read (run pieces are simple).
|
|
3397
|
+
# ---------------------------------------------------------------------------
|
|
3398
|
+
|
|
3399
|
+
const FE_MAX_TRIS := 20000
|
|
3400
|
+
|
|
3401
|
+
|
|
3402
|
+
# Two rims within 20% of each other's radius that face different ways.
|
|
3403
|
+
static func _run_piece(cands: Array) -> bool:
|
|
3404
|
+
for i in cands.size():
|
|
3405
|
+
for j in range(i + 1, cands.size()):
|
|
3406
|
+
var ri: float = cands[i][2]
|
|
3407
|
+
var rj: float = cands[j][2]
|
|
3408
|
+
if absf(ri - rj) <= 0.2 * maxf(ri, rj) and (cands[i][1] as Vector3).dot(cands[j][1] as Vector3) < 0.9:
|
|
3409
|
+
return true
|
|
3410
|
+
return false
|
|
3411
|
+
|
|
3412
|
+
# Open ends of a mesh: its boundary edges chained into
|
|
3413
|
+
# loops; a loop counts when every vertex lies on one face of the mesh's
|
|
3414
|
+
# bounds (2% of its size, at least 1 cm), it encloses at least half of its
|
|
3415
|
+
# bounding rectangle (a rim, not a seam along a side) and spans 3 cm or
|
|
3416
|
+
# more. [centre, outward, (radius, vertices, 0)] per end, cached per mesh.
|
|
3417
|
+
func _gk_mesh_ends(mesh: Mesh, info: Dictionary) -> PackedVector3Array:
|
|
3418
|
+
if info.has("gk_ends"):
|
|
3419
|
+
return info["gk_ends"]
|
|
3420
|
+
var bnd := _gk_boundary(mesh, info)
|
|
3421
|
+
var maab := mesh.get_aabb()
|
|
3422
|
+
var by_pt: Dictionary = {}
|
|
3423
|
+
var nedge := bnd.size() / 4
|
|
3424
|
+
for k in nedge:
|
|
3425
|
+
for q in [bnd[k * 4], bnd[k * 4 + 1]]:
|
|
3426
|
+
var key := Vector3i(((q as Vector3) * GK_WELD).round())
|
|
3427
|
+
if not by_pt.has(key):
|
|
3428
|
+
by_pt[key] = []
|
|
3429
|
+
(by_pt[key] as Array).append(k)
|
|
3430
|
+
var used := PackedByteArray()
|
|
3431
|
+
used.resize(nedge)
|
|
3432
|
+
var out := PackedVector3Array()
|
|
3433
|
+
for k0 in nedge:
|
|
3434
|
+
if used[k0] == 1:
|
|
3435
|
+
continue
|
|
3436
|
+
var loop := PackedVector3Array()
|
|
3437
|
+
var k := k0
|
|
3438
|
+
var cur: Vector3 = bnd[k * 4 + 1]
|
|
3439
|
+
used[k] = 1
|
|
3440
|
+
loop.append(bnd[k * 4])
|
|
3441
|
+
var guard := 0
|
|
3442
|
+
while guard < 4096:
|
|
3443
|
+
guard += 1
|
|
3444
|
+
loop.append(cur)
|
|
3445
|
+
var nxt := -1
|
|
3446
|
+
for e in (by_pt.get(Vector3i((cur * GK_WELD).round()), []) as Array):
|
|
3447
|
+
if used[int(e)] == 0:
|
|
3448
|
+
nxt = int(e)
|
|
3449
|
+
break
|
|
3450
|
+
if nxt < 0:
|
|
3451
|
+
break
|
|
3452
|
+
used[nxt] = 1
|
|
3453
|
+
var a: Vector3 = bnd[nxt * 4]
|
|
3454
|
+
var b: Vector3 = bnd[nxt * 4 + 1]
|
|
3455
|
+
cur = b if Vector3i((a * GK_WELD).round()) == Vector3i((cur * GK_WELD).round()) else a
|
|
3456
|
+
if loop.size() < 4:
|
|
3457
|
+
continue
|
|
3458
|
+
var face := -1
|
|
3459
|
+
for ax in 3:
|
|
3460
|
+
for side in 2:
|
|
3461
|
+
var plane: float = maab.end[ax] if side == 1 else maab.position[ax]
|
|
3462
|
+
var tol := maxf(0.01, maab.size[ax] * 0.02)
|
|
3463
|
+
var all_on := true
|
|
3464
|
+
for q in loop:
|
|
3465
|
+
if absf(q[ax] - plane) > tol:
|
|
3466
|
+
all_on = false
|
|
3467
|
+
break
|
|
3468
|
+
if all_on:
|
|
3469
|
+
face = ax * 2 + side
|
|
3470
|
+
if face < 0:
|
|
3471
|
+
continue
|
|
3472
|
+
var fa := face / 2
|
|
3473
|
+
var centre := Vector3.ZERO
|
|
3474
|
+
var lo := loop[0]
|
|
3475
|
+
var hi := loop[0]
|
|
3476
|
+
for q in loop:
|
|
3477
|
+
centre += q
|
|
3478
|
+
lo = lo.min(q)
|
|
3479
|
+
hi = hi.max(q)
|
|
3480
|
+
centre /= float(loop.size())
|
|
3481
|
+
# Enclosed area (Newell) against the loop's bounding rectangle.
|
|
3482
|
+
var area := Vector3.ZERO
|
|
3483
|
+
for i in loop.size():
|
|
3484
|
+
var p0: Vector3 = loop[i]
|
|
3485
|
+
var p1: Vector3 = loop[(i + 1) % loop.size()]
|
|
3486
|
+
area += p0.cross(p1)
|
|
3487
|
+
var ext := hi - lo
|
|
3488
|
+
var rect := 1.0
|
|
3489
|
+
for kk in 3:
|
|
3490
|
+
if kk != fa:
|
|
3491
|
+
rect *= maxf(ext[kk], 1e-6)
|
|
3492
|
+
var r := 0.0
|
|
3493
|
+
for q in loop:
|
|
3494
|
+
r = maxf(r, (q as Vector3).distance_to(centre))
|
|
3495
|
+
# A rim, not a bolt hole in a flange: at least a quarter of the face.
|
|
3496
|
+
var face_min := INF
|
|
3497
|
+
for kk in 3:
|
|
3498
|
+
if kk != fa:
|
|
3499
|
+
face_min = minf(face_min, maab.size[kk])
|
|
3500
|
+
if r < 0.015 or r < 0.25 * face_min or absf(area[fa]) * 0.5 < 0.5 * rect:
|
|
3501
|
+
continue
|
|
3502
|
+
var nrm := Vector3.ZERO
|
|
3503
|
+
nrm[fa] = 1.0 if face % 2 == 1 else -1.0
|
|
3504
|
+
out.append_array([centre, nrm, Vector3(r, loop.size(), 0)])
|
|
3505
|
+
info["gk_ends"] = out
|
|
3506
|
+
return out
|
|
3507
|
+
|
|
3508
|
+
|
|
3509
|
+
func _scan_fixture_ends() -> Dictionary:
|
|
3510
|
+
_gk_setup()
|
|
3511
|
+
if not _gk_sphere.is_valid():
|
|
3512
|
+
_gk_sphere = PhysicsServer3D.sphere_shape_create()
|
|
3513
|
+
_shapes.append(_gk_sphere)
|
|
3514
|
+
_gk_sphere_q = PhysicsShapeQueryParameters3D.new()
|
|
3515
|
+
_gk_sphere_q.collide_with_areas = false
|
|
3516
|
+
_gk_sphere_q.collision_mask = MASK_SOLID
|
|
3517
|
+
_gk_sphere_q.shape_rid = _gk_sphere
|
|
3518
|
+
var out: Array = []
|
|
3519
|
+
var dbg: Array = []
|
|
3520
|
+
var total := 0
|
|
3521
|
+
var done := 0
|
|
3522
|
+
var ends := 0
|
|
3523
|
+
for rec in _inst:
|
|
3524
|
+
var role := String(rec["role"])
|
|
3525
|
+
if not bool(rec["in"]) or (role != "prop" and role != "struct"):
|
|
3526
|
+
continue
|
|
3527
|
+
total += 1
|
|
3528
|
+
if _over():
|
|
3529
|
+
continue
|
|
3530
|
+
done += 1
|
|
3531
|
+
# The open rims of each simple mesh (at most FE_MAX_TRIS triangles):
|
|
3532
|
+
# [world centre, outward, radius].
|
|
3533
|
+
var cands: Array = []
|
|
3534
|
+
for m in (rec["meshes"] as Array):
|
|
3535
|
+
var mesh: Mesh = m[1]
|
|
3536
|
+
var info: Dictionary = _mesh_info.get(mesh.get_instance_id(), {})
|
|
3537
|
+
if info.is_empty() or int(info.get("tris", 0)) > FE_MAX_TRIS:
|
|
3538
|
+
continue
|
|
3539
|
+
var mxf: Transform3D = m[2]
|
|
3540
|
+
var ol := _gk_mesh_ends(mesh, info)
|
|
3541
|
+
for k in range(0, ol.size(), 3):
|
|
3542
|
+
cands.append([mxf * ol[k], (mxf.basis * ol[k + 1]).normalized(), ol[k + 2].x * mxf.basis.get_scale().x])
|
|
3543
|
+
# A run piece (a section, an elbow, a tee) has two or more rims of one
|
|
3544
|
+
# size facing different ways; an end piece (a cap, a funnel, an outlet
|
|
3545
|
+
# whose mouth differs from its socket) is open by design.
|
|
3546
|
+
if not _run_piece(cands):
|
|
3547
|
+
continue
|
|
3548
|
+
var ex: Array[RID] = []
|
|
3549
|
+
for r in (rec["bodies"] as Array):
|
|
3550
|
+
ex.append(r)
|
|
3551
|
+
for cd in cands:
|
|
3552
|
+
ends += 1
|
|
3553
|
+
var c: Vector3 = cd[0]
|
|
3554
|
+
var o: Vector3 = cd[1]
|
|
3555
|
+
var r: float = cd[2]
|
|
3556
|
+
# Sleeve tolerance: anything just beyond the opening, out to its
|
|
3557
|
+
# radius plus 2.5 cm (the next section, a coupler sleeve around the
|
|
3558
|
+
# joint, the wall the end runs into).
|
|
3559
|
+
PhysicsServer3D.shape_set_data(_gk_sphere, 0.5 * r + 0.025)
|
|
3560
|
+
_gk_sphere_q.transform = Transform3D(Basis(), c + o * (0.5 * r))
|
|
3561
|
+
_gk_sphere_q.exclude = ex
|
|
3562
|
+
var cover := _state.intersect_shape(_gk_sphere_q, 1)
|
|
3563
|
+
if not cover.is_empty():
|
|
3564
|
+
if dbg.size() < 120:
|
|
3565
|
+
dbg.append([int(rec["i"]), _a3(c), _a3(o, 0.01), snappedf(r, 0.001), "covered", int(_body_inst.get((cover[0]["rid"] as RID).get_id(), -1))])
|
|
3566
|
+
continue
|
|
3567
|
+
# Seen from the open side or beside it (a downpipe ending in mid-air
|
|
3568
|
+
# shows its cut end from eye height): within 107 deg of its axis.
|
|
3569
|
+
var ve := _gk_visible_eyes(c + o * 0.05, 1, 40, o, -0.3)
|
|
3570
|
+
if ve.is_empty():
|
|
3571
|
+
if dbg.size() < 120:
|
|
3572
|
+
dbg.append([int(rec["i"]), _a3(c), _a3(o, 0.01), snappedf(r, 0.001), "unseen", -1])
|
|
3573
|
+
continue
|
|
3574
|
+
out.append([int(rec["i"]), _a3(c), _a3(o, 0.001), snappedf(r, 0.001), _a3(ve[0], 0.01), "loops"])
|
|
3575
|
+
if out.size() >= 200:
|
|
3576
|
+
break
|
|
3577
|
+
_count("open_fixture_end", done, total)
|
|
3578
|
+
return {"ends": out, "dropped": dbg, "stats": {"pieces": total, "ends": ends}}
|
|
3579
|
+
|
|
3580
|
+
|
|
3581
|
+
# ---------------------------------------------------------------------------
|
|
3582
|
+
# depth_step (C): perpendicular ray rows across each facade line's visible
|
|
3583
|
+
# sides, at the band levels (from the band pieces on the line, else a 2 m
|
|
3584
|
+
# depth profile) and every 1.25 m between them; runs of recessed or proud
|
|
3585
|
+
# depth: band recesses and missing runs, seams, proud modules, holes. One
|
|
3586
|
+
# look-level row per merged run that walkable space sees.
|
|
3587
|
+
# ---------------------------------------------------------------------------
|
|
3588
|
+
|
|
3589
|
+
func _scan_depth_steps() -> Dictionary:
|
|
3590
|
+
_gk_setup()
|
|
3591
|
+
var raw: Array = []
|
|
3592
|
+
var rays_before := _rays
|
|
3593
|
+
var band_levels: Dictionary = {}
|
|
3594
|
+
for rec in _inst:
|
|
3595
|
+
if rec["role"] != "struct" or String(rec.get("why", "")) != "band":
|
|
3596
|
+
continue
|
|
3597
|
+
var ab: AABB = rec["waabb"]
|
|
3598
|
+
if ab.size.y >= 1.2 or maxf(ab.size.x, ab.size.z) < 0.5:
|
|
3599
|
+
continue
|
|
3600
|
+
var li := _gk_line_of(ab, 0.6)
|
|
3601
|
+
if li < 0:
|
|
3602
|
+
continue
|
|
3603
|
+
if not band_levels.has(li):
|
|
3604
|
+
band_levels[li] = []
|
|
3605
|
+
(band_levels[li] as Array).append([ab.position.y, ab.end.y])
|
|
3606
|
+
# The row plan per visible line side; every band row of every line runs
|
|
3607
|
+
# before any wall row (a budget stop leaves the wall rows).
|
|
3608
|
+
var band_rows: Array = []
|
|
3609
|
+
var wall_rows: Array = []
|
|
3610
|
+
for li in _gk_lines.size():
|
|
3611
|
+
var ln: Dictionary = _gk_lines[li]
|
|
3612
|
+
var in_scope := false
|
|
3613
|
+
for pi in (ln["pieces"] as Array):
|
|
3614
|
+
if bool((_inst[int(pi)] as Dictionary)["in"]):
|
|
3615
|
+
in_scope = true
|
|
3616
|
+
break
|
|
3617
|
+
if not in_scope:
|
|
3618
|
+
continue
|
|
3619
|
+
for sd in (ln["sides"] as Array):
|
|
3620
|
+
for row in _depth_rows(li, ln, int(sd), band_levels.get(li, []) as Array):
|
|
3621
|
+
if String(row["kind"]) == "band":
|
|
3622
|
+
band_rows.append(row)
|
|
3623
|
+
else:
|
|
3624
|
+
wall_rows.append(row)
|
|
3625
|
+
var total := band_rows.size() + wall_rows.size()
|
|
3626
|
+
var done := 0
|
|
3627
|
+
_q.collision_mask = MASK_SOLID
|
|
3628
|
+
_q.hit_back_faces = true
|
|
3629
|
+
_ex.clear()
|
|
3630
|
+
_q.exclude = _ex
|
|
3631
|
+
_ex_set = false
|
|
3632
|
+
for row in band_rows + wall_rows:
|
|
3633
|
+
if _over():
|
|
3634
|
+
break
|
|
3635
|
+
done += 1
|
|
3636
|
+
_depth_row(row, raw)
|
|
3637
|
+
_count("depth_step", done, total)
|
|
3638
|
+
var merged := _gk_merge_c(raw)
|
|
3639
|
+
return {"flags": merged, "stats": {"rows": total, "band_rows": band_rows.size(), "raw": raw.size(), "rays": _rays - rays_before}}
|
|
3640
|
+
|
|
3641
|
+
|
|
3642
|
+
# One line side's rows: each band level at mid height (and a quarter up when
|
|
3643
|
+
# 30 cm or taller), 3 cm steps; wall rows every 1.25 m between the bands, 5 cm
|
|
3644
|
+
# steps. Band levels come from the band pieces on the line, else from a depth
|
|
3645
|
+
# profile every 2 m (heights where 30% of the positions stand 1.5 cm or more
|
|
3646
|
+
# proud).
|
|
3647
|
+
func _depth_rows(li: int, ln: Dictionary, s: int, levels: Array) -> Array:
|
|
3648
|
+
var ax: int = ln["ax"]
|
|
3649
|
+
var th := 2 - ax
|
|
3650
|
+
var D := 1.2
|
|
3651
|
+
var face: float = float(ln["tmax"]) if s == 1 else float(ln["tmin"])
|
|
3652
|
+
var o_th := face + float(s) * D
|
|
3653
|
+
var t_th := face - float(s) * D
|
|
3654
|
+
var y0: float = ln["y0"]
|
|
3655
|
+
var y1: float = float(ln["y1"]) + 0.8
|
|
3656
|
+
var lo: float = ln["lo"]
|
|
3657
|
+
var hi: float = ln["hi"]
|
|
3658
|
+
var base := {"li": li, "ax": ax, "th": th, "face": face, "o_th": o_th, "t_th": t_th, "s": s, "lo": lo, "hi": hi, "D": D}
|
|
3659
|
+
var bands: Array = []
|
|
3660
|
+
if not levels.is_empty():
|
|
3661
|
+
levels.sort_custom(func(a, b): return float(a[0]) < float(b[0]))
|
|
3662
|
+
for lv in levels:
|
|
3663
|
+
if not bands.is_empty() and float(lv[0]) <= float(bands[-1][1]) + 0.02:
|
|
3664
|
+
bands[-1][1] = maxf(float(bands[-1][1]), float(lv[1]))
|
|
3665
|
+
else:
|
|
3666
|
+
bands.append([float(lv[0]), float(lv[1])])
|
|
3667
|
+
else:
|
|
3668
|
+
var prof: Array = []
|
|
3669
|
+
var sp := lo + 0.2537
|
|
3670
|
+
while sp < hi - 0.1:
|
|
3671
|
+
prof.append(sp)
|
|
3672
|
+
sp += 2.0
|
|
3673
|
+
var nh := int((y1 - y0) / 0.02)
|
|
3674
|
+
if prof.is_empty() or nh < 5:
|
|
3675
|
+
return []
|
|
3676
|
+
var depth_by_h: Array = []
|
|
3677
|
+
var all_d := PackedFloat32Array()
|
|
3678
|
+
for h in nh:
|
|
3679
|
+
var row := PackedFloat32Array()
|
|
3680
|
+
for spos in prof:
|
|
3681
|
+
var o := Vector3.ZERO
|
|
3682
|
+
o[ax] = float(spos)
|
|
3683
|
+
o[th] = o_th
|
|
3684
|
+
o.y = y0 + 0.0113 + h * 0.02
|
|
3685
|
+
var t := o
|
|
3686
|
+
t[th] = t_th
|
|
3687
|
+
var hit := _ray(o, t, MASK_SOLID, [], true)
|
|
3688
|
+
var dd: float = 9.0 if hit.is_empty() else (hit["position"] as Vector3).distance_to(o) - D
|
|
3689
|
+
row.append(dd)
|
|
3690
|
+
if dd < 2.0:
|
|
3691
|
+
all_d.append(dd)
|
|
3692
|
+
depth_by_h.append(row)
|
|
3693
|
+
if all_d.size() < 10:
|
|
3694
|
+
return []
|
|
3695
|
+
all_d.sort()
|
|
3696
|
+
var wall_d: float = all_d[int(all_d.size() * 0.6)]
|
|
3697
|
+
var h := 0
|
|
3698
|
+
while h < nh:
|
|
3699
|
+
var hj := h - 1
|
|
3700
|
+
while hj + 1 < nh:
|
|
3701
|
+
var r2: PackedFloat32Array = depth_by_h[hj + 1]
|
|
3702
|
+
var c2 := 0
|
|
3703
|
+
for dd in r2:
|
|
3704
|
+
if dd < wall_d - 0.015 and dd > wall_d - 0.6:
|
|
3705
|
+
c2 += 1
|
|
3706
|
+
if c2 * 10 < r2.size() * 3:
|
|
3707
|
+
break
|
|
3708
|
+
hj += 1
|
|
3709
|
+
if hj >= h:
|
|
3710
|
+
var by0 := y0 + h * 0.02
|
|
3711
|
+
var by1 := y0 + (hj + 1) * 0.02
|
|
3712
|
+
if by1 - by0 >= 0.04 and by1 - by0 <= 1.2:
|
|
3713
|
+
bands.append([by0, by1])
|
|
3714
|
+
h = hj + 1
|
|
3715
|
+
else:
|
|
3716
|
+
h += 1
|
|
3717
|
+
var rows: Array = []
|
|
3718
|
+
for b in bands:
|
|
3719
|
+
var bh: float = float(b[1]) - float(b[0])
|
|
3720
|
+
var r := base.duplicate()
|
|
3721
|
+
r.merge({"y": (float(b[0]) + float(b[1])) * 0.5, "kind": "band", "step": 0.03})
|
|
3722
|
+
rows.append(r)
|
|
3723
|
+
if bh >= 0.3:
|
|
3724
|
+
var r2 := base.duplicate()
|
|
3725
|
+
r2.merge({"y": float(b[0]) + bh * 0.25, "kind": "band", "step": 0.03})
|
|
3726
|
+
rows.append(r2)
|
|
3727
|
+
var wy := y0 + 0.2571
|
|
3728
|
+
while wy < y1 - 0.1:
|
|
3729
|
+
var inband := false
|
|
3730
|
+
for b in bands:
|
|
3731
|
+
if wy >= float(b[0]) - 0.03 and wy <= float(b[1]) + 0.03:
|
|
3732
|
+
inband = true
|
|
3733
|
+
if not inband:
|
|
3734
|
+
var r3 := base.duplicate()
|
|
3735
|
+
r3.merge({"y": wy, "kind": "wall", "step": 0.05})
|
|
3736
|
+
rows.append(r3)
|
|
3737
|
+
wy += C_WALL_ROW
|
|
3738
|
+
return rows
|
|
3739
|
+
|
|
3740
|
+
|
|
3741
|
+
func _depth_row(row: Dictionary, raw: Array) -> void:
|
|
3742
|
+
var ax: int = row["ax"]
|
|
3743
|
+
var th: int = row["th"]
|
|
3744
|
+
var step: float = row["step"]
|
|
3745
|
+
var lo: float = row["lo"]
|
|
3746
|
+
var hi: float = row["hi"]
|
|
3747
|
+
var D: float = row["D"]
|
|
3748
|
+
var s_lo := lo + 0.0037
|
|
3749
|
+
var npos := int((hi - s_lo) / step) + 1
|
|
3750
|
+
var dep := PackedFloat32Array()
|
|
3751
|
+
dep.resize(npos)
|
|
3752
|
+
var vals := PackedFloat32Array()
|
|
3753
|
+
var o := Vector3.ZERO
|
|
3754
|
+
o[th] = float(row["o_th"])
|
|
3755
|
+
o.y = float(row["y"])
|
|
3756
|
+
var t := o
|
|
3757
|
+
t[th] = float(row["t_th"])
|
|
3758
|
+
for k in npos:
|
|
3759
|
+
o[ax] = s_lo + k * step
|
|
3760
|
+
t[ax] = o[ax]
|
|
3761
|
+
_q.from = o
|
|
3762
|
+
_q.to = t
|
|
3763
|
+
var hit := _state.intersect_ray(_q)
|
|
3764
|
+
dep[k] = 9.0 if hit.is_empty() else (hit["position"] as Vector3).distance_to(o) - D
|
|
3765
|
+
if dep[k] < 2.0:
|
|
3766
|
+
vals.append(dep[k])
|
|
3767
|
+
_rays += npos
|
|
3768
|
+
if vals.size() < 10:
|
|
3769
|
+
return
|
|
3770
|
+
vals.sort()
|
|
3771
|
+
var ref: float = vals[int(vals.size() * 0.5)]
|
|
3772
|
+
var rf: Array = []
|
|
3773
|
+
_gk_scan_runs(rf, dep, ref, row, ax, th, float(row["face"]), int(row["s"]), s_lo, step, lo, hi, int(row["li"]))
|
|
3774
|
+
if String(row["kind"]) == "band":
|
|
3775
|
+
# A periodic row (sills, lintels, window heads) is not a band.
|
|
3776
|
+
var nrec := 0
|
|
3777
|
+
for f in rf:
|
|
3778
|
+
if String(f["kind"]).begins_with("band"):
|
|
3779
|
+
nrec += 1
|
|
3780
|
+
if nrec > maxi(3, int((hi - lo) / 3.5)):
|
|
3781
|
+
return
|
|
3782
|
+
raw.append_array(rf)
|
|
3783
|
+
|
|
3784
|
+
|
|
3785
|
+
func _gk_scan_runs(flags: Array, dep: PackedFloat32Array, ref: float, row: Dictionary, ax: int, th: int, face: float, s: int, s_lo: float, step: float, lo: float, hi: float, li: int) -> void:
|
|
3786
|
+
var n := dep.size()
|
|
3787
|
+
var k := 0
|
|
3788
|
+
var band := String(row["kind"]) == "band"
|
|
3789
|
+
while k < n:
|
|
3790
|
+
var rel := dep[k] - ref
|
|
3791
|
+
var rec := rel >= 0.03
|
|
3792
|
+
var proud := rel <= -0.015 and rel >= -0.10
|
|
3793
|
+
if not rec and not proud:
|
|
3794
|
+
k += 1
|
|
3795
|
+
continue
|
|
3796
|
+
var j := k
|
|
3797
|
+
if rec:
|
|
3798
|
+
while j + 1 < n and dep[j + 1] - ref >= 0.03:
|
|
3799
|
+
j += 1
|
|
3800
|
+
else:
|
|
3801
|
+
while j + 1 < n and dep[j + 1] - ref <= -0.015 and dep[j + 1] - ref >= -0.10:
|
|
3802
|
+
j += 1
|
|
3803
|
+
var w := (j - k + 1) * step
|
|
3804
|
+
var flush_l := k > 0 and absf(dep[k - 1] - ref) < 0.015
|
|
3805
|
+
var flush_r := j < n - 1 and absf(dep[j + 1] - ref) < 0.015
|
|
3806
|
+
var s0 := s_lo + k * step
|
|
3807
|
+
var s1 := s_lo + (j + 1) * step
|
|
3808
|
+
var inside := s0 >= lo - 0.02 and s1 <= hi + 0.02
|
|
3809
|
+
var mx := 0.0
|
|
3810
|
+
for q in range(k, j + 1):
|
|
3811
|
+
mx = maxf(mx, dep[q] - ref)
|
|
3812
|
+
var kind := ""
|
|
3813
|
+
# A miss (9.0: nothing within 1.2 m behind the face) is either an
|
|
3814
|
+
# opening right through or sky over a lower roof; _gk_merge_c keeps it
|
|
3815
|
+
# only where the facade bounds it (open: true).
|
|
3816
|
+
var open := mx >= 2.0
|
|
3817
|
+
if rec and band:
|
|
3818
|
+
if w <= 1.5 and (flush_l or flush_r) and mx <= 0.30:
|
|
3819
|
+
kind = "band_recess" if (flush_l and flush_r) else "band_end_recess"
|
|
3820
|
+
elif w > 1.5 and mx <= 0.30 and (flush_l or flush_r):
|
|
3821
|
+
kind = "band_missing_run"
|
|
3822
|
+
elif (flush_l or flush_r) and mx > 0.30 and w >= 0.06 and not open:
|
|
3823
|
+
kind = "band_through"
|
|
3824
|
+
elif rec:
|
|
3825
|
+
if w <= 0.06 and flush_l and flush_r:
|
|
3826
|
+
kind = "seam"
|
|
3827
|
+
elif w >= 0.9 and mx >= 0.50 and inside:
|
|
3828
|
+
kind = "hole"
|
|
3829
|
+
elif proud and not band:
|
|
3830
|
+
if w >= 0.9 and inside:
|
|
3831
|
+
kind = "proud"
|
|
3832
|
+
if kind != "":
|
|
3833
|
+
var p := Vector3.ZERO
|
|
3834
|
+
p[ax] = (s0 + s1) * 0.5
|
|
3835
|
+
p[th] = face
|
|
3836
|
+
p.y = float(row["y"])
|
|
3837
|
+
flags.append({"kind": kind, "c": p, "s0": s0, "s1": s1, "y": float(row["y"]), "depth": mx, "line": li, "side": s, "w": w, "open": open})
|
|
3838
|
+
k = j + 1
|
|
3839
|
+
|
|
3840
|
+
|
|
3841
|
+
# An open hole (rays that find nothing behind the face) is a hole only when
|
|
3842
|
+
# the facade bounds it: line pieces reaching its top row on both sides along
|
|
3843
|
+
# the line, or a piece above it. Sky over a lower block's roof is not.
|
|
3844
|
+
func _gk_bounded(li: int, s0: float, s1: float, y0: float, y1: float) -> bool:
|
|
3845
|
+
var ln: Dictionary = _gk_lines[li]
|
|
3846
|
+
var ax: int = ln["ax"]
|
|
3847
|
+
var left := false
|
|
3848
|
+
var right := false
|
|
3849
|
+
var above := false
|
|
3850
|
+
var mid := (s0 + s1) * 0.5
|
|
3851
|
+
for pi in (ln["pieces"] as Array):
|
|
3852
|
+
var b: AABB = (_inst[int(pi)] as Dictionary)["waabb"]
|
|
3853
|
+
if b.position[ax] <= mid and b.end[ax] >= mid and b.position.y >= y1 - 0.1:
|
|
3854
|
+
above = true
|
|
3855
|
+
if b.position.y > y1 or b.end.y < y1 - 0.05:
|
|
3856
|
+
continue
|
|
3857
|
+
if b.end[ax] >= s0 - 0.15 and b.position[ax] < s0 - 0.05:
|
|
3858
|
+
left = true
|
|
3859
|
+
if b.position[ax] <= s1 + 0.15 and b.end[ax] > s1 + 0.05:
|
|
3860
|
+
right = true
|
|
3861
|
+
return above or (left and right)
|
|
3862
|
+
|
|
3863
|
+
|
|
3864
|
+
# Merge runs of one line, side and kind that overlap along the line and lie
|
|
3865
|
+
# within one wall-row step (1.3 m) in height; holes and proud runs need two
|
|
3866
|
+
# rows; every run must be seen from walkable space. [kind, line, side, s0, s1, y0, y1, rows,
|
|
3867
|
+
# depth, centre].
|
|
3868
|
+
func _gk_merge_c(raw: Array) -> Array:
|
|
3869
|
+
var out: Array = []
|
|
3870
|
+
for f in raw:
|
|
3871
|
+
var merged := false
|
|
3872
|
+
for o in out:
|
|
3873
|
+
if int(o["line"]) == int(f["line"]) and int(o["side"]) == int(f["side"]) and o["kind"] == f["kind"] and float(f["s0"]) <= float(o["s1"]) + 0.03 and float(f["s1"]) >= float(o["s0"]) - 0.03 and float(f["y"]) >= float(o["y0"]) - C_MERGE_Y and float(f["y"]) <= float(o["y1"]) + C_MERGE_Y:
|
|
3874
|
+
o["s0"] = minf(float(o["s0"]), float(f["s0"]))
|
|
3875
|
+
o["s1"] = maxf(float(o["s1"]), float(f["s1"]))
|
|
3876
|
+
o["y0"] = minf(float(o["y0"]), float(f["y"]))
|
|
3877
|
+
o["y1"] = maxf(float(o["y1"]), float(f["y"]))
|
|
3878
|
+
o["rows"] = int(o["rows"]) + 1
|
|
3879
|
+
o["depth"] = maxf(float(o["depth"]), float(f["depth"]))
|
|
3880
|
+
o["open"] = bool(o["open"]) or bool(f.get("open", false))
|
|
3881
|
+
merged = true
|
|
3882
|
+
break
|
|
3883
|
+
if not merged:
|
|
3884
|
+
out.append({"kind": f["kind"], "line": f["line"], "side": f["side"], "s0": f["s0"], "s1": f["s1"], "y0": f["y"], "y1": f["y"], "rows": 1, "depth": f["depth"], "c": f["c"], "open": bool(f.get("open", false))})
|
|
3885
|
+
var res: Array = []
|
|
3886
|
+
for o in out:
|
|
3887
|
+
if (o["kind"] == "hole" or o["kind"] == "proud") and int(o["rows"]) < 2:
|
|
3888
|
+
continue
|
|
3889
|
+
if o["kind"] == "hole" and bool(o["open"]) and not _gk_bounded(int(o["line"]), float(o["s0"]), float(o["s1"]), float(o["y0"]), float(o["y1"])):
|
|
3890
|
+
continue
|
|
3891
|
+
var ln: Dictionary = _gk_lines[int(o["line"])]
|
|
3892
|
+
var ax: int = ln["ax"]
|
|
3893
|
+
var p: Vector3 = o["c"]
|
|
3894
|
+
p[ax] = (float(o["s0"]) + float(o["s1"])) * 0.5
|
|
3895
|
+
p.y = (float(o["y0"]) + float(o["y1"])) * 0.5
|
|
3896
|
+
if not _gk_point_visible(p, 2 - ax):
|
|
3897
|
+
continue
|
|
3898
|
+
res.append([String(o["kind"]), int(o["line"]), int(o["side"]), snappedf(float(o["s0"]), 0.01), snappedf(float(o["s1"]), 0.01), snappedf(float(o["y0"]), 0.01), snappedf(float(o["y1"]), 0.01), int(o["rows"]), snappedf(float(o["depth"]), 0.001), _a3(p)])
|
|
3899
|
+
if res.size() >= 300:
|
|
3900
|
+
break
|
|
3901
|
+
return res
|
|
3902
|
+
|
|
3903
|
+
|
|
3904
|
+
return out
|
|
3905
|
+
|
|
3906
|
+
|
|
3907
|
+
# ---------------------------------------------------------------------------
|
|
3908
|
+
# lights: per-mesh omni/spot pairing (light culling AABB vs mesh AABB),
|
|
3909
|
+
# soft-rim spots, shadowed lights
|
|
3910
|
+
# ---------------------------------------------------------------------------
|
|
3911
|
+
|
|
3912
|
+
func _light_aabb(light: Light3D) -> AABB:
|
|
3913
|
+
var xf := light.global_transform
|
|
3914
|
+
var local := AABB()
|
|
3915
|
+
if light is OmniLight3D:
|
|
3916
|
+
var r := (light as OmniLight3D).omni_range
|
|
3917
|
+
local = AABB(-Vector3(r, r, r), Vector3(r, r, r) * 2.0)
|
|
3918
|
+
elif light is SpotLight3D:
|
|
3919
|
+
var sp := light as SpotLight3D
|
|
3920
|
+
var size := tan(deg_to_rad(minf(sp.spot_angle, 89.0))) * sp.spot_range
|
|
3921
|
+
local = AABB(Vector3(-size, -size, -sp.spot_range), Vector3(size * 2.0, size * 2.0, sp.spot_range))
|
|
3922
|
+
var out := AABB()
|
|
3923
|
+
var first := true
|
|
3924
|
+
for c in _corners(local):
|
|
3925
|
+
var w: Vector3 = xf * c
|
|
3926
|
+
if first:
|
|
3927
|
+
out = AABB(w, Vector3.ZERO)
|
|
3928
|
+
first = false
|
|
3929
|
+
else:
|
|
3930
|
+
out = out.expand(w)
|
|
3931
|
+
return out
|
|
3932
|
+
|
|
3933
|
+
|
|
3934
|
+
func _scan_lights() -> Dictionary:
|
|
3935
|
+
var limit := int(ProjectSettings.get_setting("rendering/limits/opengl/max_lights_per_object", 8))
|
|
3936
|
+
var omni: Array = []
|
|
3937
|
+
var spot: Array = []
|
|
3938
|
+
var counts := {"omni": 0, "spot": 0, "directional": 0}
|
|
3939
|
+
var shadowed := {"omni": 0, "spot": 0, "directional": 0}
|
|
3940
|
+
var hard_rim: Array = []
|
|
3941
|
+
for light in _lights:
|
|
3942
|
+
var l := light as Light3D
|
|
3943
|
+
if not l.is_visible_in_tree():
|
|
3944
|
+
continue
|
|
3945
|
+
var kind := "directional"
|
|
3946
|
+
if l is OmniLight3D:
|
|
3947
|
+
kind = "omni"
|
|
3948
|
+
omni.append([l, _light_aabb(l)])
|
|
3949
|
+
elif l is SpotLight3D:
|
|
3950
|
+
kind = "spot"
|
|
3951
|
+
spot.append([l, _light_aabb(l)])
|
|
3952
|
+
var att := (l as SpotLight3D).spot_angle_attenuation
|
|
3953
|
+
if att < 3.0 and _in_root(l):
|
|
3954
|
+
hard_rim.append([_rel(l), snappedf(att, 0.001), _a3(l.global_position)])
|
|
3955
|
+
counts[kind] = int(counts[kind]) + 1
|
|
3956
|
+
if l.shadow_enabled:
|
|
3957
|
+
shadowed[kind] = int(shadowed[kind]) + 1
|
|
3958
|
+
var over: Array = []
|
|
3959
|
+
var done := 0
|
|
3960
|
+
var total := 0
|
|
3961
|
+
for rec in _inst:
|
|
3962
|
+
if not bool(rec["in"]):
|
|
3963
|
+
continue
|
|
3964
|
+
total += 1
|
|
3965
|
+
if _over():
|
|
3966
|
+
continue
|
|
3967
|
+
done += 1
|
|
3968
|
+
var worst: Array = []
|
|
3969
|
+
for m in (rec["meshes"] as Array):
|
|
3970
|
+
var node: Node = m[0]
|
|
3971
|
+
var layers := 1
|
|
3972
|
+
if node is VisualInstance3D:
|
|
3973
|
+
layers = (node as VisualInstance3D).layers
|
|
3974
|
+
var box: AABB = (m[2] as Transform3D) * (m[1] as Mesh).get_aabb()
|
|
3975
|
+
var no := 0
|
|
3976
|
+
var ns := 0
|
|
3977
|
+
var names: Array = []
|
|
3978
|
+
for pair in omni:
|
|
3979
|
+
if (pair[0] as Light3D).light_cull_mask & layers and (pair[1] as AABB).intersects(box):
|
|
3980
|
+
no += 1
|
|
3981
|
+
if names.size() < 3:
|
|
3982
|
+
names.append(String((pair[0] as Node).name))
|
|
3983
|
+
for pair in spot:
|
|
3984
|
+
if (pair[0] as Light3D).light_cull_mask & layers and (pair[1] as AABB).intersects(box):
|
|
3985
|
+
ns += 1
|
|
3986
|
+
if names.size() < 3:
|
|
3987
|
+
names.append(String((pair[0] as Node).name))
|
|
3988
|
+
if (no > limit or ns > limit) and (worst.is_empty() or maxi(no, ns) > maxi(int(worst[2]), int(worst[3]))):
|
|
3989
|
+
worst = [int(rec["i"]), _rel(node), no, ns, names]
|
|
3990
|
+
if not worst.is_empty():
|
|
3991
|
+
over.append(worst)
|
|
3992
|
+
_count("lights", done, total)
|
|
3993
|
+
var renderer := RenderingServer.get_current_rendering_method()
|
|
3994
|
+
return {"renderer": renderer, "limit": limit, "counts": counts, "shadowed": shadowed, "over": over, "hard_rim": hard_rim}
|
|
3995
|
+
|
|
3996
|
+
|
|
3997
|
+
# ---------------------------------------------------------------------------
|
|
3998
|
+
# resource: missing dependency files, empty meshes, surfaces without material
|
|
3999
|
+
# ---------------------------------------------------------------------------
|
|
4000
|
+
|
|
4001
|
+
func _scan_resources() -> Dictionary:
|
|
4002
|
+
var missing: Array = []
|
|
4003
|
+
var checked: Dictionary = {}
|
|
4004
|
+
var scenes: Dictionary = {}
|
|
4005
|
+
var root_scene := String(_cfg.get("scene_path", ""))
|
|
4006
|
+
if root_scene != "":
|
|
4007
|
+
scenes[root_scene] = -1
|
|
4008
|
+
for rec in _inst:
|
|
4009
|
+
var sc := String(rec["scene"])
|
|
4010
|
+
if sc != "" and not scenes.has(sc):
|
|
4011
|
+
scenes[sc] = int(rec["i"])
|
|
4012
|
+
var budget := 3000
|
|
4013
|
+
var queue: Array = []
|
|
4014
|
+
for sc in scenes:
|
|
4015
|
+
queue.append([sc, sc, int(scenes[sc]), 0])
|
|
4016
|
+
while not queue.is_empty() and budget > 0:
|
|
4017
|
+
if _over():
|
|
4018
|
+
break
|
|
4019
|
+
var item: Array = queue.pop_front()
|
|
4020
|
+
var path := String(item[0])
|
|
4021
|
+
if checked.has(path):
|
|
4022
|
+
continue
|
|
4023
|
+
checked[path] = true
|
|
4024
|
+
budget -= 1
|
|
4025
|
+
if not path.begins_with("res://"):
|
|
4026
|
+
continue
|
|
4027
|
+
for dep in ResourceLoader.get_dependencies(path):
|
|
4028
|
+
var dp := String(dep)
|
|
4029
|
+
if dp.get_slice_count("::") >= 3:
|
|
4030
|
+
dp = dp.get_slice("::", 2)
|
|
4031
|
+
if dp == "" or not dp.begins_with("res://"):
|
|
4032
|
+
continue
|
|
4033
|
+
if not ResourceLoader.exists(dp):
|
|
4034
|
+
if missing.size() < 32:
|
|
4035
|
+
missing.append([dp, String(item[1]), int(item[2])])
|
|
4036
|
+
continue
|
|
4037
|
+
var ext := dp.get_extension().to_lower()
|
|
4038
|
+
if int(item[3]) < 2 and (ext == "tres" or ext == "res" or ext == "material" or ext == "tscn"):
|
|
4039
|
+
queue.append([dp, String(item[1]), int(item[2]), int(item[3]) + 1])
|
|
4040
|
+
_count("resource", checked.size(), checked.size() + (queue.size() if budget > 0 else 0))
|
|
4041
|
+
var no_mat: Array = []
|
|
4042
|
+
for rec in _inst:
|
|
4043
|
+
if not bool(rec["in"]):
|
|
4044
|
+
continue
|
|
4045
|
+
var count := 0
|
|
4046
|
+
for m in (rec["meshes"] as Array):
|
|
4047
|
+
var node: Node = m[0]
|
|
4048
|
+
var mesh: Mesh = m[1]
|
|
4049
|
+
if not (node is MeshInstance3D):
|
|
4050
|
+
continue
|
|
4051
|
+
var mi := node as MeshInstance3D
|
|
4052
|
+
if mi.material_override != null:
|
|
4053
|
+
continue
|
|
4054
|
+
for s in mesh.get_surface_count():
|
|
4055
|
+
if mesh.surface_get_material(s) == null and mi.get_surface_override_material(s) == null:
|
|
4056
|
+
count += 1
|
|
4057
|
+
else:
|
|
4058
|
+
var mat := mi.get_surface_override_material(s)
|
|
4059
|
+
if mat == null:
|
|
4060
|
+
mat = mesh.surface_get_material(s)
|
|
4061
|
+
if mat is ShaderMaterial and (mat as ShaderMaterial).shader == null and _shader_no_code.size() < 16:
|
|
4062
|
+
_shader_no_code.append([int(rec["i"]), _rel(mi), s])
|
|
4063
|
+
if count > 0 and no_mat.size() < 64:
|
|
4064
|
+
no_mat.append([int(rec["i"]), count])
|
|
4065
|
+
var empty: Array = []
|
|
4066
|
+
for e in _empty_meshes:
|
|
4067
|
+
empty.append(e)
|
|
4068
|
+
return {"missing": missing, "checked": checked.size(), "empty": empty, "no_material": no_mat, "shader_missing": _shader_no_code}
|
|
4069
|
+
|
|
4070
|
+
|
|
4071
|
+
# ---------------------------------------------------------------------------
|
|
4072
|
+
# Clearance around each instance for framing (render: "sheet"): 8 directions
|
|
4073
|
+
# at 20 deg and 45 deg up, two-sided (any surface counts, front or back).
|
|
4074
|
+
# ---------------------------------------------------------------------------
|
|
4075
|
+
|
|
4076
|
+
func _scan_clearances() -> void:
|
|
4077
|
+
var done := 0
|
|
4078
|
+
var total := 0
|
|
4079
|
+
for rec in _inst:
|
|
4080
|
+
if not bool(rec["in"]):
|
|
4081
|
+
continue
|
|
4082
|
+
total += 1
|
|
4083
|
+
if _over():
|
|
4084
|
+
continue
|
|
4085
|
+
done += 1
|
|
4086
|
+
var wa: AABB = rec["waabb"]
|
|
4087
|
+
var c := wa.get_center()
|
|
4088
|
+
var ex: Array = rec["bodies"]
|
|
4089
|
+
var rows: Array = []
|
|
4090
|
+
for elev in [20.0, 45.0]:
|
|
4091
|
+
var ce := cos(deg_to_rad(elev))
|
|
4092
|
+
var se := sin(deg_to_rad(elev))
|
|
4093
|
+
for k in 8:
|
|
4094
|
+
var ang := TAU * float(k) / 8.0
|
|
4095
|
+
var dir := Vector3(sin(ang) * ce, se, cos(ang) * ce)
|
|
4096
|
+
rows.append(snappedf(_clear(c, dir, 10.0, ex), 0.01))
|
|
4097
|
+
rec["clear"] = rows
|
|
4098
|
+
_count("poses", done, total)
|
|
4099
|
+
|
|
4100
|
+
|
|
4101
|
+
# ---------------------------------------------------------------------------
|
|
4102
|
+
# Instance rows (compact): what the TypeScript side needs for every check.
|
|
4103
|
+
# ---------------------------------------------------------------------------
|
|
4104
|
+
|
|
4105
|
+
func _instance_rows() -> Array:
|
|
4106
|
+
var rows: Array = []
|
|
4107
|
+
for rec in _inst:
|
|
4108
|
+
var xf: Transform3D = rec["xf"]
|
|
4109
|
+
var b := xf.basis
|
|
4110
|
+
var wa: AABB = rec["waabb"]
|
|
4111
|
+
var la: AABB = rec["laabb"]
|
|
4112
|
+
var bad := not (is_finite(xf.origin.x) and is_finite(xf.origin.y) and is_finite(xf.origin.z) and is_finite(b.x.x) and is_finite(b.y.y) and is_finite(b.z.z) and is_finite(b.x.y) and is_finite(b.x.z) and is_finite(b.y.x) and is_finite(b.y.z) and is_finite(b.z.x) and is_finite(b.z.y))
|
|
4113
|
+
var row := {
|
|
4114
|
+
"p": rec["path"], "k": rec["piece"], "s": rec["scene"], "r": rec["role"], "in": rec["in"],
|
|
4115
|
+
"o": _a3(xf.origin, 0.0001), "b": _b9(b), "sc": [snappedf(b.x.length(), 0.0001), snappedf(b.y.length(), 0.0001), snappedf(b.z.length(), 0.0001)],
|
|
4116
|
+
"det": snappedf(b.determinant(), 0.0001), "c": _a3(wa.get_center()), "e": _a3(wa.size), "le": _a3(la.size), "lc": _a3(la.get_center()),
|
|
4117
|
+
"m": (rec["meshes"] as Array).size(), "f": _a3(rec.get("front", Vector3.ZERO), 1.0),
|
|
4118
|
+
}
|
|
4119
|
+
if bad:
|
|
4120
|
+
row["nan"] = true
|
|
4121
|
+
var node: Node = rec["node"]
|
|
4122
|
+
if node is Node3D:
|
|
4123
|
+
row["lo"] = _a3((node as Node3D).position, 0.0001)
|
|
4124
|
+
# transform: an identity LOCAL transform under an identity parent is
|
|
4125
|
+
# what a never-placed piece looks like; a placed one is not.
|
|
4126
|
+
row["li"] = _is_identity((node as Node3D).transform)
|
|
4127
|
+
var par := node.get_parent()
|
|
4128
|
+
row["pi"] = not (par is Node3D) or _is_identity((par as Node3D).global_transform)
|
|
4129
|
+
if rec.has("clear"):
|
|
4130
|
+
row["cl"] = rec["clear"]
|
|
4131
|
+
# Why it has its role (geometry and engine data: see _classify_shapes).
|
|
4132
|
+
row["w"] = String(rec.get("why", ""))
|
|
4133
|
+
if rec.has("line"):
|
|
4134
|
+
row["ln"] = int(rec["line"])
|
|
4135
|
+
rows.append(row)
|
|
4136
|
+
return rows
|