lecodes-sdk 0.19.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 (367) hide show
  1. package/README.md +114 -0
  2. package/dist/global.d.ts +503 -0
  3. package/dist/host.d.ts +61 -0
  4. package/dist/inject.js +4730 -0
  5. package/dist/types/animate/animate.d.ts +20 -0
  6. package/dist/types/animate/bezier.d.ts +2 -0
  7. package/dist/types/animate/easings.d.ts +21 -0
  8. package/dist/types/canvas/Canvas.d.ts +168 -0
  9. package/dist/types/core/Aspect.d.ts +171 -0
  10. package/dist/types/core/InspectorUI.d.ts +88 -0
  11. package/dist/types/core/StateMachine.d.ts +81 -0
  12. package/dist/types/core/color.d.ts +13 -0
  13. package/dist/types/core/compWrite.d.ts +11 -0
  14. package/dist/types/core/events.d.ts +11 -0
  15. package/dist/types/core/fields.d.ts +58 -0
  16. package/dist/types/core/registry.d.ts +7 -0
  17. package/dist/types/core/signals.d.ts +29 -0
  18. package/dist/types/core/time.d.ts +31 -0
  19. package/dist/types/g2/Camera2D.d.ts +19 -0
  20. package/dist/types/g2/CharacterController2D.d.ts +134 -0
  21. package/dist/types/g2/Node2D.d.ts +91 -0
  22. package/dist/types/g2/OneWay2D.d.ts +24 -0
  23. package/dist/types/g2/Physics2D.d.ts +147 -0
  24. package/dist/types/g2/Scene2D.d.ts +72 -0
  25. package/dist/types/g2/Shape2D.d.ts +58 -0
  26. package/dist/types/g2/Sprite.d.ts +51 -0
  27. package/dist/types/g2/SpriteAnimation.d.ts +50 -0
  28. package/dist/types/g2/SpriteSheet.d.ts +69 -0
  29. package/dist/types/g2/Texture2D.d.ts +16 -0
  30. package/dist/types/g2/Tilemap.d.ts +20 -0
  31. package/dist/types/g2/Tileset.d.ts +32 -0
  32. package/dist/types/g2/Trigger2D.d.ts +20 -0
  33. package/dist/types/g2/autotile.d.ts +133 -0
  34. package/dist/types/g2/cells.d.ts +14 -0
  35. package/dist/types/g2/defineScene2d.d.ts +141 -0
  36. package/dist/types/g2/groups2d.d.ts +16 -0
  37. package/dist/types/g2/loop.d.ts +4 -0
  38. package/dist/types/g2/scenarios2d.d.ts +30 -0
  39. package/dist/types/g2/touch.d.ts +2 -0
  40. package/dist/types/gl/Camera.d.ts +79 -0
  41. package/dist/types/gl/CameraPlace.d.ts +26 -0
  42. package/dist/types/gl/CharacterController.d.ts +98 -0
  43. package/dist/types/gl/Gearbox.d.ts +86 -0
  44. package/dist/types/gl/Geometry.d.ts +39 -0
  45. package/dist/types/gl/IK.d.ts +53 -0
  46. package/dist/types/gl/InstancedMesh.d.ts +46 -0
  47. package/dist/types/gl/Light.d.ts +73 -0
  48. package/dist/types/gl/Lightmap.d.ts +85 -0
  49. package/dist/types/gl/Locomotion.d.ts +279 -0
  50. package/dist/types/gl/Material.d.ts +100 -0
  51. package/dist/types/gl/Mesh.d.ts +33 -0
  52. package/dist/types/gl/Model.d.ts +53 -0
  53. package/dist/types/gl/NavAgent.d.ts +99 -0
  54. package/dist/types/gl/NavMesh.d.ts +169 -0
  55. package/dist/types/gl/Node.d.ts +99 -0
  56. package/dist/types/gl/Noise.d.ts +20 -0
  57. package/dist/types/gl/Particles.d.ts +277 -0
  58. package/dist/types/gl/Physics.d.ts +91 -0
  59. package/dist/types/gl/Plane.d.ts +19 -0
  60. package/dist/types/gl/Ragdoll.d.ts +86 -0
  61. package/dist/types/gl/Ray.d.ts +8 -0
  62. package/dist/types/gl/Scene.d.ts +221 -0
  63. package/dist/types/gl/Shape.d.ts +122 -0
  64. package/dist/types/gl/Terrain.d.ts +280 -0
  65. package/dist/types/gl/Texture.d.ts +27 -0
  66. package/dist/types/gl/Trigger.d.ts +10 -0
  67. package/dist/types/gl/Vehicle.d.ts +191 -0
  68. package/dist/types/gl/Wheel.d.ts +95 -0
  69. package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
  70. package/dist/types/gl/animation/Animator.d.ts +219 -0
  71. package/dist/types/gl/animation/Layer.d.ts +31 -0
  72. package/dist/types/gl/animation/Loop.d.ts +17 -0
  73. package/dist/types/gl/animation/Playback.d.ts +36 -0
  74. package/dist/types/gl/animation/core.d.ts +367 -0
  75. package/dist/types/gl/controls.d.ts +19 -0
  76. package/dist/types/gl/physicsEvents.d.ts +1 -0
  77. package/dist/types/gl/scenarios.d.ts +98 -0
  78. package/dist/types/gl/state.d.ts +5 -0
  79. package/dist/types/gl/terrainMesh.d.ts +46 -0
  80. package/dist/types/gl/touch.d.ts +2 -0
  81. package/dist/types/inject.d.ts +123 -0
  82. package/dist/types/math/Mathf.d.ts +39 -0
  83. package/dist/types/math/mat4.d.ts +66 -0
  84. package/dist/types/math/quat.d.ts +53 -0
  85. package/dist/types/math/vec.d.ts +129 -0
  86. package/dist/types/net/codec.d.ts +25 -0
  87. package/dist/types/net/core.d.ts +106 -0
  88. package/dist/types/net/index.d.ts +80 -0
  89. package/dist/types/net/replication.d.ts +101 -0
  90. package/dist/types/plugins/camera.d.ts +25 -0
  91. package/dist/types/plugins/geolocation.d.ts +39 -0
  92. package/dist/types/plugins/oauth.d.ts +25 -0
  93. package/dist/types/plugins/permission.d.ts +1 -0
  94. package/dist/types/plugins/push.d.ts +46 -0
  95. package/dist/types/plugins/qr.d.ts +21 -0
  96. package/dist/types/plugins/service.d.ts +33 -0
  97. package/dist/types/runtime/app.d.ts +67 -0
  98. package/dist/types/runtime/appEvents.d.ts +7 -0
  99. package/dist/types/runtime/channel.d.ts +1 -0
  100. package/dist/types/runtime/clipboard.d.ts +7 -0
  101. package/dist/types/runtime/datetime.d.ts +80 -0
  102. package/dist/types/runtime/device.d.ts +126 -0
  103. package/dist/types/runtime/fetch.d.ts +31 -0
  104. package/dist/types/runtime/files.d.ts +33 -0
  105. package/dist/types/runtime/input.d.ts +121 -0
  106. package/dist/types/runtime/media.d.ts +46 -0
  107. package/dist/types/runtime/misc.d.ts +11 -0
  108. package/dist/types/runtime/net.d.ts +15 -0
  109. package/dist/types/runtime/rpc.d.ts +44 -0
  110. package/dist/types/runtime/service.d.ts +1 -0
  111. package/dist/types/runtime/share.d.ts +2 -0
  112. package/dist/types/runtime/storage.d.ts +5 -0
  113. package/dist/types/runtime/touch.d.ts +56 -0
  114. package/dist/types/scene/defineScene.d.ts +296 -0
  115. package/dist/types/scene/editorPlugins.d.ts +82 -0
  116. package/dist/types/scene/gizmos.d.ts +56 -0
  117. package/dist/types/scene/grammar.d.ts +56 -0
  118. package/dist/types/scene/level.d.ts +98 -0
  119. package/dist/types/scene/material.d.ts +76 -0
  120. package/dist/types/server/auth/appConfig.d.ts +16 -0
  121. package/dist/types/server/auth/global.d.ts +56 -0
  122. package/dist/types/server/auth/models.d.ts +164 -0
  123. package/dist/types/server/auth/types.d.ts +56 -0
  124. package/dist/types/server/channel.d.ts +28 -0
  125. package/dist/types/server/context.d.ts +27 -0
  126. package/dist/types/server/db/defineDb.d.ts +36 -0
  127. package/dist/types/server/db/fields.d.ts +146 -0
  128. package/dist/types/server/db/index.d.ts +6 -0
  129. package/dist/types/server/db/marci/query.d.ts +326 -0
  130. package/dist/types/server/db/types.d.ts +163 -0
  131. package/dist/types/server/errors.d.ts +8 -0
  132. package/dist/types/server/inject.d.ts +5 -0
  133. package/dist/types/ui/NativeView.d.ts +38 -0
  134. package/dist/types/ui/UI.d.ts +27 -0
  135. package/dist/types/ui/UIBottomSheet.d.ts +71 -0
  136. package/dist/types/ui/UIButton.d.ts +30 -0
  137. package/dist/types/ui/UIContainer.d.ts +33 -0
  138. package/dist/types/ui/UIImage.d.ts +35 -0
  139. package/dist/types/ui/UIInput.d.ts +110 -0
  140. package/dist/types/ui/UIModal.d.ts +70 -0
  141. package/dist/types/ui/UINode.d.ts +363 -0
  142. package/dist/types/ui/UIPager.d.ts +123 -0
  143. package/dist/types/ui/UIPopover.d.ts +39 -0
  144. package/dist/types/ui/UIScreen.d.ts +57 -0
  145. package/dist/types/ui/UIScrollable.d.ts +40 -0
  146. package/dist/types/ui/UISpacer.d.ts +9 -0
  147. package/dist/types/ui/UITabs.d.ts +96 -0
  148. package/dist/types/ui/UIText.d.ts +19 -0
  149. package/dist/types/ui/UIVideo.d.ts +28 -0
  150. package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
  151. package/dist/types/ui/UIWidget.d.ts +65 -0
  152. package/dist/types/ui/fonts.d.ts +9 -0
  153. package/dist/types/ui/presentable.d.ts +68 -0
  154. package/dist/types/ui/router.d.ts +40 -0
  155. package/dist/types/ui/theme.d.ts +26 -0
  156. package/dist/types.json +1 -0
  157. package/package.json +46 -0
  158. package/prompts/2d.md +395 -0
  159. package/prompts/3d-scene-files.md +113 -0
  160. package/prompts/3d-scene.md +119 -0
  161. package/prompts/3d.md +303 -0
  162. package/prompts/README.md +142 -0
  163. package/prompts/ar.md +109 -0
  164. package/prompts/canvas.md +27 -0
  165. package/prompts/compose.ts +46 -0
  166. package/prompts/concept.md +49 -0
  167. package/prompts/core-design.md +75 -0
  168. package/prompts/core.md +264 -0
  169. package/prompts/design.md +218 -0
  170. package/prompts/dist/2d-game.md +1530 -0
  171. package/prompts/dist/3d-app.md +1672 -0
  172. package/prompts/dist/ar-app.md +1548 -0
  173. package/prompts/dist/concept.md +49 -0
  174. package/prompts/dist/design.md +498 -0
  175. package/prompts/dist/ui-app.md +1134 -0
  176. package/prompts/index.md +37 -0
  177. package/prompts/intake-prompt.md +33 -0
  178. package/prompts/namer-prompt.md +12 -0
  179. package/prompts/router-prompt.md +50 -0
  180. package/prompts/select.ts +111 -0
  181. package/prompts/ui-design.md +203 -0
  182. package/prompts/ui.md +803 -0
  183. package/src/animate/animate.ts +238 -0
  184. package/src/animate/bezier.ts +138 -0
  185. package/src/animate/easings.ts +126 -0
  186. package/src/bridges.d.ts +1345 -0
  187. package/src/canvas/Canvas.ts +305 -0
  188. package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
  189. package/src/compile/__tests__/assetMacro.test.ts +74 -0
  190. package/src/compile/__tests__/assetName.test.ts +55 -0
  191. package/src/compile/__tests__/compile.test.ts +287 -0
  192. package/src/compile/__tests__/detectEntry.test.ts +132 -0
  193. package/src/compile/__tests__/fontMacro.test.ts +199 -0
  194. package/src/compile/aspectMacro.ts +86 -0
  195. package/src/compile/assetIconMacro.ts +384 -0
  196. package/src/compile/assetMacro.ts +146 -0
  197. package/src/compile/assetName.ts +50 -0
  198. package/src/compile/bundler.ts +287 -0
  199. package/src/compile/compileProject.ts +127 -0
  200. package/src/compile/detectEntry.ts +128 -0
  201. package/src/compile/fontMacro.ts +459 -0
  202. package/src/compile/fontRegistry.ts +78 -0
  203. package/src/compile/header.ts +67 -0
  204. package/src/compile/index.ts +100 -0
  205. package/src/compile/libraryImports.ts +52 -0
  206. package/src/compile/liteMaterial.ts +247 -0
  207. package/src/compile/sceneEditor.ts +88 -0
  208. package/src/compile/serverSplit.ts +233 -0
  209. package/src/compile/serverTypes.ts +227 -0
  210. package/src/compile/sfnt.ts +98 -0
  211. package/src/compile/shaderSchema.ts +202 -0
  212. package/src/compile/shaderTargets.ts +77 -0
  213. package/src/compile/sourcemap.ts +25 -0
  214. package/src/core/Aspect.ts +568 -0
  215. package/src/core/InspectorUI.ts +212 -0
  216. package/src/core/StateMachine.ts +308 -0
  217. package/src/core/__tests__/stateMachine.test.ts +132 -0
  218. package/src/core/color.ts +66 -0
  219. package/src/core/compWrite.ts +42 -0
  220. package/src/core/events.ts +38 -0
  221. package/src/core/fields.ts +120 -0
  222. package/src/core/registry.ts +23 -0
  223. package/src/core/signals.ts +277 -0
  224. package/src/core/time.ts +84 -0
  225. package/src/g2/Camera2D.ts +40 -0
  226. package/src/g2/CharacterController2D.ts +276 -0
  227. package/src/g2/Node2D.ts +267 -0
  228. package/src/g2/OneWay2D.ts +66 -0
  229. package/src/g2/Physics2D.ts +346 -0
  230. package/src/g2/Scene2D.ts +209 -0
  231. package/src/g2/Shape2D.ts +259 -0
  232. package/src/g2/Sprite.ts +89 -0
  233. package/src/g2/SpriteAnimation.ts +171 -0
  234. package/src/g2/SpriteSheet.ts +166 -0
  235. package/src/g2/Texture2D.ts +47 -0
  236. package/src/g2/Tilemap.ts +41 -0
  237. package/src/g2/Tileset.ts +71 -0
  238. package/src/g2/Trigger2D.ts +77 -0
  239. package/src/g2/autotile.ts +433 -0
  240. package/src/g2/cells.ts +91 -0
  241. package/src/g2/defineScene2d.ts +381 -0
  242. package/src/g2/groups2d.ts +106 -0
  243. package/src/g2/loop.ts +50 -0
  244. package/src/g2/scenarios2d.ts +69 -0
  245. package/src/g2/touch.ts +83 -0
  246. package/src/gl/Camera.ts +160 -0
  247. package/src/gl/CameraPlace.ts +52 -0
  248. package/src/gl/CharacterController.ts +238 -0
  249. package/src/gl/Gearbox.ts +212 -0
  250. package/src/gl/Geometry.ts +279 -0
  251. package/src/gl/IK.ts +193 -0
  252. package/src/gl/InstancedMesh.ts +132 -0
  253. package/src/gl/Light.ts +135 -0
  254. package/src/gl/Lightmap.ts +402 -0
  255. package/src/gl/Locomotion.ts +498 -0
  256. package/src/gl/Material.ts +279 -0
  257. package/src/gl/Mesh.ts +83 -0
  258. package/src/gl/Model.ts +124 -0
  259. package/src/gl/NavAgent.ts +337 -0
  260. package/src/gl/NavMesh.ts +397 -0
  261. package/src/gl/Node.ts +350 -0
  262. package/src/gl/Noise.ts +30 -0
  263. package/src/gl/Particles.ts +676 -0
  264. package/src/gl/Physics.ts +222 -0
  265. package/src/gl/Plane.ts +53 -0
  266. package/src/gl/Ragdoll.ts +270 -0
  267. package/src/gl/Ray.ts +16 -0
  268. package/src/gl/Scene.ts +505 -0
  269. package/src/gl/Shape.ts +409 -0
  270. package/src/gl/Terrain.ts +1092 -0
  271. package/src/gl/Texture.ts +63 -0
  272. package/src/gl/Trigger.ts +45 -0
  273. package/src/gl/Vehicle.ts +473 -0
  274. package/src/gl/Wheel.ts +240 -0
  275. package/src/gl/animation/AnimationClip.ts +204 -0
  276. package/src/gl/animation/Animator.ts +329 -0
  277. package/src/gl/animation/Layer.ts +39 -0
  278. package/src/gl/animation/Loop.ts +31 -0
  279. package/src/gl/animation/Playback.ts +52 -0
  280. package/src/gl/animation/core.ts +670 -0
  281. package/src/gl/controls.ts +95 -0
  282. package/src/gl/physicsEvents.ts +20 -0
  283. package/src/gl/scenarios.ts +291 -0
  284. package/src/gl/state.ts +6 -0
  285. package/src/gl/terrainMesh.ts +219 -0
  286. package/src/gl/touch.ts +68 -0
  287. package/src/host.d.ts +61 -0
  288. package/src/inject.ts +203 -0
  289. package/src/math/Mathf.ts +118 -0
  290. package/src/math/mat4.ts +278 -0
  291. package/src/math/quat.ts +232 -0
  292. package/src/math/vec.ts +255 -0
  293. package/src/net/codec.ts +119 -0
  294. package/src/net/core.ts +384 -0
  295. package/src/net/index.ts +181 -0
  296. package/src/net/replication.ts +622 -0
  297. package/src/plugins/camera.ts +81 -0
  298. package/src/plugins/geolocation.ts +123 -0
  299. package/src/plugins/oauth.ts +61 -0
  300. package/src/plugins/permission.ts +7 -0
  301. package/src/plugins/push.ts +132 -0
  302. package/src/plugins/qr.ts +73 -0
  303. package/src/plugins/service.ts +47 -0
  304. package/src/runtime/app.ts +101 -0
  305. package/src/runtime/appEvents.ts +54 -0
  306. package/src/runtime/channel.ts +61 -0
  307. package/src/runtime/clipboard.ts +20 -0
  308. package/src/runtime/datetime.ts +329 -0
  309. package/src/runtime/device.ts +293 -0
  310. package/src/runtime/fetch.ts +77 -0
  311. package/src/runtime/files.ts +108 -0
  312. package/src/runtime/input.ts +175 -0
  313. package/src/runtime/media.ts +111 -0
  314. package/src/runtime/misc.ts +16 -0
  315. package/src/runtime/net.ts +36 -0
  316. package/src/runtime/rpc.ts +218 -0
  317. package/src/runtime/service.ts +83 -0
  318. package/src/runtime/share.ts +9 -0
  319. package/src/runtime/storage.ts +13 -0
  320. package/src/runtime/touch.ts +76 -0
  321. package/src/scene/defineScene.ts +1396 -0
  322. package/src/scene/editorPlugins.ts +110 -0
  323. package/src/scene/gizmos.ts +148 -0
  324. package/src/scene/grammar.ts +120 -0
  325. package/src/scene/level.ts +296 -0
  326. package/src/scene/material.ts +188 -0
  327. package/src/server/auth/appConfig.ts +12 -0
  328. package/src/server/auth/global.ts +80 -0
  329. package/src/server/auth/host.ts +318 -0
  330. package/src/server/auth/models.ts +83 -0
  331. package/src/server/auth/types.ts +50 -0
  332. package/src/server/channel.ts +56 -0
  333. package/src/server/context.ts +36 -0
  334. package/src/server/db/defineDb.ts +237 -0
  335. package/src/server/db/fields.ts +132 -0
  336. package/src/server/db/httpTransport.ts +93 -0
  337. package/src/server/db/index.ts +7 -0
  338. package/src/server/db/marci/query.ts +412 -0
  339. package/src/server/db/types.ts +202 -0
  340. package/src/server/errors.ts +12 -0
  341. package/src/server/host.ts +74 -0
  342. package/src/server/inject.ts +13 -0
  343. package/src/server/runtime.ts +133 -0
  344. package/src/server/validate.ts +87 -0
  345. package/src/ui/NativeView.ts +142 -0
  346. package/src/ui/UI.ts +39 -0
  347. package/src/ui/UIBottomSheet.ts +139 -0
  348. package/src/ui/UIButton.ts +101 -0
  349. package/src/ui/UIContainer.ts +60 -0
  350. package/src/ui/UIImage.ts +83 -0
  351. package/src/ui/UIInput.ts +185 -0
  352. package/src/ui/UIModal.ts +139 -0
  353. package/src/ui/UINode.ts +830 -0
  354. package/src/ui/UIPager.ts +362 -0
  355. package/src/ui/UIPopover.ts +100 -0
  356. package/src/ui/UIScreen.ts +123 -0
  357. package/src/ui/UIScrollable.ts +87 -0
  358. package/src/ui/UISpacer.ts +14 -0
  359. package/src/ui/UITabs.ts +236 -0
  360. package/src/ui/UIText.ts +51 -0
  361. package/src/ui/UIVideo.ts +88 -0
  362. package/src/ui/UIVirtualizedList.ts +241 -0
  363. package/src/ui/UIWidget.ts +127 -0
  364. package/src/ui/fonts.ts +13 -0
  365. package/src/ui/presentable.ts +117 -0
  366. package/src/ui/router.ts +132 -0
  367. package/src/ui/theme.ts +84 -0
