lecodes-sdk 2.4.6 → 2.4.8

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.
@@ -0,0 +1,93 @@
1
+ // A texture a render view draws into (creator-gl renderview.cpp; creator.h createRenderTexture / createRenderView /
2
+ // setRenderViewOptions / setRenderViewProjection / readRenderTexturePixels). Only the texture: the view drawing into it
3
+ // belongs to what made it — a secondary Camera (`camera.renderToTexture`, a 2D picture) or a ReflectionProbe (a cube) —
4
+ // which holds the view's owned handle, its schedule and its projection. The helpers below are those two makers' shared
5
+ // half: the host's texture, and the view started once their node is in a scene (Node.ts awaitScene).
6
+
7
+ import { Texture } from "./Texture"
8
+
9
+ /** @internal `_creator.createRenderTexture` flags (creator.h CREATOR_RENDER_TEXTURE_*). */
10
+ export const RT_FLAG_CUBEMAP = 1
11
+ /** @internal */
12
+ export const RT_FLAG_HDR = 2
13
+ /** @internal */
14
+ export const RT_FLAG_PREFILTER = 4
15
+ /** @internal `_creator.createRenderView` flags bit: no shadow maps in this view (creator.h CREATOR_RENDER_VIEW_SHADOWS_OFF). */
16
+ const VIEW_FLAG_SHADOWS_OFF = 1
17
+ /** @internal The longest side the host takes (creator.h createRenderTexture). */
18
+ export const RT_MAX_SIDE = 4096
19
+
20
+ /**
21
+ * A texture a render view draws into — made by `camera.renderToTexture({ width, height })` (a 2D picture: the
22
+ * tone-mapped image as the screen would show it) or by a `ReflectionProbe` (`probe.texture`: a cube of linear HDR light).
23
+ * It IS a `Texture`: a 2D one goes wherever an image does (`material.map` — a monitor, a rear-view mirror), a cube
24
+ * wherever a cubemap does (`scene.environment` takes the probe itself).
25
+ *
26
+ * ROW ORDER: a 2D render texture's first texel row is the BOTTOM of the camera's picture (OpenGL's order, on every
27
+ * backend) — an image's first row is its top. A mesh whose v runs UP its face (`Mesh.plane`) shows it upright as is; one
28
+ * laid out like a glTF (v down) samples it with v flipped. A cube face is laid out as every GPU's cube lookup reads it.
29
+ *
30
+ * LIFETIME: the texture lives while something holds it — the camera or probe that made it (each keeps its texture), a
31
+ * material showing it — and keeps its last picture when its view stops (`enabled = false`, the camera's next
32
+ * `renderToTexture`, the camera gone).
33
+ */
34
+ export class RenderTexture extends Texture {
35
+ /** A cube (a `ReflectionProbe`'s) rather than a 2D picture (a `Camera`'s). A cube's `width` and `height` are both its
36
+ * edge. */
37
+ readonly cubemap: boolean
38
+
39
+ /** @internal Made by `Camera.renderToTexture` and `ReflectionProbe` (through `makeRenderTexture`): the size, the
40
+ * kind and the texture's owned handle. */
41
+ constructor(width: number, height: number, cubemap: boolean, texture: Handle) {
42
+ super(width, height, texture)
43
+ this.cubemap = cubemap
44
+ }
45
+
46
+ /**
47
+ * RGBA8 bytes (`width × height × 4`) of a rectangle of the texture as the NEXT frame drawn leaves it — a GPU
48
+ * readback, for checks and tools, not a per-frame path. Rows come in the texture's own order: the first is texel row
49
+ * `y`, which for a 2D texture counts from the picture's BOTTOM. `face` 0..5 = +X −X +Y −Y +Z −Z of a cube (0 for a 2D
50
+ * picture). A camera's (LDR) texture gives its stored sRGB bytes, a probe's (HDR) its linear light clamped to 0..1 —
51
+ * a prefiltered probe its sharpest level. Rejects on a host without the call, or when the host refuses the read (the
52
+ * rectangle outside the texture, the engine gone first).
53
+ */
54
+ readPixels(x: number, y: number, width: number, height: number, face = 0): Promise<Uint8Array> {
55
+ if (!_creator.readRenderTexturePixels) return Promise.reject(new Error("RenderTexture.readPixels: this host has no readRenderTexturePixels"))
56
+ return new Promise<Uint8Array>((resolve, reject) => {
57
+ _creator.readRenderTexturePixels!(this._h.id, face, x, y, width, height, (pixels) => {
58
+ if (pixels) resolve(pixels)
59
+ else reject(new Error(`RenderTexture.readPixels: the host refused (${x}, ${y}) ${width}×${height} of face ${face}`))
60
+ })
61
+ })
62
+ }
63
+ }
64
+
65
+ /** @internal Throw for `who` on a host without render textures (createRenderTexture + createRenderView). */
66
+ export const requireRenderTextures = (who: string): void => {
67
+ if (!_creator.createRenderTexture || !_creator.createRenderView) {
68
+ throw new Error(`${who}: this host has no render textures (createRenderTexture) — the web hosts do not yet`)
69
+ }
70
+ }
71
+
72
+ /** @internal The host's render texture (`flags` RT_FLAG_*; a cube's `height` is its `width`), or a throw naming `who`. */
73
+ export const makeRenderTexture = (who: string, width: number, height: number, flags: number, samples = 0): RenderTexture => {
74
+ requireRenderTextures(who)
75
+ const cube = (flags & RT_FLAG_CUBEMAP) !== 0
76
+ const texture = _creator.createRenderTexture!(width, cube ? 0 : height, flags, samples)
77
+ if (!texture || texture.id === 0xFFFFFFFF) {
78
+ throw new Error(`${who}: the host refused a ${cube ? `cube of ${width}` : `${width}×${height}`} render texture (sides 1..${RT_MAX_SIDE})`)
79
+ }
80
+ return new RenderTexture(width, cube ? width : height, cube, texture)
81
+ }
82
+
83
+ /** @internal Start a render view of scene `sceneId` into `texture` from entity `poseId` (createRenderView), or warn for
84
+ * `who` and give null — the host refuses an overlay scene. */
85
+ export const startRenderView = (
86
+ who: string, sceneId: number, texture: RenderTexture, poseId: number,
87
+ layers: number, fov: number, near: number, far: number, shadows: boolean,
88
+ ): Handle | null => {
89
+ const view = _creator.createRenderView!(sceneId, texture._h.id, poseId, layers & 0xff, fov, near, far, shadows ? 0 : VIEW_FLAG_SHADOWS_OFF)
90
+ if (view && view.id !== 0) return view
91
+ console.warn(`[${who}] the host made no render view in scene ${sceneId} (an overlay scene has none) — its texture stays empty`)
92
+ return null
93
+ }
package/src/gl/Scene.ts CHANGED
@@ -18,8 +18,9 @@ import { Camera } from "./Camera"
18
18
  import { SceneAudio } from "./audio/SceneAudio"
