lecodes-sdk 2.4.9 → 2.5.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 (74) hide show
  1. package/dist/global.d.ts +35 -0
  2. package/dist/types/core/env.d.ts +25 -0
  3. package/dist/types/core/particlesConfig.d.ts +196 -0
  4. package/dist/types/g2/Font2D.d.ts +19 -0
  5. package/dist/types/g2/Material2D.d.ts +24 -0
  6. package/dist/types/g2/Node2D.d.ts +6 -5
  7. package/dist/types/g2/Particles2D.d.ts +169 -0
  8. package/dist/types/g2/Path2D.d.ts +95 -0
  9. package/dist/types/g2/Physics2D.d.ts +1 -1
  10. package/dist/types/g2/Scene2D.d.ts +15 -17
  11. package/dist/types/g2/Sprite.d.ts +12 -1
  12. package/dist/types/g2/SpriteSheet.d.ts +4 -1
  13. package/dist/types/g2/Text2D.d.ts +96 -0
  14. package/dist/types/g2/Tilemap.d.ts +8 -0
  15. package/dist/types/g2/defineScene2d.d.ts +69 -11
  16. package/dist/types/gl/Foliage.d.ts +0 -7
  17. package/dist/types/gl/Lightmap.d.ts +101 -2
  18. package/dist/types/gl/Material.d.ts +21 -15
  19. package/dist/types/gl/Model.d.ts +8 -3
  20. package/dist/types/gl/Particles.d.ts +3 -135
  21. package/dist/types/gl/Statics.d.ts +56 -0
  22. package/dist/types/gl/staticsFormat.d.ts +101 -0
  23. package/dist/types/inject.d.ts +11 -3
  24. package/dist/types/runtime/env.d.ts +10 -0
  25. package/dist/types/runtime/envValues.d.ts +8 -0
  26. package/dist/types/scene/defineScene.d.ts +17 -12
  27. package/dist/types/server/env.d.ts +14 -0
  28. package/dist/types/server/inject.d.ts +1 -0
  29. package/dist/types/ui/fonts.d.ts +4 -0
  30. package/dist/types/version.d.ts +1 -1
  31. package/dist/types.json +1 -1
  32. package/package.json +1 -1
  33. package/src/bridges/2d.d.ts +65 -5
  34. package/src/bridges/gl.d.ts +34 -15
  35. package/src/compile/bakeMacro.ts +3 -0
  36. package/src/compile/compileProject.ts +25 -4
  37. package/src/compile/envCompile.ts +74 -0
  38. package/src/compile/fontMacro.ts +11 -6
  39. package/src/compile/index.ts +6 -0
  40. package/src/compile/resourceSlots.ts +1 -0
  41. package/src/compile/serverSplit.ts +47 -4
  42. package/src/core/env.ts +47 -0
  43. package/src/core/particlesConfig.ts +422 -0
  44. package/src/g2/Font2D.ts +87 -0
  45. package/src/g2/Material2D.ts +64 -0
  46. package/src/g2/Node2D.ts +10 -6
  47. package/src/g2/Particles2D.ts +337 -0
  48. package/src/g2/Path2D.ts +215 -0
  49. package/src/g2/Physics2D.ts +1 -1
  50. package/src/g2/Scene2D.ts +16 -22
  51. package/src/g2/Sprite.ts +23 -4
  52. package/src/g2/SpriteSheet.ts +9 -5
  53. package/src/g2/Text2D.ts +178 -0
  54. package/src/g2/Tilemap.ts +10 -0
  55. package/src/g2/defineScene2d.ts +108 -21
  56. package/src/g2/groups2d.ts +2 -2
  57. package/src/gl/Foliage.ts +1 -16
  58. package/src/gl/Lightmap.ts +79 -26
  59. package/src/gl/Material.ts +51 -21
  60. package/src/gl/Model.ts +19 -5
  61. package/src/gl/Particles.ts +22 -361
  62. package/src/gl/Statics.ts +301 -0
  63. package/src/gl/nav/NavMesh.ts +4 -1
  64. package/src/gl/staticsFormat.ts +240 -0
  65. package/src/inject.ts +18 -3
  66. package/src/runtime/env.ts +13 -0
  67. package/src/runtime/envValues.ts +8 -0
  68. package/src/runtime/fetch.ts +18 -0
  69. package/src/scene/defineScene.ts +107 -27
  70. package/src/server/env.ts +25 -0
  71. package/src/server/host.ts +6 -0
  72. package/src/server/inject.ts +2 -1
  73. package/src/ui/fonts.ts +46 -0
  74. package/src/version.ts +1 -1
