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.
Files changed (366) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +27 -5
  3. package/.codex-plugin/plugin.json +28 -6
  4. package/.cursor-plugin/plugin.json +27 -5
  5. package/.factory-plugin/plugin.json +27 -5
  6. package/.opencode/plugins/summer.js +1 -1
  7. package/CHANGELOG.md +102 -0
  8. package/README.md +18 -3
  9. package/assets/audit/scene_audit.gd +4136 -0
  10. package/assets/seeing/seeing_probe.gd +1873 -0
  11. package/dist/cli/commands/skills.js +20 -2
  12. package/dist/core/api-client.d.ts +9 -1
  13. package/dist/core/api-client.js +21 -4
  14. package/dist/core/capabilities/agent-playbook.js +46 -0
  15. package/dist/core/capabilities/audit/accept.d.ts +55 -0
  16. package/dist/core/capabilities/audit/accept.js +147 -0
  17. package/dist/core/capabilities/audit/args.d.ts +95 -0
  18. package/dist/core/capabilities/audit/args.js +89 -0
  19. package/dist/core/capabilities/audit/audit.d.ts +56 -0
  20. package/dist/core/capabilities/audit/audit.js +287 -0
  21. package/dist/core/capabilities/audit/frame.d.ts +14 -0
  22. package/dist/core/capabilities/audit/frame.js +35 -0
  23. package/dist/core/capabilities/audit/gaps.d.ts +96 -0
  24. package/dist/core/capabilities/audit/gaps.js +257 -0
  25. package/dist/core/capabilities/audit/judge.d.ts +162 -0
  26. package/dist/core/capabilities/audit/judge.js +1134 -0
  27. package/dist/core/capabilities/audit/math.d.ts +110 -0
  28. package/dist/core/capabilities/audit/math.js +263 -0
  29. package/dist/core/capabilities/audit/summary.d.ts +74 -0
  30. package/dist/core/capabilities/audit/summary.js +212 -0
  31. package/dist/core/capabilities/board.d.ts +31 -0
  32. package/dist/core/capabilities/board.js +43 -0
  33. package/dist/core/capabilities/camera-view.d.ts +4 -2
  34. package/dist/core/capabilities/camera-view.js +6 -3
  35. package/dist/core/capabilities/connect-signal.d.ts +85 -0
  36. package/dist/core/capabilities/connect-signal.js +285 -0
  37. package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
  38. package/dist/core/capabilities/engine-fallbacks.js +5 -0
  39. package/dist/core/capabilities/engine-ops.d.ts +5 -1
  40. package/dist/core/capabilities/engine-ops.js +0 -3
  41. package/dist/core/capabilities/engine-receipt.d.ts +12 -0
  42. package/dist/core/capabilities/engine-receipt.js +26 -0
  43. package/dist/core/capabilities/file-read.d.ts +64 -0
  44. package/dist/core/capabilities/file-read.js +325 -0
  45. package/dist/core/capabilities/game-task-plan.js +4 -2
  46. package/dist/core/capabilities/grep.d.ts +60 -0
  47. package/dist/core/capabilities/grep.js +190 -0
  48. package/dist/core/capabilities/inspect-node.d.ts +56 -0
  49. package/dist/core/capabilities/inspect-node.js +225 -0
  50. package/dist/core/capabilities/inspect-resource.d.ts +61 -0
  51. package/dist/core/capabilities/inspect-resource.js +376 -0
  52. package/dist/core/capabilities/math3d.d.ts +28 -0
  53. package/dist/core/capabilities/math3d.js +88 -0
  54. package/dist/core/capabilities/motion-generation.d.ts +62 -0
  55. package/dist/core/capabilities/motion-generation.js +138 -0
  56. package/dist/core/capabilities/placement-batch.d.ts +102 -0
  57. package/dist/core/capabilities/placement-batch.js +483 -0
  58. package/dist/core/capabilities/placement-math.d.ts +70 -0
  59. package/dist/core/capabilities/placement-math.js +209 -0
  60. package/dist/core/capabilities/placement-script.d.ts +42 -0
  61. package/dist/core/capabilities/placement-script.js +1096 -0
  62. package/dist/core/capabilities/placement.d.ts +485 -0
  63. package/dist/core/capabilities/placement.js +1731 -0
  64. package/dist/core/capabilities/project-context.d.ts +33 -2
  65. package/dist/core/capabilities/project-context.js +87 -4
  66. package/dist/core/capabilities/replace-node.d.ts +91 -0
  67. package/dist/core/capabilities/replace-node.js +423 -0
  68. package/dist/core/capabilities/runtime-control.d.ts +18 -2
  69. package/dist/core/capabilities/runtime-control.js +67 -2
  70. package/dist/core/capabilities/scene-batch.d.ts +63 -0
  71. package/dist/core/capabilities/scene-batch.js +394 -0
  72. package/dist/core/capabilities/scene-readback.d.ts +36 -0
  73. package/dist/core/capabilities/scene-readback.js +54 -0
  74. package/dist/core/capabilities/seeing/args.d.ts +348 -0
  75. package/dist/core/capabilities/seeing/args.js +178 -0
  76. package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
  77. package/dist/core/capabilities/seeing/candidates.js +282 -0
  78. package/dist/core/capabilities/seeing/math.d.ts +134 -0
  79. package/dist/core/capabilities/seeing/math.js +269 -0
  80. package/dist/core/capabilities/seeing/probe.d.ts +62 -0
  81. package/dist/core/capabilities/seeing/probe.js +188 -0
  82. package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
  83. package/dist/core/capabilities/seeing/scoring.js +639 -0
  84. package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
  85. package/dist/core/capabilities/seeing/seeing.js +1341 -0
  86. package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
  87. package/dist/core/capabilities/seeing/shot-store.js +204 -0
  88. package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
  89. package/dist/core/capabilities/surface-snap-mesh.js +425 -0
  90. package/dist/core/capabilities/surface-snap.d.ts +77 -0
  91. package/dist/core/capabilities/surface-snap.js +461 -0
  92. package/dist/core/capabilities/tool-dispatch.js +231 -72
  93. package/dist/core/capabilities/tscn.d.ts +85 -0
  94. package/dist/core/capabilities/tscn.js +391 -0
  95. package/dist/core/capabilities/world-snapshot.d.ts +58 -0
  96. package/dist/core/capabilities/world-snapshot.js +150 -0
  97. package/dist/core/capability-skew.d.ts +5 -5
  98. package/dist/core/capability-skew.js +14 -6
  99. package/dist/core/headless/worker-engine-client.d.ts +1 -1
  100. package/dist/core/headless/worker-engine-client.js +2 -2
  101. package/dist/core/library-read.d.ts +37 -5
  102. package/dist/core/library-read.js +264 -33
  103. package/dist/core/library-search.d.ts +2 -2
  104. package/dist/installer/agent-config.js +5 -3
  105. package/dist/installer/agent-table.d.ts +16 -0
  106. package/dist/installer/agent-table.js +52 -17
  107. package/dist/mcp/server.js +9 -3
  108. package/dist/mcp/tools/asset-tools.js +21 -3
  109. package/dist/mcp/tools/audit-tools.d.ts +23 -0
  110. package/dist/mcp/tools/audit-tools.js +60 -0
  111. package/dist/mcp/tools/debug-tools.js +3 -1
  112. package/dist/mcp/tools/file-tools.js +17 -4
  113. package/dist/mcp/tools/generate-tools.js +74 -66
  114. package/dist/mcp/tools/library-tools.js +2 -0
  115. package/dist/mcp/tools/perception-tools.js +6 -19
  116. package/dist/mcp/tools/placement-tools.d.ts +24 -0
  117. package/dist/mcp/tools/placement-tools.js +91 -0
  118. package/dist/mcp/tools/project-tools.js +18 -15
  119. package/dist/mcp/tools/scene-tools.js +71 -83
  120. package/dist/mcp/tools/seeing-tools.d.ts +24 -0
  121. package/dist/mcp/tools/seeing-tools.js +64 -0
  122. package/dist/mcp/tools/spatial-tools.js +17 -9
  123. package/dist/mcp/tools/visual-tools.js +59 -7
  124. package/gemini-extension.json +26 -4
  125. package/integrations/amp/README.md +1 -1
  126. package/integrations/crush/README.md +1 -1
  127. package/integrations/grok-build/README.md +1 -1
  128. package/integrations/hermes/README.md +1 -1
  129. package/integrations/kimi-code/README.md +1 -1
  130. package/integrations/mistral-vibe/README.md +1 -1
  131. package/integrations/opencode/README.md +1 -1
  132. package/integrations/rovo-dev/README.md +1 -1
  133. package/integrations/vscode-copilot/README.md +1 -1
  134. package/integrations/warp/README.md +1 -1
  135. package/integrations/windsurf/README.md +1 -1
  136. package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
  137. package/library/references/mcp-tools-reference/resource.yaml +1 -1
  138. package/library/references/summer-folder/resource.yaml +1 -1
  139. package/library/references/summer-folder/summer-folder.md +1 -1
  140. package/library/skills/3d-lighting/SKILL.md +1 -1
  141. package/library/skills/README.md +36 -14
  142. package/library/skills/adaptive-music/SKILL.md +1 -1
  143. package/library/skills/agent-playtesting/SKILL.md +1 -1
  144. package/library/skills/ai-characters-in-games/SKILL.md +220 -0
  145. package/library/skills/ai-characters-in-games/resource.yaml +30 -0
  146. package/library/skills/ambient-bed/SKILL.md +1 -1
  147. package/library/skills/animated-loop/SKILL.md +1 -1
  148. package/library/skills/animation-tree/SKILL.md +1 -1
  149. package/library/skills/art-direction/SKILL.md +1 -1
  150. package/library/skills/asset-strategy/SKILL.md +2 -1
  151. package/library/skills/asset-strategy/resource.yaml +2 -1
  152. package/library/skills/audio-direction/SKILL.md +1 -1
  153. package/library/skills/auto-fire-targeting/SKILL.md +1 -1
  154. package/library/skills/brainstorm-game/SKILL.md +1 -1
  155. package/library/skills/brainstorming/SKILL.md +1 -1
  156. package/library/skills/browse-templates/SKILL.md +3 -3
  157. package/library/skills/browse-templates/resource.yaml +1 -1
  158. package/library/skills/camera-rigs/SKILL.md +128 -0
  159. package/library/skills/camera-rigs/resource.yaml +29 -0
  160. package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
  161. package/library/skills/character-animation-wiring/SKILL.md +1 -1
  162. package/library/skills/character-model/SKILL.md +1 -1
  163. package/library/skills/character-movement/SKILL.md +326 -0
  164. package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
  165. package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
  166. package/library/skills/character-movement/references/movement-3d.md +136 -0
  167. package/library/skills/character-movement/references/platformer-feel.md +83 -0
  168. package/library/skills/character-movement/resource.yaml +40 -0
  169. package/library/skills/character-portrait/SKILL.md +1 -1
  170. package/library/skills/cinematic-cutscene/SKILL.md +1 -1
  171. package/library/skills/combat-basics/SKILL.md +159 -0
  172. package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
  173. package/library/skills/combat-basics/resource.yaml +34 -0
  174. package/library/skills/concept-art/SKILL.md +1 -1
  175. package/library/skills/create-asset-sheet/SKILL.md +1 -1
  176. package/library/skills/debug/SKILL.md +1 -1
  177. package/library/skills/debugging-game-feel/SKILL.md +1 -1
  178. package/library/skills/design-level/SKILL.md +1 -1
  179. package/library/skills/design-mechanic/SKILL.md +1 -1
  180. package/library/skills/design-npc/SKILL.md +1 -1
  181. package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
  182. package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
  183. package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
  184. package/library/skills/environment-kit/SKILL.md +1 -1
  185. package/library/skills/export-and-ship/SKILL.md +1 -1
  186. package/library/skills/fabricating-assets/SKILL.md +1 -1
  187. package/library/skills/facial-and-lipsync/SKILL.md +1 -1
  188. package/library/skills/fps-controller/SKILL.md +9 -7
  189. package/library/skills/fps-controller/resource.yaml +4 -1
  190. package/library/skills/game-feel/SKILL.md +1 -1
  191. package/library/skills/gameskill/SKILL.md +1 -1
  192. package/library/skills/gdscript-patterns/SKILL.md +1 -1
  193. package/library/skills/generate-motion/SKILL.md +9 -9
  194. package/library/skills/generate-motion/resource.yaml +2 -1
  195. package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
  196. package/library/skills/headless-scripting/SKILL.md +1 -1
  197. package/library/skills/in-game-purchases/SKILL.md +254 -0
  198. package/library/skills/in-game-purchases/resource.yaml +29 -0
  199. package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
  200. package/library/skills/investigating-bugs/SKILL.md +1 -1
  201. package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
  202. package/library/skills/make-game/SKILL.md +25 -3
  203. package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
  204. package/library/skills/make-game/references/2d-platformer.md +117 -0
  205. package/library/skills/make-game/references/2d-rpg-content.md +234 -0
  206. package/library/skills/make-game/references/2d-rpg.md +241 -0
  207. package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
  208. package/library/skills/make-game/references/2d-survivors.md +237 -0
  209. package/library/skills/make-game/references/3d-fps-systems.md +216 -0
  210. package/library/skills/make-game/references/3d-fps.md +177 -0
  211. package/library/skills/make-game/references/3d-horror-systems.md +186 -0
  212. package/library/skills/make-game/references/3d-horror.md +201 -0
  213. package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
  214. package/library/skills/make-game/references/3d-third-person.md +215 -0
  215. package/library/skills/make-game/resource.yaml +6 -1
  216. package/library/skills/multiplayer/SKILL.md +150 -0
  217. package/library/skills/multiplayer/resource.yaml +33 -0
  218. package/library/skills/multiplayer-movement/SKILL.md +510 -0
  219. package/library/skills/multiplayer-movement/files.md +501 -0
  220. package/library/skills/multiplayer-movement/resource.yaml +30 -0
  221. package/library/skills/multiplayer-project/SKILL.md +521 -0
  222. package/library/skills/multiplayer-project/files.md +448 -0
  223. package/library/skills/multiplayer-project/resource.yaml +36 -0
  224. package/library/skills/multiplayer-publish/SKILL.md +229 -0
  225. package/library/skills/multiplayer-publish/export-errors.md +89 -0
  226. package/library/skills/multiplayer-publish/resource.yaml +28 -0
  227. package/library/skills/multiplayer-state/SKILL.md +438 -0
  228. package/library/skills/multiplayer-state/files.md +592 -0
  229. package/library/skills/multiplayer-state/resource.yaml +35 -0
  230. package/library/skills/multiplayer-testing/SKILL.md +236 -0
  231. package/library/skills/multiplayer-testing/resource.yaml +39 -0
  232. package/library/skills/music-track/SKILL.md +1 -1
  233. package/library/skills/navigate-summer/SKILL.md +1 -1
  234. package/library/skills/new-project/SKILL.md +1 -1
  235. package/library/skills/organic-model/SKILL.md +1 -1
  236. package/library/skills/pixel-art/SKILL.md +1 -1
  237. package/library/skills/play/SKILL.md +1 -1
  238. package/library/skills/playtesting-a-feature/SKILL.md +1 -1
  239. package/library/skills/procedural-animation/SKILL.md +2 -2
  240. package/library/skills/procedural-animation/resource.yaml +1 -1
  241. package/library/skills/prop-model/SKILL.md +1 -1
  242. package/library/skills/psx-retro-rendering/SKILL.md +1 -1
  243. package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
  244. package/library/skills/remote-deploy/SKILL.md +1 -1
  245. package/library/skills/retarget/SKILL.md +1 -1
  246. package/library/skills/running-in-the-cloud/SKILL.md +1 -1
  247. package/library/skills/save-load/SKILL.md +156 -0
  248. package/library/skills/save-load/resource.yaml +27 -0
  249. package/library/skills/scene-composition/SKILL.md +1 -1
  250. package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
  251. package/library/skills/scene-scripting/SKILL.md +1 -1
  252. package/library/skills/scene-to-level/SKILL.md +1 -1
  253. package/library/skills/skill-create/SKILL.md +1 -1
  254. package/library/skills/skill-improve/SKILL.md +1 -1
  255. package/library/skills/skill-test/SKILL.md +1 -1
  256. package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
  257. package/library/skills/skybox-panorama/SKILL.md +1 -1
  258. package/library/skills/sound-effect/SKILL.md +1 -1
  259. package/library/skills/spatial-placement/SKILL.md +134 -3
  260. package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
  261. package/library/skills/spatial-placement/resource.yaml +16 -2
  262. package/library/skills/sprite-sheet/SKILL.md +1 -1
  263. package/library/skills/summer-analytics/SKILL.md +147 -0
  264. package/library/skills/summer-analytics/resource.yaml +28 -0
  265. package/library/skills/summer-friends/SKILL.md +226 -0
  266. package/library/skills/summer-friends/resource.yaml +28 -0
  267. package/library/skills/summer-leaderboards/SKILL.md +231 -0
  268. package/library/skills/summer-leaderboards/resource.yaml +30 -0
  269. package/library/skills/summer-match-results/SKILL.md +286 -0
  270. package/library/skills/summer-match-results/resource.yaml +32 -0
  271. package/library/skills/summer-matchmaking/SKILL.md +301 -0
  272. package/library/skills/summer-matchmaking/resource.yaml +32 -0
  273. package/library/skills/summer-parties/SKILL.md +253 -0
  274. package/library/skills/summer-parties/resource.yaml +30 -0
  275. package/library/skills/summer-player-data/SKILL.md +322 -0
  276. package/library/skills/summer-player-data/resource.yaml +30 -0
  277. package/library/skills/summer-store/SKILL.md +271 -0
  278. package/library/skills/summer-store/resource.yaml +29 -0
  279. package/library/skills/summer-world-chat/SKILL.md +195 -0
  280. package/library/skills/summer-world-chat/resource.yaml +30 -0
  281. package/library/skills/summer-world-saves/SKILL.md +284 -0
  282. package/library/skills/summer-world-saves/resource.yaml +29 -0
  283. package/library/skills/text-to-motion/SKILL.md +99 -0
  284. package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
  285. package/library/skills/text-to-motion/resource.yaml +41 -0
  286. package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
  287. package/library/skills/tileable-texture/SKILL.md +1 -1
  288. package/library/skills/trailer-shot/SKILL.md +1 -1
  289. package/library/skills/tune-performance/SKILL.md +1 -1
  290. package/library/skills/ui-basics/SKILL.md +10 -1
  291. package/library/skills/ui-graphics/SKILL.md +1 -1
  292. package/library/skills/use-widget-asset/SKILL.md +1 -1
  293. package/library/skills/using-summer/SKILL.md +2 -2
  294. package/library/skills/using-summer/resource.yaml +2 -2
  295. package/library/skills/vehicle-model/SKILL.md +1 -1
  296. package/library/skills/verification-before-completion/SKILL.md +1 -1
  297. package/library/skills/verifying-scenes/SKILL.md +76 -2
  298. package/library/skills/verifying-scenes/resource.yaml +13 -2
  299. package/library/skills/vfx-dissolve/SKILL.md +1 -1
  300. package/library/skills/vfx-fire/SKILL.md +1 -1
  301. package/library/skills/vfx-hit-spark/SKILL.md +1 -1
  302. package/library/skills/vfx-lightning/SKILL.md +1 -1
  303. package/library/skills/vfx-magic-glow/SKILL.md +1 -1
  304. package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
  305. package/library/skills/vfx-smoke/SKILL.md +1 -1
  306. package/library/skills/vfx-water-ripple/SKILL.md +1 -1
  307. package/library/skills/voice-line/SKILL.md +1 -1
  308. package/library/skills/web-and-touch-input/SKILL.md +38 -0
  309. package/library/skills/web-and-touch-input/resource.yaml +28 -0
  310. package/library/skills/web-compatibility-look/SKILL.md +62 -0
  311. package/library/skills/web-compatibility-look/resource.yaml +30 -0
  312. package/library/skills/world-building-3d/SKILL.md +8 -1
  313. package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
  314. package/library/skills/world-building-3d/resource.yaml +2 -1
  315. package/library/skills/writing-plans/SKILL.md +1 -1
  316. package/library/skills/writing-skills/SKILL.md +1 -1
  317. package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
  318. package/library/tools/attach-to-surface/resource.yaml +142 -0
  319. package/library/tools/batch/resource.yaml +9 -3
  320. package/library/tools/connect-ports/resource.yaml +120 -0
  321. package/library/tools/connect-signal/resource.yaml +3 -2
  322. package/library/tools/debug-views/resource.yaml +94 -0
  323. package/library/tools/frame-nodes/resource.yaml +104 -0
  324. package/library/tools/frame-shot/resource.yaml +134 -0
  325. package/library/tools/generate-motion/resource.yaml +44 -9
  326. package/library/tools/get-board/resource.yaml +50 -0
  327. package/library/tools/get-project-context/resource.yaml +13 -2
  328. package/library/tools/grep/resource.yaml +77 -0
  329. package/library/tools/inspect-asset/resource.yaml +71 -0
  330. package/library/tools/inspect-node/resource.yaml +9 -2
  331. package/library/tools/inspect-resource/resource.yaml +16 -5
  332. package/library/tools/instantiate-scene/resource.yaml +27 -2
  333. package/library/tools/measure/resource.yaml +96 -0
  334. package/library/tools/place-adjacent/resource.yaml +113 -0
  335. package/library/tools/play/resource.yaml +15 -2
  336. package/library/tools/raycast/resource.yaml +101 -0
  337. package/library/tools/read-file/resource.yaml +36 -2
  338. package/library/tools/read-library/resource.yaml +2 -2
  339. package/library/tools/repeat-along/resource.yaml +129 -0
  340. package/library/tools/replace-node/resource.yaml +11 -5
  341. package/library/tools/scene-audit/resource.yaml +144 -0
  342. package/library/tools/screenshot/resource.yaml +13 -2
  343. package/library/tools/search-assets/resource.yaml +18 -0
  344. package/library/tools/shot-sheet/resource.yaml +110 -0
  345. package/library/tools/snap-to-surface/resource.yaml +4 -1
  346. package/library/tools/world-snapshot/resource.yaml +24 -3
  347. package/library/tools/zoom/resource.yaml +105 -0
  348. package/package.json +1 -1
  349. package/registry/generated/aliases.json +17 -3
  350. package/registry/generated/counts.json +3 -3
  351. package/registry/generated/gemini-extension.json +26 -4
  352. package/registry/generated/index.json +2154 -469
  353. package/registry/generated/marketplace.claude.json +2 -2
  354. package/registry/generated/plugin.claude.json +27 -5
  355. package/registry/generated/plugin.codex.json +28 -6
  356. package/registry/generated/plugin.cursor.json +27 -5
  357. package/registry/generated/plugin.factory.json +27 -5
  358. package/registry/generated/skills-index.md +36 -14
  359. package/registry/generated/skills-registry.json +421 -124
  360. package/registry/generated/templates-registry.json +2 -2
  361. package/library/skills/host-authoritative-state/SKILL.md +0 -332
  362. package/library/skills/host-authoritative-state/resource.yaml +0 -28
  363. package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
  364. package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
  365. package/library/skills/setup-multiplayer/SKILL.md +0 -336
  366. 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
+ }