lecodes-sdk 1.1.0 → 2.0.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 (257) 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 +11 -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/Particles.d.ts +15 -0
  25. package/dist/types/gl/Scene.d.ts +41 -13
  26. package/dist/types/gl/Texture.d.ts +1 -1
  27. package/dist/types/gl/animation/Locomotion.d.ts +59 -3
  28. package/dist/types/gl/terrain/Terrain.d.ts +8 -0
  29. package/dist/types/inject.d.ts +11 -11
  30. package/dist/types/inject.editor.d.ts +1 -0
  31. package/dist/types/net/core.d.ts +7 -0
  32. package/dist/types/plugin.d.ts +76 -0
  33. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  34. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  35. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  36. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  37. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  38. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  39. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  40. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  41. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  42. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  43. package/dist/types/runtime/app.d.ts +9 -2
  44. package/dist/types/runtime/fetch.d.ts +2 -0
  45. package/dist/types/runtime/input.d.ts +8 -1
  46. package/dist/types/runtime/media.d.ts +6 -10
  47. package/dist/types/runtime/misc.d.ts +4 -1
  48. package/dist/types/runtime/net.d.ts +3 -2
  49. package/dist/types/runtime/touch.d.ts +32 -0
  50. package/dist/types/scene/defineScene.d.ts +43 -2
  51. package/dist/types/scene/editor.d.ts +52 -0
  52. package/dist/types/scene/gizmos.d.ts +7 -4
  53. package/dist/types/ui/NativeView.d.ts +6 -4
  54. package/dist/types/ui/UI.d.ts +1 -1
  55. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  56. package/dist/types/ui/UIButton.d.ts +12 -14
  57. package/dist/types/ui/UIContainer.d.ts +0 -6
  58. package/dist/types/ui/UIImage.d.ts +1 -4
  59. package/dist/types/ui/UIInput.d.ts +8 -24
  60. package/dist/types/ui/UIModal.d.ts +0 -2
  61. package/dist/types/ui/UINode.d.ts +70 -56
  62. package/dist/types/ui/UIPager.d.ts +28 -27
  63. package/dist/types/ui/UIPopover.d.ts +0 -2
  64. package/dist/types/ui/UIScreen.d.ts +12 -17
  65. package/dist/types/ui/UIScrollable.d.ts +1 -4
  66. package/dist/types/ui/UIText.d.ts +0 -2
  67. package/dist/types/ui/UIVideo.d.ts +3 -5
  68. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  69. package/dist/types/ui/UIWidget.d.ts +10 -9
  70. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  71. package/dist/types/ui/presentable.d.ts +46 -32
  72. package/dist/types/ui/router.d.ts +18 -7
  73. package/dist/types/ui/styleColor.d.ts +1 -0
  74. package/dist/types/ui/transitions.d.ts +18 -0
  75. package/dist/types/ui/tree.d.ts +75 -0
  76. package/dist/types/version.d.ts +10 -0
  77. package/dist/types.json +1 -1
  78. package/package.json +12 -3
  79. package/prompts/2d.md +2 -6
  80. package/prompts/3d.md +1 -5
  81. package/prompts/README.md +142 -142
  82. package/prompts/canvas.md +9 -8
  83. package/prompts/compose.ts +1 -1
  84. package/prompts/core.md +3 -3
  85. package/prompts/design.md +1 -1
  86. package/prompts/dist/2d-game.md +77 -54
  87. package/prompts/dist/3d-app.md +75 -52
  88. package/prompts/dist/ar-app.md +74 -51
  89. package/prompts/dist/design.md +30 -8
  90. package/prompts/dist/ui-app.md +73 -46
  91. package/prompts/ui-design.md +2 -3
  92. package/prompts/ui.md +45 -37
  93. package/src/animate/tween/Animation.ts +262 -378
  94. package/src/animate/tween/animateValue.ts +103 -100
  95. package/src/animate/tween/easing.ts +10 -3
  96. package/src/animate/tween/spec.ts +505 -479
  97. package/src/audio/Sound.ts +3 -3
  98. package/src/audio/audio.ts +162 -161
  99. package/src/bridges/2d.d.ts +317 -0
  100. package/src/bridges/app.d.ts +91 -0
  101. package/src/bridges/audio.d.ts +97 -0
  102. package/src/bridges/canvas.d.ts +79 -0
  103. package/src/bridges/device.d.ts +72 -0
  104. package/src/bridges/fetch.d.ts +80 -0
  105. package/src/bridges/files.d.ts +70 -0
  106. package/src/bridges/gl.d.ts +1133 -0
  107. package/src/bridges/input.d.ts +72 -0
  108. package/src/bridges/media.d.ts +55 -0
  109. package/src/bridges/nav.d.ts +71 -0
  110. package/src/bridges/net.d.ts +52 -0
  111. package/src/bridges/service.d.ts +52 -0
  112. package/src/bridges/socket.d.ts +31 -0
  113. package/src/bridges/storage.d.ts +33 -0
  114. package/src/bridges/tree.d.ts +301 -0
  115. package/src/bridges/types.d.ts +49 -0
  116. package/src/canvas/Canvas.ts +114 -159
  117. package/src/canvas/gen/cssColor.ts +224 -0
  118. package/src/canvas/gen/recorder.ts +212 -0
  119. package/src/canvas/gen/spec.ts +201 -0
  120. package/src/chisel.ts +193 -0
  121. package/src/compile/assetMacro.ts +1 -1
  122. package/src/compile/bundler.ts +11 -2
  123. package/src/compile/compileProject.ts +43 -4
  124. package/src/compile/fontMacro.ts +3 -4
  125. package/src/compile/header.ts +26 -5
  126. package/src/compile/index.ts +3 -1
  127. package/src/compile/liteMaterial.ts +1 -1
  128. package/src/compile/sceneEditor.ts +11 -26
  129. package/src/core/color.ts +73 -30
  130. package/src/core/pins.ts +51 -0
  131. package/src/core/signals.ts +8 -1
  132. package/src/g2/CharacterController2D.ts +3 -3
  133. package/src/g2/Node2D.ts +57 -39
  134. package/src/g2/Physics2D.ts +2 -2
  135. package/src/g2/Scene2D.ts +35 -23
  136. package/src/g2/Texture2D.ts +1 -1
  137. package/src/g2/loop.ts +4 -4
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +13 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Model.ts +172 -167
  144. package/src/gl/Node.ts +91 -24
  145. package/src/gl/Particles.ts +20 -1
  146. package/src/gl/Scene.ts +100 -44
  147. package/src/gl/Texture.ts +8 -7
  148. package/src/gl/animation/AnimationClip.ts +1 -1
  149. package/src/gl/animation/Locomotion.ts +77 -8
  150. package/src/gl/nav/NavMesh.ts +3 -4
  151. package/src/gl/physics/Physics.ts +2 -2
  152. package/src/gl/physics/physicsEvents.ts +3 -3
  153. package/src/gl/terrain/Terrain.ts +33 -5
  154. package/src/gl/touch.ts +14 -15
  155. package/src/host.d.ts +2 -3
  156. package/src/inject.editor.ts +7 -0
  157. package/src/inject.ts +233 -236
  158. package/src/net/core.ts +6 -5
  159. package/src/net/index.ts +1 -1
  160. package/src/net/replication.ts +1 -1
  161. package/src/plugin.ts +191 -0
  162. package/src/plugins/gen/camera/contract.d.ts +27 -0
  163. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  164. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  165. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  166. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  167. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  168. package/src/plugins/gen/map/contract.d.ts +144 -0
  169. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  170. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  171. package/src/plugins/gen/push/contract.d.ts +61 -0
  172. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  173. package/src/plugins/gen/push/sdk/push.ts +105 -0
  174. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  175. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  176. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  177. package/src/plugins/permission.ts +5 -4
  178. package/src/runtime/app.ts +20 -9
  179. package/src/runtime/appEvents.ts +5 -4
  180. package/src/runtime/channel.ts +18 -15
  181. package/src/runtime/clipboard.ts +4 -3
  182. package/src/runtime/datetime.ts +2 -1
  183. package/src/runtime/device.ts +17 -15
  184. package/src/runtime/fetch.ts +30 -20
  185. package/src/runtime/files.ts +16 -15
  186. package/src/runtime/input.ts +21 -8
  187. package/src/runtime/media.ts +50 -46
  188. package/src/runtime/misc.ts +7 -3
  189. package/src/runtime/net.ts +8 -7
  190. package/src/runtime/rpc.ts +1 -3
  191. package/src/runtime/service.ts +19 -14
  192. package/src/runtime/share.ts +4 -3
  193. package/src/runtime/storage.ts +6 -4
  194. package/src/runtime/touch.ts +32 -0
  195. package/src/scene/defineScene.ts +61 -365
  196. package/src/scene/editor.ts +408 -0
  197. package/src/scene/editorPlugins.ts +3 -3
  198. package/src/scene/gizmos.ts +154 -148
  199. package/src/server/db/marci/query.ts +1 -1
  200. package/src/server/host.ts +1 -1
  201. package/src/server/runtime.ts +1 -1
  202. package/src/ui/NativeView.ts +72 -25
  203. package/src/ui/UI.ts +3 -3
  204. package/src/ui/UIBottomSheet.ts +16 -17
  205. package/src/ui/UIButton.ts +54 -16
  206. package/src/ui/UIContainer.ts +0 -6
  207. package/src/ui/UIImage.ts +34 -30
  208. package/src/ui/UIInput.ts +29 -37
  209. package/src/ui/UIModal.ts +1 -3
  210. package/src/ui/UINode.ts +347 -297
  211. package/src/ui/UIPager.ts +93 -78
  212. package/src/ui/UIPopover.ts +0 -2
  213. package/src/ui/UIScreen.ts +41 -36
  214. package/src/ui/UIScrollable.ts +19 -13
  215. package/src/ui/UISpacer.ts +1 -1
  216. package/src/ui/UITabs.ts +8 -6
  217. package/src/ui/UIText.ts +7 -17
  218. package/src/ui/UIVideo.ts +30 -27
  219. package/src/ui/UIVirtualizedList.ts +58 -59
  220. package/src/ui/UIWidget.ts +38 -18
  221. package/src/ui/colorKeys.gen.ts +37 -0
  222. package/src/ui/fonts.ts +2 -2
  223. package/src/ui/presentable.ts +59 -42
  224. package/src/ui/router.ts +52 -35
  225. package/src/ui/styleColor.ts +56 -0
  226. package/src/ui/theme.ts +7 -6
  227. package/src/ui/transitions.ts +249 -0
  228. package/src/ui/tree.ts +346 -0
  229. package/src/version.ts +24 -0
  230. package/tests/helpers/engineWorld.ts +11 -0
  231. package/tests/helpers/fakeTree.ts +353 -0
  232. package/tests/helpers/hostStubs.ts +31 -0
  233. package/tests/helpers/index.ts +14 -0
  234. package/tests/helpers/memoryMarci.ts +124 -0
  235. package/tests/helpers/phases.ts +23 -0
  236. package/tests/helpers/preload.ts +18 -0
  237. package/tests/helpers/stubApp.ts +2 -0
  238. package/tests/helpers/stubDevice.ts +2 -0
  239. package/tests/helpers/stubFetch.ts +2 -0
  240. package/tests/helpers/stubInput.ts +2 -0
  241. package/dist/inject.js +0 -4730
  242. package/dist/types/core/registry.d.ts +0 -7
  243. package/dist/types/plugins/camera.d.ts +0 -25
  244. package/dist/types/plugins/push.d.ts +0 -46
  245. package/src/bridges.d.ts +0 -1760
  246. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  247. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  248. package/src/compile/__tests__/assetName.test.ts +0 -55
  249. package/src/compile/__tests__/compile.test.ts +0 -310
  250. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  251. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  252. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  253. package/src/core/__tests__/stateMachine.test.ts +0 -132
  254. package/src/core/registry.ts +0 -23
  255. package/src/plugins/camera.ts +0 -81
  256. package/src/plugins/push.ts +0 -132
  257. package/src/plugins/qr.ts +0 -73
