summer-engine 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +27 -5
- package/.codex-plugin/plugin.json +28 -6
- package/.cursor-plugin/plugin.json +27 -5
- package/.factory-plugin/plugin.json +27 -5
- package/.opencode/plugins/summer.js +1 -1
- package/CHANGELOG.md +102 -0
- package/README.md +18 -3
- package/assets/audit/scene_audit.gd +4136 -0
- package/assets/seeing/seeing_probe.gd +1873 -0
- package/dist/cli/commands/skills.js +20 -2
- package/dist/core/api-client.d.ts +9 -1
- package/dist/core/api-client.js +21 -4
- package/dist/core/capabilities/agent-playbook.js +46 -0
- package/dist/core/capabilities/audit/accept.d.ts +55 -0
- package/dist/core/capabilities/audit/accept.js +147 -0
- package/dist/core/capabilities/audit/args.d.ts +95 -0
- package/dist/core/capabilities/audit/args.js +89 -0
- package/dist/core/capabilities/audit/audit.d.ts +56 -0
- package/dist/core/capabilities/audit/audit.js +287 -0
- package/dist/core/capabilities/audit/frame.d.ts +14 -0
- package/dist/core/capabilities/audit/frame.js +35 -0
- package/dist/core/capabilities/audit/gaps.d.ts +96 -0
- package/dist/core/capabilities/audit/gaps.js +257 -0
- package/dist/core/capabilities/audit/judge.d.ts +162 -0
- package/dist/core/capabilities/audit/judge.js +1134 -0
- package/dist/core/capabilities/audit/math.d.ts +110 -0
- package/dist/core/capabilities/audit/math.js +263 -0
- package/dist/core/capabilities/audit/summary.d.ts +74 -0
- package/dist/core/capabilities/audit/summary.js +212 -0
- package/dist/core/capabilities/board.d.ts +31 -0
- package/dist/core/capabilities/board.js +43 -0
- package/dist/core/capabilities/camera-view.d.ts +4 -2
- package/dist/core/capabilities/camera-view.js +6 -3
- package/dist/core/capabilities/connect-signal.d.ts +85 -0
- package/dist/core/capabilities/connect-signal.js +285 -0
- package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
- package/dist/core/capabilities/engine-fallbacks.js +5 -0
- package/dist/core/capabilities/engine-ops.d.ts +5 -1
- package/dist/core/capabilities/engine-ops.js +0 -3
- package/dist/core/capabilities/engine-receipt.d.ts +12 -0
- package/dist/core/capabilities/engine-receipt.js +26 -0
- package/dist/core/capabilities/file-read.d.ts +64 -0
- package/dist/core/capabilities/file-read.js +325 -0
- package/dist/core/capabilities/game-task-plan.js +4 -2
- package/dist/core/capabilities/grep.d.ts +60 -0
- package/dist/core/capabilities/grep.js +190 -0
- package/dist/core/capabilities/inspect-node.d.ts +56 -0
- package/dist/core/capabilities/inspect-node.js +225 -0
- package/dist/core/capabilities/inspect-resource.d.ts +61 -0
- package/dist/core/capabilities/inspect-resource.js +376 -0
- package/dist/core/capabilities/math3d.d.ts +28 -0
- package/dist/core/capabilities/math3d.js +88 -0
- package/dist/core/capabilities/motion-generation.d.ts +62 -0
- package/dist/core/capabilities/motion-generation.js +138 -0
- package/dist/core/capabilities/placement-batch.d.ts +102 -0
- package/dist/core/capabilities/placement-batch.js +483 -0
- package/dist/core/capabilities/placement-math.d.ts +70 -0
- package/dist/core/capabilities/placement-math.js +209 -0
- package/dist/core/capabilities/placement-script.d.ts +42 -0
- package/dist/core/capabilities/placement-script.js +1096 -0
- package/dist/core/capabilities/placement.d.ts +485 -0
- package/dist/core/capabilities/placement.js +1731 -0
- package/dist/core/capabilities/project-context.d.ts +33 -2
- package/dist/core/capabilities/project-context.js +87 -4
- package/dist/core/capabilities/replace-node.d.ts +91 -0
- package/dist/core/capabilities/replace-node.js +423 -0
- package/dist/core/capabilities/runtime-control.d.ts +18 -2
- package/dist/core/capabilities/runtime-control.js +67 -2
- package/dist/core/capabilities/scene-batch.d.ts +63 -0
- package/dist/core/capabilities/scene-batch.js +394 -0
- package/dist/core/capabilities/scene-readback.d.ts +36 -0
- package/dist/core/capabilities/scene-readback.js +54 -0
- package/dist/core/capabilities/seeing/args.d.ts +348 -0
- package/dist/core/capabilities/seeing/args.js +178 -0
- package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
- package/dist/core/capabilities/seeing/candidates.js +282 -0
- package/dist/core/capabilities/seeing/math.d.ts +134 -0
- package/dist/core/capabilities/seeing/math.js +269 -0
- package/dist/core/capabilities/seeing/probe.d.ts +62 -0
- package/dist/core/capabilities/seeing/probe.js +188 -0
- package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
- package/dist/core/capabilities/seeing/scoring.js +639 -0
- package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
- package/dist/core/capabilities/seeing/seeing.js +1341 -0
- package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
- package/dist/core/capabilities/seeing/shot-store.js +204 -0
- package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
- package/dist/core/capabilities/surface-snap-mesh.js +425 -0
- package/dist/core/capabilities/surface-snap.d.ts +77 -0
- package/dist/core/capabilities/surface-snap.js +461 -0
- package/dist/core/capabilities/tool-dispatch.js +231 -72
- package/dist/core/capabilities/tscn.d.ts +85 -0
- package/dist/core/capabilities/tscn.js +391 -0
- package/dist/core/capabilities/world-snapshot.d.ts +58 -0
- package/dist/core/capabilities/world-snapshot.js +150 -0
- package/dist/core/capability-skew.d.ts +5 -5
- package/dist/core/capability-skew.js +14 -6
- package/dist/core/headless/worker-engine-client.d.ts +1 -1
- package/dist/core/headless/worker-engine-client.js +2 -2
- package/dist/core/library-read.d.ts +37 -5
- package/dist/core/library-read.js +264 -33
- package/dist/core/library-search.d.ts +2 -2
- package/dist/installer/agent-config.js +5 -3
- package/dist/installer/agent-table.d.ts +16 -0
- package/dist/installer/agent-table.js +52 -17
- package/dist/mcp/server.js +9 -3
- package/dist/mcp/tools/asset-tools.js +21 -3
- package/dist/mcp/tools/audit-tools.d.ts +23 -0
- package/dist/mcp/tools/audit-tools.js +60 -0
- package/dist/mcp/tools/debug-tools.js +3 -1
- package/dist/mcp/tools/file-tools.js +17 -4
- package/dist/mcp/tools/generate-tools.js +74 -66
- package/dist/mcp/tools/library-tools.js +2 -0
- package/dist/mcp/tools/perception-tools.js +6 -19
- package/dist/mcp/tools/placement-tools.d.ts +24 -0
- package/dist/mcp/tools/placement-tools.js +91 -0
- package/dist/mcp/tools/project-tools.js +18 -15
- package/dist/mcp/tools/scene-tools.js +71 -83
- package/dist/mcp/tools/seeing-tools.d.ts +24 -0
- package/dist/mcp/tools/seeing-tools.js +64 -0
- package/dist/mcp/tools/spatial-tools.js +17 -9
- package/dist/mcp/tools/visual-tools.js +59 -7
- package/gemini-extension.json +26 -4
- package/integrations/amp/README.md +1 -1
- package/integrations/crush/README.md +1 -1
- package/integrations/grok-build/README.md +1 -1
- package/integrations/hermes/README.md +1 -1
- package/integrations/kimi-code/README.md +1 -1
- package/integrations/mistral-vibe/README.md +1 -1
- package/integrations/opencode/README.md +1 -1
- package/integrations/rovo-dev/README.md +1 -1
- package/integrations/vscode-copilot/README.md +1 -1
- package/integrations/warp/README.md +1 -1
- package/integrations/windsurf/README.md +1 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/references/summer-folder/resource.yaml +1 -1
- package/library/references/summer-folder/summer-folder.md +1 -1
- package/library/skills/3d-lighting/SKILL.md +1 -1
- package/library/skills/README.md +36 -14
- package/library/skills/adaptive-music/SKILL.md +1 -1
- package/library/skills/agent-playtesting/SKILL.md +1 -1
- package/library/skills/ai-characters-in-games/SKILL.md +220 -0
- package/library/skills/ai-characters-in-games/resource.yaml +30 -0
- package/library/skills/ambient-bed/SKILL.md +1 -1
- package/library/skills/animated-loop/SKILL.md +1 -1
- package/library/skills/animation-tree/SKILL.md +1 -1
- package/library/skills/art-direction/SKILL.md +1 -1
- package/library/skills/asset-strategy/SKILL.md +2 -1
- package/library/skills/asset-strategy/resource.yaml +2 -1
- package/library/skills/audio-direction/SKILL.md +1 -1
- package/library/skills/auto-fire-targeting/SKILL.md +1 -1
- package/library/skills/brainstorm-game/SKILL.md +1 -1
- package/library/skills/brainstorming/SKILL.md +1 -1
- package/library/skills/browse-templates/SKILL.md +3 -3
- package/library/skills/browse-templates/resource.yaml +1 -1
- package/library/skills/camera-rigs/SKILL.md +128 -0
- package/library/skills/camera-rigs/resource.yaml +29 -0
- package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
- package/library/skills/character-animation-wiring/SKILL.md +1 -1
- package/library/skills/character-model/SKILL.md +1 -1
- package/library/skills/character-movement/SKILL.md +326 -0
- package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
- package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
- package/library/skills/character-movement/references/movement-3d.md +136 -0
- package/library/skills/character-movement/references/platformer-feel.md +83 -0
- package/library/skills/character-movement/resource.yaml +40 -0
- package/library/skills/character-portrait/SKILL.md +1 -1
- package/library/skills/cinematic-cutscene/SKILL.md +1 -1
- package/library/skills/combat-basics/SKILL.md +159 -0
- package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
- package/library/skills/combat-basics/resource.yaml +34 -0
- package/library/skills/concept-art/SKILL.md +1 -1
- package/library/skills/create-asset-sheet/SKILL.md +1 -1
- package/library/skills/debug/SKILL.md +1 -1
- package/library/skills/debugging-game-feel/SKILL.md +1 -1
- package/library/skills/design-level/SKILL.md +1 -1
- package/library/skills/design-mechanic/SKILL.md +1 -1
- package/library/skills/design-npc/SKILL.md +1 -1
- package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
- package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
- package/library/skills/environment-kit/SKILL.md +1 -1
- package/library/skills/export-and-ship/SKILL.md +1 -1
- package/library/skills/fabricating-assets/SKILL.md +1 -1
- package/library/skills/facial-and-lipsync/SKILL.md +1 -1
- package/library/skills/fps-controller/SKILL.md +9 -7
- package/library/skills/fps-controller/resource.yaml +4 -1
- package/library/skills/game-feel/SKILL.md +1 -1
- package/library/skills/gameskill/SKILL.md +1 -1
- package/library/skills/gdscript-patterns/SKILL.md +1 -1
- package/library/skills/generate-motion/SKILL.md +9 -9
- package/library/skills/generate-motion/resource.yaml +2 -1
- package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
- package/library/skills/headless-scripting/SKILL.md +1 -1
- package/library/skills/in-game-purchases/SKILL.md +254 -0
- package/library/skills/in-game-purchases/resource.yaml +29 -0
- package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
- package/library/skills/investigating-bugs/SKILL.md +1 -1
- package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
- package/library/skills/make-game/SKILL.md +25 -3
- package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
- package/library/skills/make-game/references/2d-platformer.md +117 -0
- package/library/skills/make-game/references/2d-rpg-content.md +234 -0
- package/library/skills/make-game/references/2d-rpg.md +241 -0
- package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
- package/library/skills/make-game/references/2d-survivors.md +237 -0
- package/library/skills/make-game/references/3d-fps-systems.md +216 -0
- package/library/skills/make-game/references/3d-fps.md +177 -0
- package/library/skills/make-game/references/3d-horror-systems.md +186 -0
- package/library/skills/make-game/references/3d-horror.md +201 -0
- package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
- package/library/skills/make-game/references/3d-third-person.md +215 -0
- package/library/skills/make-game/resource.yaml +6 -1
- package/library/skills/multiplayer/SKILL.md +150 -0
- package/library/skills/multiplayer/resource.yaml +33 -0
- package/library/skills/multiplayer-movement/SKILL.md +510 -0
- package/library/skills/multiplayer-movement/files.md +501 -0
- package/library/skills/multiplayer-movement/resource.yaml +30 -0
- package/library/skills/multiplayer-project/SKILL.md +521 -0
- package/library/skills/multiplayer-project/files.md +448 -0
- package/library/skills/multiplayer-project/resource.yaml +36 -0
- package/library/skills/multiplayer-publish/SKILL.md +229 -0
- package/library/skills/multiplayer-publish/export-errors.md +89 -0
- package/library/skills/multiplayer-publish/resource.yaml +28 -0
- package/library/skills/multiplayer-state/SKILL.md +438 -0
- package/library/skills/multiplayer-state/files.md +592 -0
- package/library/skills/multiplayer-state/resource.yaml +35 -0
- package/library/skills/multiplayer-testing/SKILL.md +236 -0
- package/library/skills/multiplayer-testing/resource.yaml +39 -0
- package/library/skills/music-track/SKILL.md +1 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/new-project/SKILL.md +1 -1
- package/library/skills/organic-model/SKILL.md +1 -1
- package/library/skills/pixel-art/SKILL.md +1 -1
- package/library/skills/play/SKILL.md +1 -1
- package/library/skills/playtesting-a-feature/SKILL.md +1 -1
- package/library/skills/procedural-animation/SKILL.md +2 -2
- package/library/skills/procedural-animation/resource.yaml +1 -1
- package/library/skills/prop-model/SKILL.md +1 -1
- package/library/skills/psx-retro-rendering/SKILL.md +1 -1
- package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
- package/library/skills/remote-deploy/SKILL.md +1 -1
- package/library/skills/retarget/SKILL.md +1 -1
- package/library/skills/running-in-the-cloud/SKILL.md +1 -1
- package/library/skills/save-load/SKILL.md +156 -0
- package/library/skills/save-load/resource.yaml +27 -0
- package/library/skills/scene-composition/SKILL.md +1 -1
- package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
- package/library/skills/scene-scripting/SKILL.md +1 -1
- package/library/skills/scene-to-level/SKILL.md +1 -1
- package/library/skills/skill-create/SKILL.md +1 -1
- package/library/skills/skill-improve/SKILL.md +1 -1
- package/library/skills/skill-test/SKILL.md +1 -1
- package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
- package/library/skills/skybox-panorama/SKILL.md +1 -1
- package/library/skills/sound-effect/SKILL.md +1 -1
- package/library/skills/spatial-placement/SKILL.md +134 -3
- package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
- package/library/skills/spatial-placement/resource.yaml +16 -2
- package/library/skills/sprite-sheet/SKILL.md +1 -1
- package/library/skills/summer-analytics/SKILL.md +147 -0
- package/library/skills/summer-analytics/resource.yaml +28 -0
- package/library/skills/summer-friends/SKILL.md +226 -0
- package/library/skills/summer-friends/resource.yaml +28 -0
- package/library/skills/summer-leaderboards/SKILL.md +231 -0
- package/library/skills/summer-leaderboards/resource.yaml +30 -0
- package/library/skills/summer-match-results/SKILL.md +286 -0
- package/library/skills/summer-match-results/resource.yaml +32 -0
- package/library/skills/summer-matchmaking/SKILL.md +301 -0
- package/library/skills/summer-matchmaking/resource.yaml +32 -0
- package/library/skills/summer-parties/SKILL.md +253 -0
- package/library/skills/summer-parties/resource.yaml +30 -0
- package/library/skills/summer-player-data/SKILL.md +322 -0
- package/library/skills/summer-player-data/resource.yaml +30 -0
- package/library/skills/summer-store/SKILL.md +271 -0
- package/library/skills/summer-store/resource.yaml +29 -0
- package/library/skills/summer-world-chat/SKILL.md +195 -0
- package/library/skills/summer-world-chat/resource.yaml +30 -0
- package/library/skills/summer-world-saves/SKILL.md +284 -0
- package/library/skills/summer-world-saves/resource.yaml +29 -0
- package/library/skills/text-to-motion/SKILL.md +99 -0
- package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
- package/library/skills/text-to-motion/resource.yaml +41 -0
- package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
- package/library/skills/tileable-texture/SKILL.md +1 -1
- package/library/skills/trailer-shot/SKILL.md +1 -1
- package/library/skills/tune-performance/SKILL.md +1 -1
- package/library/skills/ui-basics/SKILL.md +10 -1
- package/library/skills/ui-graphics/SKILL.md +1 -1
- package/library/skills/use-widget-asset/SKILL.md +1 -1
- package/library/skills/using-summer/SKILL.md +2 -2
- package/library/skills/using-summer/resource.yaml +2 -2
- package/library/skills/vehicle-model/SKILL.md +1 -1
- package/library/skills/verification-before-completion/SKILL.md +1 -1
- package/library/skills/verifying-scenes/SKILL.md +76 -2
- package/library/skills/verifying-scenes/resource.yaml +13 -2
- package/library/skills/vfx-dissolve/SKILL.md +1 -1
- package/library/skills/vfx-fire/SKILL.md +1 -1
- package/library/skills/vfx-hit-spark/SKILL.md +1 -1
- package/library/skills/vfx-lightning/SKILL.md +1 -1
- package/library/skills/vfx-magic-glow/SKILL.md +1 -1
- package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
- package/library/skills/vfx-smoke/SKILL.md +1 -1
- package/library/skills/vfx-water-ripple/SKILL.md +1 -1
- package/library/skills/voice-line/SKILL.md +1 -1
- package/library/skills/web-and-touch-input/SKILL.md +38 -0
- package/library/skills/web-and-touch-input/resource.yaml +28 -0
- package/library/skills/web-compatibility-look/SKILL.md +62 -0
- package/library/skills/web-compatibility-look/resource.yaml +30 -0
- package/library/skills/world-building-3d/SKILL.md +8 -1
- package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
- package/library/skills/world-building-3d/resource.yaml +2 -1
- package/library/skills/writing-plans/SKILL.md +1 -1
- package/library/skills/writing-skills/SKILL.md +1 -1
- package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
- package/library/tools/attach-to-surface/resource.yaml +142 -0
- package/library/tools/batch/resource.yaml +9 -3
- package/library/tools/connect-ports/resource.yaml +120 -0
- package/library/tools/connect-signal/resource.yaml +3 -2
- package/library/tools/debug-views/resource.yaml +94 -0
- package/library/tools/frame-nodes/resource.yaml +104 -0
- package/library/tools/frame-shot/resource.yaml +134 -0
- package/library/tools/generate-motion/resource.yaml +44 -9
- package/library/tools/get-board/resource.yaml +50 -0
- package/library/tools/get-project-context/resource.yaml +13 -2
- package/library/tools/grep/resource.yaml +77 -0
- package/library/tools/inspect-asset/resource.yaml +71 -0
- package/library/tools/inspect-node/resource.yaml +9 -2
- package/library/tools/inspect-resource/resource.yaml +16 -5
- package/library/tools/instantiate-scene/resource.yaml +27 -2
- package/library/tools/measure/resource.yaml +96 -0
- package/library/tools/place-adjacent/resource.yaml +113 -0
- package/library/tools/play/resource.yaml +15 -2
- package/library/tools/raycast/resource.yaml +101 -0
- package/library/tools/read-file/resource.yaml +36 -2
- package/library/tools/read-library/resource.yaml +2 -2
- package/library/tools/repeat-along/resource.yaml +129 -0
- package/library/tools/replace-node/resource.yaml +11 -5
- package/library/tools/scene-audit/resource.yaml +144 -0
- package/library/tools/screenshot/resource.yaml +13 -2
- package/library/tools/search-assets/resource.yaml +18 -0
- package/library/tools/shot-sheet/resource.yaml +110 -0
- package/library/tools/snap-to-surface/resource.yaml +4 -1
- package/library/tools/world-snapshot/resource.yaml +24 -3
- package/library/tools/zoom/resource.yaml +105 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +17 -3
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +26 -4
- package/registry/generated/index.json +2154 -469
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +27 -5
- package/registry/generated/plugin.codex.json +28 -6
- package/registry/generated/plugin.cursor.json +27 -5
- package/registry/generated/plugin.factory.json +27 -5
- package/registry/generated/skills-index.md +36 -14
- package/registry/generated/skills-registry.json +421 -124
- package/registry/generated/templates-registry.json +2 -2
- package/library/skills/host-authoritative-state/SKILL.md +0 -332
- package/library/skills/host-authoritative-state/resource.yaml +0 -28
- package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
- package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
- package/library/skills/setup-multiplayer/SKILL.md +0 -336
- package/library/skills/setup-multiplayer/resource.yaml +0 -28
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart framing, part 1: candidate camera poses per shot type. Pure and
|
|
3
|
+
* engine-free — the engine only measures what these poses would see
|
|
4
|
+
* (assets/seeing/seeing_probe.gd "measure"); scoring.ts ranks the result.
|
|
5
|
+
*
|
|
6
|
+
* Every candidate sits at the distance where the subject fills the shot
|
|
7
|
+
* type's target share of the frame (fitDistance), on a ring or hemisphere
|
|
8
|
+
* around the subject, or along a corridor's free line. Low angles follow the
|
|
9
|
+
* low-angle rule (closer + wider instead of buried in the ground).
|
|
10
|
+
*/
|
|
11
|
+
import { aabbCenter, aabbCorners, aabbMax, add, applyLowAngleRule, directionFromAngles, fitDistance, lerp, normalize, rotateY, scale, sub, thirdsYawOffset, } from "./math.js";
|
|
12
|
+
export const SHOT_TYPES = ["establishing", "eye_level", "low_angle", "detail", "corridor"];
|
|
13
|
+
export const SHOT_DEFAULTS = {
|
|
14
|
+
establishing: { fov: 55, fill: 0.78 },
|
|
15
|
+
eye_level: { fov: 70, fill: 0.5 },
|
|
16
|
+
low_angle: { fov: 60, fill: 0.85 },
|
|
17
|
+
detail: { fov: 40, fill: 0.7 },
|
|
18
|
+
corridor: { fov: 65, fill: 0 },
|
|
19
|
+
};
|
|
20
|
+
export const LOW_ANGLE_CLEARANCE = 0.35;
|
|
21
|
+
export const DEFAULT_CLEARANCE = 0.3;
|
|
22
|
+
export const MAX_FOV = 100;
|
|
23
|
+
export const DEFAULT_EYE_HEIGHT = 1.6;
|
|
24
|
+
/** A standing eye without an explicit eye_height: 1.5-1.8 m above the walkable
|
|
25
|
+
* surface under the camera. */
|
|
26
|
+
export const EYE_BAND = [1.5, 1.8];
|
|
27
|
+
/** The eye band: exactly eye_height when given, else EYE_BAND. */
|
|
28
|
+
export function eyeBand(eyeHeight) {
|
|
29
|
+
return eyeHeight !== undefined ? [eyeHeight, eyeHeight] : [EYE_BAND[0], EYE_BAND[1]];
|
|
30
|
+
}
|
|
31
|
+
/** Nine points the thick sweep aims at: the center plus the eight corners
|
|
32
|
+
* pulled 40% toward it, so a subject standing on the ground still offers
|
|
33
|
+
* points above the ground plane. */
|
|
34
|
+
export function subjectSamples(box) {
|
|
35
|
+
const c = aabbCenter(box);
|
|
36
|
+
return [c, ...aabbCorners(box).map((corner) => lerp(c, corner, 0.6))];
|
|
37
|
+
}
|
|
38
|
+
const AZIMUTHS_12 = Array.from({ length: 12 }, (_, i) => i * 30);
|
|
39
|
+
const AZIMUTHS_16 = Array.from({ length: 16 }, (_, i) => i * 22.5);
|
|
40
|
+
function ringCandidates(prefix, box, lookAt, elevations, fov, fill, aspect, options) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const samples = subjectSamples(box);
|
|
43
|
+
for (const elevation of elevations) {
|
|
44
|
+
for (const azimuth of AZIMUTHS_12) {
|
|
45
|
+
const toCamera = directionFromAngles(azimuth, elevation);
|
|
46
|
+
const dist = fitDistance(box, lookAt, toCamera, fov, aspect, fill);
|
|
47
|
+
let position = add(lookAt, scale(toCamera, dist));
|
|
48
|
+
let camFov = fov;
|
|
49
|
+
let note;
|
|
50
|
+
if (options.lowAngle && options.minY !== undefined) {
|
|
51
|
+
const rule = applyLowAngleRule(lookAt, toCamera, dist, fov, options.minY, MAX_FOV);
|
|
52
|
+
if (rule.impossible)
|
|
53
|
+
continue;
|
|
54
|
+
position = rule.position;
|
|
55
|
+
camFov = rule.fov;
|
|
56
|
+
if (rule.adjusted)
|
|
57
|
+
note = `low-angle rule: ${dist.toFixed(1)} m -> ${rule.distance.toFixed(1)} m, fov ${fov} -> ${rule.fov.toFixed(1)}`;
|
|
58
|
+
}
|
|
59
|
+
out.push({
|
|
60
|
+
id: `${prefix}_az${String(azimuth).padStart(3, "0")}_el${elevation}`,
|
|
61
|
+
position,
|
|
62
|
+
look_at: lookAt,
|
|
63
|
+
fov: Math.round(camFov * 100) / 100,
|
|
64
|
+
samples,
|
|
65
|
+
min_clearance: options.clearance,
|
|
66
|
+
low_angle_rule: options.lowAngle === true,
|
|
67
|
+
...(note ? { note } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function eyeOf(ctx) {
|
|
74
|
+
const spawn = ctx.spawn;
|
|
75
|
+
if (ctx.eyeHeight === undefined && spawn.camera)
|
|
76
|
+
return { eye: spawn.camera.position, source: `camera ${spawn.camera.path}` };
|
|
77
|
+
const h = ctx.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
78
|
+
return { eye: add(spawn.origin, [0, h, 0]), source: `${h} m above the walkable surface at ${spawn.path}` };
|
|
79
|
+
}
|
|
80
|
+
function eyeLevelCandidates(ctx, fovDefault) {
|
|
81
|
+
const { eye, source } = eyeOf(ctx);
|
|
82
|
+
const [min, max] = eyeBand(ctx.eyeHeight);
|
|
83
|
+
// The engine stands the eye on the walkable surface under it: eye_height
|
|
84
|
+
// (or 1.6 m) above it, or a spawn camera's own height clamped into
|
|
85
|
+
// 1.5-1.8 m. Never higher.
|
|
86
|
+
const ownCamera = ctx.eyeHeight === undefined && ctx.spawn.camera !== undefined;
|
|
87
|
+
const eyeSpec = { min, max, ...(ownCamera ? {} : { target: ctx.eyeHeight ?? DEFAULT_EYE_HEIGHT }), stand: [...ctx.spawn.origin] };
|
|
88
|
+
const out = [];
|
|
89
|
+
const fovs = ctx.fov !== undefined ? [ctx.fov] : [fovDefault - 10, fovDefault + 5];
|
|
90
|
+
if (ctx.subject) {
|
|
91
|
+
const c = aabbCenter(ctx.subject);
|
|
92
|
+
const flat = normalize([c[0] - eye[0], 0, c[2] - eye[2]]);
|
|
93
|
+
const horizontal = Math.hypot(c[0] - eye[0], c[2] - eye[2]);
|
|
94
|
+
const samples = subjectSamples(ctx.subject);
|
|
95
|
+
for (const fov of fovs) {
|
|
96
|
+
const yawStep = thirdsYawOffset(fov, ctx.aspect);
|
|
97
|
+
for (const [tag, yaw] of [["c", 0], ["l3", yawStep], ["r3", -yawStep]]) {
|
|
98
|
+
for (const [ptag, aimed] of [["level", false], ["aim", true]]) {
|
|
99
|
+
const dir = rotateY(flat, yaw);
|
|
100
|
+
// Eye level means a level camera; "aim" tilts at most 12 degrees
|
|
101
|
+
// toward the subject's center, the way a player glances up.
|
|
102
|
+
const rise = aimed ? Math.max(-0.21, Math.min(0.21, (c[1] - eye[1]) / Math.max(horizontal, 0.1))) : 0;
|
|
103
|
+
out.push({
|
|
104
|
+
id: `eye_${tag}_${ptag}_fov${fov}`,
|
|
105
|
+
position: eye,
|
|
106
|
+
look_at: add(eye, scale(normalize([dir[0], rise, dir[2]]), 10)),
|
|
107
|
+
fov,
|
|
108
|
+
samples,
|
|
109
|
+
min_clearance: 0,
|
|
110
|
+
low_angle_rule: false,
|
|
111
|
+
eye: eyeSpec,
|
|
112
|
+
note: `eye ${source}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
const fov = ctx.fov ?? fovDefault;
|
|
120
|
+
const forward = normalize([ctx.spawn.camera?.forward[0] ?? ctx.spawn.forward[0], 0, ctx.spawn.camera?.forward[2] ?? ctx.spawn.forward[2]]);
|
|
121
|
+
out.push({ id: "eye_spawn_forward", position: eye, look_at: add(eye, scale(forward, 10)), fov, samples: [], min_clearance: 0, low_angle_rule: false, eye: eyeSpec, note: `eye ${source}` });
|
|
122
|
+
for (const azimuth of AZIMUTHS_16) {
|
|
123
|
+
const dir = directionFromAngles(azimuth, 0);
|
|
124
|
+
out.push({
|
|
125
|
+
id: `eye_az${String(azimuth).padStart(5, "0")}`,
|
|
126
|
+
position: eye,
|
|
127
|
+
look_at: add(eye, scale(dir, 10)),
|
|
128
|
+
fov,
|
|
129
|
+
samples: [],
|
|
130
|
+
min_clearance: 0,
|
|
131
|
+
low_angle_rule: false,
|
|
132
|
+
eye: eyeSpec,
|
|
133
|
+
note: `eye ${source}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/** Where a ray from `origin` along `dir` leaves `box` (slab test), or Infinity. */
|
|
139
|
+
export function exitDistance(box, origin, dir) {
|
|
140
|
+
const lo = box.position;
|
|
141
|
+
const hi = aabbMax(box);
|
|
142
|
+
let tExit = Infinity;
|
|
143
|
+
for (let k = 0; k < 3; k++) {
|
|
144
|
+
const d = dir[k];
|
|
145
|
+
if (Math.abs(d) < 1e-9)
|
|
146
|
+
continue;
|
|
147
|
+
const t1 = (lo[k] - origin[k]) / d;
|
|
148
|
+
const t2 = (hi[k] - origin[k]) / d;
|
|
149
|
+
tExit = Math.min(tExit, Math.max(t1, t2));
|
|
150
|
+
}
|
|
151
|
+
return Math.max(0, tExit);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Pick the corridor axis from the engine's free-run scan: the longest free
|
|
155
|
+
* line (clipped to the subject bounds + 2 m on each end) that is also narrow
|
|
156
|
+
* enough to read as a corridor and wide enough to walk.
|
|
157
|
+
*/
|
|
158
|
+
export function chooseCorridorAxis(runs, box) {
|
|
159
|
+
const scored = [];
|
|
160
|
+
for (const run of runs) {
|
|
161
|
+
const exitFwd = exitDistance(box, run.seed, run.dir);
|
|
162
|
+
const exitBack = exitDistance(box, run.seed, scale(run.dir, -1));
|
|
163
|
+
const usableFwd = Math.min(run.fwd, exitFwd + 2);
|
|
164
|
+
const usableBack = Math.min(run.back, exitBack + 2);
|
|
165
|
+
const total = usableFwd + usableBack;
|
|
166
|
+
const width = run.left + run.right;
|
|
167
|
+
if (total < 4 || width < 1)
|
|
168
|
+
continue;
|
|
169
|
+
const slenderness = total / Math.max(width, 0.5);
|
|
170
|
+
const score = total * Math.min(1, slenderness / 3);
|
|
171
|
+
scored.push({ ...run, usableFwd, usableBack, width, score, openFwd: run.fwd > exitFwd + 0.5, openBack: run.back > exitBack + 0.5 });
|
|
172
|
+
}
|
|
173
|
+
scored.sort((a, b) => b.score - a.score);
|
|
174
|
+
// Distinct axes only: drop near-duplicates (same direction, nearby seed).
|
|
175
|
+
const distinct = [];
|
|
176
|
+
for (const axis of scored) {
|
|
177
|
+
const dup = distinct.some((d) => Math.abs(d.dir[0] * axis.dir[0] + d.dir[2] * axis.dir[2]) > 0.95 && Math.hypot(d.seed[0] - axis.seed[0], d.seed[2] - axis.seed[2]) < Math.max(2, d.width));
|
|
178
|
+
if (!dup)
|
|
179
|
+
distinct.push(axis);
|
|
180
|
+
if (distinct.length >= 3)
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
return distinct;
|
|
184
|
+
}
|
|
185
|
+
/** The walkable floor along a corridor axis: the scan put its seed at eye
|
|
186
|
+
* height above the floor it found there. */
|
|
187
|
+
export function corridorFloorY(axis, eyeHeight) {
|
|
188
|
+
return axis.seed[1] - (eyeHeight ?? DEFAULT_EYE_HEIGHT);
|
|
189
|
+
}
|
|
190
|
+
function corridorCandidates(ctx, fovDefault) {
|
|
191
|
+
const axis = ctx.corridor;
|
|
192
|
+
// The floor under the corridor line (the scan's seed), not the subject's
|
|
193
|
+
// lowest point: walls sunk into the ground or a backing plane below the
|
|
194
|
+
// floor would put every camera too low or, after the ground check, high.
|
|
195
|
+
const floorY = corridorFloorY(axis, ctx.eyeHeight);
|
|
196
|
+
const height = ctx.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
197
|
+
const [min, max] = eyeBand(ctx.eyeHeight);
|
|
198
|
+
const eyeSpec = { min, max, target: height, stand: [...axis.seed] };
|
|
199
|
+
const dir = normalize([axis.dir[0], 0, axis.dir[2]]);
|
|
200
|
+
const perp = [dir[2], 0, -dir[0]];
|
|
201
|
+
const halfWidth = Math.min(axis.left, axis.right, axis.width / 2);
|
|
202
|
+
const fovs = ctx.fov !== undefined ? [ctx.fov] : [fovDefault - 5, fovDefault + 10];
|
|
203
|
+
const out = [];
|
|
204
|
+
// "ab" starts at the back end and looks toward the forward end.
|
|
205
|
+
const intoOf = (fromOpen, toOpen) => (fromOpen === toOpen ? 0.5 : fromOpen ? 1 : 0);
|
|
206
|
+
const intoAb = intoOf(axis.openBack, axis.openFwd);
|
|
207
|
+
// A corridor shot is about walking it: every camera stands at eye height.
|
|
208
|
+
// The alternatives are horizontal only (how far in from the end, how far
|
|
209
|
+
// off the centre line), never a raised eye.
|
|
210
|
+
for (const reach of [0.92, 0.75]) {
|
|
211
|
+
const endB = add(axis.seed, scale(dir, axis.usableFwd * reach));
|
|
212
|
+
const endA = add(axis.seed, scale(dir, -axis.usableBack * reach));
|
|
213
|
+
for (const [tag, from, to] of [["ab", endA, endB], ["ba", endB, endA]]) {
|
|
214
|
+
const into = tag === "ab" ? intoAb : 1 - intoAb;
|
|
215
|
+
const far = [to[0], floorY + height, to[2]];
|
|
216
|
+
const samples = [0.3, 0.55, 0.8, 1].map((t) => {
|
|
217
|
+
const p = lerp(from, to, t);
|
|
218
|
+
return [p[0], floorY + height, p[2]];
|
|
219
|
+
});
|
|
220
|
+
const mid = lerp(from, to, 0.55);
|
|
221
|
+
for (const side of [-0.3, 0.3]) {
|
|
222
|
+
const lat = scale(perp, side * halfWidth);
|
|
223
|
+
samples.push([mid[0] + lat[0], floorY + height, mid[2] + lat[2]]);
|
|
224
|
+
}
|
|
225
|
+
for (const lateral of [-0.3, 0, 0.3]) {
|
|
226
|
+
const offset = scale(perp, lateral * halfWidth);
|
|
227
|
+
const position = [from[0] + offset[0], floorY + height, from[2] + offset[2]];
|
|
228
|
+
for (const fov of fovs) {
|
|
229
|
+
out.push({
|
|
230
|
+
id: `corridor_${tag}_r${reach}_x${lateral}_fov${fov}`,
|
|
231
|
+
position,
|
|
232
|
+
look_at: far,
|
|
233
|
+
fov,
|
|
234
|
+
samples,
|
|
235
|
+
min_clearance: DEFAULT_CLEARANCE,
|
|
236
|
+
low_angle_rule: false,
|
|
237
|
+
eye: eyeSpec,
|
|
238
|
+
into,
|
|
239
|
+
...(into === 1 ? { note: "looks in from the open end" } : into === 0 ? { note: "looks out from the dead end" } : {}),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
export function generateCandidates(ctx) {
|
|
248
|
+
const defaults = SHOT_DEFAULTS[ctx.shot];
|
|
249
|
+
const fov = ctx.fov ?? defaults.fov;
|
|
250
|
+
switch (ctx.shot) {
|
|
251
|
+
case "establishing": {
|
|
252
|
+
const box = ctx.subject;
|
|
253
|
+
return ringCandidates("wide", box, aabbCenter(box), [12, 25, 40], fov, defaults.fill, ctx.aspect, { clearance: DEFAULT_CLEARANCE });
|
|
254
|
+
}
|
|
255
|
+
case "detail": {
|
|
256
|
+
const box = ctx.subject;
|
|
257
|
+
return ringCandidates("detail", box, aabbCenter(box), [8, 22, 38], fov, defaults.fill, ctx.aspect, { clearance: DEFAULT_CLEARANCE });
|
|
258
|
+
}
|
|
259
|
+
case "low_angle": {
|
|
260
|
+
const box = ctx.subject;
|
|
261
|
+
const ground = ctx.groundY ?? box.position[1];
|
|
262
|
+
return ringCandidates("low", box, aabbCenter(box), [-6, -14, -24], fov, defaults.fill, ctx.aspect, {
|
|
263
|
+
minY: ground + LOW_ANGLE_CLEARANCE,
|
|
264
|
+
lowAngle: true,
|
|
265
|
+
clearance: LOW_ANGLE_CLEARANCE,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
case "eye_level":
|
|
269
|
+
return eyeLevelCandidates(ctx, defaults.fov);
|
|
270
|
+
case "corridor":
|
|
271
|
+
return corridorCandidates(ctx, defaults.fov);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Corridor "subject" box used for frame metrics: the free line it looks down. */
|
|
275
|
+
export function corridorBox(axis, floorY) {
|
|
276
|
+
const dir = normalize([axis.dir[0], 0, axis.dir[2]]);
|
|
277
|
+
const a = add(axis.seed, scale(dir, -axis.usableBack));
|
|
278
|
+
const b = add(axis.seed, scale(dir, axis.usableFwd));
|
|
279
|
+
const lo = [Math.min(a[0], b[0]) - 0.5, floorY, Math.min(a[2], b[2]) - 0.5];
|
|
280
|
+
const hi = [Math.max(a[0], b[0]) + 0.5, floorY + 2.5, Math.max(a[2], b[2]) + 0.5];
|
|
281
|
+
return { position: lo, size: sub(hi, lo) };
|
|
282
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type Vec3 = readonly [number, number, number];
|
|
2
|
+
export interface Aabb {
|
|
3
|
+
position: Vec3;
|
|
4
|
+
size: Vec3;
|
|
5
|
+
}
|
|
6
|
+
export interface CameraPose {
|
|
7
|
+
position: Vec3;
|
|
8
|
+
look_at: Vec3;
|
|
9
|
+
/** Vertical field of view, degrees. */
|
|
10
|
+
fov: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const add: (a: Vec3, b: Vec3) => Vec3;
|
|
13
|
+
export declare const sub: (a: Vec3, b: Vec3) => Vec3;
|
|
14
|
+
export declare const scale: (a: Vec3, s: number) => Vec3;
|
|
15
|
+
export declare const dot: (a: Vec3, b: Vec3) => number;
|
|
16
|
+
export declare const cross: (a: Vec3, b: Vec3) => Vec3;
|
|
17
|
+
export declare const length: (a: Vec3) => number;
|
|
18
|
+
export declare const distance: (a: Vec3, b: Vec3) => number;
|
|
19
|
+
export declare const lerp: (a: Vec3, b: Vec3, t: number) => Vec3;
|
|
20
|
+
export declare const deg: (rad: number) => number;
|
|
21
|
+
export declare const rad: (degrees: number) => number;
|
|
22
|
+
export declare const clamp: (v: number, lo: number, hi: number) => number;
|
|
23
|
+
export declare function normalize(a: Vec3): Vec3;
|
|
24
|
+
export declare function aabbCenter(box: Aabb): Vec3;
|
|
25
|
+
export declare function aabbMax(box: Aabb): Vec3;
|
|
26
|
+
export declare function aabbCorners(box: Aabb): Vec3[];
|
|
27
|
+
export declare function mergeAabbs(boxes: readonly Aabb[]): Aabb;
|
|
28
|
+
/** Camera basis as three column vectors (x right, y up, z back) — Basis.looking_at. */
|
|
29
|
+
export interface Basis3 {
|
|
30
|
+
x: Vec3;
|
|
31
|
+
y: Vec3;
|
|
32
|
+
z: Vec3;
|
|
33
|
+
}
|
|
34
|
+
export declare function lookAtBasis(position: Vec3, target: Vec3): Basis3;
|
|
35
|
+
export interface Projected {
|
|
36
|
+
u: number;
|
|
37
|
+
v: number;
|
|
38
|
+
/** Distance along the view direction; <= 0 means behind the lens. */
|
|
39
|
+
depth: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function project(pose: CameraPose, aspect: number, point: Vec3): Projected;
|
|
42
|
+
/** World direction of the ray through screen point (u, v). */
|
|
43
|
+
export declare function rayDirection(pose: CameraPose, aspect: number, u: number, v: number): Vec3;
|
|
44
|
+
export interface ScreenBox {
|
|
45
|
+
/** Clipped to the frame, normalized 0..1. */
|
|
46
|
+
u0: number;
|
|
47
|
+
v0: number;
|
|
48
|
+
u1: number;
|
|
49
|
+
v1: number;
|
|
50
|
+
/** Unclipped extent (may exceed 0..1); NaN when a corner is behind the lens. */
|
|
51
|
+
rawWidth: number;
|
|
52
|
+
rawHeight: number;
|
|
53
|
+
/** Fraction of the 8 corners that land inside the frame. */
|
|
54
|
+
cornersInFrame: number;
|
|
55
|
+
anyBehind: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare function screenBox(pose: CameraPose, aspect: number, box: Aabb): ScreenBox;
|
|
58
|
+
/** How much of the frame the box spans along its limiting dimension
|
|
59
|
+
* (max of width and height fractions). Infinity when a corner is behind. */
|
|
60
|
+
export declare function frameFill(pose: CameraPose, aspect: number, box: Aabb): number;
|
|
61
|
+
/** Unit vector from azimuth (0 = +Z, 90 = +X) and elevation (degrees, + = up). */
|
|
62
|
+
export declare function directionFromAngles(azimuthDeg: number, elevationDeg: number): Vec3;
|
|
63
|
+
/**
|
|
64
|
+
* Distance from `lookAt` along `toCamera` (unit) at which the box fills
|
|
65
|
+
* `fill` of the frame along its limiting dimension. Binary search: fill falls
|
|
66
|
+
* monotonically as the camera backs away once every corner is in front.
|
|
67
|
+
*/
|
|
68
|
+
export declare function fitDistance(box: Aabb, lookAt: Vec3, toCamera: Vec3, fov: number, aspect: number, fill: number): number;
|
|
69
|
+
/** Direction presets the screenshot tool uses (direction FROM the subject TO
|
|
70
|
+
* the camera). "iso" matches ScenePreview's 3/4 view (camera at -X, +Y, -Z). */
|
|
71
|
+
export declare const DIRECTION_PRESETS: {
|
|
72
|
+
readonly front: readonly [0, 0, 1];
|
|
73
|
+
readonly back: readonly [0, 0, -1];
|
|
74
|
+
readonly left: readonly [-1, 0, 0];
|
|
75
|
+
readonly right: readonly [1, 0, 0];
|
|
76
|
+
readonly top: readonly [0, 1, 0];
|
|
77
|
+
readonly iso: readonly [-1, 0.7, -1];
|
|
78
|
+
};
|
|
79
|
+
export type DirectionPreset = keyof typeof DIRECTION_PRESETS;
|
|
80
|
+
export declare const DIRECTION_PRESET_NAMES: readonly ["front", "back", "left", "right", "top", "iso"];
|
|
81
|
+
/** Parse a Godot "Vector3(x, y, z)" literal; throws ToolInputError (nothing sent). */
|
|
82
|
+
export declare function parseVector3(literal: string, label: string): Vec3;
|
|
83
|
+
/** "Vector3(x, y, z)" with 3 decimals — the literal the engine echoes. */
|
|
84
|
+
export declare function formatVector3(v: Vec3): string;
|
|
85
|
+
export declare function roundVec(v: Vec3, digits?: number): Vec3;
|
|
86
|
+
/** Rotate `v` about +Y by `degrees` (counter-clockwise seen from above). */
|
|
87
|
+
export declare function rotateY(v: Vec3, degrees: number): Vec3;
|
|
88
|
+
/** Horizontal angle (degrees) that moves a centered subject to a vertical
|
|
89
|
+
* third line for this fov/aspect. */
|
|
90
|
+
export declare function thirdsYawOffset(fov: number, aspect: number): number;
|
|
91
|
+
/** Horizon line position (v, 0 = top) for a level camera pitched by `pitchDeg`
|
|
92
|
+
* (+ = looking up); null when the horizon is outside the frame. */
|
|
93
|
+
export declare function horizonV(pitchDeg: number, fov: number): number | null;
|
|
94
|
+
export declare function pitchOf(pose: CameraPose): number;
|
|
95
|
+
/**
|
|
96
|
+
* The low-angle rule (camera-collision-avoidance): a camera that would sit
|
|
97
|
+
* below `minY` on its line of sight to `lookAt` moves CLOSER along that line
|
|
98
|
+
* and widens its FOV so the subject keeps its size in frame, instead of being
|
|
99
|
+
* buried in the ground or pushed up out of the low angle.
|
|
100
|
+
*/
|
|
101
|
+
export declare function applyLowAngleRule(lookAt: Vec3, toCamera: Vec3, dist: number, fov: number, minY: number, maxFov: number): {
|
|
102
|
+
position: Vec3;
|
|
103
|
+
distance: number;
|
|
104
|
+
fov: number;
|
|
105
|
+
adjusted: boolean;
|
|
106
|
+
impossible?: boolean;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* The zoom window for a region of a reference frame, honoured EXACTLY: the
|
|
110
|
+
* region plus `pad` of its size on each side, clipped to the frame — never
|
|
111
|
+
* grown to an aspect ratio. `widenedBecause` names anything that made the
|
|
112
|
+
* window larger than the region asked for (only the pad can).
|
|
113
|
+
*/
|
|
114
|
+
export declare function exactCrop(rect: {
|
|
115
|
+
u0: number;
|
|
116
|
+
v0: number;
|
|
117
|
+
u1: number;
|
|
118
|
+
v1: number;
|
|
119
|
+
}, pad?: number): {
|
|
120
|
+
crop: [number, number, number, number];
|
|
121
|
+
widenedBecause: string[];
|
|
122
|
+
clipped: boolean;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Lay a window of pixel aspect `regionAspect` into an output image whose
|
|
126
|
+
* longest edge is `maxEdge`. The image takes the window's own aspect, clamped
|
|
127
|
+
* to minAspect..maxAspect; past the clamp the window is centred with dark bars
|
|
128
|
+
* (letterbox / pillarbox) instead of being stretched or widened.
|
|
129
|
+
*/
|
|
130
|
+
export declare function letterbox(regionAspect: number, maxEdge: number, minAspect?: number, maxAspect?: number): {
|
|
131
|
+
canvas: [number, number];
|
|
132
|
+
rect: [number, number, number, number];
|
|
133
|
+
letterboxed: boolean;
|
|
134
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera math for the seeing tools — pure, engine-free, unit-tested.
|
|
3
|
+
*
|
|
4
|
+
* Conventions match the engine (Godot 4): right-handed, +Y up, a camera looks
|
|
5
|
+
* down its local -Z, `lookAtBasis` mirrors Basis.looking_at, and the vertical
|
|
6
|
+
* field of view is fixed (Camera3D KEEP_HEIGHT), so the horizontal extent
|
|
7
|
+
* scales with the aspect ratio. Screen coordinates are normalized: u,v in 0..1
|
|
8
|
+
* with v = 0 at the TOP of the image.
|
|
9
|
+
*/
|
|
10
|
+
import { ToolInputError } from "../../tool-errors.js";
|
|
11
|
+
export const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
12
|
+
export const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
13
|
+
export const scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
|
|
14
|
+
export const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
15
|
+
export const cross = (a, b) => [
|
|
16
|
+
a[1] * b[2] - a[2] * b[1],
|
|
17
|
+
a[2] * b[0] - a[0] * b[2],
|
|
18
|
+
a[0] * b[1] - a[1] * b[0],
|
|
19
|
+
];
|
|
20
|
+
export const length = (a) => Math.sqrt(dot(a, a));
|
|
21
|
+
export const distance = (a, b) => length(sub(a, b));
|
|
22
|
+
export const lerp = (a, b, t) => add(a, scale(sub(b, a), t));
|
|
23
|
+
export const deg = (rad) => (rad * 180) / Math.PI;
|
|
24
|
+
export const rad = (degrees) => (degrees * Math.PI) / 180;
|
|
25
|
+
export const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
26
|
+
export function normalize(a) {
|
|
27
|
+
const l = length(a);
|
|
28
|
+
if (l < 1e-9)
|
|
29
|
+
return [0, 0, 0];
|
|
30
|
+
return [a[0] / l, a[1] / l, a[2] / l];
|
|
31
|
+
}
|
|
32
|
+
export function aabbCenter(box) {
|
|
33
|
+
return add(box.position, scale(box.size, 0.5));
|
|
34
|
+
}
|
|
35
|
+
export function aabbMax(box) {
|
|
36
|
+
return add(box.position, box.size);
|
|
37
|
+
}
|
|
38
|
+
export function aabbCorners(box) {
|
|
39
|
+
const [x0, y0, z0] = box.position;
|
|
40
|
+
const [x1, y1, z1] = aabbMax(box);
|
|
41
|
+
return [
|
|
42
|
+
[x0, y0, z0], [x1, y0, z0], [x0, y1, z0], [x1, y1, z0],
|
|
43
|
+
[x0, y0, z1], [x1, y0, z1], [x0, y1, z1], [x1, y1, z1],
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
export function mergeAabbs(boxes) {
|
|
47
|
+
if (!boxes.length)
|
|
48
|
+
throw new Error("mergeAabbs needs at least one box");
|
|
49
|
+
let lo = boxes[0].position;
|
|
50
|
+
let hi = aabbMax(boxes[0]);
|
|
51
|
+
for (const box of boxes.slice(1)) {
|
|
52
|
+
const bmax = aabbMax(box);
|
|
53
|
+
lo = [Math.min(lo[0], box.position[0]), Math.min(lo[1], box.position[1]), Math.min(lo[2], box.position[2])];
|
|
54
|
+
hi = [Math.max(hi[0], bmax[0]), Math.max(hi[1], bmax[1]), Math.max(hi[2], bmax[2])];
|
|
55
|
+
}
|
|
56
|
+
return { position: lo, size: sub(hi, lo) };
|
|
57
|
+
}
|
|
58
|
+
export function lookAtBasis(position, target) {
|
|
59
|
+
const forward = normalize(sub(target, position));
|
|
60
|
+
// Same guard as the engine: looking_at refuses an up parallel to the view.
|
|
61
|
+
const up = Math.abs(dot(forward, [0, 1, 0])) > 0.999 ? [0, 0, -1] : [0, 1, 0];
|
|
62
|
+
const z = scale(forward, -1);
|
|
63
|
+
const x = normalize(cross(up, z));
|
|
64
|
+
const y = cross(z, x);
|
|
65
|
+
return { x, y, z };
|
|
66
|
+
}
|
|
67
|
+
export function project(pose, aspect, point) {
|
|
68
|
+
const basis = lookAtBasis(pose.position, pose.look_at);
|
|
69
|
+
const rel = sub(point, pose.position);
|
|
70
|
+
const cx = dot(rel, basis.x);
|
|
71
|
+
const cy = dot(rel, basis.y);
|
|
72
|
+
const cz = dot(rel, basis.z);
|
|
73
|
+
const depth = -cz;
|
|
74
|
+
const tanV = Math.tan(rad(pose.fov) / 2);
|
|
75
|
+
const tanH = tanV * aspect;
|
|
76
|
+
if (depth <= 1e-6)
|
|
77
|
+
return { u: Number.NaN, v: Number.NaN, depth };
|
|
78
|
+
const xNdc = cx / (depth * tanH);
|
|
79
|
+
const yNdc = cy / (depth * tanV);
|
|
80
|
+
return { u: (xNdc + 1) / 2, v: (1 - yNdc) / 2, depth };
|
|
81
|
+
}
|
|
82
|
+
/** World direction of the ray through screen point (u, v). */
|
|
83
|
+
export function rayDirection(pose, aspect, u, v) {
|
|
84
|
+
const basis = lookAtBasis(pose.position, pose.look_at);
|
|
85
|
+
const tanV = Math.tan(rad(pose.fov) / 2);
|
|
86
|
+
const x = (u * 2 - 1) * tanV * aspect;
|
|
87
|
+
const y = (1 - v * 2) * tanV;
|
|
88
|
+
return normalize(add(add(scale(basis.x, x), scale(basis.y, y)), scale(basis.z, -1)));
|
|
89
|
+
}
|
|
90
|
+
export function screenBox(pose, aspect, box) {
|
|
91
|
+
let u0 = Infinity;
|
|
92
|
+
let v0 = Infinity;
|
|
93
|
+
let u1 = -Infinity;
|
|
94
|
+
let v1 = -Infinity;
|
|
95
|
+
let inside = 0;
|
|
96
|
+
let behind = false;
|
|
97
|
+
const corners = aabbCorners(box);
|
|
98
|
+
for (const corner of corners) {
|
|
99
|
+
const p = project(pose, aspect, corner);
|
|
100
|
+
if (p.depth <= 1e-6) {
|
|
101
|
+
behind = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
u0 = Math.min(u0, p.u);
|
|
105
|
+
v0 = Math.min(v0, p.v);
|
|
106
|
+
u1 = Math.max(u1, p.u);
|
|
107
|
+
v1 = Math.max(v1, p.v);
|
|
108
|
+
if (p.u >= 0 && p.u <= 1 && p.v >= 0 && p.v <= 1)
|
|
109
|
+
inside += 1;
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isFinite(u0)) {
|
|
112
|
+
return { u0: 0, v0: 0, u1: 0, v1: 0, rawWidth: Number.NaN, rawHeight: Number.NaN, cornersInFrame: 0, anyBehind: true };
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
u0: clamp(u0, 0, 1),
|
|
116
|
+
v0: clamp(v0, 0, 1),
|
|
117
|
+
u1: clamp(u1, 0, 1),
|
|
118
|
+
v1: clamp(v1, 0, 1),
|
|
119
|
+
rawWidth: behind ? Number.NaN : u1 - u0,
|
|
120
|
+
rawHeight: behind ? Number.NaN : v1 - v0,
|
|
121
|
+
cornersInFrame: inside / corners.length,
|
|
122
|
+
anyBehind: behind,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** How much of the frame the box spans along its limiting dimension
|
|
126
|
+
* (max of width and height fractions). Infinity when a corner is behind. */
|
|
127
|
+
export function frameFill(pose, aspect, box) {
|
|
128
|
+
const s = screenBox(pose, aspect, box);
|
|
129
|
+
if (s.anyBehind || !Number.isFinite(s.rawWidth))
|
|
130
|
+
return Infinity;
|
|
131
|
+
return Math.max(s.rawWidth, s.rawHeight);
|
|
132
|
+
}
|
|
133
|
+
/** Unit vector from azimuth (0 = +Z, 90 = +X) and elevation (degrees, + = up). */
|
|
134
|
+
export function directionFromAngles(azimuthDeg, elevationDeg) {
|
|
135
|
+
const az = rad(azimuthDeg);
|
|
136
|
+
const el = rad(elevationDeg);
|
|
137
|
+
return [Math.cos(el) * Math.sin(az), Math.sin(el), Math.cos(el) * Math.cos(az)];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Distance from `lookAt` along `toCamera` (unit) at which the box fills
|
|
141
|
+
* `fill` of the frame along its limiting dimension. Binary search: fill falls
|
|
142
|
+
* monotonically as the camera backs away once every corner is in front.
|
|
143
|
+
*/
|
|
144
|
+
export function fitDistance(box, lookAt, toCamera, fov, aspect, fill) {
|
|
145
|
+
const dir = normalize(toCamera);
|
|
146
|
+
const radius = Math.max(0.05, length(box.size) / 2);
|
|
147
|
+
let lo = 0.05;
|
|
148
|
+
let hi = Math.max(1, radius / Math.tan(rad(Math.min(fov, fov * aspect)) / 2) * 50);
|
|
149
|
+
const fillAt = (d) => frameFill({ position: add(lookAt, scale(dir, d)), look_at: lookAt, fov }, aspect, box);
|
|
150
|
+
if (fillAt(hi) > fill)
|
|
151
|
+
return hi;
|
|
152
|
+
for (let i = 0; i < 60; i++) {
|
|
153
|
+
const mid = (lo + hi) / 2;
|
|
154
|
+
if (fillAt(mid) > fill)
|
|
155
|
+
lo = mid;
|
|
156
|
+
else
|
|
157
|
+
hi = mid;
|
|
158
|
+
}
|
|
159
|
+
return hi;
|
|
160
|
+
}
|
|
161
|
+
/** Direction presets the screenshot tool uses (direction FROM the subject TO
|
|
162
|
+
* the camera). "iso" matches ScenePreview's 3/4 view (camera at -X, +Y, -Z). */
|
|
163
|
+
export const DIRECTION_PRESETS = {
|
|
164
|
+
front: [0, 0, 1],
|
|
165
|
+
back: [0, 0, -1],
|
|
166
|
+
left: [-1, 0, 0],
|
|
167
|
+
right: [1, 0, 0],
|
|
168
|
+
top: [0, 1, 0],
|
|
169
|
+
iso: [-1, 0.7, -1],
|
|
170
|
+
};
|
|
171
|
+
export const DIRECTION_PRESET_NAMES = ["front", "back", "left", "right", "top", "iso"];
|
|
172
|
+
const NUM = "[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?";
|
|
173
|
+
const VECTOR3_LITERAL = new RegExp(`^\\s*Vector3\\s*\\(\\s*(${NUM})\\s*,\\s*(${NUM})\\s*,\\s*(${NUM})\\s*\\)\\s*$`);
|
|
174
|
+
/** Parse a Godot "Vector3(x, y, z)" literal; throws ToolInputError (nothing sent). */
|
|
175
|
+
export function parseVector3(literal, label) {
|
|
176
|
+
const match = VECTOR3_LITERAL.exec(literal);
|
|
177
|
+
if (!match) {
|
|
178
|
+
throw new ToolInputError(`${label} must be a Godot literal like "Vector3(0, 5, 12)" (got ${JSON.stringify(literal)}). Nothing was sent.`);
|
|
179
|
+
}
|
|
180
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
181
|
+
}
|
|
182
|
+
/** "Vector3(x, y, z)" with 3 decimals — the literal the engine echoes. */
|
|
183
|
+
export function formatVector3(v) {
|
|
184
|
+
const f = (n) => {
|
|
185
|
+
const r = Math.round(n * 1000) / 1000;
|
|
186
|
+
return Object.is(r, -0) ? "0" : String(r);
|
|
187
|
+
};
|
|
188
|
+
return `Vector3(${f(v[0])}, ${f(v[1])}, ${f(v[2])})`;
|
|
189
|
+
}
|
|
190
|
+
export function roundVec(v, digits = 3) {
|
|
191
|
+
const k = 10 ** digits;
|
|
192
|
+
return [Math.round(v[0] * k) / k, Math.round(v[1] * k) / k, Math.round(v[2] * k) / k];
|
|
193
|
+
}
|
|
194
|
+
/** Rotate `v` about +Y by `degrees` (counter-clockwise seen from above). */
|
|
195
|
+
export function rotateY(v, degrees) {
|
|
196
|
+
const a = rad(degrees);
|
|
197
|
+
const c = Math.cos(a);
|
|
198
|
+
const s = Math.sin(a);
|
|
199
|
+
return [v[0] * c + v[2] * s, v[1], -v[0] * s + v[2] * c];
|
|
200
|
+
}
|
|
201
|
+
/** Horizontal angle (degrees) that moves a centered subject to a vertical
|
|
202
|
+
* third line for this fov/aspect. */
|
|
203
|
+
export function thirdsYawOffset(fov, aspect) {
|
|
204
|
+
const tanH = Math.tan(rad(fov) / 2) * aspect;
|
|
205
|
+
return deg(Math.atan(tanH / 3));
|
|
206
|
+
}
|
|
207
|
+
/** Horizon line position (v, 0 = top) for a level camera pitched by `pitchDeg`
|
|
208
|
+
* (+ = looking up); null when the horizon is outside the frame. */
|
|
209
|
+
export function horizonV(pitchDeg, fov) {
|
|
210
|
+
const v = 0.5 + Math.tan(rad(pitchDeg)) / (2 * Math.tan(rad(fov) / 2));
|
|
211
|
+
return v >= 0 && v <= 1 ? v : null;
|
|
212
|
+
}
|
|
213
|
+
export function pitchOf(pose) {
|
|
214
|
+
const f = normalize(sub(pose.look_at, pose.position));
|
|
215
|
+
return deg(Math.asin(clamp(f[1], -1, 1)));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The low-angle rule (camera-collision-avoidance): a camera that would sit
|
|
219
|
+
* below `minY` on its line of sight to `lookAt` moves CLOSER along that line
|
|
220
|
+
* and widens its FOV so the subject keeps its size in frame, instead of being
|
|
221
|
+
* buried in the ground or pushed up out of the low angle.
|
|
222
|
+
*/
|
|
223
|
+
export function applyLowAngleRule(lookAt, toCamera, dist, fov, minY, maxFov) {
|
|
224
|
+
const dir = normalize(toCamera);
|
|
225
|
+
const position = add(lookAt, scale(dir, dist));
|
|
226
|
+
if (position[1] >= minY || dir[1] >= -0.02)
|
|
227
|
+
return { position, distance: dist, fov, adjusted: false };
|
|
228
|
+
const d2 = (minY - lookAt[1]) / dir[1];
|
|
229
|
+
if (!(d2 >= 0.5))
|
|
230
|
+
return { position, distance: dist, fov, adjusted: false, impossible: true };
|
|
231
|
+
const fov2 = clamp(deg(2 * Math.atan((Math.tan(rad(fov) / 2) * dist) / d2)), fov, maxFov);
|
|
232
|
+
return { position: add(lookAt, scale(dir, d2)), distance: d2, fov: fov2, adjusted: true };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The zoom window for a region of a reference frame, honoured EXACTLY: the
|
|
236
|
+
* region plus `pad` of its size on each side, clipped to the frame — never
|
|
237
|
+
* grown to an aspect ratio. `widenedBecause` names anything that made the
|
|
238
|
+
* window larger than the region asked for (only the pad can).
|
|
239
|
+
*/
|
|
240
|
+
export function exactCrop(rect, pad = 0) {
|
|
241
|
+
const w = Math.max(1e-4, rect.u1 - rect.u0);
|
|
242
|
+
const h = Math.max(1e-4, rect.v1 - rect.v0);
|
|
243
|
+
const raw = [rect.u0 - w * pad, rect.v0 - h * pad, rect.u1 + w * pad, rect.v1 + h * pad];
|
|
244
|
+
const crop = [clamp(raw[0], 0, 1), clamp(raw[1], 0, 1), clamp(raw[2], 0, 1), clamp(raw[3], 0, 1)];
|
|
245
|
+
const clipped = crop.some((v, i) => Math.abs(v - raw[i]) > 1e-9);
|
|
246
|
+
return { crop, widenedBecause: pad > 0 ? [`pad ${pad} of the region's size added on each side`] : [], clipped };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Lay a window of pixel aspect `regionAspect` into an output image whose
|
|
250
|
+
* longest edge is `maxEdge`. The image takes the window's own aspect, clamped
|
|
251
|
+
* to minAspect..maxAspect; past the clamp the window is centred with dark bars
|
|
252
|
+
* (letterbox / pillarbox) instead of being stretched or widened.
|
|
253
|
+
*/
|
|
254
|
+
export function letterbox(regionAspect, maxEdge, minAspect = 0.5, maxAspect = 3.2) {
|
|
255
|
+
const even = (n) => Math.max(16, Math.round(n / 2) * 2);
|
|
256
|
+
const canvasAspect = clamp(regionAspect, minAspect, maxAspect);
|
|
257
|
+
const canvas = canvasAspect >= 1 ? [even(maxEdge), even(maxEdge / canvasAspect)] : [even(maxEdge * canvasAspect), even(maxEdge)];
|
|
258
|
+
let w = canvas[0];
|
|
259
|
+
let h = Math.round(w / regionAspect);
|
|
260
|
+
if (h > canvas[1]) {
|
|
261
|
+
h = canvas[1];
|
|
262
|
+
w = Math.round(h * regionAspect);
|
|
263
|
+
}
|
|
264
|
+
w = Math.max(16, Math.min(canvas[0], w));
|
|
265
|
+
h = Math.max(16, Math.min(canvas[1], h));
|
|
266
|
+
const x = Math.floor((canvas[0] - w) / 2);
|
|
267
|
+
const y = Math.floor((canvas[1] - h) / 2);
|
|
268
|
+
return { canvas, rect: [x, y, w, h], letterboxed: w < canvas[0] - 1 || h < canvas[1] - 1 };
|
|
269
|
+
}
|