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,346 @@
1
+ // A rigid body, as an aspect on a Node2D, backed by Box2D v3 in creator-2d. Mirrors the 3D Physics
2
+ // aspect. Requires a Shape2D aspect on the same node (its geometry becomes the body's solid fixture).
3
+ //
4
+ // ground.aspect(Shape2D, { box: [500, 8] }).aspect(Physics2D, { motion: 'static' })
5
+ // crate.aspect(Shape2D, {}).aspect(Physics2D, { mass: 4, friction: 0.7, bounce: 0.1 })
6
+ // crate.physics.applyImpulse([0, 400])
7
+ // crate.addEventListener('enter', (other, c) => spark(c.point, c.normal))
8
+ //
9
+ // Physics2D.configure() is OPTIONAL — the world is created on the first body with sane defaults, and
10
+ // calling configure later just changes gravity (it used to destroy and rebuild the world, orphaning
11
+ // every handle that was already out).
12
+ //
13
+ // For a `dynamic` body, physics OWNS the transform — don't set node.position per frame; drive it via
14
+ // velocity / applyImpulse. A `position` write is a TELEPORT that reaches the body (Node2D routes it),
15
+ // which is also how a kinematic body is placed. Reading node.position always works.
16
+ //
17
+ // Keep bodies on ROOT nodes: a body parented to a moving node does not follow it (see
18
+ // docs/2d-physics-plan.md §6).
19
+
20
+ import { Aspect } from "../core/Aspect"
21
+ import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
22
+ import { node2dRegistry, type Node2D } from "./Node2D"
23
+ import { Shape2D } from "./Shape2D"
24
+ import {
25
+ PhysicsGroup2D, _addGroup, _watchGroupChanges, _unwatchGroupChanges,
26
+ categoryOf, solidMask, defaultGroup,
27
+ } from "./groups2d"
28
+
29
+ export type MotionType = "static" | "kinematic" | "dynamic"
30
+ const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
31
+
32
+ /** A neutral solid surface. Deliberately the same default as 3D `Physics.friction`. */
33
+ export const DEFAULT_FRICTION = 0.6
34
+
35
+ export interface PhysicsConfig {
36
+ /** Gravity in world units/s² (Y-up: down is negative). Default [0, -980]. */
37
+ gravity?: Vec2Like
38
+ /** Pixels per meter — tunes Box2D's internal tolerances; you still author in world units. Default 64.
39
+ * Only applies before the world exists (it is a global solver tolerance). */
40
+ pixelsPerMeter?: number
41
+ /** Solver sub-steps per fixed step. Default 4. */
42
+ subSteps?: number
43
+ }
44
+
45
+ /** Closest raycast hit. */
46
+ export interface RayHit {
47
+ node: Node2D | null
48
+ point: Vec2
49
+ normal: Vec2
50
+ fraction: number
51
+ }
52
+
53
+ // Resolve a native entity id to its Node2D (0 = none).
54
+ const node2dById = (id: number): Node2D | null => (id ? node2dRegistry.get(id) ?? null : null)
55
+
56
+ export class Physics2D extends Aspect<"physics", Node2D> {
57
+ static readonly aspect = "physics"
58
+
59
+ private _motion: MotionType = "dynamic"
60
+ /** Static (walls, floors), kinematic (script-driven platforms), or dynamic (fully simulated).
61
+ * Default "dynamic", and LIVE: freeze a crate into `'static'`, hand a ragdoll to `'dynamic'`. */
62
+ get motion(): MotionType { return this._motion }
63
+ set motion(m: MotionType) {
64
+ this._motion = m
65
+ this.node._physicsDriven = m !== "static"
66
+ if (this._h) _creator2d.physicsSetMotionType(this._h, MOTION[m])
67
+ }
68
+ /** Lock rotation (essential for platformer characters). */
69
+ fixedRotation = false
70
+ /** Continuous collision for fast-moving bodies (projectiles). */
71
+ bullet = false
72
+
73
+ private _mass = 0 // 0 = derive from the collider area
74
+ private _friction = DEFAULT_FRICTION
75
+ private _bounce = 0
76
+ private _gravityScale = 1
77
+ private _linearDamping = 0
78
+ private _angularDamping = 0
79
+ private _group: PhysicsGroup2D | undefined
80
+ private _repush = (): void => {
81
+ if (this._h) _creator2d.physicsSetFilter(this._h, categoryOf(this._group), solidMask(this._group))
82
+ }
83
+ private _h = 0 // native body id (0 = none / no physics support)
84
+
85
+ /** Which collision group this body belongs to (see Physics2D.addGroup). Live. */
86
+ get group(): PhysicsGroup2D | undefined { return this._group }
87
+ set group(g: PhysicsGroup2D | undefined) {
88
+ this._group = g
89
+ this.node.get(Shape2D)?._setFilter(categoryOf(g), solidMask(g))
90
+ this._repush()
91
+ }
92
+
93
+ /** Mass in world units. Omitted (or 0) = derived from the collider's AREA, so a big crate really is
94
+ * heavier than a small one. Setting it scales the shape-derived mass data, keeping the rotational
95
+ * inertia in the same ratio — a lopsided collider still tips the way its geometry says. */
96
+ get mass(): number { return this._h ? _creator2d.physicsGetMass(this._h) : this._mass }
97
+ set mass(v: number) { this._mass = v; if (this._h) _creator2d.physicsSetMass(this._h, v) }
98
+
99
+ /** Surface friction. 0 is frictionless, ~1 grippy; two bodies COMBINE as sqrt(a×b), so the lower
100
+ * value wins the argument. Default 0.6 — a neutral solid, matching 3D. */
101
+ get friction(): number { return this._friction }
102
+ set friction(v: number) { this._friction = v; this._pushMaterial() }
103
+
104
+ /** Bounciness, 0..1 — the fraction of the approach speed returned on impact. Two bodies combine as
105
+ * MAX (the bouncier one wins — the opposite of friction), and below ~64 units/s no bounce is
106
+ * applied at all, which is what stops a ball jittering on the floor forever. */
107
+ get bounce(): number { return this._bounce }
108
+ set bounce(v: number) { this._bounce = v; this._pushMaterial() }
109
+
110
+ // The live setters go to the BODY, but a later collider rebuild re-creates the fixture from the
111
+ // Shape2D — so the shape keeps its own copy of the material or a crouch would silently reset it.
112
+ private _pushMaterial(): void {
113
+ if (!this._h) return
114
+ _creator2d.physicsSetFriction(this._h, this._friction)
115
+ _creator2d.physicsSetBounce(this._h, this._bounce)
116
+ this.node.get(Shape2D)?._setMaterial(this._friction, this._bounce)
117
+ }
118
+
119
+ /** Native physics body id. 0 until attached, or if the build has no physics support. Mirrors 3D `Physics.id`. */
120
+ get id(): number { return this._h }
121
+
122
+ // NOTE — read every option through its PUBLIC accessor here, never the `_backing` field. chisel
123
+ // tree-shakes class members and keeps `_*` unconditionally, but it DROPS an accessor pair whose
124
+ // name no bundled code reads as a PROPERTY: an object-literal key (`aspect(Physics2D, { gravityScale: 0 })`)
125
+ // does NOT count. With the pair gone, Object.assign writes a plain own property, the backing field
126
+ // keeps its default, and the option silently does nothing — measured: a `gravityScale: 0` body fell.
127
+ // One `this.gravityScale` read in bundled SDK code keeps the whole pair alive.
128
+ onAttach(): void {
129
+ if (!_creator2d.physicsHasSupport || !_creator2d.physicsHasSupport()) return
130
+ const shape = this.node.get(Shape2D)
131
+ if (!shape) {
132
+ throw new Error("Physics2D requires a Shape2D aspect — add it first: node.aspect(Shape2D, {…}).aspect(Physics2D, {…})")
133
+ }
134
+ this._h = _creator2d.physicsCreateBody(this.node.id, MOTION[this.motion])
135
+ if (!this._h) return
136
+ shape._addTo(this._h, {
137
+ sensor: false, friction: this.friction, bounce: this.bounce,
138
+ category: categoryOf(this.group), mask: solidMask(this.group),
139
+ staticBody: this.motion === "static",
140
+ })
141
+ // Only a body that named a group cares when the topology changes.
142
+ if (this._group) _watchGroupChanges(this._repush)
143
+ // dynamic/kinematic bodies are moved by the native step → the node must re-sync its transform
144
+ // cache from native on read. Static bodies never move, so they keep the free cache.
145
+ if (this.motion !== "static") this.node._physicsDriven = true
146
+ // Route node.position / .rotation writes to the body (this is what replaced physics.moveTo).
147
+ this.node._xf = this._h
148
+ this.node._xfKind = 2
149
+ if (this._mass > 0) _creator2d.physicsSetMass(this._h, this.mass)
150
+ if (this.fixedRotation) _creator2d.physicsSetFixedRotation(this._h, true)
151
+ if (this.gravityScale !== 1) _creator2d.physicsSetGravityScale(this._h, this.gravityScale)
152
+ if (this.linearDamping !== 0) _creator2d.physicsSetLinearDamping(this._h, this.linearDamping)
153
+ if (this.angularDamping !== 0) _creator2d.physicsSetAngularDamping(this._h, this.angularDamping)
154
+ if (this.bullet) _creator2d.physicsSetBullet(this._h, true)
155
+ // The listener may have been added before the body existed; contact events are per-body opt-in.
156
+ if (this.node._wantsContacts) _creator2d.physicsSetContactEvents(this._h, true)
157
+ }
158
+
159
+ onDetach(): void {
160
+ _unwatchGroupChanges(this._repush)
161
+ if (this._h) { _creator2d.physicsRemoveBody(this._h); this._h = 0 }
162
+ this.node._physicsDriven = false
163
+ this.node._xf = 0
164
+ this.node._xfKind = 0
165
+ }
166
+
167
+ /** Re-applying options (`node.aspect(Physics2D, {…})`) pushes the live ones through. */
168
+ onReconfigure(): void {
169
+ if (!this._h) return
170
+ this._pushMaterial()
171
+ _creator2d.physicsSetFixedRotation(this._h, this.fixedRotation)
172
+ _creator2d.physicsSetBullet(this._h, this.bullet)
173
+ _creator2d.physicsSetGravityScale(this._h, this.gravityScale)
174
+ _creator2d.physicsSetLinearDamping(this._h, this.linearDamping)
175
+ _creator2d.physicsSetAngularDamping(this._h, this.angularDamping)
176
+ if (this._mass > 0) _creator2d.physicsSetMass(this._h, this.mass)
177
+ }
178
+
179
+ /** Linear velocity in world units/second (fresh Vec2 on read). */
180
+ get velocity(): Vec2 {
181
+ const o = this._h ? _creator2d.physicsGetLinearVelocity(this._h) : null
182
+ return o ? new Vec2(o[0], o[1]) : new Vec2(0, 0)
183
+ }
184
+ set velocity(v: Vec2Like) { if (this._h) _creator2d.physicsSetLinearVelocity(this._h, cx(v), cy(v)) }
185
+
186
+ /** Angular velocity in degrees/second. */
187
+ get angularVelocity(): number { return this._h ? _creator2d.physicsGetAngularVelocity(this._h) : 0 }
188
+ set angularVelocity(degPerSec: number) { if (this._h) _creator2d.physicsSetAngularVelocity(this._h, degPerSec) }
189
+
190
+ /** Per-body gravity multiplier: 1 = full world gravity, 0 = floats, <0 = repelled. */
191
+ get gravityScale(): number { return this._gravityScale }
192
+ set gravityScale(scale: number) { this._gravityScale = scale; if (this._h) _creator2d.physicsSetGravityScale(this._h, scale) }
193
+
194
+ get linearDamping(): number { return this._linearDamping }
195
+ set linearDamping(d: number) { this._linearDamping = d; if (this._h) _creator2d.physicsSetLinearDamping(this._h, d) }
196
+
197
+ get angularDamping(): number { return this._angularDamping }
198
+ set angularDamping(d: number) { this._angularDamping = d; if (this._h) _creator2d.physicsSetAngularDamping(this._h, d) }
199
+
200
+ /** Pause this body — it stops simulating and stops colliding until re-enabled. */
201
+ set enabled(v: boolean) { if (this._h) _creator2d.physicsSetEnabled(this._h, v) }
202
+
203
+ /** Box2D puts resting bodies to sleep; wake one to make it react to a change you made by hand. */
204
+ set awake(v: boolean) { if (this._h) _creator2d.physicsSetAwake(this._h, v) }
205
+
206
+ /** Apply an instantaneous impulse at the centre of mass (never spins the body) and wake it. */
207
+ applyImpulse(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyLinearImpulse(this._h, cx(v), cy(v)); return this }
208
+ /** Apply an impulse at a WORLD point — the lever arm becomes spin. Use this when the push has a real
209
+ * point of contact: a bullet's hit point, a blast pushing a prop at its base. */
210
+ applyImpulseAt(v: Vec2Like, worldPoint: Vec2Like): this {
211
+ if (this._h) _creator2d.physicsApplyImpulseAt(this._h, cx(v), cy(v), cx(worldPoint), cy(worldPoint))
212
+ return this
213
+ }
214
+ /** Apply a continuous force (world units) and wake the body. */
215
+ applyForce(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyForce(this._h, cx(v), cy(v)); return this }
216
+
217
+ // --- world (static) -------------------------------------------------------------------------
218
+ /** Set the world's gravity / solver settings. OPTIONAL: the world is created on the first body with
219
+ * gravity [0, -980], 64 px/m and 4 sub-steps. Calling this later changes gravity LIVE and leaves
220
+ * bodies alone. */
221
+ static configure(config: PhysicsConfig = {}): void {
222
+ const g = config.gravity ?? [0, -980]
223
+ _creator2d.physicsConfigure(cx(g), cy(g), config.pixelsPerMeter ?? 64, config.subSteps ?? 4)
224
+ }
225
+
226
+ /** World gravity, live. */
227
+ static get gravity(): Vec2 { return new Vec2(Physics2D._gravity) }
228
+ static set gravity(g: Vec2Like) {
229
+ Physics2D._gravity = [ cx(g), cy(g) ]
230
+ _creator2d.physicsConfigure(cx(g), cy(g), 0, 0)
231
+ }
232
+ private static _gravity: [number, number] = [ 0, -980 ]
233
+
234
+ /** Whether this build has physics support (CREATOR_2D_PHYSICS). NOTE: this reports the BUILD, not
235
+ * the world — there is nothing else to guard, since the world creates itself. */
236
+ static get supported(): boolean { return !!_creator2d.physicsHasSupport && _creator2d.physicsHasSupport() }
237
+
238
+ /** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
239
+ * off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
240
+ * smoothness — bodies then advance in discrete steps. */
241
+ private static _interpolation = true
242
+ static get interpolation(): boolean { return Physics2D._interpolation }
243
+ static set interpolation(on: boolean) { Physics2D._interpolation = on; _creator2d.setInterpolation(on) }
244
+
245
+ /**
246
+ * Register a collision group. Groups are SUBTRACTIVE and SYMMETRIC (see groups2d.ts):
247
+ *
248
+ * const player = Physics2D.addGroup()
249
+ * const bullets = Physics2D.addGroup().ignoreSelf().ignore(player)
250
+ */
251
+ static addGroup(label?: string): PhysicsGroup2D { return _addGroup(label) }
252
+
253
+ /** The implicit group of every body that never named one - name it to exclude "everything else". */
254
+ static get defaultGroup(): PhysicsGroup2D { return defaultGroup }
255
+
256
+ // --- queries (static) -----------------------------------------------------------------------
257
+ /** Closest body hit by the segment, or null. `node` resolves the hit entity. */
258
+ static raycast(from: Vec2Like, to: Vec2Like, q?: Query2D): RayHit | null {
259
+ const o = _creator2d.physicsRaycastClosest(cx(from), cy(from), cx(to), cy(to), maskOf(q), ignoreOf(q), ignoreCount(q))
260
+ if (o[0] < 0.5) return null
261
+ return { node: node2dById(o[1]), point: new Vec2(o[2], o[3]), normal: new Vec2(o[4], o[5]), fraction: o[6] }
262
+ }
263
+
264
+ /** Every body along the segment, NEAREST FIRST - a piercing bullet, a laser, "how many walls are
265
+ * between us". Capped at 32 hits. */
266
+ static raycastAll(from: Vec2Like, to: Vec2Like, q?: Query2D): RayHit[] {
267
+ const o = _creator2d.physicsRaycastAll(cx(from), cy(from), cx(to), cy(to), maskOf(q), ignoreOf(q), ignoreCount(q), MAX_HITS)
268
+ const hits: RayHit[] = []
269
+ for (let i = 0; i + 5 < o.length; i += 6) {
270
+ if (o[i] === 0) break
271
+ hits.push({ node: node2dById(o[i]), point: new Vec2(o[i + 1], o[i + 2]), normal: new Vec2(o[i + 3], o[i + 4]), fraction: o[i + 5] })
272
+ }
273
+ return hits
274
+ }
275
+
276
+ /** Topmost body (solid or sensor) whose shape contains the world point, or null. "Topmost" means
277
+ * by DRAW order - layer, then z - so picking agrees with what the player sees. */
278
+ static overlapPoint(p: Vec2Like, q?: Query2D): Node2D | null {
279
+ return node2dById(_creator2d.physicsOverlapPoint(cx(p), cy(p), maskOf(q), ignoreOf(q), ignoreCount(q)))
280
+ }
281
+
282
+ /** Everything within `radius` of a point - an explosion, an aura, an aggro range. */
283
+ static overlapCircle(centre: Vec2Like, radius: number, q?: Query2D): Node2D[] {
284
+ return overlapNodes([ cx(centre), cy(centre) ], radius, q)
285
+ }
286
+
287
+ /** Everything inside an axis-aligned box (centre + HALF-extents) - a room, a camera bound. */
288
+ static overlapBox(centre: Vec2Like, half: Vec2Like, q?: Query2D): Node2D[] {
289
+ const x = cx(centre), y = cy(centre), hw = cx(half), hh = cy(half)
290
+ return overlapNodes([ x - hw, y - hh, x + hw, y - hh, x + hw, y + hh, x - hw, y + hh ], 0, q)
291
+ }
292
+
293
+ /**
294
+ * Everything touching a capsule from-to of the given radius. A capsule is the shape a DIRECTED
295
+ * question has: a beam with width, a sword arc, "does my radius fit through this gap" - none of
296
+ * which an axis-aligned box can express.
297
+ *
298
+ * It is also the SWEPT test: a circle of radius r moving from A to B during a frame sweeps exactly
299
+ * this capsule, so a fast projectile checked this way cannot tunnel through a thin wall the way two
300
+ * separate position checks do.
301
+ */
302
+ static overlapCapsule(from: Vec2Like, to: Vec2Like, radius: number, q?: Query2D): Node2D[] {
303
+ return overlapNodes([ cx(from), cy(from), cx(to), cy(to) ], radius, q)
304
+ }
305
+ }
306
+
307
+ /** Which groups a query may hit, and what to skip. */
308
+ export type Query2D = {
309
+ /** Only hit bodies in these groups. Omitted = everything. */
310
+ groups?: PhysicsGroup2D[]
311
+ /** Skip these nodes - the "don't hit myself" case a ray fired from inside its own body needs. */
312
+ ignore?: Node2D | Node2D[]
313
+ }
314
+
315
+ const MAX_HITS = 32
316
+ const emptyIgnore = new Float32Array(0)
317
+ let ignoreBuf = new Float32Array(8)
318
+
319
+ const maskOf = (q?: Query2D): number => {
320
+ if (!q?.groups || q.groups.length === 0) return 0 // 0 = everything, native-side
321
+ let m = 0
322
+ for (const g of q.groups) m |= g.category
323
+ return m >>> 0
324
+ }
325
+ const ignoreCount = (q?: Query2D): number =>
326
+ !q?.ignore ? 0 : Array.isArray(q.ignore) ? q.ignore.length : 1
327
+ const ignoreOf = (q?: Query2D): Float32Array => {
328
+ const n = ignoreCount(q)
329
+ if (n === 0) return emptyIgnore
330
+ if (ignoreBuf.length < n) ignoreBuf = new Float32Array(n)
331
+ const ig = q!.ignore!
332
+ if (Array.isArray(ig)) ig.forEach((node, i) => { ignoreBuf[i] = node.id })
333
+ else ignoreBuf[0] = (ig as Node2D).id
334
+ return ignoreBuf
335
+ }
336
+ const overlapNodes = (pts: number[], radius: number, q?: Query2D): Node2D[] => {
337
+ const o = _creator2d.physicsOverlap(new Float32Array(pts), pts.length / 2, radius,
338
+ maskOf(q), ignoreOf(q), ignoreCount(q), MAX_HITS)
339
+ const out: Node2D[] = []
340
+ for (let i = 0; i < o.length; i++) {
341
+ if (o[i] === 0) break
342
+ const n = node2dById(o[i])
343
+ if (n) out.push(n)
344
+ }
345
+ return out
346
+ }
@@ -0,0 +1,209 @@
1
+ // A 2D scene: a draw set + an orthographic Camera2D. Opening a scene asks the host to bring up the
2
+ // 2D engine and starts its RAF loop; only the active scene renders. Grown from the Phase 0 seed.
3
+
4
+ import { Color, type ColorInput } from "../core/color"
5
+ import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem, _detachAllSystems, type Aspect, type AspectCtor, type FieldOf, type TargetOf } from "../core/Aspect"
6
+ import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "../ui/presentable"
7
+ import { cx, cy, type Vec2Like } from "../math/vec"
8
+ import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
9
+ import { Camera2D } from "./Camera2D"
10
+ import { node2dRegistry, type Node2D } from "./Node2D"
11
+ import { ensurePointerEvents } from "./touch"
12
+
13
+ // Route aspect update(dt) through the render-synced native phases (c2dRender early = before physics,
14
+ // late = after animations / before draw), so followers/cameras are in lockstep with the draw. Done from
15
+ // the 2D layer (not core) so a 3D-only bundle never pulls in the 2D bridge. Installed once, lazily on
16
+ // the first Scene2D. On an older wasm without the hooks the installer returns false → setLoop fallback.
17
+ let framesInstalled = false
18
+ const installRenderSyncedFrames = (): void => {
19
+ if (framesInstalled) return
20
+ framesInstalled = true
21
+ _installAspectFrames((early, late) => {
22
+ if (typeof _creator2d.setEarlyUpdate !== "function") return false
23
+ _creator2d.setEarlyUpdate(early)
24
+ _creator2d.setLateUpdate(late)
25
+ return true
26
+ })
27
+ // Time.scale / Time.paused reach the engine's physics + animation clocks (feature-detected).
28
+ _installTimeScale((scale) => { _creator2d.setTimeScale?.(scale) })
29
+ }
30
+
31
+ export type Scene2DOptions = {
32
+ /** Background color — '#10131a', 0x10131a, or [r,g,b]/[r,g,b,a] in 0..1. */
33
+ background?: ColorInput
34
+ /** Texture sampling for the 2D renderer: `'nearest'` (default — crisp pixel art) or `'linear'`
35
+ * (smooth — for hi-res / non-pixel-art assets). Applied when the scene is opened, so switching
36
+ * scenes restores the active scene's choice. NOTE: this is a GLOBAL renderer setting (one sampler
37
+ * for ALL textures, not per-texture), so the currently-open scene's filter wins. Omit to leave the
38
+ * current setting (engine default is nearest). */
39
+ filter?: 'nearest' | 'linear'
40
+ }
41
+
42
+ /** Returned by scene2d.layer(n); chain .ySort() to enable top-down depth sorting for that layer. */
43
+ class LayerHandle {
44
+ private readonly sceneId: number
45
+ private readonly layer: number
46
+ constructor(sceneId: number, layer: number) { this.sceneId = sceneId; this.layer = layer }
47
+ /** In this layer, lower-on-screen sprites overlap higher ones (top-down feet sorting). */
48
+ ySort(enabled = true): this {
49
+ _creator2d.setLayerYSort(this.sceneId, this.layer, enabled)
50
+ return this
51
+ }
52
+ }
53
+
54
+ export class Scene2D implements Presentable {
55
+ /** Native scene handle. */
56
+ readonly id: number
57
+ readonly camera: Camera2D
58
+
59
+ private static _active: Scene2D | null = null
60
+ static get active(): Scene2D | null { return Scene2D._active }
61
+
62
+ /** @internal — scene-level pointer listeners (the catch-all; target = hit node or null). */
63
+ readonly _clickListeners: Array<(ev: ClickEvent<Node2D | null>) => void> = []
64
+ readonly _touchStartListeners: Array<(ev: TouchStartEvent<Node2D | null>) => void> = []
65
+
66
+ /** Texture filter to (re)assert on open(); the sampler is global, so each scene reclaims it. */
67
+ private readonly _filter?: 'nearest' | 'linear'
68
+
69
+ constructor(options: Scene2DOptions = {}) {
70
+ installRenderSyncedFrames()
71
+ this.id = _creator2d.createScene()
72
+ this.camera = new Camera2D(this.id)
73
+ this._filter = options.filter
74
+ if (options.background !== undefined) this.background = options.background
75
+ }
76
+
77
+ set background(color: ColorInput) {
78
+ const [ r, g, b, a ] = Color.toRgba01(color)
79
+ _creator2d.sceneSetClearColor(this.id, r, g, b, a)
80
+ }
81
+
82
+ /** Address a draw layer to configure it, e.g. scene2d.layer(1).ySort(). */
83
+ layer(n: number): LayerHandle {
84
+ return new LayerHandle(this.id, n)
85
+ }
86
+
87
+ /** Topmost visible sprite whose world bounds contain the world point, or null. Body-free
88
+ * hit-test for tap/click picking — pair with camera.screenToWorld(). */
89
+ pick(worldPoint: Vec2Like): Node2D | null {
90
+ const id = _creator2d.pickSprite(this.id, cx(worldPoint), cy(worldPoint))
91
+ return id ? node2dRegistry.get(id) ?? null : null
92
+ }
93
+
94
+ /** Scene-level pointer events. The handler fires for every tap/press in the scene, with
95
+ * `ev.target` = the hit Physics2D node (or null). On 'touchstart' you can ev.track(...) a drag. */
96
+ addEventListener(channel: "click", callback: (ev: ClickEvent<Node2D | null>) => void): void
97
+ addEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<Node2D | null>) => void): void
98
+ addEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
99
+ const list = channel === "click" ? this._clickListeners : this._touchStartListeners
100
+ if (!list.includes(callback)) { list.push(callback); ensurePointerEvents() }
101
+ }
102
+ removeEventListener(channel: "click" | "touchstart", callback: (ev: any) => void): void {
103
+ const list = channel === "click" ? this._clickListeners : this._touchStartListeners
104
+ const i = list.indexOf(callback)
105
+ if (i >= 0) list.splice(i, 1)
106
+ }
107
+
108
+ add(...nodes: Node2D[]): this {
109
+ for (const n of nodes) _creator2d.addEntityToScene(this.id, n.id)
110
+ return this
111
+ }
112
+ remove(...nodes: Node2D[]): this {
113
+ for (const n of nodes) _creator2d.removeEntityFromScene(this.id, n.id)
114
+ return this
115
+ }
116
+
117
+ // ---- systems: aspects of the scene (core/Aspect.ts `System<'x', Scene2D>`) ----
118
+ /** @internal class → instance, the authoritative store (named accessors mirror this). */
119
+ readonly _systems = new Map<Function, Aspect<any, any, any>>()
120
+
121
+ /** Attach (and configure) a system, or reconfigure it if already present. Returns the scene typed
122
+ * as now-having it (`scene.system(Hud).hud`). A 3D `System<'x'>` (target Scene) is rejected here. */
123
+ // `this: Self` (not `Self & Scene2D`): with the intersection TS infers Self = Scene2D and a chain
124
+ // `scene.system(A).system(B)` loses A's accessor from the result type.
125
+ system<Self extends TargetOf<A>, A extends Aspect<any, any, any>>(
126
+ this: Self,
127
+ ctor: AspectCtor<A>,
128
+ opts?: Partial<A>,
129
+ ): Self & FieldOf<A> {
130
+ return _attachSystem(this as unknown as Scene2D, ctor, opts) as unknown as Self & FieldOf<A>
131
+ }
132
+ /** Safe access — undefined if the system isn't attached. */
133
+ get<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): A | undefined {
134
+ return this._systems.get(ctor) as A | undefined
135
+ }
136
+ /** Existence check AND type guard: inside `if (scene.has(Hud))`, `scene.hud` is present. */
137
+ has<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
138
+ return this._systems.has(ctor)
139
+ }
140
+ /** Detach a system (runs its onDetach). Chainable. */
141
+ removeSystem<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this {
142
+ _detachSystem(this, ctor)
143
+ return this
144
+ }
145
+
146
+ // ---- Presentable (docs/navigation-presentable-plan.md) ----
147
+
148
+ /** @internal presentation lifecycle — fired by the host on present/dismiss (new navigation),
149
+ * or synchronously by open()/close() on legacy hosts. */
150
+ readonly ol: (() => void)[] = []
151
+ readonly cl: (() => void)[] = []
152
+ _backButtonCallback?: () => void
153
+ private _vd?: object
154
+
155
+ /** Fires when the scene becomes the visible destination (incl. a pop revealing it). */
156
+ onOpen(callback: () => void): this {
157
+ this.ol.push(callback)
158
+ return this
159
+ }
160
+ /** Fires when the scene stops being visible (closed, replaced, or covered by a push). */
161
+ onClose(callback: () => void): this {
162
+ this.cl.push(callback)
163
+ return this
164
+ }
165
+ onBackPressed(callback: () => void): this {
166
+ this._backButtonCallback = callback
167
+ return this
168
+ }
169
+
170
+ /** @internal stable wire descriptor; hosts hand it back via router onChange (`_p` = this). */
171
+ _viewDesc(): object {
172
+ return this._vd ??= { type: "scene2d", sceneId: this.id, _p: this }
173
+ }
174
+
175
+ /** Make this the active scene — shows it as the current destination (replaces a screen /
176
+ * another scene; only the active scene renders). */
177
+ open(options?: PresentOptions): this {
178
+ _bumpNavEpoch()
179
+ // Reassert this scene's texture filter (the sampler is global — see Scene2DOptions.filter).
180
+ if (this._filter !== undefined) _creator2d.setDefaultFilter(this._filter === 'linear')
181
+ if (_navSupported()) {
182
+ // The host activates the engine (openScene) as part of presenting the destination.
183
+ _creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
184
+ } else {
185
+ _creator2d.openScene(this.id)
186
+ for (const cb of this.ol) cb()
187
+ }
188
+ Scene2D._active = this
189
+ _setCurrent(this)
190
+ return this
191
+ }
192
+ close(): this {
193
+ _bumpNavEpoch()
194
+ if (_navSupported()) {
195
+ _creatorUI.closeView!()
196
+ } else {
197
+ _creator2d.closeScene()
198
+ for (const cb of this.cl) cb()
199
+ }
200
+ if (Scene2D._active === this) Scene2D._active = null
201
+ if (Presentable.current === this) _setCurrent(null)
202
+ return this
203
+ }
204
+ destroy(): void {
205
+ _detachAllSystems(this) // systems go first (last-attached first), while the scene still exists
206
+ this.close()
207
+ _creator2d.destroyScene(this.id)
208
+ }
209
+ }