package/src/gl/Model.ts CHANGED
@@ -1,167 +1,172 @@
1
- // A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
2
- // distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
3
- // model.anim (an Animator over the GLB's clips — crossfades, blend spaces, layers when you need them):
4
- // const hero = await Model.load(asset('./hero.glb'))
5
- // hero.anim.play('Run', { loop: true })
6
-
7
- import { fetch, type FetchResponse } from "../runtime/fetch"
8
- import { Node } from "./Node"
9
- import { Animator, _pushLod, type LodMode } from "./animation/Animator"
10
- import { Material } from "./Material"
11
-
12
- /** One polygon under a screen point — what `Model.pickTriangle` returns. `bones` are the vertex's raw
13
- * JOINTS_0 / WEIGHTS_0 pairs (weight > 0; empty = unweighted), `bind` its position in mesh space,
14
- * `world` its skinned position this frame. */
15
- export interface TrianglePick {
16
- /** the Model's root entity, and the mesh node's own entity (0 when the host has no entity for it) */
17
- entity: number
18
- node: number
19
- nodeName: string
20
- mesh: string
21
- primitive: number
22
- material: string
23
- /** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
24
- triangle: number
25
- backface: boolean
26
- distance: number
27
- point: [number, number, number]
28
- bary: [number, number, number]
29
- skin: string
30
- vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
31
- }
32
-
33
- export class Model extends Node {
34
- /** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
35
- * more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
36
- declare readonly anim: Animator
37
- /** @internal loaded through the lightmap material (a baked static); clones inherit it. */
38
- _lightmapped = false
39
- /** @internal loaded through the foliage tier (`Foliage`); clones inherit it. */
40
- _foliage = false
41
- /** @internal the shadow pair, mirrored here because the bridge writes both at once. */
42
- _castShadows = true
43
- /** @internal */
44
- _receiveShadows = true
45
- /** @internal */
46
- _culling = true
47
- /** @internal -1 = auto (see `lod`) */
48
- _lodMesh = -1
49
-
50
- constructor(internalId: number) {
51
- super(internalId)
52
- this.aspect(Animator)
53
- }
54
-
55
- /** Does this model cast a real-time shadow? Unlike Mesh (one renderable) a GLB is a whole
56
- * hierarchy, so the flag goes to EVERY renderable of the instance. A first-person viewmodel —
57
- * arms, weapon, attachments — sets it false: it lives in front of the camera and its shadow
58
- * is never wanted. */
59
- get castShadows(): boolean { return this._castShadows }
60
- set castShadows(v: boolean) { this._castShadows = v; this._applyShadows() }
61
-
62
- /** Is this model lit by other casters' shadows? Same instance-wide reach as castShadows. */
63
- get receiveShadows(): boolean { return this._receiveShadows }
64
- set receiveShadows(v: boolean) { this._receiveShadows = v; this._applyShadows() }
65
-
66
- /** Frustum culling for the whole instance: a model the camera cannot see skips the draw and the
67
- * shadow pass. ON by default where the host keeps a skinned mesh's bounds honest — the engine refits
68
- * them to the joints every frame, so a walking, kneeling or ragdolled body is never culled while on
69
- * screen (`_creator.skinnedCullingSupported`); OFF (always draw) on hosts without that, where
70
- * Filament would cull an animated body by its bind-pose box. `false` = always draw (a skybox-sized
71
- * mesh, a debugging aid); `true` forces it on regardless of the host. */
72
- get culling(): boolean { return this._culling }
73
- set culling(v: boolean) { this._culling = v; _creator.setGlbCulling(this.id, v) }
74
- /** Level of detail (docs/lod-plan.md). `'auto'` (default): the engine shows the `_LOD<n>` mesh level
75
- * that fits the model's size on screen (`lecodes assets doctor --lod` makes them) and scales the
76
- * animation rate with it; a number 0–3 pins that level for both — `0` = always full detail (a hero,
77
- * a showcase), `2`/`3` = always cheap (a crowd filler). `model.anim.lod = 'full'` keeps the animation
78
- * exact while the mesh still switches. No-op on hosts without the LOD pass. */
79
- get lod(): LodMode { return this._lodMesh < 0 ? "auto" : (this._lodMesh as 0 | 1 | 2 | 3) }
80
- set lod(v: LodMode) { this._lodMesh = v === "auto" ? -1 : Math.max(0, Math.min(3, Math.round(v))); _pushLod(this) }
81
-
82
- /** @internal the default for a fresh instance (load / clone), decided once per host. */
83
- static _cullingDefault(): boolean {
84
- if (Model._cullDefault === undefined) Model._cullDefault = !!_creator.skinnedCullingSupported?.()
85
- return Model._cullDefault
86
- }
87
- private static _cullDefault: boolean | undefined
88
-
89
- /** @internal both flags travel together — the bridge walks the instance once. */
90
- _applyShadows(): void { _creator.setGlbShadows?.(this.id, this._castShadows, this._receiveShadows) }
91
-
92
- /** DEBUG: the closest polygon of this model under a screen point (logical px — `Input.mouse.position`,
93
- * a touch event's clientX/Y), tested against the CPU-skinned CURRENT pose, both faces. Names the
94
- * triangle, its three vertices (bind + skinned positions) and their raw bone weights, so a stretched
95
- * or misbound polygon can be traced to its binding. One full CPU skin of the model per call: click-rate
96
- * only. null = miss, or a host without the pick (desktop today). */
97
- pickTriangle(screenX: number, screenY: number): TrianglePick | null {
98
- const json = _creator.pickTriangle?.(this.id, screenX, screenY)
99
- return json ? JSON.parse(json) as TrianglePick : null
100
- }
101
-
102
- /** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
103
- * the same parent and scene and sharing this model's current transform. The clone has its own
104
- * independent animation state (reach it via clone.anim). Mirrors this model's culling flag. */
105
- clone(): Model {
106
- const id = _creator.cloneEntity(this.id)
107
- const m = new Model(id)
108
- m._lightmapped = this._lightmapped
109
- m._foliage = this._foliage
110
- if (!this._culling) { m._culling = false; _creator.setGlbCulling(id, false) } // the engine default is on
111
- if (this._lodMesh >= 0) { m._lodMesh = this._lodMesh; _pushLod(m) }
112
- // the clone is a FRESH gltfio instance — it comes back with the asset's own shadow flags,
113
- // not this model's, so a cleared flag has to be re-applied
114
- m._castShadows = this._castShadows
115
- m._receiveShadows = this._receiveShadows
116
- if (!this._castShadows || !this._receiveShadows) m._applyShadows()
117
- return m
118
- }
119
-
120
- /** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
121
- static load(
122
- source: string | FetchResponse,
123
- options: {
124
- /** Frustum culling — see `culling` (default: on where the host refits skinned bounds, else off). */
125
- culling?: boolean
126
- onProgress?: (p: { loaded: number, total?: number }) => void
127
- /** Baked lighting (packages/creator-bake). `true` = a STATIC: loads through the lightmap material so
128
- * `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
129
- * --lightmap-uv`) and takes nothing from the real-time lights once the bake applies. Omitted / `false` =
130
- * the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
131
- lightmap?: boolean
132
- /** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
133
- * per-copy tint. Hosts without the tier fall back to the standard shader. */
134
- foliage?: boolean
135
- } = {},
136
- ): Promise<Model> {
137
- if (typeof source === "string") {
138
- return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
139
- if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`))
140
- return Model.load(resp, options)
141
- })
142
- }
143
- return new Promise<Model>((resolve, reject) => {
144
- // the provider path: a foliage model takes the vegetation tier's pair when the host ships it (fetchLocal
145
- // id 0 = it does not), a static the lightmap material + its masked twin; anything else the ubershader
146
- const foliageWanted = !!options.foliage && !!_creator.setNextGlbLightmapped
147
- let foliage = false
148
- if (foliageWanted) {
149
- const fol = Material.idOf(Material._foliageTemplate())
150
- const folMasked = fol ? Material.idOf(Material._foliageMaskedTemplate()) : 0
151
- foliage = fol > 0 && folMasked > 0
152
- if (foliage) _creator.setNextGlbLightmapped!(fol, folMasked)
153
- }
154
- const lightmapped = !foliage && !!options.lightmap && !!_creator.setNextGlbLightmapped
155
- if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
156
- _creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
157
- if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
158
- const model = new Model(entityId)
159
- model._lightmapped = lightmapped
160
- model._foliage = foliage
161
- const culling = options.culling ?? Model._cullingDefault()
162
- if (!culling) { model._culling = false; _creator.setGlbCulling(entityId, false) }
163
- resolve(model)
164
- }, reject)
165
- })
166
- }
167
- }
1
+ // A loaded GLB model — its own node kind (a GLB is a node hierarchy with baked animation clips),
2
+ // distinct from Mesh (raw primitives, no animation). Animation is always present, reached as
3
+ // model.anim (an Animator over the GLB's clips — crossfades, blend spaces, layers when you need them):
4
+ // const hero = await Model.load(asset('./hero.glb'))
5
+ // hero.anim.play('Run', { loop: true })
6
+
7
+ import { fetch, type FetchResponse } from "../runtime/fetch"
8
+ import { Node, nodes } from "./Node"
9
+ import { Animator, _pushLod, type LodMode } from "./animation/Animator"
10
+ import { Material } from "./Material"
11
+
12
+ /** One polygon under a screen point — what `Model.pickTriangle` returns. `bones` are the vertex's raw
13
+ * JOINTS_0 / WEIGHTS_0 pairs (weight > 0; empty = unweighted), `bind` its position in mesh space,
14
+ * `world` its skinned position this frame. */
15
+ export interface TrianglePick {
16
+ /** the Model's root entity, and the mesh node's own entity (0 when the host has no entity for it) */
17
+ entity: number
18
+ node: number
19
+ nodeName: string
20
+ mesh: string
21
+ primitive: number
22
+ material: string
23
+ /** the node's lightmap group (scene-file extras), when it has one */
24
+ lightmapGroup?: number
25
+ /** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
26
+ triangle: number
27
+ backface: boolean
28
+ distance: number
29
+ point: [number, number, number]
30
+ bary: [number, number, number]
31
+ /** the lightmap uv (TEXCOORD_1) at the hit, when the primitive carries one */
32
+ uv1?: [number, number]
33
+ skin: string
34
+ vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
35
+ }
36
+
37
+ export class Model extends Node {
38
+ /** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
39
+ * more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
40
+ declare readonly anim: Animator
41
+ /** @internal loaded through the lightmap material (a baked static); clones inherit it. */
42
+ _lightmapped = false
43
+ /** @internal loaded through the foliage tier (`Foliage`); clones inherit it. */
44
+ _foliage = false
45
+ /** @internal the shadow pair, mirrored here because the bridge writes both at once. */
46
+ _castShadows = true
47
+ /** @internal */
48
+ _receiveShadows = true
49
+ /** @internal */
50
+ _culling = true
51
+ /** @internal -1 = auto (see `lod`) */
52
+ _lodMesh = -1
53
+
54
+ constructor(internalId: number | Handle) {
55
+ super(internalId)
56
+ this.aspect(Animator)
57
+ }
58
+
59
+ /** Does this model cast a real-time shadow? Unlike Mesh (one renderable) a GLB is a whole
60
+ * hierarchy, so the flag goes to EVERY renderable of the instance. A first-person viewmodel —
61
+ * arms, weapon, attachments — sets it false: it lives in front of the camera and its shadow
62
+ * is never wanted. */
63
+ get castShadows(): boolean { return this._castShadows }
64
+ set castShadows(v: boolean) { this._castShadows = v; this._applyShadows() }
65
+
66
+ /** Is this model lit by other casters' shadows? Same instance-wide reach as castShadows. */
67
+ get receiveShadows(): boolean { return this._receiveShadows }
68
+ set receiveShadows(v: boolean) { this._receiveShadows = v; this._applyShadows() }
69
+
70
+ /** Frustum culling for the whole instance: a model the camera cannot see skips the draw and the
71
+ * shadow pass. ON by default where the host keeps a skinned mesh's bounds honest — the engine refits
72
+ * them to the joints every frame, so a walking, kneeling or ragdolled body is never culled while on
73
+ * screen (`_creator.skinnedCullingSupported`); OFF (always draw) on hosts without that, where
74
+ * Filament would cull an animated body by its bind-pose box. `false` = always draw (a skybox-sized
75
+ * mesh, a debugging aid); `true` forces it on regardless of the host. */
76
+ get culling(): boolean { return this._culling }
77
+ set culling(v: boolean) { this._culling = v; _creator.setGlbCulling(this.id, v) }
78
+ /** Level of detail (docs/lod-plan.md). `'auto'` (default): the engine shows the `_LOD<n>` mesh level
79
+ * that fits the model's size on screen (`lecodes assets doctor --lod` makes them) and scales the
80
+ * animation rate with it; a number 0–3 pins that level for both — `0` = always full detail (a hero,
81
+ * a showcase), `2`/`3` = always cheap (a crowd filler). `model.anim.lod = 'full'` keeps the animation
82
+ * exact while the mesh still switches. No-op on hosts without the LOD pass. */
83
+ get lod(): LodMode { return this._lodMesh < 0 ? "auto" : (this._lodMesh as 0 | 1 | 2 | 3) }
84
+ set lod(v: LodMode) { this._lodMesh = v === "auto" ? -1 : Math.max(0, Math.min(3, Math.round(v))); _pushLod(this) }
85
+
86
+ /** @internal the default for a fresh instance (load / clone), decided once per host. */
87
+ static _cullingDefault(): boolean {
88
+ if (Model._cullDefault === undefined) Model._cullDefault = !!_creator.skinnedCullingSupported?.()
89
+ return Model._cullDefault
90
+ }
91
+ private static _cullDefault: boolean | undefined
92
+
93
+ /** @internal both flags travel together — the bridge walks the instance once. */
94
+ _applyShadows(): void { _creator.setGlbShadows?.(this.id, this._castShadows, this._receiveShadows) }
95
+
96
+ /** DEBUG: the closest polygon of this model under a screen point (logical px — `Input.mouse.position`,
97
+ * a touch event's clientX/Y), tested against the CPU-skinned CURRENT pose, both faces. Names the
98
+ * triangle, its three vertices (bind + skinned positions) and their raw bone weights, so a stretched
99
+ * or misbound polygon can be traced to its binding. One full CPU skin of the model per call: click-rate
100
+ * only. null = miss, or a host without the pick (desktop today). */
101
+ pickTriangle(screenX: number, screenY: number): TrianglePick | null {
102
+ return _creator.pickTriangle?.(this.id, screenX, screenY) ?? null
103
+ }
104
+
105
+ /** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
106
+ * the same parent and scene and sharing this model's current transform. The clone has its own
107
+ * independent animation state (reach it via clone.anim). Mirrors this model's culling flag. */
108
+ clone(): Model {
109
+ const m = new Model(_creator.cloneEntity(this.id))
110
+ const id = m.id
111
+ if (_creator.entityIsOwned(id)) nodes.pin(m) // cloned into this model's parent / scene: native owns it
112
+ m._lightmapped = this._lightmapped
113
+ m._foliage = this._foliage
114
+ if (!this._culling) { m._culling = false; _creator.setGlbCulling(id, false) } // the engine default is on
115
+ if (this._lodMesh >= 0) { m._lodMesh = this._lodMesh; _pushLod(m) }
116
+ // the clone is a FRESH gltfio instance — it comes back with the asset's own shadow flags,
117
+ // not this model's, so a cleared flag has to be re-applied
118
+ m._castShadows = this._castShadows
119
+ m._receiveShadows = this._receiveShadows
120
+ if (!this._castShadows || !this._receiveShadows) m._applyShadows()
121
+ return m
122
+ }
123
+
124
+ /** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
125
+ static load(
126
+ source: string | FetchResponse,
127
+ options: {
128
+ /** Frustum culling — see `culling` (default: on where the host refits skinned bounds, else off). */
129
+ culling?: boolean
130
+ onProgress?: (p: { loaded: number, total?: number }) => void
131
+ /** Baked lighting (engines/bake). `true` = a STATIC: loads through the lightmap material so
132
+ * `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
133
+ * --lightmap-uv`) and takes nothing from the real-time lights once the bake applies. Omitted / `false` =
134
+ * the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
135
+ lightmap?: boolean
136
+ /** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
137
+ * per-copy tint. Hosts without the tier fall back to the standard shader. */
138
+ foliage?: boolean
139
+ } = {},
140
+ ): Promise<Model> {
141
+ if (typeof source === "string") {
142
+ return fetch(source, { useOnce: true, onProgress: options.onProgress }).then((resp) => {
143
+ if (resp.status >= 400) return Promise.reject(new Error(`Failed to load GLB from ${source}. HTTP ${resp.status}`))
144
+ return Model.load(resp, options)
145
+ })
146
+ }
147
+ return new Promise<Model>((resolve, reject) => {
148
+ // the provider path: a foliage model takes the vegetation tier's pair when the host carries it, a
149
+ // static the lightmap material + its masked twin; anything else the ubershader
150
+ const foliageWanted = !!options.foliage && !!_creator.setNextGlbLightmapped
151
+ let foliage = false
152
+ if (foliageWanted) {
153
+ const fol = Material._foliageTemplate()
154
+ const folMasked = fol && Material._foliageMaskedTemplate()
155
+ foliage = !!fol && !!folMasked
156
+ if (fol && folMasked) _creator.setNextGlbLightmapped!(Material.idOf(fol), Material.idOf(folMasked))
157
+ }
158
+ const lightmapped = !foliage && !!options.lightmap && !!_creator.setNextGlbLightmapped
159
+ if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
160
+ _creator.createGlb((source as unknown as { _h: Handle })._h.id, (root) => {
161
+ if (!root) { reject(new Error("Failed to load GLB")); return }
162
+ const model = new Model(root)
163
+ const entityId = model.id
164
+ model._lightmapped = lightmapped
165
+ model._foliage = foliage
166
+ const culling = options.culling ?? Model._cullingDefault()
167
+ if (!culling) { model._culling = false; _creator.setGlbCulling(entityId, false) }
168
+ resolve(model)
169
+ }, reject)
170
+ })
171
+ }
172
+ }
package/src/gl/Node.ts CHANGED
@@ -6,7 +6,8 @@
6
6
  // A GLB import is the Model node kind (with the Animator aspect at model.anim). Hierarchy lives here.
7
7
 
8
8
  import { AspectHost } from "../core/Aspect"
9
- import { Registry } from "../core/registry"
9
+ import { DEAD_HANDLE, Pins, registerFreedKind } from "../core/pins"
10
+ import { OWNED_KIND_GL_ENTITY } from "../ui/tree"
10
11
  import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
11
12
  import { Quat, cw, type QuatLike } from "../math/quat"
12
13
  import { Mat4, type Mat4Like } from "../math/mat4"
@@ -31,7 +32,38 @@ export type NodeTweenProps = {
31
32
  }
32
33
 
33
34
  /** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
34
- export const nodeRegistry = new Registry<Node>()
35
+ /** @internal The nodes native owns right now (in a scene / under a parent, or engine-created), by
36
+ * entity id — what a pick, a contact or an animation event resolves to. See core/pins.ts. */
37
+ export const nodes = new Pins<Node>()
38
+
39
+ // The loose wrappers, WEAKLY: a read-back (`parent` / `children` / `traverse` / `find`) must hand
40
+ // back the app's OWN handle while it exists. A loose root has no native owner, so no pin — the
41
+ // only other way to reach it from below was a throwaway wrapper, and anything put on that wrapper
42
+ // (IK's rig Animator on a bare tree's top) was lost to the app's handle. WeakRef keeps the owned
43
+ // model intact: the registry never holds a handle alive, a dropped root still finalizes, and a
44
+ // dead entry is dropped when met (or in the sweep every SWEEP registrations).
45
+ const loose = new Map<number, WeakRef<Node>>()
46
+ const SWEEP = 256
47
+ let sinceSweep = 0
48
+ const registerLoose = (n: Node): void => {
49
+ loose.set(n._h.id, new WeakRef(n))
50
+ if (++sinceSweep < SWEEP) return
51
+ sinceSweep = 0
52
+ for (const [id, ref] of loose) { const w = ref.deref(); if (!w || w._h.id !== id) loose.delete(id) }
53
+ }
54
+ /** @internal The wrapper for an entity id the runtime handed back: the pinned one, else the
55
+ * app's live loose handle, else a throwaway over the engine-owned id. */
56
+ export const nodeOf = (id: number): Node => {
57
+ const pinned = nodes.get(id)
58
+ if (pinned) return pinned
59
+ const ref = loose.get(id)
60
+ if (ref) {
61
+ const w = ref.deref()
62
+ if (w && w._h.id === id) return w
63
+ loose.delete(id)
64
+ }
65
+ return new Node(id)
66
+ }
35
67
 
36
68
  export type NodeEvents = {
37
69
  click: (ev: ClickEvent<Node | null>) => void
@@ -67,8 +99,12 @@ const findNodeByWalk = (rootId: number, name: string): number => {
67
99
  const worldScratch = new Float32Array(3)
68
100
 
69
101
  export class Node extends AspectHost<NodeEvents> implements CompWriter {
70
- /** Native entity handle. */
71
- readonly id: number
102
+ /** @internal The entity's owned handle (`_h.id` = the entity id; DEAD_HANDLE once destroyed). An
103
+ * engine-created entity (a glTF part, an anchor) is wrapped from its number and pinned as the
104
+ * engine's; an entity WE created is loose until it gets a parent or a scene. */
105
+ _h: Handle
106
+ /** Native entity id. */
107
+ get id(): number { return this._h.id }
72
108
 
73
109
  /** AR anchors only — whether the anchor is currently being tracked. Kept in step with the
74
110
  * 'track' / 'untrack' events; `false` for any non-anchor node. */
@@ -80,10 +116,20 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
80
116
  /** Materials assigned through setMaterial, by primitive slot (a Mesh fills slot 0 itself). */
81
117
  protected _materials?: Material[]
82
118
 
83
- constructor(internalId?: number) {
119
+ constructor(internalId?: number | Handle) {
84
120
  super()
85
- this.id = internalId ?? _creator.createEntity()
86
- nodeRegistry.set(this.id, this)
121
+ registerFreedKind(OWNED_KIND_GL_ENTITY, nodes)
122
+ if (internalId === undefined) {
123
+ const h = _creator.createEntity()
124
+ this._h = typeof h === "number" ? { id: h } : h // a number only from a test stub
125
+ registerLoose(this)
126
+ } else if (typeof internalId === "number") {
127
+ this._h = { id: internalId } // engine-owned: findable while the engine keeps it
128
+ nodes.pin(this)
129
+ } else {
130
+ this._h = internalId // an owned root the runtime handed us (createGlb, cloneEntity)
131
+ registerLoose(this)
132
+ }
87
133
  }
88
134
 
89
135
  /** Geometry, if this node is a Mesh (used by physics auto-shape). Overridden by Mesh. */
@@ -263,29 +309,37 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
263
309
  // --- hierarchy ---
264
310
  get parent(): Node | null {
265
311
  const id = _creator.getParent(this.id)
266
- return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
312
+ return id === 0 ? null : nodeOf(id)
267
313
  }
268
314
  get children(): Node[] {
269
- return _creator.getChildren(this.id).map((id) => nodeRegistry.get(id) ?? new Node(id))
315
+ const n = _creator.getChildCount(this.id)
316
+ const out: Node[] = []
317
+ for (let i = 0; i < n; i++) { const id = _creator.getChild(this.id, i); out.push(nodeOf(id)) }
318
+ return out
270
319
  }
271
320
  get childCount(): number { return _creator.getChildCount(this.id) }
272
321
  getChild(index: number): Node | null {
273
322
  const id = _creator.getChild(this.id, index)
274
- return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
323
+ return id === 0 ? null : nodeOf(id)
275
324
  }
276
325
  /** Parent the given nodes under this one. */
277
326
  add(...children: Node[]): this {
278
- for (const c of children) _creator.setParent(c.id, this.id, false)
327
+ for (const c of children) { _creator.setParent(c.id, this.id, false); nodes.pin(c) }
279
328
  return this
280
329
  }
281
330
  setParent(parent: Node | null, worldPositionStays = false): this {
282
- if (parent === null) _creator.setParentNull(this.id, worldPositionStays)
283
- else _creator.setParent(this.id, parent.id, worldPositionStays)
331
+ if (parent === null) {
332
+ _creator.setParentNull(this.id, worldPositionStays)
333
+ if (!_creator.entityIsOwned(this.id)) nodes.unpin(this.id) // loose again: the handle owns it
334
+ } else {
335
+ _creator.setParent(this.id, parent.id, worldPositionStays)
336
+ nodes.pin(this)
337
+ }
284
338
  return this
285
339
  }
286
340
  traverse(callback: (node: Node) => void): void {
287
341
  callback(this)
288
- _creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new Node(id)))
342
+ _creator.traverse(this.id, (id) => callback(nodeOf(id)))
289
343
  }
290
344
  // --- materials (any renderable: a Mesh, or an internal node of a loaded Model) ---
291
345
  /** The material assigned to slot 0 through this API (null before one is set — a GLB part's own
@@ -313,7 +367,7 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
313
367
  const hit = cache.get(name)
314
368
  if (hit !== undefined) return hit
315
369
  const id = _creator.findNode ? _creator.findNode(this.id, name) : findNodeByWalk(this.id, name)
316
- const found = id ? nodeRegistry.get(id) ?? new Node(id) : null
370
+ const found = id ? nodeOf(id) : null
317
371
  cache.set(name, found)
318
372
  return found
319
373
  }
@@ -372,16 +426,29 @@ export class Node extends AspectHost<NodeEvents> implements CompWriter {
372
426
  * `destroyEntity` frees the entity tree but knows nothing about JS-side aspects, and a destroyed
373
427
  * node's `update()` must not keep ticking. Deepest nodes go first, then this one. */
374
428
  destroy(): void {
429
+ if (this._h.id === 0) return
375
430
  const subtree: Node[] = [this]
376
- _creator.traverse(this.id, (id) => { const n = nodeRegistry.get(id); if (n) subtree.push(n) })
377
- for (let i = subtree.length - 1; i >= 0; i--) {
378
- const n = subtree[i]
379
- n._detachAll()
380
- n._xf = 0
381
- n._xfKind = 0
382
- nodeRegistry.delete(n.id)
383
- }
384
- _creator.destroyEntity(this.id)
431
+ _creator.traverse(this.id, (id) => { const n = nodes.get(id); if (n) subtree.push(n) })
432
+ for (let i = subtree.length - 1; i >= 0; i--) subtree[i]._detachAll()
433
+ const id = this.id
434
+ // Dead NOW — entity ids are recycled, and the runtime's freed report lands a drain later.
435
+ for (const n of subtree) { nodes.unpin(n.id); n._dead() }
436
+ _creator.destroyEntity(id)
437
+ }
438
+
439
+ /** @internal The wrapper's resource is gone: no more native calls from here. */
440
+ _dead(): void {
441
+ this._xf = 0
442
+ this._xfKind = 0
443
+ loose.delete(this._h.id)
444
+ this._h = DEAD_HANDLE
445
+ }
446
+ /** @internal The runtime freed the entity underneath (an ancestor destroyed or dropped): the
447
+ * aspects detach (their onDetach runs after the entity is gone) and the wrapper is dead. */
448
+ _freed(): void {
449
+ if (this._h.id === 0) return
450
+ this._detachAll()
451
+ this._dead()
385
452
  }
386
453
  }
387
454
 
@@ -4,7 +4,7 @@
4
4
  // All emitter/curve config crosses the bridge as ONE Float32Array of [tag, payloadLen, ...payload]
5
5
  // records (_creator.setParticleSystemConfig) — the constructor batches every option into a single
6
6
  // call, live setters send a one-record buffer. The tag values mirror CPART_TAG_* in
7
- // creator-particles/include/creator-particles/creator-particles.h (guarded by tests/particles-tags.test.ts).
7
+ // creator-particles/include/creator-particles/creator-particles.h (guarded by tests/gl/particles-tags.test.ts).
8
8
  //
9
9
  // Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
10
10
  // below. A builder's whole state is the flat `_data` array (the exact record payload), so the
@@ -42,6 +42,7 @@ const TAG_VELOCITY_LIFE = 19
42
42
  const TAG_ORDER = 20
43
43
  const TAG_SMOOTH_PATH = 21
44
44
  const TAG_RIBBON_ORIENT = 22
45
+ const TAG_LIT = 23
45
46
  /** `TrailOptions.orient` → the native axis code (0 = the camera-facing default). */
46
47
  const RIBBON_AXIS: Record<"camera" | "x" | "y" | "z", number> = { camera: 0, x: 1, y: 2, z: 3 }
47
48
 
@@ -54,6 +55,7 @@ export const _particleTags = {
54
55
  RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
55
56
  ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
56
57
  ORDER: TAG_ORDER, SMOOTH_PATH: TAG_SMOOTH_PATH, RIBBON_ORIENT: TAG_RIBBON_ORIENT,
58
+ LIT: TAG_LIT,
57
59
  }
58
60
 
59
61
  // --- curve builders -----------------------------------------------------------------------------
@@ -304,6 +306,15 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
304
306
  * without this the engine picks who covers whom per frame (smoke popping over a fireball).
305
307
  * Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
306
308
  order?: number
309
+ /** Sprites take the light of the PLACE each one is in — the level's baked light grid (`env.lightmap.volume`).
310
+ * A sprite is unlit: its colour is the picture, which is right for fire and wrong for dust — a puff kicked up
311
+ * under an awning glows as if it stood in the sun. With `lit` every particle's colour is multiplied by the light
312
+ * where it is, relative to the level's OPEN ground: exactly the authored colour out in the sun, the ambient's
313
+ * share of it (tinted the way the shade is) under a roof — per particle, so a trail of puffs laid from the sun
314
+ * into the shade is lit along its length. Author the colour for the open; nothing else to tune. Without a grid
315
+ * — a level with no bake, a host that has none — the colours are drawn as authored. Points, quads, stretch and
316
+ * `Trail`; mesh particles are lit by their material. Default false. */
317
+ lit?: boolean
307
318
  /** Coarse draw order among ALL blended draws, 0 (first) … 7 (last) — see `Mesh.renderPriority`.
308
319
  * Default 5: meshes sit at 4 and decals at 3, so smoke covers a car's glass and its skid marks
309
320
  * whatever the camera does (the engine's depth sort compares object centres, and an emitter's
@@ -492,6 +503,7 @@ export class Particles extends Node {
492
503
  if (options.rateOverDistance !== undefined) out.push(TAG_RATE_DISTANCE, 1, options.rateOverDistance)
493
504
  if (options.smooth !== undefined) out.push(TAG_SMOOTH_PATH, 1, options.smooth ? 1 : 0)
494
505
  if (options.order !== undefined) out.push(TAG_ORDER, 1, options.order)
506
+ if (options.lit !== undefined) out.push(TAG_LIT, 1, options.lit ? 1 : 0)
495
507
  if (options.rate !== undefined) out.push(TAG_RATE, 1, options.rate)
496
508
  if (options.shape !== undefined) pushShape(out, options.shape)
497
509
  if (options.velocityOverLife !== undefined) pushVelocityLife(out, options.velocityOverLife)
@@ -547,6 +559,8 @@ export class Particles extends Node {
547
559
  /** Lay a frame's spawns along a curve through the emitter's path, not the straight chord. */
548
560
  set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
549
561
  set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
562
+ /** Colours × the baked light where each particle is — see `ParticlesOptions.lit`. */
563
+ set lit(val: boolean) { this._send([ TAG_LIT, 1, val ? 1 : 0 ]) }
550
564
  /** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
551
565
  set renderPriority(v: number) {
552
566
  if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
@@ -644,6 +658,9 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
644
658
  * frame rate, since a longer frame bows further off its own chord. Default false; see
645
659
  * `ParticlesOptions.smooth`. */
646
660
  smooth?: boolean
661
+ /** The strip takes the baked light of the place each of its points is at — `ParticlesOptions.lit`. Smoke and
662
+ * dust trails; leave it off for a glowing one. Default false. */
663
+ lit?: boolean
647
664
  /** Which way the strip's WIDTH points.
648
665
  *
649
666
  * `'camera'` (default) rolls the strip about its own length to stay flat to the viewer — it can
@@ -689,6 +706,7 @@ export class Trail extends Node {
689
706
  TAG_LIFETIME, 2, time, time,
690
707
  ]
691
708
  if (options.smooth) out.push(TAG_SMOOTH_PATH, 1, 1)
709
+ if (options.lit) out.push(TAG_LIT, 1, 1)
692
710
  if (options.orient !== undefined || options.faceCamera !== undefined) {
693
711
  this._orient = RIBBON_AXIS[options.orient ?? "camera"]
694
712
  if (options.faceCamera !== undefined) this._faceCamera = options.faceCamera
@@ -732,6 +750,7 @@ export class Trail extends Node {
732
750
 
733
751
  /** Follow a curve through the emitter's path instead of the straight chord between frames. */
734
752
  set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
753
+ set lit(val: boolean) { this._send([ TAG_LIT, 1, val ? 1 : 0 ]) }
735
754
 
736
755
  /** Which way the strip's width points — see `TrailOptions.orient`. Keeps the current `faceCamera`. */
737
756
  set orient(val: "camera" | "x" | "y" | "z") {