19
19
  import { Material } from "./Material"
20
20
  import { PostProcessing, type PostProcessingOptions } from "./postProcessing"
21
+ import { ReflectionProbe } from "./ReflectionProbe"
21
22
  import { Texture } from "./Texture"
22
- import { Node, nodes } from "./Node"
23
+ import { Node, bindPendingViews, nodes, sceneIdOf } from "./Node"
23
24
  import { glState } from "./state"
24
25
  import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
25
26
 
@@ -176,6 +177,12 @@ export class Scene implements Presentable {
176
177
  /** @internal what `environmentIntensity` currently asks for — the accessor's backing field, so it
177
178
  * answers on hosts whose engine has no live setter too. */
178
179
  _environmentIntensity = 20000
180
+ /** @internal The probe the constructor named for `setDefaultIbl` (-1 = none: the project-wide
181
+ * `ibl.ktx`) — re-sent when `environment` goes back to null. */
182
+ _iblId = -1
183
+ /** @internal What `environment` lights the scene from (null = the scene's own probe). Holding it
184
+ * keeps the probe — its cube and the view drawing into it — alive while it lights. */
185
+ _environment: ReflectionProbe | null = null
179
186
  readonly camera: Camera
180
187
  /** The listener + global 3D audio knobs (docs/audio-plan.md). */
181
188
  readonly audio: SceneAudio = new SceneAudio()
@@ -226,6 +233,7 @@ export class Scene implements Presentable {
226
233
  // "no probe named", which leaves the host on the project-wide `ibl.ktx` (the old behaviour).
227
234
  const src = typeof options.ibl === "string" ? options.ibl : undefined
228
235
  const iblId = src === undefined ? -1 : src.startsWith("id:") ? Number(src.slice(3)) : _creatorFetch.local(src)
236
+ this._iblId = iblId
229
237
  _creator.setDefaultIbl(this._id, options.environmentIntensity ?? 20000, iblId)
230
238
  }
231
239
  this._environmentIntensity = options.ibl === false ? 0 : (options.environmentIntensity ?? 20000)
@@ -341,6 +349,44 @@ export class Scene implements Presentable {
341
349
  }
342
350
  }