@@ -0,0 +1,422 @@
1
+ // The particle config vocabulary — the ONE language of the 3D `Particles` / `Trail` nodes
2
+ // (gl/Particles.ts) and the 2D `Particles2D` node (g2/Particles2D.ts). Every engine runs the same
3
+ // simulation (creator-particles), so every node speaks it the same way: this module holds the record
4
+ // tags, the curve builders, the engine-neutral option types and the record writers; the nodes add only
5
+ // what is theirs (a 3D material / mesh / light, a 2D sheet texture and Material2D blend).
6
+ //
7
+ // The format: all emitter/curve config crosses the bridge as ONE Float32Array of
8
+ // [tag, payloadLen, ...payload] records — `_creator.setParticleSystemConfig` in 3D,
9
+ // `_creator2d.particlesConfig` in 2D. A constructor batches every option into a single call, a live
10
+ // setter sends a one-record buffer; unknown tags are skipped by length natively. The tag values mirror
11
+ // CPART_TAG_* in engines/particles/include/creator-particles/creator-particles.h (guarded by
12
+ // tests/gl/particles-tags.test.ts).
13
+ //
14
+ // Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
15
+ // below. A builder's whole state is the flat `_data` array (the exact record payload), so the
16
+ // chisel compiler can constant-fold literal chains into the buffer at compile time.
17
+
18
+ import { Color, type ColorInput } from "./color"
19
+ import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
20
+
21
+ export type Range<T> = T | { min: T, max: T }
22
+
23
+ // --- config record tags (keep in sync with CPART_TAG_* in creator-particles.h) ------------------
24
+
25
+ export const TAG_SHAPE = 1
26
+ export const TAG_LIFETIME = 2
27
+ export const TAG_RATE = 3
28
+ export const TAG_VELOCITY = 4
29
+ export const TAG_VELOCITY_RND = 5
30
+ export const TAG_ACCELERATION = 6
31
+ export const TAG_DRAG = 7
32
+ export const TAG_NOISE = 8
33
+ export const TAG_SEED = 9
34
+ export const TAG_PARAM_CURVE = 10
35
+ export const TAG_COLOR_CURVE = 11
36
+ export const TAG_SPACE = 12
37
+ export const TAG_INHERIT_VELOCITY = 13
38
+ export const TAG_RATE_DISTANCE = 14
39
+ export const TAG_RENDER_MODE = 15
40
+ export const TAG_MESH_GEOMETRY = 16
41
+ export const TAG_ANGULAR_VELOCITY = 17
42
+ export const TAG_GROUND = 18
43
+ export const TAG_VELOCITY_LIFE = 19
44
+ export const TAG_ORDER = 20
45
+ export const TAG_SMOOTH_PATH = 21
46
+ export const TAG_RIBBON_ORIENT = 22
47
+ export const TAG_LIT = 23
48
+ /** 0 / 1 — an XY-plane system; the 2D engine sets it itself at creation (CPART_TAG_PLANAR), the SDK never writes it. */
49
+ export const TAG_PLANAR = 24
50
+
51
+ /** RENDER_MODE payloads (CPART_MODE_* in creator-particles.h). */
52
+ export const MODE_POINTS = 0
53
+ export const MODE_QUADS = 1
54
+ export const MODE_RIBBON = 2
55
+ export const MODE_MESH = 3
56
+
57
+ /** The PARAM_CURVE slots the named params alias (custom[0..3]). */
58
+ export const SLOT_SIZE = 0
59
+ export const SLOT_ROTATION = 1
60
+ export const SLOT_OPACITY = 2
61
+ export const SLOT_FRAME = 3
62
+
63
+ /** @internal — exported for the tag-sync test only. */
64
+ export const _particleTags = {
65
+ SHAPE: TAG_SHAPE, LIFETIME: TAG_LIFETIME, RATE: TAG_RATE, VELOCITY: TAG_VELOCITY,
66
+ VELOCITY_RND: TAG_VELOCITY_RND, ACCELERATION: TAG_ACCELERATION, DRAG: TAG_DRAG,
67
+ NOISE: TAG_NOISE, SEED: TAG_SEED, PARAM_CURVE: TAG_PARAM_CURVE, COLOR_CURVE: TAG_COLOR_CURVE,
68
+ SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
69
+ RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
70
+ ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
71
+ ORDER: TAG_ORDER, SMOOTH_PATH: TAG_SMOOTH_PATH, RIBBON_ORIENT: TAG_RIBBON_ORIENT,
72
+ LIT: TAG_LIT,
73
+ PLANAR: TAG_PLANAR,
74
+ }
75
+
76
+ // --- curve builders -----------------------------------------------------------------------------
77
+
78
+ // A value curve reads as a journey over the particle's life:
79
+ // curve(0.3).from(0).via(0.15, 1).via(0.6, 1).to(0) // fade in, hold, fade out — or:
80
+ // curve(0.3).fade(0.15, 0.4)
81
+ // The curve multiplies the base value (or adds to it with mode 'add'); the base and any stop can be
82
+ // a { min, max } range, randomized per particle. With no `.from` the curve starts at the identity
83
+ // (1 for multiply, 0 for add); with no `.to` it holds its last value to end of life.
84
+ //
85
+ // chisel constant-folds a literal chain into its `_data` buffer at compile time and validates it
86
+ // (bad stop order, >8 stops, unparsable color → a build diagnostic): chisel-core/src/curve.rs mirrors
87
+ // these method bodies op-for-op, against a verbatim copy of them in chisel's fixtures/curve-sdk/. Keep
88
+ // the two in step — a change here means a change there (chisel's scripts/curve-exact.mjs compares the
89
+ // fused payload against the real builder, buffer for buffer).
90
+
91
+ /** @internal A range as its [lo, hi] pair. */
92
+ export const lohi = (v: Range<number>): [number, number] =>
93
+ typeof v === "number" ? [ v, v ] : [ v.min, v.max ]
94
+
95
+ export class CurveBuilder {
96
+ private _kind: 0 | 1
97
+ private _lo: number
98
+ private _hi: number
99
+ private _stops: number[] = [] // flat (t, lo, hi)
100
+
101
+ constructor(base: Range<number>, mode: "multiply" | "add") {
102
+ this._kind = mode === "add" ? 1 : 0
103
+ ;[ this._lo, this._hi ] = lohi(base)
104
+ }
105
+
106
+ /** Curve value at birth (t = 0). Must come before any other stop. */
107
+ from(v: Range<number>): this {
108
+ if (this._stops.length > 0) throw new Error("curve: .from() must come first")
109
+ return this.via(0, v)
110
+ }
111
+
112
+ /** Curve value at `t` (0..1 of the particle's life). Stops must be added in ascending t order. */
113
+ via(t: number, v: Range<number>): this {
114
+ const [ lo, hi ] = lohi(v)
115
+ this._stops.push(t, lo, hi)
116
+ return this
117
+ }
118
+
119
+ /** Curve value at death (t = 1). */
120
+ to(v: Range<number>): this {
121
+ return this.via(1, v)
122
+ }
123
+
124
+ /** The classic opacity envelope: rise over the first `fadeIn`, fall over the last `fadeOut`. */
125
+ fade(fadeIn = 0.15, fadeOut = fadeIn): this {
126
+ const up = Math.min(Math.max(fadeIn, 0), 1)
127
+ const down = Math.min(Math.max(fadeOut, 0), 1 - up)
128
+ if (up > 0) this.via(0, 0).via(up, 1)
129
+ else this.via(0, 1)
130
+ if (down > 0) {
131
+ if (1 - down > up) this.via(1 - down, 1)
132
+ this.via(1, 0)
133
+ }
134
+ return this
135
+ }
136
+
137
+ /** The PARAM_CURVE record payload (sans slot): [kind, baseLo, baseHi, n, (t, lo, hi)×n]. */
138
+ get _data(): Float32Array {
139
+ const stops = this._stops
140
+ const prepend = stops.length > 0 && stops[0] > 0
141
+ const n = stops.length / 3 + (prepend ? 1 : 0)
142
+ const out = new Float32Array(4 + n * 3)
143
+ out[0] = this._kind
144
+ out[1] = this._lo
145
+ out[2] = this._hi
146
+ out[3] = n
147
+ let o = 4
148
+ if (prepend) {
149
+ const identity = this._kind === 1 ? 0 : 1
150
+ out[o + 1] = identity
151
+ out[o + 2] = identity
152
+ o += 3
153
+ }
154
+ out.set(stops, o)
155
+ return out
156
+ }
157
+ }
158
+
159
+ export class ColorCurveBuilder {
160
+ private _lo: [number, number, number, number]
161
+ private _hi: [number, number, number, number]
162
+ private _stops: number[] = [] // flat (t, lo rgba, hi rgba)
163
+
164
+ constructor(base: Range<ColorInput>) {
165
+ if (isColorRange(base)) {
166
+ this._lo = Color.toRgba01(base.min)
167
+ this._hi = Color.toRgba01(base.max)
168
+ } else {
169
+ this._lo = Color.toRgba01(base)
170
+ this._hi = this._lo
171
+ }
172
+ }
173
+
174
+ /** Curve color at birth (t = 0). Must come first. The curve multiplies the base color. */
175
+ from(c: Range<ColorInput>): this {
176
+ if (this._stops.length > 0) throw new Error("colorCurve: .from() must come first")
177
+ return this.via(0, c)
178
+ }
179
+
180
+ /** Curve color at `t` (0..1 of the particle's life), in ascending t order. */
181
+ via(t: number, c: Range<ColorInput>): this {
182
+ let lo: number[], hi: number[]
183
+ if (isColorRange(c)) {
184
+ lo = Color.toRgba01(c.min)
185
+ hi = Color.toRgba01(c.max)
186
+ } else {
187
+ lo = hi = Color.toRgba01(c)
188
+ }
189
+ this._stops.push(t, ...lo, ...hi)
190
+ return this
191
+ }
192
+
193
+ /** Curve color at death (t = 1). */
194
+ to(c: Range<ColorInput>): this {
195
+ return this.via(1, c)
196
+ }
197
+
198
+ /** The COLOR_CURVE record payload: [baseLo rgba, baseHi rgba, n, (t, lo rgba, hi rgba)×n]. */
199
+ get _data(): Float32Array {
200
+ const stops = this._stops
201
+ const prepend = stops.length > 0 && stops[0] > 0
202
+ const n = stops.length / 9 + (prepend ? 1 : 0)
203
+ const out = new Float32Array(9 + n * 9)
204
+ out.set(this._lo, 0)
205
+ out.set(this._hi, 4)
206
+ out[8] = n
207
+ let o = 9
208
+ if (prepend) {
209
+ for (let i = 1; i < 9; i++) out[o + i] = 1 // identity multiplier: white, t = 0
210
+ o += 9
211
+ }
212
+ out.set(stops, o)
213
+ return out
214
+ }
215
+ }
216
+
217
+ /** A value curve over each particle's lifetime. Default mode `multiply` (the curve scales the
218
+ * base); `add` offsets it instead. */
219
+ export const curve = (base: Range<number> = 1, mode: "multiply" | "add" = "multiply"): CurveBuilder =>
220
+ new CurveBuilder(base, mode)
221
+
222
+ /** A color curve over each particle's lifetime — always multiplies the base color. */
223
+ export const colorCurve = (base: Range<ColorInput> = "#ffffff"): ColorCurveBuilder =>
224
+ new ColorCurveBuilder(base)
225
+
226
+ // --- option types -------------------------------------------------------------------------------
227
+
228
+ export type ParticleValue = Range<number> | CurveBuilder | { _data: Float32Array }
229
+ export type ParticleColor = ColorInput | { min: ColorInput, max: ColorInput } | ColorCurveBuilder | { _data: Float32Array }
230
+
231
+ /** A start velocity by direction + speed (the object form of `startVelocity`). */
232
+ export type VelocityValue = {
233
+ speed?: Range<number>
234
+ /** Cone half-angle in radians — jitter the launch direction uniformly within it. */
235
+ spread?: number
236
+ /** Asymmetric jitter: two angles (u, v — the x/y components) in radians. */
237
+ randomizeAngle?: { min: Vec3Like, max: Vec3Like }
238
+ } & ({ from: Vec3Like } | { to: Vec3Like } | { dir: Vec3Like } | {})
239
+
240
+ /** Where particles are born, in emitter space. Sphere / hemisphere / cone have an axis of +Y (the
241
+ * hemisphere is the +Y half, the cone opens along +Y from its base disc at `center`) — aim the
242
+ * node with `lookAt(target, 'y')`. `thickness` 0 emits from the surface only, 1 (default) from the
243
+ * whole volume. A sphere or cone also sets the LAUNCH DIRECTION (outward / from the cone's apex)
244
+ * when `startVelocity` gives a speed but no direction, like Unity's shapes. */
245
+ export type ParticleShape =
246
+ | { type: "point", v: Vec3Like }
247
+ | { type: "box", min: Vec3Like, max: Vec3Like }
248
+ | { type: "circle", center: Vec3Like, radius: number }
249
+ | { type: "sphere", center?: Vec3Like, radius: number, thickness?: number }
250
+ | { type: "hemisphere", center?: Vec3Like, radius: number, thickness?: number }
251
+ | { type: "cone", center?: Vec3Like, radius: number, /** half-angle, radians */ angle?: number, /** emit this far up the axis too */ length?: number, thickness?: number }
252
+
253
+ /** Extra velocity over each particle's life — evaluated from the curves every frame and ADDED as a
254
+ * displacement (not accumulated like `acceleration`): `radial` pushes outward along the particle's
255
+ * birth direction (a burst that decays: `curve(4).to(0)`), `x`/`y`/`z` drift in the simulation
256
+ * space (rising smoke: `y: curve(2).from(0).to(1)`). Unity's velocityOverLifetime. */
257
+ export type VelocityOverLife = { radial?: ParticleValue, x?: ParticleValue, y?: ParticleValue, z?: ParticleValue }
258
+
259
+ export type ParticleNoise = {
260
+ strength?: number, frequency?: number, speed?: number,
261
+ /** How much of the displacement a particle earns with AGE (default 0 = all of it from birth).
262
+ * The field is sampled by position, so at 0 every particle born at the same spot is pushed the
263
+ * same way and a plume's root wanders off its emitter by up to `strength` metres; at 1 a particle
264
+ * is born exactly where the emitter put it and drifts into the field over its life — what you
265
+ * want whenever the source is visible (an exhaust pipe, a contact patch, a muzzle). */
266
+ ramp?: number,
267
+ }
268
+
269
+ /** Ground plane for bouncing particles (all render modes). `height` is in the simulation space:
270
+ * world y with `space: 'world'`, emitter-local y otherwise. */
271
+ export type ParticleGround = {
272
+ /** Plane height (default 0). */
273
+ height?: number
274
+ /** Contact radius — particles stop `radius` above the plane (a mesh's half-extent; default 0). */
275
+ radius?: number
276
+ /** Fraction of the vertical speed kept on impact, 0..1 (default 0.3). */
277
+ bounce?: number
278
+ /** Tangential damping while in contact, per second — like `drag` (default 8). */
279
+ friction?: number
280
+ }
281
+
282
+ /** The mesh a mesh-mode system draws per particle — structurally a `Geometry`. */
283
+ export type ParticleMeshData = {
284
+ readonly vertices: ArrayLike<number>
285
+ readonly normals: ArrayLike<number>
286
+ readonly uv: ArrayLike<number>
287
+ readonly indices: ArrayLike<number>
288
+ }
289
+
290
+ // --- record writers -----------------------------------------------------------------------------
291
+ // Each appends its record(s) to `out`; the node turns the whole array into one Float32Array.
292
+
293
+ const isColorRange = (v: unknown): v is { min: ColorInput, max: ColorInput } =>
294
+ typeof v === "object" && v !== null && !Array.isArray(v) && "min" in v
295
+
296
+ const hasData = (v: unknown): v is { _data: Float32Array } =>
297
+ typeof v === "object" && v !== null && "_data" in v
298
+
299
+ /** A PARAM_CURVE record: `slot` 0 size / 1 rotation / 2 opacity / 3 frame. */
300
+ export const pushParam = (out: number[], slot: number, value: ParticleValue): void => {
301
+ if (typeof value === "number") {
302
+ out.push(TAG_PARAM_CURVE, 5, slot, 0, value, value, 0)
303
+ } else if (hasData(value)) {
304
+ const d = value._data
305
+ out.push(TAG_PARAM_CURVE, d.length + 1, slot)
306
+ for (let i = 0; i < d.length; i++) out.push(d[i])
307
+ } else {
308
+ out.push(TAG_PARAM_CURVE, 5, slot, 0, value.min, value.max, 0)
309
+ }
310
+ }
311
+
312
+ export const pushColor = (out: number[], value: ParticleColor): void => {
313
+ let d: Float32Array
314
+ if (hasData(value)) {
315
+ d = value._data
316
+ } else if (isColorRange(value)) {
317
+ d = new ColorCurveBuilder(value)._data
318
+ } else {
319
+ d = new ColorCurveBuilder(value as ColorInput)._data
320
+ }
321
+ out.push(TAG_COLOR_CURVE, d.length)
322
+ for (let i = 0; i < d.length; i++) out.push(d[i])
323
+ }
324
+
325
+ export const pushMesh = (out: number[], g: ParticleMeshData): void => {
326
+ const nv = g.vertices.length / 3
327
+ const ni = g.indices.length
328
+ out.push(TAG_MESH_GEOMETRY, 2 + nv * 8 + ni, nv, ni)
329
+ for (let i = 0; i < nv * 3; i++) out.push(g.vertices[i])
330
+ for (let i = 0; i < nv * 3; i++) out.push(g.normals[i])
331
+ for (let i = 0; i < nv * 2; i++) out.push(g.uv[i])
332
+ for (let i = 0; i < ni; i++) out.push(g.indices[i])
333
+ }
334
+
335
+ export const pushAngularVelocity = (out: number[], v: Vec3Like | { min: Vec3Like, max: Vec3Like }): void => {
336
+ const r = v as { min: Vec3Like, max: Vec3Like }
337
+ if (typeof r === "object" && r !== null && !Array.isArray(r) && "min" in r && !(v instanceof Float32Array) && !(v instanceof Vec3)) {
338
+ out.push(TAG_ANGULAR_VELOCITY, 6, cx(r.min), cy(r.min), cz(r.min), cx(r.max), cy(r.max), cz(r.max))
339
+ } else {
340
+ const a = v as Vec3Like
341
+ out.push(TAG_ANGULAR_VELOCITY, 6, cx(a), cy(a), cz(a), cx(a), cy(a), cz(a))
342
+ }
343
+ }
344
+
345
+ export const pushGround = (out: number[], g: ParticleGround | null): void => {
346
+ if (g === null) out.push(TAG_GROUND, 5, 0, 0, 0, 0, 0)
347
+ else out.push(TAG_GROUND, 5, 1, g.height ?? 0, g.radius ?? 0, g.bounce ?? 0.3, g.friction ?? 8)
348
+ }
349
+
350
+ export const pushShape = (out: number[], shape: ParticleShape): void => {
351
+ if (shape.type === "point") out.push(TAG_SHAPE, 4, 0, cx(shape.v), cy(shape.v), cz(shape.v))
352
+ else if (shape.type === "box") out.push(TAG_SHAPE, 7, 1, cx(shape.min), cy(shape.min), cz(shape.min), cx(shape.max), cy(shape.max), cz(shape.max))
353
+ else if (shape.type === "circle") out.push(TAG_SHAPE, 5, 2, cx(shape.center), cy(shape.center), cz(shape.center), shape.radius)
354
+ else {
355
+ const c = shape.center ?? [ 0, 0, 0 ]
356
+ const thickness = shape.thickness ?? 1
357
+ if (shape.type === "cone") out.push(TAG_SHAPE, 8, 5, cx(c), cy(c), cz(c), shape.radius, shape.angle ?? 0, shape.length ?? 0, thickness)
358
+ else out.push(TAG_SHAPE, 6, shape.type === "sphere" ? 3 : 4, cx(c), cy(c), cz(c), shape.radius, thickness)
359
+ }
360
+ }
361
+
362
+ export const pushVelocityLife = (out: number[], v: VelocityOverLife): void => {
363
+ const axes: Array<[number, ParticleValue | undefined]> = [ [ 0, v.radial ], [ 1, v.x ], [ 2, v.y ], [ 3, v.z ] ]
364
+ for (const [ axis, value ] of axes) {
365
+ if (value === undefined) continue
366
+ if (typeof value === "number") out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value, value, 0)
367
+ else if (hasData(value)) {
368
+ const d = value._data
369
+ out.push(TAG_VELOCITY_LIFE, d.length + 1, axis)
370
+ for (let i = 0; i < d.length; i++) out.push(d[i])
371
+ } else out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value.min, value.max, 0)
372
+ }
373
+ }
374
+
375
+ export const pushVelocity = (out: number[], val: Vec3Like | VelocityValue): void => {
376
+ if (Array.isArray(val) || val instanceof Float32Array || val instanceof Vec3) {
377
+ // a plain vector: launch along it, its length is the speed (native derives it)
378
+ out.push(TAG_VELOCITY, 6, 1, cx(val), cy(val), cz(val), 0, 0)
379
+ out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
380
+ return
381
+ }
382
+ const v = val as VelocityValue
383
+ let mode = 0
384
+ let p: Vec3Like = [ 0, 0, 0 ]
385
+ if ("dir" in v) { mode = 1; p = v.dir }
386
+ else if ("from" in v) { mode = 2; p = v.from }
387
+ else if ("to" in v) { mode = 3; p = v.to }
388
+ let lo = 0, hi = 0
389
+ if (v.speed !== undefined) {
390
+ if (typeof v.speed === "number") lo = hi = v.speed
391
+ else { lo = v.speed.min; hi = v.speed.max }
392
+ }
393
+ out.push(TAG_VELOCITY, 6, mode, cx(p), cy(p), cz(p), lo, hi)
394
+ if (v.spread !== undefined) {
395
+ out.push(TAG_VELOCITY_RND, 5, 1, -v.spread, -v.spread, v.spread, v.spread)
396
+ } else if (v.randomizeAngle) {
397
+ const a = v.randomizeAngle
398
+ out.push(TAG_VELOCITY_RND, 5, 1, cx(a.min), cy(a.min), cx(a.max), cy(a.max))
399
+ } else {
400
+ out.push(TAG_VELOCITY_RND, 5, 0, 0, 0, 0, 0)
401
+ }
402
+ }
403
+
404
+ /** A NOISE record; `null` turns the field off. */
405
+ export const pushNoise = (out: number[], n: ParticleNoise | null): void => {
406
+ out.push(TAG_NOISE, 4, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0, n ? n.ramp ?? 0 : 0)
407
+ }
408
+
409
+ /** A LIFETIME record. */
410
+ export const pushLifetime = (out: number[], val: Range<number>): void => {
411
+ const [ lo, hi ] = lohi(val)
412
+ out.push(TAG_LIFETIME, 2, lo, hi)
413
+ }
414
+
415
+ /** The default flipbook: with a sheet of more than one frame and no explicit `frame`, one full sheet
416
+ * cycle over each particle's lifetime, starting at `startFrame` (`'random'` = desynchronized loops). */
417
+ export const pushDefaultFrames = (out: number[], cols: number, rows: number, startFrame: number | "random" = "random"): void => {
418
+ const frames = cols * rows
419
+ if (frames <= 1) return
420
+ const base = startFrame === "random" ? { min: 0, max: frames - 1 } : startFrame
421
+ pushParam(out, SLOT_FRAME, curve(base, "add").from(0).to(frames))
422
+ }
@@ -0,0 +1,87 @@
1
+ // A font for 2D text (Text2D): a TTF / OTF file parsed natively by creator-2d, whose glyphs are
2
+ // rasterized on demand as signed distance fields into ONE atlas per font — the same atlas serves every
3
+ // size, so a Font2D is loaded once and used at any fontSize.
4
+ //
5
+ // const inter = await Font2D.load(font("inter")) // a family the font() macro registered
6
+ // const bold = await Font2D.load(font("inter"), { weight: 700 })
7
+ // const brand = await Font2D.load(asset('./fonts/Brand.ttf')) // a file of the project, fetched as is
8
+ //
9
+ // A family name resolves through the faces the `font()` compile macro registered (ui/fonts.ts records
10
+ // each face's TTF URL beside its UI registration — the native parser reads TTF / OTF, never the woff2
11
+ // a browser's UI text may use): the exact weight / style, else the nearest registered weight of that
12
+ // style. A name nobody registered fails with an error listing the registered families. Anything that
13
+ // is not a bare name (it has a "/", "." or ":") is a URL / asset path and is fetched as is.
14
+ //
15
+ // Loads are cached per resolved URL, so every `Font2D.load("Inter")` of a project shares one native
16
+ // font (and one atlas). `destroy()` frees it for every holder.
17
+
18
+ import { fetch, type FetchResponse, type File } from "../runtime/fetch"
19
+ import { _fontUrlOf, _registeredFontFamilies } from "../ui/fonts"
20
+
21
+ export type Font2DOptions = {
22
+ /** For a registered family: the face's weight (default 400; the nearest registered one is used). */
23
+ weight?: number
24
+ /** For a registered family: the italic face (default false). */
25
+ italic?: boolean
26
+ }
27
+
28
+ // A bare family name ("Inter", "Brand Sans") — a URL or asset path always carries one of / . :
29
+ const FAMILY_NAME = /^[^/\\.:]+$/
30
+
31
+ let loaded: Map<string, Promise<Font2D>> | undefined // resolved URL → the (pending) font
32
+ const cache = (): Map<string, Promise<Font2D>> => (loaded ??= new Map())
33
+
34
+ const resolveUrl = (source: string, options: Font2DOptions): string => {
35
+ if (!FAMILY_NAME.test(source)) return source
36
+ const italic = options.italic ?? false
37
+ const url = _fontUrlOf(source, options.weight ?? 400, italic)
38
+ if (url !== undefined) return url
39
+ const families = _registeredFontFamilies()
40
+ if (families.some((f) => f.toLowerCase() === source.toLowerCase())) {
41
+ throw new Error(`Font2D.load("${source}"): the family has no ${italic ? "italic" : "upright"} face registered — add it to the font("…") call (e.g. { italic: true }).`)
42
+ }
43
+ throw new Error(
44
+ `Font2D.load("${source}"): no font family "${source}" is registered — declare it with font("…") (a registry id or './fonts/X.ttf'), ` +
45
+ `or pass a URL / asset('./x.ttf'). Registered families: ${families.length > 0 ? families.join(", ") : "none"}.`,
46
+ )
47
+ }
48
+
49
+ export class Font2D {
50
+ /** Native font handle. */
51
+ readonly id: number
52
+ private _key?: string
53
+
54
+ /** @internal Use {@link Font2D.load}. */
55
+ constructor(id: number) {
56
+ this.id = id
57
+ }
58
+
59
+ /** A registered family name (what `font("inter")` compiles to, e.g. "Inter") picks the ttf of
60
+ * (weight ?? 400, italic); anything else is a URL / asset path / response / File fetched as is
61
+ * (`asset('./fonts/Brand.ttf')`). Cached per resolved URL: a family loads once. */
62
+ static load(source: string | FetchResponse | File, options: Font2DOptions = {}): Promise<Font2D> {
63
+ if (typeof source !== "string") {
64
+ return new Promise<Font2D>((resolve, reject) => {
65
+ _creator2d.createFont((source as any)._h.id, (id) => resolve(new Font2D(id)), (e) => reject(e instanceof Error ? e : new Error(`Font2D.load: not a TTF / OTF font (${String(e)})`)))
66
+ })
67
+ }
68
+ let url: string
69
+ try { url = resolveUrl(source, options) } catch (e) { return Promise.reject(e) }
70
+ const hit = cache().get(url)
71
+ if (hit) return hit
72
+ const pending = fetch(url, { useOnce: true }).then((resp) => {
73
+ if (resp.status >= 400) throw new Error(`Failed to fetch font from ${url}. HTTP ${resp.status}`)
74
+ return Font2D.load(resp)
75
+ }).then((font) => { font._key = url; return font })
76
+ pending.catch(() => { if (cache().get(url) === pending) cache().delete(url) })
77
+ cache().set(url, pending)
78
+ return pending
79
+ }
80
+
81
+ /** Free the native font (and its atlas) — for every holder of this cached font; a later load
82
+ * parses it again. Texts still using it stop drawing. */
83
+ destroy(): void {
84
+ if (this._key !== undefined) { cache().delete(this._key); this._key = undefined }
85
+ _creator2d.destroyFont(this.id)
86
+ }
87
+ }
@@ -0,0 +1,64 @@
1
+ // A 2D material: how a sprite or tilemap blends into what is under it and how its texture is
2
+ // sampled. That is all a material is in 2D — no shader, no uniforms:
3
+ // - blend 'additive' adds the colour instead of covering: glow, fire, sparks, muzzle flashes, light
4
+ // shafts (black is transparent, overlaps brighten).
5
+ // - wrap 'repeat' tiles the texture when a sprite's UV `frame` reaches past [0,1]: a scrolling or
6
+ // parallax background is one sprite with a growing frame offset.
7
+ // - filter per texture: crisp 'nearest' pixel art beside smooth 'linear' hi-res art in one scene.
8
+ // Unset = the renderer default (the Scene2D `filter`).
9
+ //
10
+ // new Sprite({ texture: flare, material: Material2D.additive() })
11
+ // sky.material = Material2D.sprite({ wrap: 'repeat', filter: 'linear' })
12
+ //
13
+ // The shape mirrors the 3D `Material.lit()` / `Material.unlit()` factories, but a Material2D is DATA,
14
+ // not a native handle: the engine takes three ints per node, so one value can be shared by any number
15
+ // of nodes and is immutable — to change a node's material, assign a new one.
16
+
17
+ export type Blend2D = "alpha" | "additive"
18
+ export type Filter2D = "nearest" | "linear"
19
+ export type Wrap2D = "clamp" | "repeat"
20
+
21
+ export type Material2DOptions = {
22
+ /** `'alpha'` (default) covers what is under it by its alpha; `'additive'` adds its colour. */
23
+ blend?: Blend2D
24
+ /** Texture sampling; unset = the renderer default (the Scene2D `filter`). */
25
+ filter?: Filter2D
26
+ /** `'clamp'` (default) stretches the edge texel past [0,1]; `'repeat'` tiles the texture. */
27
+ wrap?: Wrap2D
28
+ }
29
+
30
+ export class Material2D {
31
+ readonly blend: Blend2D
32
+ /** Undefined = the renderer default (the Scene2D `filter`). */
33
+ readonly filter?: Filter2D
34
+ readonly wrap: Wrap2D
35
+
36
+ private constructor(options: Material2DOptions) {
37
+ this.blend = options.blend ?? "alpha"
38
+ this.filter = options.filter
39
+ this.wrap = options.wrap ?? "clamp"
40
+ }
41
+
42
+ /** An alpha-blended material — what every sprite and tilemap has by default. */
43
+ static sprite(options: Material2DOptions = {}): Material2D {
44
+ return new Material2D(options)
45
+ }
46
+
47
+ /** An additive material: glow, fire, sparks, flashes. */
48
+ static additive(options: Omit<Material2DOptions, "blend"> = {}): Material2D {
49
+ return new Material2D({ ...options, blend: "additive" })
50
+ }
51
+
52
+ /** The default material (alpha blend, renderer-default filter, clamp). */
53
+ static readonly default: Material2D = Material2D.sprite()
54
+
55
+ /** @internal Push this material to an entity — the engine's three ints. */
56
+ _apply(entityId: number): void {
57
+ _creator2d.setMaterial(
58
+ entityId,
59
+ this.blend === "additive" ? 1 : 0,
60
+ this.filter === "nearest" ? 1 : this.filter === "linear" ? 2 : 0,
61
+ this.wrap === "repeat" ? 1 : 0,
62
+ )
63
+ }
64
+ }
package/src/g2/Node2D.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Base 2D scene node — an empty transform. Sprite and Tilemap extend it, so every node operation
2
- // (position, layer, visibility, events) works on them. Native (creator-2d) owns the authoritative
2
+ // (position, draw order, visibility, events) works on them. Native (creator-2d) owns the authoritative
3
3
  // transform. JS caches it for free reads (JS is normally the sole writer); the exception is a node