@@ -0,0 +1,110 @@
1
+ // Editor plugins: `registerEditorWindow` / `registerEditorTool` (docs/scene-editor-plan.md,
2
+ // phase 11). Registrations live in `*.editor.ts` files — compiled and executed ONLY in
3
+ // scene-editor bundles (phase 10), so none of this reaches shipped app.js (nothing references it
4
+ // there, and method-granular DCE drops it).
5
+ //
6
+ // The scene-editor harness (packages/projects/src/scene-editor) reads the registry through the
7
+ // injected `__editorPlugins` global — the harness, the scene, and the editor files compile into
8
+ // ONE bundle, so they share this module instance. Windows and tools describe their UI through the
9
+ // same immediate-mode `InspectorUI` protocol custom inspector cards use (one widget vocabulary,
10
+ // three mount points), and mutate the scene ONLY through the `editor` doc API — every plugin write
11
+ // lands in the scene document (undoable, diffable), never in live engine state.
12
+
13
+ import type { InspectorUI } from "../core/InspectorUI"
14
+
15
+ /** A geometry hit under the viewport pointer, handed to tools. Physics builds raycast the scene's
16
+ * meshes (precise); otherwise (and on misses) the hit falls back to the ground plane (y = 0). */
17
+ export type EditorRayHit = {
18
+ point: [number, number, number]
19
+ normal: [number, number, number]
20
+ /** Absolute path of the scene-file node the hit belongs to ("city/in1/pt1") — null for
21
+ * ground-plane fallback hits. */
22
+ node: string | null
23
+ }
24
+
25
+ /**
26
+ * The editor scripting API handed to windows and tools. Nodes are addressed by ABSOLUTE PATH —
27
+ * '/'-joined names from the scene root; a root node's path is its bare name. Doc-op methods write
28
+ * the scene DOCUMENT through the editor's normal commit path (undo, file, live patching all
29
+ * included); they return false / no-op when the document can't take the edit (sibling-name
30
+ * collision, unknown path).
31
+ */
32
+ export type EditorApi = {
33
+ /** The currently selected node path (or `path::part` key for asset internals), null when
34
+ * nothing is selected. */
35
+ readonly selection: string | null
36
+ select(path: string | null): void
37
+ /** The scene document's nodes: absolute path, sibling-unique display name, source kind
38
+ * (mesh / model / light / group / …). */
39
+ nodes(): { path: string, name: string, kind: string }[]
40
+ /** First unused "base", "base2", "base3", … name among the SIBLINGS under `parentPath`
41
+ * (scene root when omitted). */
42
+ uniqueName(base: string, parentPath?: string): string
43
+ /** Add a ROOT node from plain def data (scene-file grammar; a string `model` value means an
44
+ * asset path). `name` may not contain '/' or ':'. One undo step unless grouped by `transact`. */
45
+ addNode(name: string, def: Record<string, unknown>): boolean
46
+ /** Write one def prop (transforms apply live; anything else patches/re-runs the node). */
47
+ setProp(path: string, key: string, value: unknown): boolean
48
+ removeNode(path: string): void
49
+ /** Duplicate a node; returns the copy's path (null when it can't). */
50
+ duplicate(path: string): string | null
51
+ /** Raycast the scene under a viewport pixel (same hit rules as tool clicks). */
52
+ raycast(screenX: number, screenY: number): EditorRayHit | null
53
+ /** Group every doc edit inside `fn` into ONE undo step. */
54
+ transact(fn: () => void): void
55
+ /** Read one def prop of a node as document data (`asset()` refs come as `{ $asset: "./x" }`);
56
+ * undefined when the node or key doesn't exist. */
57
+ getProp(path: string, key: string): unknown
58
+ /** Write a BINARY asset file for a node (a terrain's `.terrain`, a baked mask…). `ref` = the
59
+ * node's existing def-relative `asset()` ref to overwrite, or null to create `./<name>` at the
60
+ * project root (a free name is picked on collisions). Resolves to the ref to store in the node's
61
+ * def (`{ $asset: ref }` via `setProp`), null when the host can't write files. Not a doc edit —
62
+ * it is not undoable; keep the doc pointing at the file. */
63
+ writeAsset(path: string, ref: string | null, name: string, bytes: Uint8Array): Promise<string | null>
64
+ }
65
+
66
+ export type EditorWindowFn = (ui: InspectorUI, editor: EditorApi) => void
67
+
68
+ export type EditorToolHooks = {
69
+ /** CSS cursor for the viewport while this tool is active (default "crosshair"). */
70
+ cursor?: string
71
+ /** A short glyph (one character / emoji) for the tool's toolbar button — tools without one all
72
+ * share the generic wand icon, so any editor with two or more tools should set it. */
73
+ icon?: string
74
+ /** A viewport click while the tool is active — `hit` is the raycast result under the pointer.
75
+ * Viewport clicks route to the tool while it is active. */
76
+ onViewportClick?(hit: EditorRayHit, editor: EditorApi): void
77
+ /** Drag tools (brushes): a primary-button drag in the viewport routes here instead of orbiting
78
+ * the camera — start / every pointer move (with the hit under it, null while off-world) /
79
+ * release. A tool with `onDragStart` gets NO `onViewportClick` for the same gesture. */
80
+ onDragStart?(hit: EditorRayHit, editor: EditorApi): void
81
+ onDrag?(hit: EditorRayHit | null, editor: EditorApi): void
82
+ onDragEnd?(editor: EditorApi): void
83
+ /** The pointer moved over the viewport with no button down (throttled to the frame) — draw a
84
+ * brush cursor through `Gizmos`; null when nothing is under it. */
85
+ onHover?(hit: EditorRayHit | null, editor: EditorApi): void
86
+ }
87
+
88
+ /** @internal The registry the scene-editor harness reads (windows/tools in registration order). */
89
+ export const __editorPlugins = {
90
+ windows: [] as { title: string, render: EditorWindowFn }[],
91
+ tools: [] as { name: string, hooks: EditorToolHooks }[],
92
+ }
93
+
94
+ /** Register an editor panel — a collapsible overlay docked over the viewport (the inspector rail
95
+ * stays selection-scoped). A window that emits `ui.toolButton` for a tool becomes that tool's
96
+ * settings panel: activating the tool expands and highlights it. `render` re-runs immediate-mode
97
+ * on every interaction — same protocol as `static inspector` cards. */
98
+ export const registerEditorWindow = (title: string, render: EditorWindowFn): void => {
99
+ const existing = __editorPlugins.windows.find((w) => w.title === title)
100
+ if (existing) existing.render = render
101
+ else __editorPlugins.windows.push({ title, render })
102
+ }
103
+
104
+ /** Register a viewport tool — a toolbar entry beside move/rotate/scale (activate it there or via
105
+ * `ui.toolButton(label, name)`). While active, viewport clicks arrive as raycast hits. */
106
+ export const registerEditorTool = (name: string, hooks: EditorToolHooks): void => {
107
+ const existing = __editorPlugins.tools.find((t) => t.name === name)
108
+ if (existing) existing.hooks = hooks
109
+ else __editorPlugins.tools.push({ name, hooks })
110
+ }
@@ -0,0 +1,148 @@
1
+ // Editor gizmos: immediate-mode line drawing for code that runs while a scene is EDITED — a
2
+ // generator aspect's `rebuild()`, a `make()` factory, an editor tool's hooks. Nothing here touches
3
+ // the scene: calls accumulate WORLD-space line segments into the buffer of the run that is
4
+ // currently executing (an ambient scope the scene loader opens around each synchronous call), and
5
+ // the scene editor pushes those buffers to the lite engine's overlay layer, which draws them over
6
+ // the picture — never outlined, never picked, never rendered by Filament, absent in play mode.
7
+ //
8
+ // rebuild() {
9
+ // Gizmos.polyline(points, { color: "#5b8ef0", closed: true })
10
+ // for (const p of points) Gizmos.cross(p, 0.07)
11
+ // }
12
+ //
13
+ // Every run starts with an EMPTY buffer — what a call draws is the whole picture (like
14
+ // `generated.clear()`, only implicit). Outside any scope (play mode, hand-attached aspects, a call
15
+ // after an `await` inside rebuild) the calls are no-ops: rebuild is synchronous by contract.
16
+ //
17
+ // ANCHORED gizmos (`{ node }` in the style): points are in that node's LOCAL frame and the engine
18
+ // follows the node live — through a gizmo drag, not only after it — with the node's scale
19
+ // stripped (an empty scaled ×10 for its children keeps a normal-sized cross). An anchored picture
20
+ // is the node's own: clicking it in the viewport selects the node, and it draws in the selection
21
+ // accent while selected. World-space calls (path lines) are never pickable.
22
+
23
+ import { Mat4, type Mat4Like } from "../math/mat4"
24
+ import type { Vec3Like } from "../math/vec"
25
+
26
+ /** The anchor of a gizmo call: any object with an engine entity id (a `Node`). */
27
+ export type GizmoAnchor = { readonly id: number }
28
+
29
+ export type GizmoStyle = {
30
+ /** CSS hex color (`#rgb` / `#rrggbb`); default a neutral light gray. */
31
+ color?: string
32
+ /** 0..1, default 1. */
33
+ alpha?: number
34
+ /** Anchor: points are in this node's LOCAL frame (scale ignored); the lines follow the node
35
+ * live, are pickable (a click selects the node) and turn the selection accent when it is
36
+ * selected. Omit for world-space lines (not pickable). */
37
+ node?: GizmoAnchor | null
38
+ }
39
+
40
+ /** One color batch of LINES (flat `[x,y,z, x,y,z]` per segment) — what a host hands the engine.
41
+ * With `entityId` the segments are in that entity's local frame (see GizmoStyle.node). */
42
+ export type GizmoBatch = { color: string, alpha: number, segments: number[], entityId?: number }
43
+
44
+ const DEFAULT_COLOR = "#cfd4dd"
45
+
46
+ /** The per-run collector (owned by the scene loader's EditorRun). Batches are keyed by style so a
47
+ * hundred same-colored segments cost one draw. */
48
+ export class GizmoBuffer {
49
+ batches: GizmoBatch[] = []
50
+ private _byStyle = new Map<string, GizmoBatch>()
51
+
52
+ clear(): void {
53
+ this.batches = []
54
+ this._byStyle.clear()
55
+ }
56
+
57
+ segments(style: GizmoStyle | undefined): number[] {
58
+ const color = style?.color ?? DEFAULT_COLOR
59
+ const alpha = Math.max(0, Math.min(1, style?.alpha ?? 1))
60
+ const entityId = style?.node?.id
61
+ const key = `${color}@${alpha}@${entityId ?? ""}`
62
+ let b = this._byStyle.get(key)
63
+ if (!b) {
64
+ b = entityId ? { color, alpha, segments: [], entityId } : { color, alpha, segments: [] }
65
+ this._byStyle.set(key, b)
66
+ this.batches.push(b)
67
+ }
68
+ return b.segments
69
+ }
70
+ }
71
+
72
+ let current: GizmoBuffer | null = null
73
+ let warnedOutOfScope = false
74
+
75
+ const xyz = (p: Vec3Like): [number, number, number] =>
76
+ Array.isArray(p) ? [ p[0] ?? 0, p[1] ?? 0, p[2] ?? 0 ] : [ (p as { x: number }).x, (p as { y: number }).y, (p as { z: number }).z ]
77
+
78
+ const target = (style: GizmoStyle | undefined): number[] | null => {
79
+ if (current) return current.segments(style)
80
+ if (!warnedOutOfScope && (globalThis as { __lecodesSceneEdit?: boolean }).__lecodesSceneEdit) {
81
+ warnedOutOfScope = true
82
+ console.warn("[Gizmos] draw call outside an editor run — gizmos must be drawn synchronously inside rebuild() / a make() factory / a tool hook")
83
+ }
84
+ return null
85
+ }
86
+
87
+ /** @internal Run `fn` with `buffer` as the ambient gizmo target (cleared first). Nested scopes
88
+ * restore the outer one. */
89
+ export const withGizmoScope = <T>(buffer: GizmoBuffer, fn: () => T): T => {
90
+ const prev = current
91
+ buffer.clear()
92
+ current = buffer
93
+ try { return fn() } finally { current = prev }
94
+ }
95
+
96
+ /**
97
+ * Editor-only line drawing, available inside generator `rebuild()`, `make()` factories and editor
98
+ * tool hooks. Points are WORLD space. Drawn by the scene editor's viewport overlay (depth-test
99
+ * off — a path through a wall is still a path); invisible everywhere else.
100
+ */
101
+ export const Gizmos = {
102
+ /** One segment from `a` to `b`. */
103
+ line(a: Vec3Like, b: Vec3Like, style?: GizmoStyle): void {
104
+ const out = target(style)
105
+ if (!out) return
106
+ out.push(...xyz(a), ...xyz(b))
107
+ },
108
+
109
+ /** Consecutive segments through `points`; `closed` joins the last point back to the first. */
110
+ polyline(points: readonly Vec3Like[], style?: GizmoStyle & { closed?: boolean }): void {
111
+ if (points.length < 2) return
112
+ const out = target(style)
113
+ if (!out) return
114
+ const n = points.length
115
+ const last = style?.closed ? n : n - 1
116
+ for (let i = 0; i < last; i++) out.push(...xyz(points[i]!), ...xyz(points[(i + 1) % n]!))
117
+ },
118
+
119
+ /** A wireframe camera frustum looking down −Z: the four edges from the origin to a rect
120
+ * `length` metres ahead sized by `fov` (vertical, degrees) × `aspect` (default 16:9), plus an
121
+ * "up" fin above the rect. Anchor it (`{ node }`) for a camera node's marker — the frustum
122
+ * then follows the node's pose; `matrix` places an unanchored one in world space. */
123
+ frustum(fov: number, style?: GizmoStyle & { aspect?: number, length?: number, matrix?: Mat4Like }): void {
124
+ const out = target(style)
125
+ if (!out) return
126
+ const aspect = style?.aspect ?? 16 / 9, length = style?.length ?? 0.6
127
+ const m = style?.matrix ? new Mat4(style.matrix) : null
128
+ const h = Math.tan((fov * Math.PI) / 360) * length, w = h * aspect, z = -length
129
+ const P = (x: number, y: number, zz: number): [number, number, number] => {
130
+ if (!m) return [ x, y, zz ]
131
+ const v = m.transformPoint([ x, y, zz ])
132
+ return [ v.x, v.y, v.z ]
133
+ }
134
+ const o = P(0, 0, 0)
135
+ const c = [ P(-w, -h, z), P(w, -h, z), P(w, h, z), P(-w, h, z) ]
136
+ for (const q of c) out.push(...o, ...q)
137
+ for (let i = 0; i < 4; i++) out.push(...c[i]!, ...c[(i + 1) % 4]!)
138
+ out.push(...P(-w * 0.4, h, z), ...P(0, h * 1.6, z), ...P(0, h * 1.6, z), ...P(w * 0.4, h, z))
139
+ },
140
+
141
+ /** A three-axis cross centred on `p` (a point marker); `size` = half extent, default 0.1. */
142
+ cross(p: Vec3Like, size = 0.1, style?: GizmoStyle): void {
143
+ const out = target(style)
144
+ if (!out) return
145
+ const [ x, y, z ] = xyz(p)
146
+ out.push(x - size, y, z, x + size, y, z, x, y - size, z, x, y + size, z, x, y, z - size, x, y, z + size)
147
+ },
148
+ }
@@ -0,0 +1,120 @@
1
+ // The scene-file literal grammar markers, shared by `.scene.ts` (defineScene, 3D) and
2
+ // `.scene2d.ts` (defineScene2d, 2D): `use(Ctor, props)` aspect entries, `ref('name')` node
3
+ // references, `make(fn, args)` code-built sources — plus the ref-resolution helpers both loaders
4
+ // run. One implementation so the two formats can't drift; the engine-specific node/def types stay
5
+ // in their own modules (defineScene.ts / defineScene2d.ts), which re-export these for user code.
6
+
7
+ import type { Aspect, AspectCtor } from "../core/Aspect"
8
+ import type { Node } from "../gl/Node"
9
+
10
+ /** One aspect to attach, created by `use(Ctor, props)`. */
11
+ export type AspectEntry<A extends Aspect<any, any> = Aspect<any, any>> = {
12
+ /** @internal grammar marker. */
13
+ readonly __use: true
14
+ ctor: AspectCtor<A>
15
+ props?: Partial<A>
16
+ }
17
+
18
+ /** Reference an aspect in a scene file: `aspects: [use(Physics, { motion: 'static' })]`. Props are
19
+ * typechecked against the aspect's fields, exactly like `node.aspect(Ctor, props)`. */
20
+ export const use = <A extends Aspect<any, any>>(ctor: AspectCtor<A>, props?: Partial<A>): AspectEntry<A> =>
21
+ ({ __use: true, ctor, props })
22
+
23
+ /** A `make(fn, args)` source entry — see {@link make}. `N` is the node family the factory returns
24
+ * (3D `Node` by default; a `.scene2d.ts` file's factories return `Node2D`). */
25
+ export type MakeEntry<A extends Record<string, unknown> = Record<string, unknown>, N = Node> = {
26
+ /** @internal grammar marker. */
27
+ readonly __make: true
28
+ fn: (args: A) => N | Promise<N>
29
+ args?: A
30
+ }
31
+
32
+ /**
33
+ * A code-created source in a scene file: `tower: { make: make(buildTower, { floors: 5 }), … }`.
34
+ * The factory runs after every scene node exists (so `ref()` args resolve, forward references
35
+ * included) and its returned subtree mounts under the def node — the def's transform stays
36
+ * editor-owned, so moving the node never re-calls the factory. Args must be literal data (same
37
+ * grammar as aspect props); in the editor they edit as fields, and an arg change re-CALLS the
38
+ * factory live — the code is already in the bundle, so no compile happens. Keep factories pure
39
+ * builders: same args → same subtree, no side effects outside the returned nodes.
40
+ */
41
+ export const make = <A extends Record<string, unknown>, N>(
42
+ fn: (args: A) => N | Promise<N>, args?: A,
43
+ ): MakeEntry<A, N> => ({ __make: true, fn, args })
44
+
45
+ /**
46
+ * Reference another scene node by PATH in an aspect's props:
47
+ * `use(Road, { from: ref('pointA'), to: ref('lane/pointB') })`. Resolves to the live node when
48
+ * aspects attach — after EVERY node of the scene exists, so declaration order doesn't matter.
49
+ * Resolution is scoped upward from the host node (like variable scoping): the host's own children
50
+ * first, then its siblings, then each ancestor's scope up to the scene root. The FIRST segment
51
+ * binds the scope; the remaining segments descend from there. Refs address def nodes only (no
52
+ * `::`/`name[i]` asset-internal segments); an unknown path resolves to `null` (type your aspect
53
+ * field `Node | null`). Scene files only — hand-written code passes nodes directly:
54
+ * `node.aspect(Road, { from: nodes.pointA })`.
55
+ */
56
+ export const ref = <T = Node>(path: string): T => ({ $ref: path } as unknown as T)
57
+
58
+ export const isNodeRef = (v: unknown): v is { $ref: string } =>
59
+ typeof v === "object" && v !== null && typeof (v as { $ref?: unknown }).$ref === "string"
60
+
61
+ /** Resolve one ref string against a path-keyed nodes record, scoped upward from `hostPath` (own
62
+ * children → parent's scope (siblings + the host itself) → each ancestor scope → root, `""`).
63
+ * The ref's FIRST segment binds the scope — a match there is final even when the rest of the
64
+ * path doesn't exist (lexical shadowing). Returns the target's ABSOLUTE path, or null.
65
+ * `Object.hasOwn`, not indexing: a node named `constructor` must not resolve via the prototype. */
66
+ export const resolveRefPath = (
67
+ nodes: Record<string, unknown>, hostPath: string, ref: string,
68
+ ): string | null => {
69
+ const i = ref.indexOf("/")
70
+ const first = i < 0 ? ref : ref.slice(0, i)
71
+ for (let scope = hostPath; ; ) {
72
+ const base = scope === "" ? "" : `${scope}/`
73
+ if (Object.hasOwn(nodes, base + first)) return Object.hasOwn(nodes, base + ref) ? base + ref : null
74
+ if (scope === "") return null
75
+ const cut = scope.lastIndexOf("/")
76
+ scope = cut < 0 ? "" : scope.slice(0, cut)
77
+ }
78
+ }
79
+
80
+ /** Swap `ref()` markers in aspect props for the live nodes (top level + one array level deep),
81
+ * resolved scoped-upward from `hostPath` ("" = root scope only — the 2D flat-map case). */
82
+ export const resolveRefs = <N>(
83
+ props: Record<string, unknown> | undefined, nodes: Record<string, N>, hostPath = "",
84
+ ): Record<string, unknown> | undefined => {
85
+ if (!props) return props
86
+ const lookup = (r: string): N | null => {
87
+ const p = resolveRefPath(nodes, hostPath, r)
88
+ return p === null ? null : nodes[p]
89
+ }
90
+ let out: Record<string, unknown> | undefined
91
+ for (const [ k, v ] of Object.entries(props)) {
92
+ if (isNodeRef(v)) {
93
+ ;(out ??= { ...props })[k] = lookup(v.$ref)
94
+ } else if (Array.isArray(v) && v.some(isNodeRef)) {
95
+ ;(out ??= { ...props })[k] = v.map((el) => (isNodeRef(el) ? lookup(el.$ref) : el))
96
+ }
97
+ }
98
+ return out ?? props
99
+ }
100
+
101
+ /** ABSOLUTE paths of the nodes a props record's ref() values resolve to (editor dep tracking).
102
+ * An unresolved ref contributes no dep — the handle re-derives deps after structural changes. */
103
+ export const collectRefDeps = (
104
+ props: Record<string, unknown>, nodes: Record<string, unknown>, hostPath: string,
105
+ ): Set<string> => {
106
+ const deps = new Set<string>()
107
+ const add = (r: string): void => {
108
+ const p = resolveRefPath(nodes, hostPath, r)
109
+ if (p !== null) deps.add(p)
110
+ }
111
+ for (const v of Object.values(props)) {
112
+ if (isNodeRef(v)) add(v.$ref)
113
+ else if (Array.isArray(v)) for (const el of v) if (isNodeRef(el)) add(el.$ref)
114
+ }
115
+ return deps
116
+ }
117
+
118
+ /** The edit-mode flag an editor host sets before running the bundle (shared by both formats). */
119
+ export const EDIT_FLAG = "__lecodesSceneEdit"
120
+ export const isEditMode = (): boolean => (globalThis as Record<string, unknown>)[EDIT_FLAG] === true
@@ -0,0 +1,296 @@
1
+ // Level — procedural placement helpers for building large maps in code. Designed for `make()`
2
+ // factories in scene files (scene-files.md): a factory calls Level.line / grid / ring / scatter /
3
+ // place and returns the group, so a fence line or a scattered forest is one scene node with
4
+ // editable args instead of hundreds of hand-written coordinate entries.
5
+ //
6
+ // Contracts:
7
+ // · All coordinates are LOCAL to the returned group — the make def node owns the world transform.
8
+ // · Deterministic: same options (incl. `seed`) → the same layout, so a map is reproducible and a
9
+ // re-run in the editor never shuffles what you saw.
10
+ // · `build` is a GLB url (loaded once, cloned per spot) or a function `(spot) => Node | null` for
11
+ // full control — primitives via Mesh.*, per-spot variation via `spot.rng`.
12
+ // · Helpers only place. Colliders come from `collider:` (string builds) or from whatever the
13
+ // `build` function attaches itself.
14
+
15
+ import { Node } from "../gl/Node"
16
+ import { Model } from "../gl/Model"
17
+ import { Shape } from "../gl/Shape"
18
+ import { Physics } from "../gl/Physics"
19
+ import { cx, cy, cz, type Vec3Like } from "../math/vec"
20
+
21
+ /** One placement handed to a `build` function (all coordinates local to the group). */
22
+ export type LevelSpot = {
23
+ /** 0-based placement index. */
24
+ index: number
25
+ /** Local position the node will be placed at (before jitter was already applied). */
26
+ position: [number, number, number]
27
+ /** Yaw in degrees the node will face (already resolved from the `yaw` option). */
28
+ yaw: number
29
+ /** Deterministic per-call random stream (seeded) — use for per-spot variation. */
30
+ rng: () => number
31
+ }
32
+
33
+ /** What to place: a GLB url (loaded once, cloned per spot), or a builder function. A builder
34
+ * returning `null` skips that spot (thin a line, punch a gap in a grid). */
35
+ export type LevelBuild = string | ((spot: LevelSpot) => Node | null | Promise<Node | null>)
36
+
37
+ /** Static collision for url builds: 'mesh' = exact triangles, 'convex' = convex hull,
38
+ * 'box' = auto box from the mesh bounds, 'none' (default) = no collider. */
39
+ export type LevelCollider = "mesh" | "convex" | "box" | "none"
40
+
41
+ type PlaceCommon = {
42
+ build: LevelBuild
43
+ /** Static Shape+Physics attached to url-built nodes (default 'none'). Function builds attach
44
+ * their own aspects instead. */
45
+ collider?: LevelCollider
46
+ /** Facing: degrees, 'random', 'face' (forward +Z toward the helper's direction), or 'align'
47
+ * (the model's X length-axis along the direction — what fence/wall panels want). Each helper
48
+ * picks its natural default. */
49
+ yaw?: number | "random" | "face" | "align"
50
+ /** Random XZ offset per spot, meters: a radius, or [maxX, maxZ]. */
51
+ jitter?: number | [number, number]
52
+ /** Seed for the deterministic random stream (default 1). */
53
+ seed?: number
54
+ /** Child name prefix (default: the url's basename, else 'item'). */
55
+ name?: string
56
+ }
57
+
58
+ export type LevelLineOptions = PlaceCommon & {
59
+ from: Vec3Like
60
+ to: Vec3Like
61
+ /** Fixed number of placements (endpoints included when count > 1)… */
62
+ count?: number
63
+ /** …or a spacing in meters (placements from `from` toward `to`, last one ≤ `to`). */
64
+ step?: number
65
+ }
66
+
67
+ export type LevelGridOptions = PlaceCommon & {
68
+ /** Grid center (default [0,0,0]). */
69
+ center?: Vec3Like
70
+ cols: number
71
+ rows: number
72
+ /** Cell size in meters: one number or [alongX, alongZ]. */
73
+ spacing: number | [number, number]
74
+ }
75
+
76
+ export type LevelRingOptions = PlaceCommon & {
77
+ /** Ring center (default [0,0,0]). */
78
+ center?: Vec3Like
79
+ radius: number
80
+ count: number
81
+ /** Start angle in degrees (default 0 = +X). */
82
+ start?: number
83
+ /** Arc to cover in degrees (default 360 = full ring, end exclusive). */
84
+ arc?: number
85
+ /** 'face' orientation for rings: toward the center ('in', default) or away ('out'). */
86
+ facing?: "in" | "out"
87
+ }
88
+
89
+ export type LevelScatterOptions = PlaceCommon & {
90
+ /** Rectangle center (default [0,0,0]). */
91
+ center?: Vec3Like
92
+ /** Rectangle extent in meters: [sizeX, sizeZ] or one number for a square. */
93
+ size: number | [number, number]
94
+ count: number
95
+ /** Minimum distance between any two placements (rejection-sampled; the helper places as many
96
+ * as fit within the attempt budget and logs when it falls short). */
97
+ minSpacing?: number
98
+ /** Keep-out circles `{ x, z, r }` (local), or a predicate — return true to REJECT a point. */
99
+ avoid?: { x: number, z: number, r: number }[] | ((x: number, z: number) => boolean)
100
+ /** Y for every placement (default 0 — scatter is planar; vary height in a build fn). */
101
+ y?: number
102
+ }
103
+
104
+ export type LevelPlaceOptions = PlaceCommon & {
105
+ /** Explicit spots: [x, y, z] or [x, y, z, yawDegrees]. */
106
+ at: (readonly number[])[]
107
+ }
108
+
109
+ /** mulberry32 — tiny deterministic PRNG, good enough for layout jitter. */
110
+ const mulberry32 = (seed: number): (() => number) => {
111
+ let a = seed >>> 0
112
+ return () => {
113
+ a = (a + 0x6D2B79F5) >>> 0
114
+ let t = a
115
+ t = Math.imul(t ^ (t >>> 15), t | 1)
116
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
117
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
118
+ }
119
+ }
120
+
121
+ const v3 = (v: Vec3Like | undefined, dflt: [number, number, number] = [0, 0, 0]): [number, number, number] =>
122
+ v === undefined ? dflt : [cx(v), cy(v), cz(v)]
123
+
124
+ const baseName = (build: LevelBuild, name: string | undefined): string => {
125
+ if (name) return name
126
+ if (typeof build === "string") {
127
+ // asset() urls arrive percent-encoded (the whole local path is one token) — decode first so
128
+ // the basename regex sees real separators, then strip the extension.
129
+ let s = build
130
+ try { s = decodeURIComponent(build) } catch { /* keep the raw url */ }
131
+ const m = /([^/\\]+?)(\.[a-z0-9]+)?$/i.exec(s)
132
+ if (m) return m[1]!
133
+ }
134
+ return "item"
135
+ }
136
+
137
+ const attachCollider = (node: Node, collider: LevelCollider | undefined): void => {
138
+ if (!collider || collider === "none") return
139
+ const props = collider === "mesh" ? { mesh: true as const } : collider === "convex" ? { mesh: "convex" as const } : {}
140
+ node.aspect(Shape, props).aspect(Physics, { motion: "static" })
141
+ }
142
+
143
+ /** A resolved spot list → the placed group. Url builds load the GLB once and clone per spot. */
144
+ const buildSpots = async (
145
+ spots: { position: [number, number, number], yaw: number }[],
146
+ opts: PlaceCommon,
147
+ ): Promise<Node> => {
148
+ const group = new Node()
149
+ const rng = mulberry32(opts.seed ?? 1)
150
+ const prefix = baseName(opts.build, opts.name)
151
+ let template: Model | null = null
152
+ for (let i = 0; i < spots.length; i++) {
153
+ const spot = spots[i]!
154
+ let node: Node | null
155
+ if (typeof opts.build === "string") {
156
+ if (template === null) template = await Model.load(opts.build)
157
+ node = i === spots.length - 1 ? template : template.clone()
158
+ attachCollider(node, opts.collider)
159
+ } else {
160
+ node = await opts.build({ index: i, position: spot.position, yaw: spot.yaw, rng })
161
+ if (node === null) continue
162
+ }
163
+ node.name = `${prefix}${i + 1}`
164
+ node.position = spot.position
165
+ if (spot.yaw !== 0) node.eulerAngles = [0, spot.yaw, 0]
166
+ group.add(node)
167
+ }
168
+ // an all-skipped url build never used the template — but the template IS the last spot, so it
169
+ // only leaks when a function build was used (function builds own their nodes; nothing to do).
170
+ return group
171
+ }
172
+
173
+ const resolveYaw = (opt: PlaceCommon["yaw"], face: number, rng: () => number, dflt: "face" | "align" = "face"): number => {
174
+ const mode = opt ?? dflt
175
+ if (mode === "face") return face
176
+ if (mode === "align") return face - 90
177
+ if (mode === "random") return Math.floor(rng() * 360)
178
+ return mode
179
+ }
180
+
181
+ const applyJitter = (p: [number, number, number], jitter: PlaceCommon["jitter"], rng: () => number): [number, number, number] => {
182
+ if (!jitter) return p
183
+ const [jx, jz] = typeof jitter === "number" ? [jitter, jitter] : jitter
184
+ return [p[0] + (rng() * 2 - 1) * jx, p[1], p[2] + (rng() * 2 - 1) * jz]
185
+ }
186
+
187
+ /** Placements along a segment — fences, walls, lamp rows, guard rails. Default yaw: 'align'
188
+ * (the model's X length-axis follows the segment — a wall of panels); use `yaw: 'face'` to point
189
+ * each node's forward (+Z) along the direction instead. */
190
+ const line = async (opts: LevelLineOptions): Promise<Node> => {
191
+ const from = v3(opts.from), to = v3(opts.to)
192
+ const d = [to[0] - from[0], to[1] - from[1], to[2] - from[2]]
193
+ const len = Math.hypot(d[0]!, d[2]!)
194
+ const count = opts.count ?? (opts.step && opts.step > 0 ? Math.floor(len / opts.step) + 1 : 2)
195
+ const faceYaw = (Math.atan2(d[0]!, d[2]!) * 180) / Math.PI
196
+ const rng = mulberry32((opts.seed ?? 1) ^ 0x1eaf)
197
+ const spots: { position: [number, number, number], yaw: number }[] = []
198
+ for (let i = 0; i < count; i++) {
199
+ const t = opts.step && opts.step > 0 && len > 0 ? (i * opts.step) / len : count > 1 ? i / (count - 1) : 0
200
+ const p: [number, number, number] = [from[0] + d[0]! * t, from[1] + d[1]! * t, from[2] + d[2]! * t]
201
+ spots.push({ position: applyJitter(p, opts.jitter, rng), yaw: resolveYaw(opts.yaw, faceYaw, rng, "align") })
202
+ }
203
+ return buildSpots(spots, opts)
204
+ }
205
+
206
+ /** A cols × rows lattice centered on `center` — crate stacks, plantations, parking lots. */
207
+ const grid = async (opts: LevelGridOptions): Promise<Node> => {
208
+ const center = v3(opts.center)
209
+ const [sx, sz] = typeof opts.spacing === "number" ? [opts.spacing, opts.spacing] : opts.spacing
210
+ const rng = mulberry32((opts.seed ?? 1) ^ 0x941d)
211
+ const spots: { position: [number, number, number], yaw: number }[] = []
212
+ for (let r = 0; r < opts.rows; r++) {
213
+ for (let c = 0; c < opts.cols; c++) {
214
+ const p: [number, number, number] = [
215
+ center[0] + (c - (opts.cols - 1) / 2) * sx,
216
+ center[1],
217
+ center[2] + (r - (opts.rows - 1) / 2) * sz,
218
+ ]
219
+ spots.push({ position: applyJitter(p, opts.jitter, rng), yaw: resolveYaw(opts.yaw, 0, rng) })
220
+ }
221
+ }
222
+ return buildSpots(spots, opts)
223
+ }
224
+
225
+ /** Placements on a circle (or arc) — arena pillars, camp fires, stone circles. Default facing:
226
+ * toward the center (`facing: 'out'` flips it). */
227
+ const ring = async (opts: LevelRingOptions): Promise<Node> => {
228
+ const center = v3(opts.center)
229
+ const start = ((opts.start ?? 0) * Math.PI) / 180
230
+ const arc = ((opts.arc ?? 360) * Math.PI) / 180
231
+ const full = Math.abs((opts.arc ?? 360) - 360) < 1e-6
232
+ const rng = mulberry32((opts.seed ?? 1) ^ 0x71e6)
233
+ const spots: { position: [number, number, number], yaw: number }[] = []
234
+ for (let i = 0; i < opts.count; i++) {
235
+ // full ring: end exclusive (count even slices); arc: endpoints included
236
+ const t = opts.count > 1 ? i / (full ? opts.count : opts.count - 1) : 0
237
+ const a = start + arc * t
238
+ const x = center[0] + Math.cos(a) * opts.radius
239
+ const z = center[2] + Math.sin(a) * opts.radius
240
+ const inward = (Math.atan2(center[0] - x, center[2] - z) * 180) / Math.PI
241
+ const face = opts.facing === "out" ? inward + 180 : inward
242
+ spots.push({ position: applyJitter([x, center[1], z], opts.jitter, rng), yaw: resolveYaw(opts.yaw, face, rng) })
243
+ }
244
+ return buildSpots(spots, opts)
245
+ }
246
+
247
+ /** Seeded random placements in a rectangle — forests, rubble fields, prop clutter. Default yaw:
248
+ * random. `minSpacing` + `avoid` keep spawns, roads and each other clear. */
249
+ const scatter = async (opts: LevelScatterOptions): Promise<Node> => {
250
+ const center = v3(opts.center)
251
+ const [sizeX, sizeZ] = typeof opts.size === "number" ? [opts.size, opts.size] : opts.size
252
+ const rng = mulberry32((opts.seed ?? 1) ^ 0x5ca7)
253
+ const rejected = (x: number, z: number): boolean => {
254
+ if (!opts.avoid) return false
255
+ if (typeof opts.avoid === "function") return opts.avoid(x, z)
256
+ return opts.avoid.some((c) => (x - c.x) * (x - c.x) + (z - c.z) * (z - c.z) < c.r * c.r)
257
+ }
258
+ const placed: [number, number][] = []
259
+ const spots: { position: [number, number, number], yaw: number }[] = []
260
+ const minSq = (opts.minSpacing ?? 0) * (opts.minSpacing ?? 0)
261
+ let attempts = 0
262
+ const maxAttempts = opts.count * 30
263
+ while (spots.length < opts.count && attempts < maxAttempts) {
264
+ attempts++
265
+ const x = center[0] + (rng() - 0.5) * sizeX
266
+ const z = center[2] + (rng() - 0.5) * sizeZ
267
+ if (rejected(x, z)) continue
268
+ if (minSq > 0 && placed.some(([px, pz]) => (x - px) * (x - px) + (z - pz) * (z - pz) < minSq)) continue
269
+ placed.push([x, z])
270
+ spots.push({ position: [x, opts.y ?? center[1], z], yaw: resolveYaw(opts.yaw ?? "random", 0, rng) })
271
+ }
272
+ if (spots.length < opts.count) {
273
+ console.warn(`[Level.scatter] placed ${spots.length}/${opts.count} — the area is too tight for minSpacing/avoid`)
274
+ }
275
+ return buildSpots(spots, opts)
276
+ }
277
+
278
+ /** Explicit spot list — `at: [[x, y, z], [x, y, z, yaw], …]` when the layout is hand-designed but
279
+ * the per-node boilerplate (load, clone, collider, naming) shouldn't be. */
280
+ const place = async (opts: LevelPlaceOptions): Promise<Node> => {
281
+ const rng = mulberry32((opts.seed ?? 1) ^ 0x91ac)
282
+ const spots = opts.at.map((a) => ({
283
+ position: [a[0] ?? 0, a[1] ?? 0, a[2] ?? 0] as [number, number, number],
284
+ yaw: a.length > 3 ? a[3]! : resolveYaw(opts.yaw, 0, rng),
285
+ }))
286
+ return buildSpots(spots, opts)
287
+ }
288
+
289
+ /** Procedural level building — see the module header. All helpers return a Promise<Node> group to
290
+ * return from a `make()` factory (or add to a scene from plain code). */
291
+ export const Level = {
292
+ line, grid, ring, scatter, place,
293
+ /** The seeded PRNG the helpers use (mulberry32) — for custom generators that must stay
294
+ * deterministic alongside them. */
295
+ rng: mulberry32,
296
+ }