343
351
  private static _warnedEnvIntensity = false
352
+
353
+ /**
354
+ * Light the scene from a live REFLECTION PROBE (ReflectionProbe.ts) — a cube drawn around the probe's position, the
355
+ * probe riding the car / standing in the room: chrome and glass show what is actually around them, moving objects
356
+ * included. `null` gives the light back to the scene's own probe (`SceneOptions.ibl`, at `environmentIntensity`;
357
+ * re-installing it reloads the probe — a switch, not a per-frame call). A probe in no scene yet starts its view in this
358
+ * one; one already in another scene draws only while that scene is on show.
359
+ *
360
+ * Prefiltered (the default), the probe gives the reflections at every roughness — a car body, painted metal blur it as
361
+ * they should — and the diffuse ambient too (then `environmentIntensity` no longer changes it); it costs a GPU
362
+ * filtering per completed cube (`ReflectionProbeOptions.prefilter`). `prefilter: false` gives one sharp level:
363
+ * mirror-like surfaces (roughness near 0) are right, rougher ones read the sharp cube too, and only the reflections
364
+ * change — the diffuse ambient keeps the scene's own probe's shape at `environmentIntensity`.
365
+ *
366
+ * A probe that draws while it lights reads what it writes — fine on OpenGL; a Vulkan backend may want two probes in
367
+ * turn (a prefiltered probe never does: it lights from the filtered copy). Leave the probe's own carrier out of its view
368
+ * (`layers`), or the cube is the inside of the car. Warns once on a host without the call.
369
+ */
370
+ get environment(): ReflectionProbe | null { return this._environment }
371
+ set environment(probe: ReflectionProbe | null) {
372
+ if (probe !== null && !(probe instanceof ReflectionProbe)) {
373
+ throw new Error("scene.environment: a ReflectionProbe (new ReflectionProbe({ size: 128 })) or null")
374
+ }
375
+ if (probe === this._environment) return
376
+ this._environment = probe
377
+ if (probe && sceneIdOf(probe) < 0) probe._bindView(this._id) // in no scene yet: its view starts in this one
378
+ if (!_creator.setEnvironmentTexture) {
379
+ if (!Scene._warnedEnvTexture) {
380
+ Scene._warnedEnvTexture = true
381
+ console.warn("[scene] this host has no setEnvironmentTexture — environment stays the scene's own probe")
382
+ }
383
+ return
384
+ }
385
+ if (probe) _creator.setEnvironmentTexture(this._id, probe.texture._h.id)
386
+ else _creator.setDefaultIbl(this._id, this._environmentIntensity, this._iblId) // installing a probe frees the cube's light
387
+ }
388
+ private static _warnedEnvTexture = false
389
+
344
390
  /** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
345
391
  setStencil(enabled: boolean): void {
346
392
  if (_creator.setSceneStencil) _creator.setSceneStencil(this._id, enabled)
@@ -348,12 +394,18 @@ export class Scene implements Presentable {
348
394
  }
349
395
 
350
396
  add(...list: Node[]): this {
351
- for (const n of list) { _creator.addEntityToScene(this._id, n.id); nodes.pin(n) } // the scene owns it now
397
+ for (const n of list) {
398
+ _creator.addEntityToScene(this._id, n.id)
399
+ nodes.pin(n) // the scene owns it now
400
+ n._inScene = this._id // what a render view under it starts in (Node.ts sceneIdOf)
401
+ }
402
+ bindPendingViews()
352
403
  return this
353
404
  }
354
405
  remove(...list: Node[]): this {
355
406
  for (const n of list) {
356
407
  _creator.removeEntityFromScene(this._id, n.id)
408
+ if (n._inScene === this._id) n._inScene = -1
357
409
  if (!_creator.entityIsOwned(n.id)) nodes.unpin(n.id) // loose again: its handle owns it
358
410
  }
359
411
  return this
package/src/inject.ts CHANGED
@@ -251,7 +251,11 @@ export type { ScatterPlacement, ScatterOptions } from "./gl/Scatter"
251
251
  export type { InstanceTransform } from "./gl/InstancedMesh"
252
252
  export { Texture } from "./gl/Texture"
253
253
  export type { TextureLoadOptions } from "./gl/Texture"
254
+ export { RenderTexture } from "./gl/RenderTexture"
254
255
  export { Camera } from "./gl/Camera"
256
+ export type { CameraOptions, RenderToTextureOptions } from "./gl/Camera"
257
+ export { ReflectionProbe } from "./gl/ReflectionProbe"
258
+ export type { ReflectionProbeOptions } from "./gl/ReflectionProbe"
255
259
  export { Ray } from "./gl/Ray"
256
260
  export { Plane } from "./gl/Plane"
257
261
  export { Noise } from "./gl/Noise"
package/src/math/vec.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // Fluent 2D / 3D vectors — the SDK's user-facing vector math (no gl-matrix, no out-params, no imports).
2
2
  //
3
- // Design (see PLAN.md): a vector is a *mutable little struct* — `v.x = 3`, `v.set(x, y, z)` mutate in
3
+ // Design: a vector is a *mutable little struct* — `v.x = 3`, `v.set(x, y, z)` mutate in
4
4
  // place — but every *method* is PURE: `a.add(b)` returns a NEW vector and never touches `a`. So chains
5
5
  // (`dir.normalize().scale(s)`) can't clobber their source, and the immutable-method style is the most
6
6
  // amenable to a future escape-analysis / scalar-replacement compiler pass. Backing is plain number
@@ -188,8 +188,12 @@ export type SceneNodeDef = {
188
188
  /** Baked lighting (a scene with `env.lightmap`): is this model/mesh node a lightmap STATIC — a
189
189
  * receiver and an occluder in the bake, real-time shadow casting off once the bake applies?
190
190
  * Default: static unless a `Physics` aspect moves the node (`dynamic` — Physics' default — or
191
- * `kinematic`). Set it only to override that rule; prefab subtrees inherit the verdict. */
192
- lightmap?: boolean
191
+ * `kinematic`). Set it only to override that rule; prefab subtrees inherit the verdict.
192
+ * `'cube'` = a static whose every mesh bakes as AMBIENT CUBES instead of atlas texels — an occluder (its shadow and
193
+ * bounce stay in the bake) lit where it stands, the way the bake treats small and thin props on its own: a pipe run, a
194
+ * fence, a prop whose lightmap seams show. A prefab marked `'cube'` bakes its statics that way too. By name for a whole
195
+ * kit: `env.lightmap.cube`. */
196
+ lightmap?: boolean | "cube"
193
197
  /** `model` nodes: vegetation — load through the FOLIAGE tier (wind, touch bending, per-copy tint; see
194
198
  * `Foliage`). `{ fade: [start, end] }` also thins the cards out over that distance range (m) and drops the
195
199
  * copy past `end` — for ground cover. The fade is per ASSET: every node of the same GLB shares it. */