4
4
  // driven by a dynamic/kinematic Physics2D body — there the native physics step also writes the
5
5
  // transform, so such nodes are flagged `_physicsDriven` and their transform getters sync the cache
@@ -74,7 +74,7 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
74
74
  protected _rotation = 0
75
75
  protected _sx = 1
76
76
  protected _sy = 1
77
- protected _layer = 0
77
+ protected _renderPriority = 4
78
78
  protected _z = 0
79
79
  protected _visible = true
80
80
 
@@ -183,11 +183,15 @@ export class Node2D extends AspectHost<Node2DEvents> implements CompWriter {
183
183
  }
184
184
  }
185
185
 
186
- /** Draw layer (int). Higher layers render on top. */
187
- get layer(): number { return this._layer }
188
- set layer(v: number) { this._layer = v; _creator2d.setLayer(this.id, v) }
186
+ /** Coarse draw order: 0 draws first, 7 last, 4 is the default — the 3D `Mesh.renderPriority`.
187
+ * Within one priority `z` orders, then (with `scene.ySort` on) world Y. Rounded and clamped to 0..7. */
188
+ get renderPriority(): number { return this._renderPriority }
189
+ set renderPriority(v: number) {
190
+ this._renderPriority = Math.max(0, Math.min(7, Math.round(v)))
191
+ _creator2d.setRenderPriority(this.id, this._renderPriority)
192
+ }
189
193
 
190
- /** Intra-layer depth for non-Y-sorted layers (higher z = on top). */
194
+ /** Draw order within a renderPriority (higher = on top); with `scene.ySort` on, equal z sorts by world Y. */
191
195
  get z(): number { return this._z }
192
196
  set z(v: number) { this._z = v; _creator2d.setZ(this.id, v) }
193
197