lecodes-sdk 1.2.0 → 2.0.1

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 (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
package/src/g2/Node2D.ts CHANGED
@@ -11,7 +11,8 @@
11
11
 
12
12
  import { AspectHost } from "../core/Aspect"
13
13
  import type { CompAxis, CompWriter } from "../core/compWrite"
14
- import { Registry } from "../core/registry"
14
+ import { DEAD_HANDLE, Pins, registerFreedKind } from "../core/pins"
15
+ import { OWNED_KIND_ENTITY_2D } from "../ui/tree"
15
16
  import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
16
17
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
17
18
  import { ensurePointerEvents } from "./touch"
@@ -55,11 +56,15 @@ export type Node2DEvents = {
55
56
  export type Contact2D = { point: Vec2, normal: Vec2, speed: number }
56
57
 
57
58
  /** id → Node2D, so host callbacks (animation events) route back to the owning JS object. */
58
- export const node2dRegistry = new Registry<Node2D>()
59
+ /** @internal The 2D nodes native owns right now (in a scene / under a parent), by entity id — what a
60
+ * contact or a pointer hit resolves to. See core/pins.ts. */
61
+ export const nodes2d = new Pins<Node2D>()
59
62
 
60
63
  export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
61
- /** Native entity handle. */
62
- readonly id: number
64
+ /** @internal The entity's owned handle (`_h.id` = the entity id; DEAD_HANDLE once destroyed). */
65
+ _h: Handle
66
+ /** Native entity id. */
67
+ get id(): number { return this._h.id }
63
68
 
64
69
  /** Optional debug/editor name (plain JS data — scene2d files stamp their node names here). */
65
70
  name?: string
@@ -98,13 +103,18 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
98
103
  else if (this._xfKind === 1) _creator2d.characterSetFromEntity(this._xf)
99
104
  }
100
105
 
101
- private _parent: Node2D | null = null
102
- private _children: Node2D[] = []
103
-
104
- constructor(internalId?: number) {
106
+ constructor(internalId?: number | Handle) {
105
107
  super()
106
- this.id = internalId ?? _creator2d.createEntity()
107
- node2dRegistry.set(this.id, this)
108
+ registerFreedKind(OWNED_KIND_ENTITY_2D, nodes2d)
109
+ if (internalId === undefined) {
110
+ const h = _creator2d.createEntity()
111
+ this._h = typeof h === "number" ? { id: h } : h // a number only from a test stub
112
+ } else if (typeof internalId === "number") {
113
+ this._h = { id: internalId } // engine-owned: findable while the engine keeps it
114
+ nodes2d.pin(this)
115
+ } else {
116
+ this._h = internalId
117
+ }
108
118
  }
109
119
 
110
120
  // --- transform (Y-up, LOCAL to the parent; at zoom 1, units == logical pixels) ---
@@ -185,10 +195,20 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
185
195
  set visible(v: boolean) { this._visible = v; _creator2d.setVisible(this.id, v) }
186
196
 
187
197
  // --- hierarchy (parent/child; transforms above are LOCAL to the parent) ---
188
- get parent(): Node2D | null { return this._parent }
198
+ // The tree lives in the engine (phase 10): parent / children are read back; a node the SDK never
199
+ // wrapped (none today — every 2D entity is created here) reads as absent.
200
+ get parent(): Node2D | null {
201
+ const p = _creator2d.getParent(this.id)
202
+ return p !== 0 ? nodes2d.get(p) ?? null : null
203
+ }
189
204
  set parent(p: Node2D | null) { this.setParent(p) }
190
205
 
191
- get children(): readonly Node2D[] { return this._children }
206
+ get children(): readonly Node2D[] {
207
+ const out: Node2D[] = []
208
+ const n = _creator2d.getChildCount(this.id)
209
+ for (let i = 0; i < n; i++) { const c = nodes2d.get(_creator2d.getChild(this.id, i)); if (c) out.push(c) }
210
+ return out
211
+ }
192
212
 
193
213
  /** Attach children to this node (each keeps its current world transform). Returns this. */
194
214
  add(...children: Node2D[]): this {
@@ -198,7 +218,7 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
198
218
 
199
219
  /** Detach a direct child (reparents it to root, keeping its world transform). Returns this. */
200
220
  remove(child: Node2D): this {
201
- if (child._parent === this) child.setParent(null)
221
+ if (child.parent === this) child.setParent(null)
202
222
  return this
203
223
  }
204
224
 
@@ -209,14 +229,9 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
209
229
  */
210
230
  setParent(parent: Node2D | null, keepWorld = true): this {
211
231
  if (parent === this) return this
212
- if (this._parent) {
213
- const arr = this._parent._children
214
- const i = arr.indexOf(this)
215
- if (i >= 0) arr.splice(i, 1)
216
- }
217
- this._parent = parent
218
- if (parent) parent._children.push(this)
219
232
  _creator2d.setParent(this.id, parent?.id ?? 0, keepWorld)
233
+ if (parent) nodes2d.pin(this) // the parent owns it now
234
+ else if (!_creator2d.entityIsOwned(this.id)) nodes2d.unpin(this.id) // loose again: its handle owns it
220
235
  if (keepWorld) this._syncLocalFromNative() // native recomputed our local transform
221
236
  return this
222
237
  }
@@ -278,28 +293,31 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
278
293
  this.dispatch(channel, ...args)
279
294
  }
280
295
 
296
+ /** Destroy this node and its whole subtree (children go with it — the 3D / UI contract). Every
297
+ * aspect in the subtree detaches first (physics bodies released, updaters unregistered). */
281
298
  destroy(): void {
282
- // Aspects go first: onDetach runs (a body-owning aspect releases its handle), updaters are
283
- // unregistered so a destroyed node never ticks again, listeners are dropped. Children survive
284
- // (native reparents them to the root), so only this node's aspects detach.
285
- this._detachAll()
286
- // Native frees this entity's physics bodies inside destroyEntity (they used to leak — an invisible
287
- // collider stayed in the world, and recycled entity ids then delivered its contacts to whoever
288
- // inherited the id). Clear the routing so a stale aspect can't push a dead handle.
299
+ if (this._h.id === 0) return
300
+ const subtree: Node2D[] = []
301
+ const collect = (n: Node2D) => { subtree.push(n); for (const c of n.children) collect(c) }
302
+ collect(this)
303
+ for (let i = subtree.length - 1; i >= 0; i--) subtree[i]._detachAll()
304
+ const id = this.id
305
+ // Dead NOW — entity ids are recycled, and the runtime's freed report lands a drain later.
306
+ for (const n of subtree) { nodes2d.unpin(n.id); n._dead() }
307
+ _creator2d.destroyEntity(id)
308
+ }
309
+
310
+ /** @internal The wrapper's resource is gone: no more native calls from here. */
311
+ _dead(): void {
289
312
  this._xf = 0
290
313
  this._xfKind = 0
291
314
  this._physicsDriven = false
292
- // Detach from parent; native reparents our children to root (keepWorld), so resync their caches.
293
- if (this._parent) {
294
- const arr = this._parent._children
295
- const i = arr.indexOf(this)
296
- if (i >= 0) arr.splice(i, 1)
297
- this._parent = null
298
- }
299
- const orphans = this._children.slice()
300
- this._children.length = 0
301
- node2dRegistry.delete(this.id)
302
- _creator2d.destroyEntity(this.id)
303
- for (const c of orphans) { c._parent = null; c._syncLocalFromNative() }
315
+ this._h = DEAD_HANDLE
316
+ }
317
+ /** @internal The runtime freed the entity underneath (an ancestor destroyed or dropped). */
318
+ _freed(): void {
319
+ if (this._h.id === 0) return
320
+ this._detachAll()
321
+ this._dead()
304
322
  }
305
323
  }
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { Aspect } from "../core/Aspect"
21
21
  import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
22
- import { node2dRegistry, type Node2D } from "./Node2D"
22
+ import { nodes2d, type Node2D } from "./Node2D"
23
23
  import { Shape2D } from "./Shape2D"
24
24
  import {
25
25
  PhysicsGroup2D, _addGroup, _watchGroupChanges, _unwatchGroupChanges,
@@ -51,7 +51,7 @@ export interface RayHit {
51
51
  }
52
52
 
53
53
  // Resolve a native entity id to its Node2D (0 = none).
54
- const node2dById = (id: number): Node2D | null => (id ? node2dRegistry.get(id) ?? null : null)
54
+ const node2dById = (id: number): Node2D | null => (id ? nodes2d.get(id) ?? null : null)
55
55
 
56
56
  export class Physics2D extends Aspect<"physics", Node2D> {
57
57
  static readonly aspect = "physics"
package/src/g2/Scene2D.ts CHANGED
@@ -3,11 +3,13 @@
3
3
 
4
4
  import { Color, type ColorInput } from "../core/color"
5
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"
6
+ import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOptions, type PresentOptions } from "../ui/presentable"
7
+ import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
8
+ import { TREE_DEST_SCENE2D, ensureEmitter, registerDest } from "../ui/tree"
7
9
  import { cx, cy, type Vec2Like } from "../math/vec"
8
10
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
9
11
  import { Camera2D } from "./Camera2D"
10
- import { node2dRegistry, type Node2D } from "./Node2D"
12
+ import { nodes2d, type Node2D } from "./Node2D"
11
13
  import { ensurePointerEvents } from "./touch"
12
14
 
13
15
  // Route aspect update(dt) through the render-synced native phases (c2dRender early = before physics,
@@ -88,7 +90,7 @@ export class Scene2D implements Presentable {
88
90
  * hit-test for tap/click picking — pair with camera.screenToWorld(). */
89
91
  pick(worldPoint: Vec2Like): Node2D | null {
90
92
  const id = _creator2d.pickSprite(this.id, cx(worldPoint), cy(worldPoint))
91
- return id ? node2dRegistry.get(id) ?? null : null
93
+ return id ? nodes2d.get(id) ?? null : null
92
94
  }
93
95
 
94
96
  /** Scene-level pointer events. The handler fires for every tap/press in the scene, with
@@ -106,11 +108,14 @@ export class Scene2D implements Presentable {
106
108
  }
107
109
 
108
110
  add(...nodes: Node2D[]): this {
109
- for (const n of nodes) _creator2d.addEntityToScene(this.id, n.id)
111
+ for (const n of nodes) { _creator2d.addEntityToScene(this.id, n.id); nodes2d.pin(n) } // the scene owns it now
110
112
  return this
111
113
  }
112
114
  remove(...nodes: Node2D[]): this {
113
- for (const n of nodes) _creator2d.removeEntityFromScene(this.id, n.id)
115
+ for (const n of nodes) {
116
+ _creator2d.removeEntityFromScene(this.id, n.id)
117
+ if (!_creator2d.entityIsOwned(n.id)) nodes2d.unpin(n.id) // loose again: its handle owns it
118
+ }
114
119
  return this
115
120
  }
116
121
 
@@ -162,10 +167,14 @@ export class Scene2D implements Presentable {
162
167
  this.cl.push(callback)
163
168
  return this
164
169
  }
165
- onBackPressed(callback: () => void): this {
170
+ /** Hardware/system back while the scene is current. */
171
+ onBack(callback: () => void): this {
166
172
  this._backButtonCallback = callback
167
173
  return this
168
174
  }
175
+ /** @deprecated Renamed `onBack` (2026-09-26). An alias for the projects compiled before the
176
+ * rename; removed with the release that recompiles them. */
177
+ onBackPressed(callback: () => void): this { return this.onBack(callback) }
169
178
 
170
179
  /** @internal UIImage source marker (UIImage.toSrc): `UIImage(scene)` shows this scene live — the
171
180
  * host draws it into the image node's box every frame with the scene's own camera (a minimap, a
@@ -174,9 +183,20 @@ export class Scene2D implements Presentable {
174
183
  return { scene2d: this.id }
175
184
  }
176
185
 
177
- /** @internal stable wire descriptor; hosts hand it back via router onChange (`_p` = this). */
178
- _viewDesc(): object {
179
- return this._vd ??= { type: "scene2d", sceneId: this.id, _p: this }
186
+ /** @internal The destination tuple (ui/presentable.ts). */
187
+ _dest(): DestTuple {
188
+ registerDest(TREE_DEST_SCENE2D, this.id, this)
189
+ return [TREE_DEST_SCENE2D, this.id, "", ""]
190
+ }
191
+ /** @internal presentation lifecycle from the runtime */
192
+ _emitOpen(): void { for (const cb of this.ol.slice()) cb() }
193
+ /** @internal */
194
+ _emitClose(): void { for (const cb of this.cl.slice()) cb() }
195
+ /** @internal */
196
+ _emitBack(): boolean {
197
+ if (!this._backButtonCallback) return false
198
+ this._backButtonCallback()
199
+ return true
180
200
  }
181
201
 
182
202
  /** Make this the active scene — shows it as the current destination (replaces a screen /
@@ -185,25 +205,17 @@ export class Scene2D implements Presentable {
185
205
  _bumpNavEpoch()
186
206
  // Reassert this scene's texture filter (the sampler is global — see Scene2DOptions.filter).
187
207
  if (this._filter !== undefined) _creator2d.setDefaultFilter(this._filter === 'linear')
188
- if (_navSupported()) {
189
- // The host activates the engine (openScene) as part of presenting the destination.
190
- _creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
191
- } else {
192
- _creator2d.openScene(this.id)
193
- for (const cb of this.ol) cb()
194
- }
208
+ // The runtime activates the engine (openScene) as part of presenting the destination.
209
+ ensureEmitter()
210
+ registerDest(TREE_DEST_SCENE2D, this.id, this)
211
+ _creatorTree.open(TREE_DEST_SCENE2D, this.id, "", "", _transitionId(options?.transition, TRANSITION_NONE))
195
212
  Scene2D._active = this
196
213
  _setCurrent(this)
197
214
  return this
198
215
  }
199
- close(): this {
216
+ close(options?: DismissOptions): this {
200
217
  _bumpNavEpoch()
201
- if (_navSupported()) {
202
- _creatorUI.closeView!()
203
- } else {
204
- _creator2d.closeScene()
205
- for (const cb of this.cl) cb()
206
- }
218
+ _creatorTree.close(TREE_DEST_SCENE2D, this.id, _transitionId(options?.transition, TRANSITION_NONE))
207
219
  if (Scene2D._active === this) Scene2D._active = null
208
220
  if (Presentable.current === this) _setCurrent(null)
209
221
  return this
@@ -37,7 +37,7 @@ export class Texture2D {
37
37
  })
38
38
  }
39
39
  return new Promise<Texture2D>((resolve, reject) => {
40
- _creator2d.createTexture((source as any)._id, (id, w, h) => resolve(new Texture2D(id, w, h)), reject)
40
+ _creator2d.createTexture((source as any)._h.id, (id, w, h) => resolve(new Texture2D(id, w, h)), reject)
41
41
  })
42
42
  }
43
43
 
package/src/g2/loop.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  // pulls none of this in.
7
7
 
8
8
  import { Vec2 } from "../math/vec"
9
- import { node2dRegistry } from "./Node2D"
9
+ import { nodes2d } from "./Node2D"
10
10
 
11
11
  export const onUpdate = (callback: (dt: number) => void): void => _creator2d.onUpdate(callback)
12
12
  export const offUpdate = (callback: (dt: number) => void): void => _creator2d.offUpdate(callback)
@@ -18,7 +18,7 @@ export const ensureAnimEvents = (): void => {
18
18
  if (animEventsRegistered) return
19
19
  animEventsRegistered = true
20
20
  _creator2d.setOnAnimEvent(({ entityId, clipId, type }) => {
21
- node2dRegistry.get(entityId)?._dispatchAnimEvent(clipId, type)
21
+ nodes2d.get(entityId)?._dispatchAnimEvent(clipId, type)
22
22
  })
23
23
  }
24
24
 
@@ -37,8 +37,8 @@ export const ensurePhysicsEvents = (): void => {
37
37
  if (physicsEventsRegistered) return
38
38
  physicsEventsRegistered = true
39
39
  _creator2d.setOnPhysicsEvent((a, b, type, px, py, nx, ny, speed) => {
40
- const na = node2dRegistry.get(a)
41
- const nb = node2dRegistry.get(b)
40
+ const na = nodes2d.get(a)
41
+ const nb = nodes2d.get(b)
42
42
  if (!na || !nb) return
43
43
  const channel = type === 1 || type === 3 ? "exit" : "enter"
44
44
  // Box2D's manifold normal points from A TOWARD B. Each listener wants the normal of the surface
@@ -1,52 +1,52 @@
1
- // CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
2
- // a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
3
- // side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
4
- // empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
5
- //
6
- // • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
7
- // `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
8
- // • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
9
- // owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
10
- // or anchors on it (`inst.alignTo(inst.nodes.eye)`).
11
- // • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
12
- // releases.
13
- // • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
14
- // See docs/scene-camera-place-plan.md.
15
-
16
- import { Aspect } from "../core/Aspect"
17
- import type { FieldMeta } from "../core/fields"
18
- import { Gizmos } from "../scene/gizmos"
19
- import type { Camera } from "./Camera"
20
- import type { Node } from "./Node"
21
-
22
- export class CameraPlace extends Aspect<"cameraPlace", Node> {
23
- static readonly aspect = "cameraPlace"
24
- /** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
25
- fov = 60
26
- /** Near clip distance. */
27
- near = 0.01
28
- /** Far clip distance = view range. */
29
- far = 1000
30
- /** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
31
- * ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
32
- active = false
33
- /** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
34
- static fields: FieldMeta<CameraPlace> = {
35
- fov: { label: "FOV", min: 1, max: 179, step: 1 },
36
- near: { min: 0.001, max: 10, step: 0.01 },
37
- far: { min: 1, max: 100000, step: 1 },
38
- active: { label: "Active camera" },
39
- }
40
- static editor = { rebuild: true }
41
-
42
- /** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
43
- applyTo(camera: Camera): void {
44
- camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
45
- }
46
-
47
- /** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
48
- * clicking it selects the node. Play mode never calls this. */
49
- rebuild(): void {
50
- Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
51
- }
52
- }
1
+ // CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
2
+ // a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
3
+ // side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
4
+ // empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
5
+ //
6
+ // • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
7
+ // `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
8
+ // • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
9
+ // owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
10
+ // or anchors on it (`inst.alignTo(inst.nodes.eye)`).
11
+ // • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
12
+ // releases.
13
+ // • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
14
+ // See docs/scene-camera-place-plan.md.
15
+
16
+ import { Aspect } from "../core/Aspect"
17
+ import type { FieldMeta } from "../core/fields"
18
+ import { Gizmos } from "../scene/gizmos"
19
+ import type { Camera } from "./Camera"
20
+ import type { Node } from "./Node"
21
+
22
+ export class CameraPlace extends Aspect<"cameraPlace", Node> {
23
+ static readonly aspect = "cameraPlace"
24
+ /** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
25
+ fov = 60
26
+ /** Near clip distance. */
27
+ near = 0.01
28
+ /** Far clip distance = view range. */
29
+ far = 1000
30
+ /** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
31
+ * ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
32
+ active = false
33
+ /** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
34
+ static fields: FieldMeta<CameraPlace> = {
35
+ fov: { label: "FOV", min: 1, max: 179, step: 1 },
36
+ near: { min: 0.001, max: 10, step: 0.01 },
37
+ far: { min: 1, max: 100000, step: 1 },
38
+ active: { label: "Active camera" },
39
+ }
40
+ static editor = { rebuild: true }
41
+
42
+ /** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
43
+ applyTo(camera: Camera): void {
44
+ camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
45
+ }
46
+
47
+ /** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
48
+ * clicking it selects the node. Play mode never calls this. */
49
+ rebuild(): void {
50
+ Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
51
+ }
52
+ }
package/src/gl/Foliage.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  // Foliage — the vegetation tier of the lightmap material family (creator-gl materials/src/foliage.mat).
2
2
  //
3
3
  // A model loaded with `Model.load(src, { foliage: true })` (scene files: `foliage: true` on a `model`
4
- // node) takes a shader whose vertices sway in the wind, bend away from BENDERS (the hero, enemies —
4
+ // node) takes a shader whose vertices bend in the wind, lean away from BENDERS (the hero, enemies —
5
5
  // any node registered with `Foliage.bend`) and thin out over a distance range, and whose fragment
6
6
  // tints every copy a little differently. All of it is level-wide state the engine keeps: the wind
7
- // and the sway options apply to every foliage model, present and future; benders are read from
7
+ // and the options apply to every foliage model, present and future; benders are read from
8
8
  // their live world transforms each frame (the 8 nearest the camera reach the shader); a fade is per
9
9
  // ASSET — every copy of that model, present and future.
10
10
  //
11
+ // THE WIND (2026-09-23) is the model of Unity HDRP's Wind.hlsl, what NatureManufacture's vegetation is
12
+ // authored for: a scrolling noise field read at each plant's root bends the whole plant about its root
13
+ // (the angle grows with the distance from the root: a cantilever's curve), a second, finer field read at
14
+ // each vertex moves the leaves (their shiver) — no fixed periods, the turbulence turns a tree off the mean
15
+ // wind, the gusts ride a slow third field. What differs per PLANT comes from the model, not from here:
16
+ // the material's drag / stiffness / permanent lean / shiver drag ride the GLB's material extras
17
+ // (`lecodes.wind`, written by `lecodes assets scene` from the Unity material), or default by the plant's
18
+ // size (grass under ~2 m, trees over ~5 m); the per-vertex shiver weight is the GLB's COLOR_0.a.
19
+ //
11
20
  // Hosts without the tier (no `foliageSet*` bridge) ignore every call; the models then load through
12
21
  // the ordinary lightmap tier where that exists, else the standard shader.
13
22
  import { Model } from "./Model"
@@ -16,20 +25,21 @@ import { Node } from "./Node"
16
25
  export type FoliageWind = {
17
26
  /** Direction on the ground (x, z); normalised by the engine. Default [1, 0.3]. */
18
27
  direction?: [number, number]
19
- /** Metres of sway at `bendHeight` (taller parts sway more). Default 0.07. 0 = still. */
20
- strength?: number
21
- /** Radians per second of the main sway. Default 1.2. */
28
+ /** The wind speed in m/s — the plants' permanent bend and how fast the field drifts past them. Default 8.3
29
+ * (30 km/h, the pack's demo). 0 = still. */
22
30
  speed?: number
23
- /** 0..1: how much the strength breathes over a slow envelope. Default 0.6. */
31
+ /** 0..2: the random share of the speed — how far the bend wanders off the mean wind and how much the leaves move.
32
+ * Default 0.4. */
33
+ turbulence?: number
34
+ /** 0..1: the gusts' strength — waves of extra wind that travel over the level, calm between. Default 0.35. */
24
35
  gust?: number
36
+ /** m/s the gust pattern travels at. Default 5.6 (20 km/h). */
37
+ gustSpeed?: number
25
38
  }
26
39
 
27
40
  export type FoliageOptions = {
28
41
  /** The wind, or `false` for none. */
29
42
  wind?: FoliageWind | false
30
- /** Height above the instance origin (m) where the full sway / touch applies; the base never moves.
31
- * Default 1.5. */
32
- bendHeight?: number
33
43
  /** Metres the TOP of a small plant leans away from a bender standing at its centre; the whole plant leans
34
44
  * as one, rooted at its base. Plants over ~2 m react less, trees over ~5 m not at all (the engine
35
45
  * measures each asset). Default 0.6. 0 = no touch reaction. */
@@ -39,20 +49,51 @@ export type FoliageOptions = {
39
49
  variation?: number
40
50
  /** 0..1: wrapped sun lighting — thin leaves let light through. Default 0.5. */
41
51
  sunWrap?: number
52
+ /** A multiplier on every material's shiver drag (the leaves' own motion on top of the bend): 1 = as the
53
+ * model's materials say, 0 = the leaves ride the bend alone. Default 1. */
54
+ shiver?: number
55
+ /** 0..1: the sun through a thin leaf lit from behind (a back-lit crown glows). Default 0.35. */
56
+ translucency?: number
57
+ /** 0..1: how much of a light's shadow a leaf card takes. A crown scatters light through itself, so its cards take the
58
+ * shadow map partly — at 1 a distant crown is a dark silhouette lit only on its top layer. Bark always takes the
59
+ * shadow in full. Default 0.6. */
60
+ leafShadow?: number
61
+ /** 0..1: how far the baked per-vertex sky visibility (the crown's interior, the trunk under it) darkens the ambient.
62
+ * Default 0.7. */
63
+ skyAo?: number
64
+ /** 0..1: a leaf card's shading normal, from the card's own plane (0) to the crown's hull normal (1) — what hides that a
65
+ * crown is flat quads: each card is lit as the part of the volume it sits in. Default 0.7. */
66
+ canopy?: number
67
+ /** The cards' normal-map strength (a strong map brings the planes back). Default 0.5. */
68
+ normalMap?: number
69
+ /** A card seen edge-on dissolves over this much of |N·V| — no paper edges on the silhouette. Default 0.15, 0 = none. */
70
+ edgeFade?: number
71
+ /** Coverage held over distance: the cards' alpha × (1 + coverage × mip level), so a far crown does not thin out as its
72
+ * atlas mips away. Default 0.25, 0 = none; past ~0.5 a crown turns into a solid mass. */
73
+ coverage?: number
42
74
  }
43
75
 
76
+ const DEFAULT_WIND: Required<FoliageWind> = { direction: [ 1, 0.3 ], speed: 8.3, turbulence: 0.4, gust: 0.35, gustSpeed: 5.6 }
77
+
44
78
  const state: Required<Omit<FoliageOptions, "wind">> & { wind: Required<FoliageWind> | false } = {
45
- wind: { direction: [ 1, 0.3 ], strength: 0.07, speed: 1.2, gust: 0.6 },
46
- bendHeight: 1.5,
79
+ wind: { ...DEFAULT_WIND },
47
80
  touch: 0.6,
48
81
  variation: 0.35,
49
82
  sunWrap: 0.5,
83
+ shiver: 1,
84
+ translucency: 0.35,
85
+ leafShadow: 0.6,
86
+ skyAo: 0.7,
87
+ canopy: 0.7,
88
+ normalMap: 0.5,
89
+ edgeFade: 0.15,
90
+ coverage: 0.25,
50
91
  }
51
92
 
52
93
  const pushWind = (): void => {
53
94
  const w = state.wind
54
- if (w === false) _creator.foliageSetWind?.(1, 0, 0, 1, 0)
55
- else _creator.foliageSetWind?.(w.direction[0], w.direction[1], w.strength, w.speed, w.gust)
95
+ if (w === false) _creator.foliageSetWind?.(1, 0, 0, 0, 0, 0)
96
+ else _creator.foliageSetWind?.(w.direction[0], w.direction[1], w.speed, w.turbulence, w.gust, w.gustSpeed)
56
97
  }
57
98
 
58
99
  export const Foliage = {
@@ -61,19 +102,33 @@ export const Foliage = {
61
102
  configure(options: FoliageOptions): void {
62
103
  if (options.wind !== undefined) Foliage.wind(options.wind)
63
104
  let sway = false
64
- if (options.bendHeight !== undefined) { state.bendHeight = options.bendHeight; sway = true }
65
105
  if (options.touch !== undefined) { state.touch = options.touch; sway = true }
66
106
  if (options.variation !== undefined) { state.variation = options.variation; sway = true }
67
107
  if (options.sunWrap !== undefined) { state.sunWrap = options.sunWrap; sway = true }
68
- if (sway) _creator.foliageSetOptions?.(state.bendHeight, state.touch, state.variation, state.sunWrap)
108
+ if (sway) _creator.foliageSetOptions?.(state.touch, state.variation, state.sunWrap)
109
+ let leaf = false
110
+ if (options.shiver !== undefined) { state.shiver = options.shiver; leaf = true }
111
+ if (options.translucency !== undefined) { state.translucency = options.translucency; leaf = true }
112
+ if (options.leafShadow !== undefined) { state.leafShadow = options.leafShadow; leaf = true }
113
+ if (options.skyAo !== undefined) { state.skyAo = options.skyAo; leaf = true }
114
+ if (leaf) _creator.foliageSetLeaf?.(state.shiver, state.translucency, state.leafShadow, state.skyAo)
115
+ let look = false
116
+ if (options.canopy !== undefined) { state.canopy = options.canopy; look = true }
117
+ if (options.normalMap !== undefined) { state.normalMap = options.normalMap; look = true }
118
+ if (options.edgeFade !== undefined) { state.edgeFade = options.edgeFade; look = true }
119
+ if (options.coverage !== undefined) { state.coverage = options.coverage; look = true }
120
+ if (look) _creator.foliageSetLook?.(state.canopy, state.normalMap, state.edgeFade, state.coverage)
69
121
  },
70
122
 
71
123
  /** The wind: fields given replace the current ones; `false` = calm. Live. */
72
124
  wind(wind: FoliageWind | false): void {
73
125
  if (wind === false) state.wind = false
74
126
  else {
75
- const cur = state.wind === false ? { direction: [ 1, 0.3 ] as [number, number], strength: 0.07, speed: 1.2, gust: 0.6 } : state.wind
76
- state.wind = { direction: wind.direction ?? cur.direction, strength: wind.strength ?? cur.strength, speed: wind.speed ?? cur.speed, gust: wind.gust ?? cur.gust }
127
+ const cur = state.wind === false ? DEFAULT_WIND : state.wind
128
+ state.wind = {
129
+ direction: wind.direction ?? cur.direction, speed: wind.speed ?? cur.speed, turbulence: wind.turbulence ?? cur.turbulence,
130
+ gust: wind.gust ?? cur.gust, gustSpeed: wind.gustSpeed ?? cur.gustSpeed,
131
+ }
77
132
  }
78
133
  pushWind()
79
134
  },
@@ -109,9 +109,8 @@ export class Geometry {
109
109
  if (typeof source !== "string") return Geometry.fromData(source)
110
110
  const url = source
111
111
  const resp = await fetch(url, { useOnce: true })
112
- if (resp.status >= 400) { resp.dispose(); throw new Error(`Geometry.load: ${url} → HTTP ${resp.status}`) }
112
+ if (resp.status >= 400) { throw new Error(`Geometry.load: ${url} → HTTP ${resp.status}`) }
113
113
  const data = resp.json<GeometryData>()
114
- resp.dispose()
115
114
  return Geometry.fromData(data)
116
115
  }
117
116
  }
package/src/gl/Light.ts CHANGED
@@ -38,6 +38,13 @@ export type SunOptions = {
38
38
  * the same quality and puts fewer casters in the shadow pass. 30-40 is plenty in first person.
39
39
  */
40
40
  shadowDistance?: number
41
+ /**
42
+ * How many shadow-map cascades split `shadowDistance` (default 1). One map over the whole range gives a tall
43
+ * tree 5-10 cm shadow texels on its own trunk; 3 cascades put a tight map on the first metres and coarser
44
+ * ones behind, fit to their texel grids so the shadow does not swim as the camera moves. Each cascade is
45
+ * another caster pass, so it costs in proportion to what casts: forests, not a lightmapped level.
46
+ */
47
+ shadowCascades?: 1 | 2 | 3 | 4
41
48
  }
42
49
 
43
50
  export type PointOptions = {
@@ -99,6 +106,7 @@ export class Light extends Node {
99
106
  /** @internal sun shadow options as created (creation-time in the engine). */
100
107
  _shadowsQuality = 1
101
108
  _shadowDistance = 100
109
+ _shadowCascades = 1
102
110
 
103
111
  /** A directional sun light. */
104
112
  static sun(options: SunOptions = {}): Light {
@@ -109,6 +117,7 @@ export class Light extends Node {
109
117
  light._color = options.color ?? 0xffffff
110
118
  light._shadowsQuality = options.shadowsQuality ?? 1
111
119
  light._shadowDistance = options.shadowDistance ?? 100
120
+ light._shadowCascades = options.shadowCascades ?? 1
112
121
  Light.lastSun = light
113
122
  _creator.createSunLight(
114
123
  light.id, dx, dy, dz,
@@ -116,6 +125,7 @@ export class Light extends Node {
116
125
  Color.toPackedRgb(options.color ?? 0xffffff),
117
126
  options.shadowsQuality ?? 1,
118
127
  options.shadowDistance ?? 100,
128
+ light._shadowCascades,
119
129
  )
120
130
  return light
121
131
  }