@@ -243,10 +247,19 @@ export type SceneLightmapDef = {
243
247
  * `{ through, diffuse }` splits it: `through` goes straight (the picture of the map), `diffuse` is scattered by the
244
248
  * fibres (the underside glows, a soft fill with no picture) - dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
245
249
  transmit?: Record<string, LightmapTransmit>
250
+ /** FOR THE BAKE: CUBES BY NAME — regular expressions (ECMAScript, case-sensitive) searched in every baked mesh's key
251
+ * (`<node path>@<ordinal>`): `['SM_pipe', 'SM_fence']` bakes every pipe and fence node as ambient cubes instead of atlas
252
+ * texels, like a node's `lightmap: 'cube'`. The CLI's `--cube` adds to these. */
253
+ cube?: string[]
246
254
  /** `asset('./assets/lightmap/level.lgrid')` — THE LIGHT GRID for movers: every node a Physics aspect or a
247
255
  * CharacterController moves (and what code marks with `Lightmap.track`) takes its ambient light from the bake at the
248
256
  * place it is at, whatever its shader. Absent = movers keep the scene's IBL. */
249
257
  volume?: string
258
+ /** `asset('./assets/lightmap/level-uv1.bin')` — THE PACKED LIGHTMAP UV1 the bake's rects map: it packs every mesh's
259
+ * lightmap charts into a square of their own instead of the unwrap's (once per mesh, whatever model holds it), and the
260
+ * engine swaps this into the models' TEXCOORD_1 before the rects go on. Required when the bake wrote one (the .bake's `uv1`): without it the statics
261
+ * read the wrong texels, so the bake is not applied. */
262
+ uv1?: string
250
263
  /** A DEBUG multiplier on the atlas' lux (default 1). */
251
264
  lightScale?: number
252
265
  }
