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,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart framing, part 2: score what each candidate pose would see. Pure and
|
|
3
|
+
* engine-free; the inputs are the engine's measurements (thick-sweep
|
|
4
|
+
* visibility codes, near-lens/low-angle adjustments, and a ray grid through
|
|
5
|
+
* the frame with a class and a distance per cell).
|
|
6
|
+
*
|
|
7
|
+
* Terms (each 0..1): visibility, fill, thirds, horizon, sky, void, depth,
|
|
8
|
+
* foreground, clear, balance, detail, contrast, entry, solid (no surfaces
|
|
9
|
+
* seen from behind), light (key-light direction) and edge (no world edge
|
|
10
|
+
* below the horizon). Weights depend on the shot type. Hard rejections
|
|
11
|
+
* (blocked by walls/terrain, camera behind or inside a surface, camera in
|
|
12
|
+
* geometry, subject out of frame, too much clutter, an eye-level or corridor
|
|
13
|
+
* camera off eye height) never rank, but are counted by reason. Establishing
|
|
14
|
+
* shots rank in tiers: a pose that shows more than EMPTY_LIMIT empty ground or
|
|
15
|
+
* world edge ranks below every pose that shows less, whatever the weights.
|
|
16
|
+
*/
|
|
17
|
+
import { SHOT_DEFAULTS } from "./candidates.js";
|
|
18
|
+
import { aabbCenter, clamp, deg, distance, frameFill, horizonV, pitchOf, project, rayDirection, screenBox, normalize, sub, } from "./math.js";
|
|
19
|
+
export const SCORE_TERMS = ["visibility", "fill", "thirds", "horizon", "sky", "void", "depth", "foreground", "clear", "balance", "detail", "contrast", "entry", "solid", "light", "edge"];
|
|
20
|
+
/** Establishing: "about 15%" empty ground or world edge is decisive. */
|
|
21
|
+
export const EMPTY_LIMIT = 0.15;
|
|
22
|
+
export const SHOT_PROFILES = {
|
|
23
|
+
establishing: {
|
|
24
|
+
fillTarget: SHOT_DEFAULTS.establishing.fill,
|
|
25
|
+
fillTolerance: 0.22,
|
|
26
|
+
sky: [0.12, 0.38],
|
|
27
|
+
foregroundTarget: 0.08,
|
|
28
|
+
foregroundMax: 0.3,
|
|
29
|
+
weights: { visibility: 3, fill: 2, thirds: 1, horizon: 1, sky: 1, void: 1.5, depth: 1.5, foreground: 0.5, clear: 1, balance: 0, detail: 1.5, contrast: 0.75, entry: 0, solid: 2, light: 1.25, edge: 2 },
|
|
30
|
+
emptyLimit: EMPTY_LIMIT,
|
|
31
|
+
},
|
|
32
|
+
eye_level: {
|
|
33
|
+
fillTarget: SHOT_DEFAULTS.eye_level.fill,
|
|
34
|
+
fillTolerance: 0.3,
|
|
35
|
+
sky: [0.08, 0.35],
|
|
36
|
+
foregroundTarget: 0.1,
|
|
37
|
+
foregroundMax: 0.35,
|
|
38
|
+
weights: { visibility: 2, fill: 1, thirds: 1, horizon: 1, sky: 1, void: 1.5, depth: 2, foreground: 0.5, clear: 1.5, balance: 0.5, detail: 1.5, contrast: 0.75, entry: 0, solid: 2, light: 0.5, edge: 1 },
|
|
39
|
+
},
|
|
40
|
+
low_angle: {
|
|
41
|
+
fillTarget: SHOT_DEFAULTS.low_angle.fill,
|
|
42
|
+
fillTolerance: 0.22,
|
|
43
|
+
sky: [0.2, 0.6],
|
|
44
|
+
foregroundTarget: 0.08,
|
|
45
|
+
foregroundMax: 0.3,
|
|
46
|
+
weights: { visibility: 3, fill: 2, thirds: 1, horizon: 0.5, sky: 1.5, void: 1, depth: 0.5, foreground: 0.5, clear: 1, balance: 0, detail: 1.5, contrast: 0.75, entry: 0, solid: 2, light: 0.75, edge: 1 },
|
|
47
|
+
},
|
|
48
|
+
detail: {
|
|
49
|
+
fillTarget: SHOT_DEFAULTS.detail.fill,
|
|
50
|
+
fillTolerance: 0.2,
|
|
51
|
+
sky: [0, 0.12],
|
|
52
|
+
foregroundTarget: 0.05,
|
|
53
|
+
foregroundMax: 0.25,
|
|
54
|
+
weights: { visibility: 3, fill: 2.5, thirds: 1, horizon: 0.5, sky: 0.5, void: 1, depth: 1, foreground: 0.3, clear: 1, balance: 0, detail: 1.5, contrast: 0.75, entry: 0, solid: 2, light: 0.75, edge: 1 },
|
|
55
|
+
},
|
|
56
|
+
corridor: {
|
|
57
|
+
fillTarget: 0,
|
|
58
|
+
fillTolerance: 1,
|
|
59
|
+
sky: [0.03, 0.3],
|
|
60
|
+
foregroundTarget: 0.08,
|
|
61
|
+
foregroundMax: 0.3,
|
|
62
|
+
weights: { visibility: 3, fill: 0, thirds: 0, horizon: 1, sky: 1, void: 1, depth: 3, foreground: 0.5, clear: 2, balance: 1.5, detail: 1.5, contrast: 0.75, entry: 1.5, solid: 3, light: 0, edge: 0.5 },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
/** A frame showing more than this share of surfaces from behind is a camera
|
|
66
|
+
* behind (or inside) a wall: the image would look through it. */
|
|
67
|
+
export const MAX_BACK_SHARE = 0.25;
|
|
68
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
69
|
+
function inRangeScore(value, [lo, hi], falloff = 0.25) {
|
|
70
|
+
if (value >= lo && value <= hi)
|
|
71
|
+
return 1;
|
|
72
|
+
const gap = value < lo ? lo - value : value - hi;
|
|
73
|
+
return Math.max(0, 1 - gap / falloff);
|
|
74
|
+
}
|
|
75
|
+
export function foregroundScore(share, target, max) {
|
|
76
|
+
if (share <= target)
|
|
77
|
+
return 0.6 + 0.4 * (target > 0 ? share / target : 1);
|
|
78
|
+
if (share <= max)
|
|
79
|
+
return 1 - (0.5 * (share - target)) / Math.max(1e-6, max - target);
|
|
80
|
+
return Math.max(0, 0.5 - (share - max) * 2);
|
|
81
|
+
}
|
|
82
|
+
export function thirdsScore(pose, aspect, box) {
|
|
83
|
+
const s = screenBox(pose, aspect, box);
|
|
84
|
+
const cu = (s.u0 + s.u1) / 2;
|
|
85
|
+
const cv = (s.v0 + s.v1) / 2;
|
|
86
|
+
const wide = !(s.rawWidth <= 0.6);
|
|
87
|
+
const tall = !(s.rawHeight <= 0.6);
|
|
88
|
+
const horizontal = wide
|
|
89
|
+
? 1 - Math.min(1, Math.abs(cu - 0.5) * 2)
|
|
90
|
+
: 1 - Math.min(1, Math.min(Math.abs(cu - 1 / 3), Math.abs(cu - 2 / 3)) / 0.17);
|
|
91
|
+
const vertical = tall
|
|
92
|
+
? 1 - Math.min(1, Math.abs(cv - 0.5) * 2)
|
|
93
|
+
: 1 - Math.min(1, Math.min(Math.abs(cv - 1 / 3), Math.abs(cv - 2 / 3), Math.abs(cv - 0.5) * 1.5) / 0.25);
|
|
94
|
+
return 0.7 * horizontal + 0.3 * vertical;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Poses are built with a level up vector, so roll is always zero (a level
|
|
98
|
+
* horizon by construction); what is left to judge is where the horizon cuts
|
|
99
|
+
* the frame. Eye-level and corridor shots are one-point perspective views
|
|
100
|
+
* where a centered horizon is natural; elsewhere a horizon through the dead
|
|
101
|
+
* center splits the frame in two and is marked down.
|
|
102
|
+
*/
|
|
103
|
+
export function horizonScore(pose, shot = "establishing") {
|
|
104
|
+
const v = horizonV(pitchOf(pose), pose.fov);
|
|
105
|
+
if (shot === "eye_level" || shot === "corridor")
|
|
106
|
+
return v === null ? 0.85 : 1;
|
|
107
|
+
if (v === null)
|
|
108
|
+
return 0.75;
|
|
109
|
+
return 1 - 0.6 * Math.max(0, 1 - Math.abs(v - 0.5) / 0.1);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* One featureless grid cell (image check tex "0") is empty ground when its ray
|
|
113
|
+
* points below the horizon and meets non-subject geometry outside the
|
|
114
|
+
* subject's footprint (+ 0.5 m) and below its middle: the bare ground or
|
|
115
|
+
* fogged backing plane around the subject, not the subject's own floor.
|
|
116
|
+
*/
|
|
117
|
+
export function isEmptyGround(code, d, ray, pose, subject) {
|
|
118
|
+
if (!subject || ray[1] >= 0 || d < 0 || (code !== "H" && code !== "F" && code !== "T"))
|
|
119
|
+
return false;
|
|
120
|
+
const hit = [pose.position[0] + ray[0] * d, pose.position[1] + ray[1] * d, pose.position[2] + ray[2] * d];
|
|
121
|
+
const lo = subject.position;
|
|
122
|
+
const margin = 0.5;
|
|
123
|
+
const inside = hit[0] >= lo[0] - margin && hit[0] <= lo[0] + subject.size[0] + margin && hit[2] >= lo[2] - margin && hit[2] <= lo[2] + subject.size[2] + margin;
|
|
124
|
+
return !inside && hit[1] <= lo[1] + subject.size[1] / 2;
|
|
125
|
+
}
|
|
126
|
+
export function frameStats(m, pose, opts) {
|
|
127
|
+
const grid = m.grid ?? "";
|
|
128
|
+
const dists = m.dist ?? [];
|
|
129
|
+
const total = Math.max(1, grid.length);
|
|
130
|
+
const subjectDist = opts.subject && opts.shot !== "corridor" ? distance(pose.position, aabbCenter(opts.subject)) : undefined;
|
|
131
|
+
let sky = 0;
|
|
132
|
+
let voids = 0;
|
|
133
|
+
let subject = 0;
|
|
134
|
+
let hard = 0;
|
|
135
|
+
let soft = 0;
|
|
136
|
+
let foreground = 0;
|
|
137
|
+
let wallBehind = 0;
|
|
138
|
+
let back = 0;
|
|
139
|
+
let translucent = 0;
|
|
140
|
+
let nearHard = 0;
|
|
141
|
+
let nearLeft = 0;
|
|
142
|
+
let nearRight = 0;
|
|
143
|
+
// Walls a metre or two away are what a corridor IS; only at the lens are
|
|
144
|
+
// they a blocked view.
|
|
145
|
+
const nearLimit = opts.shot === "corridor" ? 1.0 : Math.max(2.5, (subjectDist ?? 0) * 0.15);
|
|
146
|
+
const sideLimit = Math.max(4, (subjectDist ?? 0) * 0.3);
|
|
147
|
+
let depthSum = 0;
|
|
148
|
+
let depthCount = 0;
|
|
149
|
+
for (let k = 0; k < grid.length; k++) {
|
|
150
|
+
const code = grid[k];
|
|
151
|
+
const d = dists[k] ?? -1;
|
|
152
|
+
const col = k % opts.gridCols;
|
|
153
|
+
const row = Math.floor(k / opts.gridCols);
|
|
154
|
+
if (code === ".") {
|
|
155
|
+
const dir = rayDirection(pose, opts.aspect, (col + 0.5) / opts.gridCols, (row + 0.5) / opts.gridRows);
|
|
156
|
+
if (dir[1] >= 0) {
|
|
157
|
+
sky += 1;
|
|
158
|
+
depthSum += 1;
|
|
159
|
+
depthCount += 1;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
voids += 1;
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (code === "S") {
|
|
167
|
+
subject += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (code === "B") {
|
|
171
|
+
// Not drawn: the image shows whatever lies beyond it.
|
|
172
|
+
back += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const isFront = subjectDist !== undefined ? d >= 0 && d < subjectDist * 0.75 : d >= 0 && d < 6;
|
|
176
|
+
if (code === "F" || code === "T") {
|
|
177
|
+
// A transparent surface is see-through cover: half a soft cell.
|
|
178
|
+
const weight = code === "T" ? 0.5 : 1;
|
|
179
|
+
soft += weight;
|
|
180
|
+
if (code === "T")
|
|
181
|
+
translucent += 1;
|
|
182
|
+
if (isFront) {
|
|
183
|
+
foreground += weight;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
hard += 1;
|
|
189
|
+
if (d >= 0 && d < nearLimit)
|
|
190
|
+
nearHard += 1;
|
|
191
|
+
if (d >= 0 && d < sideLimit) {
|
|
192
|
+
if (col < opts.gridCols / 2)
|
|
193
|
+
nearLeft += 1;
|
|
194
|
+
else
|
|
195
|
+
nearRight += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (d < 0)
|
|
199
|
+
continue;
|
|
200
|
+
if (subjectDist !== undefined) {
|
|
201
|
+
const ratio = d / subjectDist;
|
|
202
|
+
if (code === "H" && ratio >= 0.9 && ratio <= 1.4)
|
|
203
|
+
wallBehind += 1;
|
|
204
|
+
depthSum += Math.max(0, Math.min(1, ratio - 1));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
depthSum += Math.max(0, Math.min(1, Math.log(Math.max(d, 1e-3) / 3) / Math.log(60 / 3)));
|
|
208
|
+
}
|
|
209
|
+
depthCount += 1;
|
|
210
|
+
}
|
|
211
|
+
// Image check: featureless cells (sky excluded) and value contrast near/far.
|
|
212
|
+
let flat;
|
|
213
|
+
let nearLum;
|
|
214
|
+
let farLum;
|
|
215
|
+
let emptyGround;
|
|
216
|
+
if (m.lum && m.tex && m.lum.length === grid.length && m.tex.length === grid.length) {
|
|
217
|
+
const nearRef = subjectDist ?? 12;
|
|
218
|
+
let flatCount = 0;
|
|
219
|
+
let emptyCount = 0;
|
|
220
|
+
let nearSum = 0;
|
|
221
|
+
let nearN = 0;
|
|
222
|
+
let farSum = 0;
|
|
223
|
+
let farN = 0;
|
|
224
|
+
for (let k = 0; k < grid.length; k++) {
|
|
225
|
+
const code = grid[k];
|
|
226
|
+
const d = dists[k] ?? -1;
|
|
227
|
+
const col = k % opts.gridCols;
|
|
228
|
+
const row = Math.floor(k / opts.gridCols);
|
|
229
|
+
const ray = rayDirection(pose, opts.aspect, (col + 0.5) / opts.gridCols, (row + 0.5) / opts.gridRows);
|
|
230
|
+
const isSky = code === "." && ray[1] >= 0;
|
|
231
|
+
if (!isSky && m.tex[k] === "0")
|
|
232
|
+
flatCount += 1;
|
|
233
|
+
if (m.tex[k] === "0" && isEmptyGround(code, d, ray, pose, opts.subject))
|
|
234
|
+
emptyCount += 1;
|
|
235
|
+
const lum = Number(m.lum[k]);
|
|
236
|
+
if (code === "B")
|
|
237
|
+
continue;
|
|
238
|
+
if (code !== "." && d >= 0 && d < nearRef) {
|
|
239
|
+
nearSum += lum;
|
|
240
|
+
nearN += 1;
|
|
241
|
+
}
|
|
242
|
+
else if (code !== "." && d > nearRef * 2) {
|
|
243
|
+
farSum += lum;
|
|
244
|
+
farN += 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
flat = flatCount / total;
|
|
248
|
+
if (opts.subject)
|
|
249
|
+
emptyGround = emptyCount / total;
|
|
250
|
+
if (nearN >= 3)
|
|
251
|
+
nearLum = nearSum / nearN;
|
|
252
|
+
if (farN >= 3)
|
|
253
|
+
farLum = farSum / farN;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
stats: {
|
|
257
|
+
...(flat !== undefined ? { flat } : {}),
|
|
258
|
+
...(nearLum !== undefined ? { nearLum } : {}),
|
|
259
|
+
...(farLum !== undefined ? { farLum } : {}),
|
|
260
|
+
...(emptyGround !== undefined ? { emptyGround } : {}),
|
|
261
|
+
empty: voids / total + (emptyGround ?? 0),
|
|
262
|
+
sky: sky / total,
|
|
263
|
+
void: voids / total,
|
|
264
|
+
subject: subject / total,
|
|
265
|
+
hard: hard / total,
|
|
266
|
+
soft: soft / total,
|
|
267
|
+
foreground: foreground / total,
|
|
268
|
+
wallBehind: wallBehind / total,
|
|
269
|
+
back: back / total,
|
|
270
|
+
translucent: translucent / total,
|
|
271
|
+
nearHard: nearHard / total,
|
|
272
|
+
nearLeft: nearLeft / total,
|
|
273
|
+
nearRight: nearRight / total,
|
|
274
|
+
},
|
|
275
|
+
depth: depthCount ? depthSum / depthCount : 0.5,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function median(values) {
|
|
279
|
+
if (!values.length)
|
|
280
|
+
return undefined;
|
|
281
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
282
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
283
|
+
}
|
|
284
|
+
/** Ratio (0..1) of the median wall distance in the frame's left and right
|
|
285
|
+
* quarters; 0 when one side shows no wall at all. */
|
|
286
|
+
export function edgeSymmetry(m, opts) {
|
|
287
|
+
const grid = m.grid ?? "";
|
|
288
|
+
const dists = m.dist ?? [];
|
|
289
|
+
const quarter = Math.max(1, Math.round(opts.gridCols / 4));
|
|
290
|
+
const left = [];
|
|
291
|
+
const right = [];
|
|
292
|
+
for (let k = 0; k < grid.length; k++) {
|
|
293
|
+
const d = dists[k] ?? -1;
|
|
294
|
+
if (grid[k] === "." || d < 0)
|
|
295
|
+
continue;
|
|
296
|
+
const col = k % opts.gridCols;
|
|
297
|
+
if (col < quarter)
|
|
298
|
+
left.push(d);
|
|
299
|
+
else if (col >= opts.gridCols - quarter)
|
|
300
|
+
right.push(d);
|
|
301
|
+
}
|
|
302
|
+
const l = median(left);
|
|
303
|
+
const r = median(right);
|
|
304
|
+
if (l === undefined || r === undefined)
|
|
305
|
+
return 0;
|
|
306
|
+
return Math.min(l, r) / Math.max(l, r, 1e-6);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Is this pose a view a player could have of the subject? The one rule both
|
|
310
|
+
* smart framing (rejection) and frame_nodes (a warning for an explicit pose)
|
|
311
|
+
* use. A sight line that crosses a hard or subject surface from BEHIND is as
|
|
312
|
+
* blocked as one that hits a wall's front: back faces are not drawn, so the
|
|
313
|
+
* image would look through the wall the camera stands behind. A frame that
|
|
314
|
+
* shows more than MAX_BACK_SHARE of surfaces from behind is the same failure
|
|
315
|
+
* seen through the lens; the engine's own lens checks (inside a closed shell,
|
|
316
|
+
* just behind a surface, touching geometry) come through as rejections.
|
|
317
|
+
*/
|
|
318
|
+
export function viewProblem(m, maxHardFraction = 0.34) {
|
|
319
|
+
if (m.rejected) {
|
|
320
|
+
const lens = m.near_lens_hit ? ` (${m.near_lens_hit})` : "";
|
|
321
|
+
const detail = m.rejected === "inside_volume"
|
|
322
|
+
? "the camera is inside a closed shell of one-sided surfaces: most directions meet a surface from behind, so the image looks out through the walls"
|
|
323
|
+
: m.rejected === "behind_surface"
|
|
324
|
+
? `the camera is right behind a one-sided surface${lens}: its back is not drawn, so the image looks through it`
|
|
325
|
+
: m.rejected === "near_lens_blocked"
|
|
326
|
+
? `geometry touches the lens${lens}`
|
|
327
|
+
: m.rejected === "below_ground"
|
|
328
|
+
? "the camera is below the ground"
|
|
329
|
+
: m.rejected;
|
|
330
|
+
return { reason: m.rejected, detail, ...(m.near_lens_hit ? { blockers: [m.near_lens_hit] } : {}) };
|
|
331
|
+
}
|
|
332
|
+
const vis = m.vis ?? "";
|
|
333
|
+
const n = vis.length;
|
|
334
|
+
const hard = [...vis].filter((c) => c === "H").length;
|
|
335
|
+
const back = [...vis].filter((c) => c === "B").length;
|
|
336
|
+
const grid = m.grid ?? "";
|
|
337
|
+
const backShare = grid.length ? [...grid].filter((c) => c === "B").length / grid.length : 0;
|
|
338
|
+
if (n && (vis[0] === "H" || vis[0] === "B" || (hard + back) / n > maxHardFraction)) {
|
|
339
|
+
if (vis[0] === "B" || back > hard) {
|
|
340
|
+
return {
|
|
341
|
+
reason: "behind_surface",
|
|
342
|
+
detail: `${back} of ${n} sight lines to the subject cross a surface from behind${m.blockers_back?.length ? ` (${m.blockers_back.slice(0, 2).join(", ")})` : ""}: the camera stands behind it and the image would look through it`,
|
|
343
|
+
...(m.blockers_back?.length ? { blockers: m.blockers_back } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
reason: "hard_blocked",
|
|
348
|
+
detail: `${hard + back} of ${n} sight lines to the subject are blocked${m.blockers_hard?.length ? ` by ${m.blockers_hard.slice(0, 2).join(", ")}` : ""}`,
|
|
349
|
+
...(m.blockers_hard?.length ? { blockers: m.blockers_hard } : {}),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (backShare > MAX_BACK_SHARE) {
|
|
353
|
+
return {
|
|
354
|
+
reason: "behind_surface",
|
|
355
|
+
detail: `${Math.round(backShare * 100)}% of the frame is surfaces seen from behind (not drawn): the camera stands behind or inside them and the image looks through them`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
/** Slack (m) for the probe's rounding when the eye height is checked. */
|
|
361
|
+
const EYE_SLACK = 0.03;
|
|
362
|
+
/**
|
|
363
|
+
* Eye mode guard (eye_level, corridor): the camera must stand min..max above
|
|
364
|
+
* the solid surface straight below it, and must never have been raised. The
|
|
365
|
+
* engine places eye poses that way; this refuses anything else, so a raised
|
|
366
|
+
* eye can never win. Null when the pose is at eye height.
|
|
367
|
+
*/
|
|
368
|
+
export function eyeProblem(m, candidate) {
|
|
369
|
+
const eye = candidate.eye;
|
|
370
|
+
if (!eye)
|
|
371
|
+
return null;
|
|
372
|
+
if (m.adjustments?.some((a) => a.kind === "raised"))
|
|
373
|
+
return "raised_above_eye";
|
|
374
|
+
if (typeof m.ground_y !== "number")
|
|
375
|
+
return "eye_height_unverified";
|
|
376
|
+
const height = m.position[1] - m.ground_y;
|
|
377
|
+
if (height > eye.max + EYE_SLACK)
|
|
378
|
+
return "above_eye_height";
|
|
379
|
+
if (height < eye.min - EYE_SLACK)
|
|
380
|
+
return "below_eye_height";
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
/** Camera height above the solid surface straight below it, or undefined. */
|
|
384
|
+
export function heightAboveGround(position, groundY) {
|
|
385
|
+
return typeof groundY === "number" ? position[1] - groundY : undefined;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Key-light direction relative to the view (0..1). `light` is the direction
|
|
389
|
+
* the light travels. Best: side or front-side light (the faces the camera
|
|
390
|
+
* sees get form and shadow); worst: light from straight behind the camera
|
|
391
|
+
* (a flat-lit face); backlight is in between. A sun near the zenith lights
|
|
392
|
+
* every side alike, so the term flattens toward neutral.
|
|
393
|
+
*/
|
|
394
|
+
export function lightScore(pose, light) {
|
|
395
|
+
const view = normalize(sub(pose.look_at, pose.position));
|
|
396
|
+
const dir = normalize(light);
|
|
397
|
+
const horizontal = Math.hypot(dir[0], dir[2]);
|
|
398
|
+
const viewH = Math.hypot(view[0], view[2]);
|
|
399
|
+
if (horizontal < 1e-3 || viewH < 1e-3)
|
|
400
|
+
return 0.7;
|
|
401
|
+
const cos = (dir[0] * view[0] + dir[2] * view[2]) / (horizontal * viewH);
|
|
402
|
+
// 0 = the light travels along the view (from behind the camera), 180 = into the lens.
|
|
403
|
+
const angle = deg(Math.acos(clamp(cos, -1, 1)));
|
|
404
|
+
let s;
|
|
405
|
+
if (angle < 35)
|
|
406
|
+
s = 0.25 + 0.75 * (angle / 35);
|
|
407
|
+
else if (angle <= 115)
|
|
408
|
+
s = 1;
|
|
409
|
+
else
|
|
410
|
+
s = 1 - 0.5 * ((angle - 115) / 65);
|
|
411
|
+
return 0.7 + (s - 0.7) * Math.min(1, horizontal / 0.5);
|
|
412
|
+
}
|
|
413
|
+
/** Share of the frame below the horizon that shows sky or void: the world's
|
|
414
|
+
* edge (the ground ends in view). The grid's "." cells with a downward ray. */
|
|
415
|
+
export const WORLD_EDGE_LIMIT = 0.04;
|
|
416
|
+
export function scoreMeasurement(m, candidate, opts) {
|
|
417
|
+
const profile = SHOT_PROFILES[opts.shot];
|
|
418
|
+
const pose = { position: m.position, look_at: m.look_at, fov: m.fov };
|
|
419
|
+
const base = {
|
|
420
|
+
id: candidate.id,
|
|
421
|
+
index: m.i,
|
|
422
|
+
pose,
|
|
423
|
+
total: 0,
|
|
424
|
+
terms: {},
|
|
425
|
+
...(m.adjustments?.length ? { adjustments: m.adjustments } : {}),
|
|
426
|
+
...(candidate.note ? { note: candidate.note } : {}),
|
|
427
|
+
...(m.ground_y !== undefined ? { groundY: m.ground_y } : {}),
|
|
428
|
+
};
|
|
429
|
+
if (m.rejected) {
|
|
430
|
+
return { ...base, rejected: m.rejected, ...(m.near_lens_hit ? { blockers: { lens: m.near_lens_hit } } : {}) };
|
|
431
|
+
}
|
|
432
|
+
// An eye-level or corridor camera off eye height never ranks.
|
|
433
|
+
const offEye = eyeProblem(m, candidate);
|
|
434
|
+
if (offEye)
|
|
435
|
+
return { ...base, rejected: offEye };
|
|
436
|
+
const vis = m.vis ?? "";
|
|
437
|
+
const n = vis.length;
|
|
438
|
+
const hardCount = [...vis].filter((c) => c === "H" || c === "B").length;
|
|
439
|
+
const softCount = [...vis].filter((c) => c === "F").length;
|
|
440
|
+
const seeCount = [...vis].filter((c) => c === "T").length;
|
|
441
|
+
const hardFraction = n ? hardCount / n : 0;
|
|
442
|
+
const softFraction = n ? softCount / n : 0;
|
|
443
|
+
const blockers = {
|
|
444
|
+
...(m.blockers_hard?.length ? { hard: m.blockers_hard } : {}),
|
|
445
|
+
...(m.blockers_soft?.length ? { soft: m.blockers_soft } : {}),
|
|
446
|
+
...(m.blockers_back?.length ? { back: m.blockers_back } : {}),
|
|
447
|
+
...(m.seen_through?.length ? { through: m.seen_through } : {}),
|
|
448
|
+
};
|
|
449
|
+
const withBlockers = Object.keys(blockers).length ? { blockers } : {};
|
|
450
|
+
const { stats, depth } = frameStats(m, pose, opts);
|
|
451
|
+
const terms = {};
|
|
452
|
+
// Soft cover counts 0.6 of a clear line, see-through (transparent) cover 0.8.
|
|
453
|
+
if (n)
|
|
454
|
+
terms.visibility = (n - hardCount - softCount - seeCount + 0.6 * softCount + 0.8 * seeCount) / n;
|
|
455
|
+
let fill;
|
|
456
|
+
let inFrame;
|
|
457
|
+
if (opts.subject && opts.shot !== "corridor") {
|
|
458
|
+
fill = frameFill(pose, opts.aspect, opts.subject);
|
|
459
|
+
const fillValue = Number.isFinite(fill) ? fill : 3;
|
|
460
|
+
terms.fill = Math.exp(-(((fillValue - profile.fillTarget) / profile.fillTolerance) ** 2));
|
|
461
|
+
terms.thirds = thirdsScore(pose, opts.aspect, opts.subject);
|
|
462
|
+
const samples = candidate.samples;
|
|
463
|
+
inFrame = samples.length
|
|
464
|
+
? samples.filter((p) => {
|
|
465
|
+
const q = project(pose, opts.aspect, p);
|
|
466
|
+
return q.depth > 0 && q.u >= 0 && q.u <= 1 && q.v >= 0 && q.v <= 1;
|
|
467
|
+
}).length / samples.length
|
|
468
|
+
: 1;
|
|
469
|
+
}
|
|
470
|
+
terms.horizon = horizonScore(pose, opts.shot);
|
|
471
|
+
terms.sky = inRangeScore(stats.sky, profile.sky);
|
|
472
|
+
terms.void = 1 - Math.min(1, stats.void / 0.12);
|
|
473
|
+
terms.depth = depth;
|
|
474
|
+
terms.foreground = foregroundScore(stats.foreground, profile.foregroundTarget, profile.foregroundMax);
|
|
475
|
+
// A wall at the lens is not framing; it is a blocked view.
|
|
476
|
+
terms.clear = 1 - Math.min(1, stats.nearHard / 0.3);
|
|
477
|
+
// Corridors and street views read best with walls on both sides as leading
|
|
478
|
+
// lines rather than one wall eating half the frame. For a corridor: the
|
|
479
|
+
// frame's left and right quarters should both be walls at a similar
|
|
480
|
+
// distance (symmetric recession); elsewhere: near walls evenly split.
|
|
481
|
+
if (opts.shot === "corridor") {
|
|
482
|
+
terms.balance = edgeSymmetry(m, opts);
|
|
483
|
+
if (candidate.into !== undefined)
|
|
484
|
+
terms.entry = candidate.into;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
const sides = stats.nearLeft + stats.nearRight;
|
|
488
|
+
terms.balance = sides < 0.02 ? 1 : 1 - Math.abs(stats.nearLeft - stats.nearRight) / sides;
|
|
489
|
+
}
|
|
490
|
+
// From the image check, when the engine rendered one: no empty
|
|
491
|
+
// (featureless) frame areas, and values that separate near from far.
|
|
492
|
+
if (stats.flat !== undefined)
|
|
493
|
+
terms.detail = 1 - Math.min(1, stats.flat / 0.3);
|
|
494
|
+
if (stats.nearLum !== undefined && stats.farLum !== undefined)
|
|
495
|
+
terms.contrast = Math.min(1, Math.abs(stats.nearLum - stats.farLum) / 2);
|
|
496
|
+
// Surfaces seen from behind are holes in the image (the renderer culls
|
|
497
|
+
// them): a few are open boxes or see-through gaps, many mean the camera
|
|
498
|
+
// is behind a wall (rejected below).
|
|
499
|
+
terms.solid = 1 - Math.min(1, stats.back / MAX_BACK_SHARE);
|
|
500
|
+
if (opts.keyLight)
|
|
501
|
+
terms.light = lightScore(pose, opts.keyLight);
|
|
502
|
+
// The world edge below the horizon: a much steeper penalty than void.
|
|
503
|
+
terms.edge = 1 - Math.min(1, stats.void / WORLD_EDGE_LIMIT);
|
|
504
|
+
let rejected;
|
|
505
|
+
const problem = viewProblem(m, opts.maxHardFraction);
|
|
506
|
+
if (problem)
|
|
507
|
+
rejected = problem.reason;
|
|
508
|
+
else if (inFrame !== undefined && inFrame < 0.5)
|
|
509
|
+
rejected = "subject_out_of_frame";
|
|
510
|
+
else if (n && softFraction > opts.maxSoftFraction)
|
|
511
|
+
rejected = "soft_overload";
|
|
512
|
+
else if (stats.foreground > 0.5)
|
|
513
|
+
rejected = "soft_overload";
|
|
514
|
+
let weighted = 0;
|
|
515
|
+
let weights = 0;
|
|
516
|
+
for (const term of SCORE_TERMS) {
|
|
517
|
+
const value = terms[term];
|
|
518
|
+
const w = profile.weights[term];
|
|
519
|
+
if (value === undefined || !w)
|
|
520
|
+
continue;
|
|
521
|
+
weighted += w * value;
|
|
522
|
+
weights += w;
|
|
523
|
+
}
|
|
524
|
+
const total = weights ? weighted / weights : 0;
|
|
525
|
+
const tier = profile.emptyLimit !== undefined && stats.empty > profile.emptyLimit ? 1 : 0;
|
|
526
|
+
const rounded = {};
|
|
527
|
+
for (const term of SCORE_TERMS)
|
|
528
|
+
if (terms[term] !== undefined)
|
|
529
|
+
rounded[term] = r2(terms[term]);
|
|
530
|
+
return {
|
|
531
|
+
...base,
|
|
532
|
+
total: r2(total),
|
|
533
|
+
terms: rounded,
|
|
534
|
+
stats: {
|
|
535
|
+
...(stats.flat !== undefined ? { flat: r2(stats.flat) } : {}),
|
|
536
|
+
...(stats.nearLum !== undefined ? { nearLum: r2(stats.nearLum) } : {}),
|
|
537
|
+
...(stats.farLum !== undefined ? { farLum: r2(stats.farLum) } : {}),
|
|
538
|
+
...(stats.emptyGround !== undefined ? { emptyGround: r2(stats.emptyGround) } : {}),
|
|
539
|
+
empty: r2(stats.empty ?? 0),
|
|
540
|
+
sky: r2(stats.sky),
|
|
541
|
+
void: r2(stats.void),
|
|
542
|
+
subject: r2(stats.subject),
|
|
543
|
+
hard: r2(stats.hard),
|
|
544
|
+
soft: r2(stats.soft),
|
|
545
|
+
foreground: r2(stats.foreground),
|
|
546
|
+
wallBehind: r2(stats.wallBehind),
|
|
547
|
+
back: r2(stats.back),
|
|
548
|
+
translucent: r2(stats.translucent),
|
|
549
|
+
nearHard: r2(stats.nearHard),
|
|
550
|
+
nearLeft: r2(stats.nearLeft),
|
|
551
|
+
nearRight: r2(stats.nearRight),
|
|
552
|
+
},
|
|
553
|
+
hardFraction: r2(hardFraction),
|
|
554
|
+
softFraction: r2(softFraction),
|
|
555
|
+
...(fill !== undefined ? { fill: Number.isFinite(fill) ? r2(fill) : 99 } : {}),
|
|
556
|
+
...(inFrame !== undefined ? { inFrame: r2(inFrame) } : {}),
|
|
557
|
+
...(profile.emptyLimit !== undefined ? { tier } : {}),
|
|
558
|
+
...(rejected ? { rejected } : {}),
|
|
559
|
+
...withBlockers,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/** Rank order: tier first (a pose over the empty limit loses to every pose
|
|
563
|
+
* under it), then score. */
|
|
564
|
+
export function rankOrder(a, b) {
|
|
565
|
+
return (a.tier ?? 0) - (b.tier ?? 0) || b.total - a.total;
|
|
566
|
+
}
|
|
567
|
+
/** Yaw (degrees, 0 = +Z, 90 = +X) of a pose: around `around`, else of its view. */
|
|
568
|
+
export function poseYaw(pose, around) {
|
|
569
|
+
const v = around ? [pose.position[0] - around[0], 0, pose.position[2] - around[2]] : directionOf(pose);
|
|
570
|
+
return ((deg(Math.atan2(v[0], v[2])) % 360) + 360) % 360;
|
|
571
|
+
}
|
|
572
|
+
function yawGap(a, b) {
|
|
573
|
+
const d = Math.abs(a - b) % 360;
|
|
574
|
+
return d > 180 ? 360 - d : d;
|
|
575
|
+
}
|
|
576
|
+
/** Side quadrant of a yaw: 0 front (+Z), 1 right (+X), 2 back (-Z), 3 left (-X). */
|
|
577
|
+
export function sideOf(yaw) {
|
|
578
|
+
return Math.floor((((yaw + 45) % 360) + 360) % 360 / 90);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Top `count` non-rejected candidates in score order, skipping near-duplicates
|
|
582
|
+
* (camera within `minSeparation` m AND looking within 12 degrees of a better
|
|
583
|
+
* pick). With `diversity`, the picks also spread out: first one per side
|
|
584
|
+
* (within the score margin), then any pose at least `minYaw` from every pick,
|
|
585
|
+
* and only then the plain near-duplicate rule fills what is left — so the top
|
|
586
|
+
* 3 of an establishing shot are three different views, not one front view
|
|
587
|
+
* three times.
|
|
588
|
+
*/
|
|
589
|
+
export function pickTop(scored, count, minSeparation, diversity) {
|
|
590
|
+
const ranked = scored.filter((s) => !s.rejected).sort(rankOrder);
|
|
591
|
+
const picks = [];
|
|
592
|
+
const dup = (s) => picks.some((p) => {
|
|
593
|
+
const sep = distance(p.pose.position, s.pose.position);
|
|
594
|
+
const fa = directionOf(p.pose);
|
|
595
|
+
const fb = directionOf(s.pose);
|
|
596
|
+
const cos = fa[0] * fb[0] + fa[1] * fb[1] + fa[2] * fb[2];
|
|
597
|
+
return sep < minSeparation && cos > Math.cos((12 * Math.PI) / 180);
|
|
598
|
+
});
|
|
599
|
+
// Tier by tier: a worse tier only fills what the better ones leave open.
|
|
600
|
+
const tiers = [...new Set(ranked.map((s) => s.tier ?? 0))];
|
|
601
|
+
for (const tier of tiers) {
|
|
602
|
+
const pool = ranked.filter((s) => (s.tier ?? 0) === tier);
|
|
603
|
+
const take = (accept, stopBelow) => {
|
|
604
|
+
for (const s of pool) {
|
|
605
|
+
if (picks.length >= count)
|
|
606
|
+
return;
|
|
607
|
+
if (stopBelow !== undefined && s.total < stopBelow)
|
|
608
|
+
return;
|
|
609
|
+
if (picks.includes(s) || dup(s) || !accept(s))
|
|
610
|
+
continue;
|
|
611
|
+
picks.push(s);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
if (diversity && pool.length) {
|
|
615
|
+
const minYaw = diversity.minYaw ?? 25;
|
|
616
|
+
const yawOf = (s) => poseYaw(s.pose, diversity.around);
|
|
617
|
+
const spread = (s) => picks.every((p) => yawGap(yawOf(p), yawOf(s)) >= minYaw);
|
|
618
|
+
if (diversity.sides) {
|
|
619
|
+
const floor = pool[0].total - (diversity.sideMargin ?? 0.15);
|
|
620
|
+
take((s) => spread(s) && !picks.some((p) => sideOf(yawOf(p)) === sideOf(yawOf(s))), floor);
|
|
621
|
+
}
|
|
622
|
+
take(spread);
|
|
623
|
+
}
|
|
624
|
+
take(() => true);
|
|
625
|
+
}
|
|
626
|
+
return picks.sort(rankOrder);
|
|
627
|
+
}
|
|
628
|
+
function directionOf(pose) {
|
|
629
|
+
const d = [pose.look_at[0] - pose.position[0], pose.look_at[1] - pose.position[1], pose.look_at[2] - pose.position[2]];
|
|
630
|
+
const l = Math.hypot(d[0], d[1], d[2]) || 1;
|
|
631
|
+
return [d[0] / l, d[1] / l, d[2] / l];
|
|
632
|
+
}
|
|
633
|
+
export function rejectionCounts(scored) {
|
|
634
|
+
const out = {};
|
|
635
|
+
for (const s of scored)
|
|
636
|
+
if (s.rejected)
|
|
637
|
+
out[s.rejected] = (out[s.rejected] ?? 0) + 1;
|
|
638
|
+
return out;
|
|
639
|
+
}
|