@@ -430,12 +443,13 @@ const createMesh = async (def: MeshDef, material?: MaterialDef): Promise<Mesh> =
430
443
 
431
444
  // ---- baked lighting (env.lightmap) ---------------------------------------------------------------
432
445
  // Statics are derived, not declared: a model/mesh node bakes unless a Physics aspect moves it
433
- // (`dynamic` — Physics' default — or `kinematic`); `lightmap: true | false` on the node overrides.
434
- // Keys are node paths (prefab internals: `wrapper/inner`), so lightmap.bake stays readable and a
435
- // rename honestly invalidates the rect. make() subtrees are code — they register themselves with
446
+ // (`dynamic` — Physics' default — or `kinematic`); `lightmap: true | false | 'cube'` on the node overrides ('cube': a static
447
+ // baked as ambient cubes, its prefab internals too — LightmapCtx.cube).
448
+ // Keys are node paths (prefab internals: `wrapper/inner`; a model's meshes are records `<path>@<ordinal>` of their own), so
449
+ // lightmap.bake stays readable and a rename honestly invalidates the rect. make() subtrees are code — they register themselves with
436
450
  // Lightmap.add. Edit mode never bakes or applies (the editor shows real-time shadows).
437
451
 
438
- type LightmapCtx = { prefix: string }
452
+ type LightmapCtx = { prefix: string, cube?: boolean }
439
453
 
440
454
  // A CharacterController moves its node as surely as a dynamic body does (2026-09-11: the hero's
441
455
  // capsule was read as static, so its sword baked a shadow into the terrain at the spawn point).
@@ -446,7 +460,7 @@ const movesByPhysics = (def: SceneNodeDef): boolean =>
446
460
 
447
461
  /** The static verdict for one node def (the editor's "Baked lighting" switch shows the same rule).
448
462
  * Children of a mover are movers too — buildNodes carries that down, this is the node's own verdict. */
449
- export const isLightmapStatic = (def: SceneNodeDef): boolean => def.lightmap ?? !movesByPhysics(def)
463
+ export const isLightmapStatic = (def: SceneNodeDef): boolean => def.lightmap !== undefined ? def.lightmap !== false : !movesByPhysics(def)
450
464
 
451
465
  /** A terrain node: from its file or a fresh flat grid; textures load before the node appears (like a
452
466
  * mesh's material). Stamps and holes apply later in buildNodes, once every target node exists. */
@@ -905,7 +919,8 @@ const buildNodes = async (
905
919
  // the path is derived from def keys BEFORE any await, so it is deterministic even though
906
920
  // Promise.all makes build completion (and record insertion) order nondeterministic
907
921
  const path = parentPath === "" ? name : `${parentPath}/${name}`
908
- const lmStatic = lm !== null && (nd.lightmap ?? (!parentMoves && isLightmapStatic(nd)))
922
+ const lmStatic = lm !== null && (nd.lightmap !== undefined ? nd.lightmap !== false : !parentMoves && isLightmapStatic(nd))
923
+ const lmCube = lmStatic && (nd.lightmap === "cube" || lm!.cube === true)
909
924
  // a mover in a level with a light volume reads the volume (models: the engine binds it; meshes: registered below)
910
925
  const node = await createSource(path, nd, lmStatic)
911
926
  // a `mount` def parents to an internal part of the parent asset — safe here: the parent's
@@ -914,13 +929,13 @@ const buildNodes = async (
914
929
  // Draw-set membership is separate from parenting (see docs/3d/node.md) — every def node joins.
915
930
  scene.add(node)
916
931
  applyNode(node, name, nd) // engine-side name stays the bare sibling segment
917
- if (lmStatic && (node instanceof Model || node instanceof Mesh)) Lightmap._register(node, lm!.prefix + path)
918
- else if (lmStatic && nd.terrain !== undefined) Lightmap._registerGround(node, lm!.prefix + path)
932
+ if (lmStatic && (node instanceof Model || node instanceof Mesh)) Lightmap._register(node, lm!.prefix + path, lmCube)
933
+ else if (lmStatic && nd.terrain !== undefined) Lightmap._registerGround(node, lm!.prefix + path, lmCube)
919
934
  // the top of a moving subtree: everything under it reads the level's light grid (Lightmap.track walks it at load)
920
935
  if (lm !== null && !lmStatic && !parentMoves) moverRoots.push(node)
921
936
  if (EDITOR && isEditMode()) addEditorMarker(node, nd)
922
937
  // a prefab under a static wrapper bakes; under a mover its internals stay movers (volume-lit)
923
- if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/` } : null)
938
+ if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/`, cube: lmCube } : null)
924
939
  pending.push({ path, node, def: nd })
925
940
  nodes[path] = node
926
941
  await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path, parentMoves || (lm !== null && !lmStatic))))
@@ -1020,8 +1035,8 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
1020
1035
 
1021
1036
  // the level is complete: apply the bake — or, under `lecodes lightmap bake`, run it
1022
1037
  if (lm && lightmap) {
1023
- await Lightmap.load(scene, { data: lightmap.data, light: lightmap.light, aux: lightmap.aux, direct: lightmap.direct, indirect: lightmap.indirect, probes: lightmap.probes, volume: lightmap.volume },
1024
- { lightScale: lightmap.lightScale, emissiveNits: lightmap.emissiveNits, emissiveBake: lightmap.emissiveBake, transmit: lightmap.transmit })
1038
+ await Lightmap.load(scene, { data: lightmap.data, light: lightmap.light, aux: lightmap.aux, direct: lightmap.direct, indirect: lightmap.indirect, probes: lightmap.probes, volume: lightmap.volume, uv1: lightmap.uv1 },
1039
+ { lightScale: lightmap.lightScale, emissiveNits: lightmap.emissiveNits, emissiveBake: lightmap.emissiveBake, transmit: lightmap.transmit, cube: lightmap.cube })
1025
1040
  }
1026
1041
  // navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
1027
1042
  // geometry for it (the static bodies exist by now; edit mode never loads it — v1)
package/src/ui/tree.ts CHANGED
@@ -93,6 +93,7 @@ export const OWNED_KIND_ENTITY_2D = 5
93
93
  export const OWNED_KIND_MEDIA = 6
94
94
  export const OWNED_KIND_TWEEN = 7
95
95
  export const OWNED_KIND_CANVAS = 8
96
+ export const OWNED_KIND_GL_RENDER_VIEW = 9
96
97
 
97
98
  // PanDirection (creator-pkg.h) — the low byte of a track answer.
98
99
  import { DEAD_HANDLE } from "../core/pins"
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
5
5
  // and a host's embedded version must agree on the major for the host to run the bundle; minor
6
6
  // and patch never gate anything (features are detected, never versioned).
7
- export const SDK_VERSION = "2.4.6"
7
+ export const SDK_VERSION = "2.4.8"
8
8
 
9
9
  /** The major of a semver string, or null when it is not one. */
10
10
  export const sdkMajor = (version: string | null | undefined): number | null => {