lecodes-sdk 2.2.14 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "marcidb-embedded": "^0.15.0",
44
44
  "sharp": "^0.35.2"
45
45
  },
46
- "version": "2.2.14",
46
+ "version": "2.3.1",
47
47
  "files": [
48
48
  "src",
49
49
  "dist",
@@ -127,9 +127,11 @@ declare global {
127
127
  setVignetteOptions?(sceneId: u32, enabled: boolean, midPoint: f32, roundness: f32, feather: f32, color: u32): void
128
128
  /** Colour grading: the operator and every adjustment baked into the view's ONE 3D LUT. `params` is
129
129
  * the fixed layout of `sdk/src/gl/postProcessing.ts` (`CG_*`, mirrored by creator.h's
130
- * ColorGradingParam; 39 floats, the last one = apply the LUT of setColorLut). A change re-bakes a 32³ table on the CPU, so the SDK coalesces
131
- * its writes per tick and quantises; an identical array is a no-op in the engine. Optional: a
132
- * host that predates it renders filament's default (ACES legacy). */
130
+ * ColorGradingParam; 41 floats: [38] = apply the LUT of setColorLut, [39] / [40] = the HDR look,
131
+ * strength 0..1 and paper white 1..8, read only while the output is extended range). The engine
132
+ * takes an older bundle's 39 with the HDR look at its defaults. A change re-bakes a 32³ table on
133
+ * the CPU, so the SDK coalesces its writes per tick and quantises; an identical array is a no-op
134
+ * in the engine. Optional: a host that predates it renders filament's default (ACES legacy). */
133
135
  setColorGrading?(sceneId: u32, params: Float32Array): void
134
136
  /** A custom 3D LUT after the operator, in display-referred sRGB: the TEXT of a `.cube` file
135
137
  * (`LUT_3D_SIZE n` + n³ `r g b` lines, red fastest), parsed by the engine once and applied while
@@ -156,17 +158,7 @@ declare global {
156
158
  * (1 = SDR), constant once the surface exists: the value to branch content on. Optional: hosts
157
159
  * that predate it, or render 8-bit, are SDR. */
158
160
  getDisplayHeadroom?(): f64
159
- /** @c getDisplayHeadroom */
160
161
  getDisplayMaxHeadroom?(): f64
161
- /** HDR look, engine-wide (creator.h `setHdrStrength` / `setHdrPaperWhite`): `strength` 0..1 =
162
- * how much of the picture reaches for the headroom (0 only what SDR clipped, 1 nearly
163
- * everything), `paperWhite` 1..8 = where the operator's white lands as a multiple of SDR white
164
- * (the "HDR brightness" of a console calibration screen; clamped to the headroom). A host may
165
- * pin either from its environment — the getters report what is in force. Optional. */
166
- setHdrStrength?(strength: f32): void
167
- getHdrStrength?(): f64
168
- setHdrPaperWhite?(paperWhite: f32): void
169
- getHdrPaperWhite?(): f64
170
162
  /** Screen-space ambient occlusion: the contact darkening in creases and where objects meet the
171
163
  * ground. `radius` is world-space metres, `power` the falloff contrast, `quality` 0 LOW … 3
172
164
  * ULTRA (sample count — not the buffer resolution, which stays half-res).
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { AppConfig } from "../server/auth/appConfig"
16
+ import { CODE_EXT } from "./assetMacro"
16
17
  import { bundleProjectWithMap, SERVER_INJECT, type MemoryFiles } from "./bundler"
17
18
  import type { CompileEntry } from "./compileProject"
18
19
  import { deriveEndpointParams } from "./serverTypes"
@@ -271,12 +272,18 @@ export const compileServerBundle = async (opts: CompileServerOptions): Promise<{
271
272
  // with a Figma-exported design next to app.server.ts). A stub's value is the resource's PATH: a
272
273
  // row of shared data that names a picture (`photo: "images/a.jpg"`) reaches the server as the
273
274
  // path of that picture, which a seed stores into a file field (files/db.ts). The board's `design/`
274
- // files are never server code and stay out entirely.
275
+ // files are never server code and stay out entirely. A text file that is not code follows the
276
+ // app bundle's rules (compileProject.ts entriesToFiles): a `.json` is its parsed data — a shared
277
+ // module of a price list the person gave does `import raw from "./price.json"`, and the seed
278
+ // that imports the module ran into "raw is not defined" while the text went in as code — and any
279
+ // other text (.csv, .txt, .md) is its string.
275
280
  const files: MemoryFiles = {}
276
281
  for (const e of opts.entries) {
277
282
  const path = normalizePath(e.path)
278
283
  if (/^\/design\//.test(path)) continue
279
284
  if (e.type !== "text" || path.toLowerCase().endsWith(".svg")) { files[path] = `export default ${JSON.stringify(path)}`; continue }
285
+ if (path.toLowerCase().endsWith(".json")) { files[path] = `export default ${e.text?.trim() || "null"}`; continue }
286
+ if (!CODE_EXT.test(path)) { files[path] = `export default ${JSON.stringify(e.text ?? "")}`; continue }
280
287
  files[path] = e.text ?? ""
281
288
  }
282
289
  let params = opts.params
package/src/gl/Scene.ts CHANGED
@@ -12,7 +12,6 @@ import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOp
12
12
  import { _transitionId, TRANSITION_NONE } from "../ui/transitions"
13
13
  import { TREE_DEST_SCENE3D, TREE_FLAG_BACK, ensureEmitter, registerDest, unregisterDest } from "../ui/tree"
14
14
  import { _surfaceWidgets, type LayerElement, type WidgetSet } from "../ui/UILayer"
15
- import { device } from "../runtime/device"
16
15
  import { File } from "../runtime/fetch"
17
16
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
18
17
  import { Camera } from "./Camera"
@@ -142,14 +141,6 @@ export type SceneOptions = {
142
141
  * loses its top mip levels on load, a glTF image is downsampled. Applied before this scene's
143
142
  * assets load; like `anisotropy` it does not touch textures already loaded. */
144
143
  maxTextureSize?: number
145
- /** The look on an HDR display (a screen with headroom above SDR white — Apple XDR panels, the
146
- * macOS host today); ignored on SDR. `strength` 0..1 is how much of the picture reaches for the
147
- * display's headroom (0 only what SDR clipped, 1 nearly everything; default 0.35). `paperWhite`
148
- * 1..8 is where white lands as a multiple of SDR white — the "HDR brightness" of a console
149
- * calibration screen, where 1.5–2 is the norm; default 1 keeps a white wall at the UI's white,
150
- * faithful but dim next to what "HDR on" is expected to look like. Engine-wide like `anisotropy`;
151
- * `device.hdr` is the runtime form and tells you whether the display has any headroom at all. */
152
- hdr?: { strength?: number; paperWhite?: number }
153
144
  }
154
145
 
155
146
  // Route aspect update(dt) through the render-synced native phases (render() early = before physics,
@@ -253,10 +244,6 @@ export class Scene implements Presentable {
253
244
  // place by the time assets bind their samplers.
254
245
  if (options.anisotropy !== undefined) Texture.anisotropy = options.anisotropy
255
246
  if (options.maxTextureSize !== undefined) Texture.maxSize = options.maxTextureSize
256
- if (options.hdr) {
257
- if (options.hdr.strength !== undefined) device.hdr.strength = options.hdr.strength
258
- if (options.hdr.paperWhite !== undefined) device.hdr.paperWhite = options.hdr.paperWhite
259
- }
260
247
  if (options.renderScale !== undefined || options.dynamicResolution) {
261
248
  this.setRenderOptions(options.renderScale ?? 1, options.dynamicResolution ?? false)
262
249
  }
@@ -12,12 +12,13 @@
12
12
  // Two bands inside, because the engine treats them differently:
13
13
  //
14
14
  // - The GRADING band — tone mapping, exposure, white balance, contrast / saturation / vibrance,
15
- // shadows / midtones / highlights, slope / offset / power, curves, the .cube LUT — is baked by the
16
- // engine into the ONE 3D lookup table the frame always samples. Free per frame; a CHANGE re-bakes
17
- // a 32³ table on the CPU. So writes here are COALESCED: a field write marks the band dirty and the
18
- // whole band goes to the engine once, in a microtask (the end of the current handler), and the
19
- // values are quantised to 1/1000 so a tween that barely moves does not re-bake. Not a thing to
20
- // animate every frame; a blend between two looks over a second is fine (a few dozen bakes).
15
+ // shadows / midtones / highlights, slope / offset / power, curves, the .cube LUT, the HDR look —
16
+ // is baked by the engine into the ONE 3D lookup table the frame always samples. Free per frame;
17
+ // a CHANGE re-bakes a 32³ table on the CPU. So writes here are COALESCED: a field write marks the
18
+ // band dirty and the whole band goes to the engine once, in a microtask (the end of the current
19
+ // handler), and the values are quantised to 1/1000 (the HDR look to 0.05) so a tween that barely
20
+ // moves does not re-bake. Not a thing to animate every frame; a blend between two looks over a
21
+ // second is fine (a few dozen bakes).
21
22
  // - The LIVE band — bloom, vignette, ambient occlusion, screen-space reflections, depth of field —
22
23
  // is a set of view options: written to the engine at once, free to animate (a focus pull every
23
24
  // frame is what depthOfField is for).
@@ -134,6 +135,26 @@ export type ColorCurves = {
134
135
  highlightScale?: Rgb
135
136
  }
136
137
 
138
+ /** The look on an HDR display — a screen with headroom above SDR white (Apple's XDR panels, an
139
+ * external display in HDR mode), where the operator is stretched past SDR white toward the peak.
140
+ * Ignored on SDR: stored, read back, nothing changes on screen (`device.hdr.available` says which
141
+ * display this is). Part of the grading band — a change is a bake — and quantised to 0.05 steps. */
142
+ export type HdrLook = {
143
+ /** How much of the picture reaches for the display's headroom, 0..1 (default 0.35). 0 touches only
144
+ * what SDR clipped — the SDR grade kept exactly, but a mostly-mid-tone frame then looks flat next
145
+ * to its one bright spot; 1 lifts nearly everything above black toward the peak. A sunlit
146
+ * exterior takes 1, a dim interior wants less. */
147
+ strength?: number
148
+ /** Where white lands, as a multiple of SDR white, 1..8 (default 1) — the "HDR brightness" of a
149
+ * console game's calibration screen, where 1.5–2 is the norm. It scales the WHOLE picture: at 1
150
+ * a white wall is as bright as the UI's white (faithful, and dim next to what "HDR on" is
151
+ * expected to look like), at 2 twice that, and highlights still climb above it (the engine keeps
152
+ * `headroom / paperWhite` for them, clamped at the peak). `strength` decides how much of the
153
+ * picture reaches for the peak; this decides where the picture starts — a game that wants to read
154
+ * as "HDR on" rather than "SDR with a brighter sun" wants this over `strength`. */
155
+ paperWhite?: number
156
+ }
157
+
137
158
  export type PostProcessingOptions = {
138
159
  /** The operator that folds the scene's HDR light into the display's range (default 'aces').
139
160
  * `'aces'` (filament's ACES legacy) desaturates bright colours towards white — HDR fire reads
@@ -167,6 +188,11 @@ export type PostProcessingOptions = {
167
188
  * the operator, in display-referred sRGB — a `.cube` file: `asset('./look.cube')`, or a bare
168
189
  * staged filename. `null` removes it. */
169
190
  lut?: string | null
191
+ /** The look on an HDR display: `strength` 0..1 (how much of the picture reaches for the peak:
192
+ * 0 only what SDR clipped … 1 nearly everything; default 0.35), `paperWhite` 1..8 (where white
193
+ * lands × SDR white — the console "HDR brightness", 1.5–2 the norm; default 1). Ignored on SDR.
194
+ * An overlay takes its parent's, like the rest of the grading. */
195
+ hdr?: HdrLook
170
196
  /** `true` = defaults (intensity 0.2), an object = its knobs, `false` = off. */
171
197
  bloom?: boolean | BloomOptions
172
198
  /** `true` = defaults (a soft black edge), an object = its knobs, `false` = off. */
@@ -180,12 +206,13 @@ export type PostProcessingOptions = {
180
206
  }
181
207
 
182
208
  /** The float layout of `_creator.setColorGrading` — creator.h's ColorGradingParam. Indices never
183
- * move; a new field goes at the end and COUNT grows. */
209
+ * move; a new field goes at the end and COUNT grows (the engine takes an older bundle's shorter
210
+ * array, down to the 39 before HDR_*, with the tail at its defaults). */
184
211
  export const CG = {
185
212
  TONE_MAPPING: 0, EXPOSURE: 1, TEMPERATURE: 2, TINT: 3, CONTRAST: 4, VIBRANCE: 5, SATURATION: 6,
186
213
  SHADOWS: 7, MIDTONES: 10, HIGHLIGHTS: 13, RANGES: 16, SLOPE: 20, OFFSET: 23, POWER: 26,
187
- SHADOW_GAMMA: 29, MID_POINT: 32, HIGHLIGHT_SCALE: 35, LUT: 38,
188
- COUNT: 39,
214
+ SHADOW_GAMMA: 29, MID_POINT: 32, HIGHLIGHT_SCALE: 35, LUT: 38, HDR_STRENGTH: 39, HDR_PAPER_WHITE: 40,
215
+ COUNT: 41,
189
216
  } as const
190
217
 
191
218
  const TONE_MAPPING_MODE: Record<ToneMappingOperator, number> = { aces: 0, neutral: 1, linear: 2, filmic: 3 }
@@ -196,6 +223,10 @@ const ONE: Rgb = [ 1, 1, 1 ]
196
223
  const ZERO: Rgb = [ 0, 0, 0 ]
197
224
  const rgb = (v: Rgb | undefined, d: Rgb): Rgb => v ? [ v[0], v[1], v[2] ] : d
198
225
  const pair = (v: readonly [number, number] | undefined, d: readonly [number, number]): readonly [number, number] => v ? [ v[0], v[1] ] : d
226
+ /** The HDR look on the wire: clamped to its range and quantised to 0.05 — a slider must not re-bake a
227
+ * 32³ LUT per pixel of travel (the quantum is the SDK's; the engine only clamps). */
228
+ const hdrStep = (v: number, min: number, max: number, d: number): number =>
229
+ Number.isFinite(v) ? Math.round(Math.min(max, Math.max(min, v)) * 20) / 20 : d
199
230
 
200
231
  /** The grading band, normalised: every field present. */
201
232
  type Grading = {
@@ -218,12 +249,15 @@ type Grading = {
218
249
  midPoint: Rgb
219
250
  highlightScale: Rgb
220
251
  lut: string | null
252
+ hdrStrength: number
253
+ hdrPaperWhite: number
221
254
  }
222
255
 
223
256
  const defaultGrading = (): Grading => ({
224
257
  toneMapping: "aces", exposure: 0, temperature: 0, tint: 0, contrast: 1, saturation: 1, vibrance: 1,
225
258
  shadows: ONE, midtones: ONE, highlights: ONE, rangeShadows: [ 0, 0.333 ], rangeHighlights: [ 0.55, 1 ],
226
259
  slope: ONE, offset: ZERO, power: ONE, shadowGamma: ONE, midPoint: ONE, highlightScale: ONE, lut: null,
260
+ hdrStrength: 0.35, hdrPaperWhite: 1,
227
261
  })
228
262
 
229
263
  const BLOOM_DEFAULTS: Required<BloomOptions> = { intensity: 0.2, threshold: true, highlight: 1000, levels: 6, quality: "medium", lensFlare: false, dirt: null, dirtStrength: 0.2 }
@@ -279,6 +313,7 @@ export class PostProcessing {
279
313
  if (options.power !== undefined) this.power = options.power
280
314
  if (options.curves !== undefined) this.curves = options.curves
281
315
  if (options.lut !== undefined) this.lut = options.lut
316
+ if (options.hdr !== undefined) this.hdr = options.hdr
282
317
  this.flush()
283
318
  if (options.bloom !== undefined) this.bloom = options.bloom
284
319
  if (options.vignette !== undefined) this.vignette = options.vignette
@@ -348,6 +383,12 @@ export class PostProcessing {
348
383
  }
349
384
  get lut(): string | null { return this._g.lut }
350
385
  set lut(v: string | null) { this._g.lut = v; this._lutDirty = v !== this._sentLut; this._touch() }
386
+ get hdr(): Required<HdrLook> { return { strength: this._g.hdrStrength, paperWhite: this._g.hdrPaperWhite } }
387
+ set hdr(v: HdrLook) {
388
+ if (v.strength !== undefined) this._g.hdrStrength = v.strength
389
+ if (v.paperWhite !== undefined) this._g.hdrPaperWhite = v.paperWhite
390
+ this._touch()
391
+ }
351
392
 
352
393
  // ---- the live band: view options, written at once ----
353
394
 
@@ -445,6 +486,8 @@ export class PostProcessing {
445
486
  put3(CG.MID_POINT, g.midPoint)
446
487
  put3(CG.HIGHLIGHT_SCALE, g.highlightScale)
447
488
  p[CG.LUT] = g.lut !== null ? 1 : 0
489
+ p[CG.HDR_STRENGTH] = hdrStep(g.hdrStrength, 0, 1, 0.35)
490
+ p[CG.HDR_PAPER_WHITE] = hdrStep(g.hdrPaperWhite, 1, 8, 1)
448
491
  return p
449
492
  }
450
493
  }
@@ -30,11 +30,8 @@ const onResize = (width: number, height: number): void => {
30
30
 
31
31
  // --- device.hdr state ---
32
32
  // The 3D bridge is a free global that a 2D-only bundle never defines — reach it defensively. Hosts
33
- // without the methods are SDR by definition; the look setters are remembered so the getters read
34
- // back what the app asked for.
33
+ // without the methods are SDR by definition.
35
34
  const gl = (): typeof _creator | undefined => (typeof _creator !== "undefined" ? _creator : undefined)
36
- let hdrStrengthLocal = 0.35
37
- let hdrPaperWhiteLocal = 1
38
35
  // `hdrchange` is a poll: the engine quantises the headroom to half-stops, so a change is rare and
39
36
  // a 4 Hz timer (only while someone listens) is cheaper than a per-frame bridge call.
40
37
  const hdrListeners: HdrChangeCallback[] = []
@@ -202,11 +199,13 @@ export const device = {
202
199
  return new Vec3(g.x, -g.y, g.z)
203
200
  },
204
201
  },
205
- /** The display's extended dynamic range — HDR. On a screen with headroom above SDR white (Apple's
206
- * XDR panels; the macOS host today) the 3D surface is float and the engine renders past 1.0:
207
- * 1.0 is the white of the UI and of a diffuse white surface, and the sun, emissives and speculars
208
- * climb up to `headroom` times that. Everywhere else it is SDR: `available` false, `headroom` 1,
209
- * the look setters remembered but invisible.
202
+ /** The display's extended dynamic range — HDR: the FACTS about the display. On the macOS desktop
203
+ * host, in a visible window on a screen with headroom above SDR white (Apple's XDR panels, an HDR
204
+ * external display), the 3D surface is float and the engine renders past 1.0: 1.0 is the white of
205
+ * the UI and of a diffuse white surface, and the sun, emissives and speculars climb up to
206
+ * `headroom` times that. Everywhere else it is SDR (the other hosts, a hidden window, a render to
207
+ * PNG): `available` false, `headroom` 1. The LOOK on such a display — how much of the picture
208
+ * reaches for the peak, where white lands — is the scene's: `scene.postProcessing.hdr`.
210
209
  *
211
210
  * Branch content on `available`, not on `headroom` — the live value ramps up from 1 over the
212
211
  * first seconds after launch and follows the brightness keys. Particle `emissive` is the thing
@@ -230,34 +229,6 @@ export const device = {
230
229
  const v = gl()?.getDisplayHeadroom?.() ?? 1
231
230
  return v > 1 ? v : 1
232
231
  },
233
- /** How much of the picture reaches for the headroom, 0..1 (default 0.35). 0 touches only what
234
- * SDR clipped — faithful to the SDR grade, but a mostly-mid-tone frame then looks flat next to
235
- * the one bright spot; 1 lifts nearly everything above black toward the peak. A sunlit exterior
236
- * takes 1, a dim interior wants less. A host may pin it (`LECODES_HDR_STRENGTH`). */
237
- get strength(): number {
238
- return gl()?.getHdrStrength?.() ?? hdrStrengthLocal
239
- },
240
- set strength(v: number) {
241
- const s = Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0.35
242
- hdrStrengthLocal = s
243
- gl()?.setHdrStrength?.(s)
244
- },
245
- /** Where white lands, as a multiple of SDR white, 1..8 (default 1) — the "HDR brightness" of a
246
- * console game's calibration screen, where paper white sits at ~200 nits against SDR's ~100.
247
- * It scales the WHOLE picture: at 1 a white wall is as bright as the UI's white, at 2 it is
248
- * twice that, and highlights still climb above it (the engine keeps `headroom / paperWhite` for
249
- * them, clamped at the peak). `strength` decides how much of the picture reaches for the
250
- * peak; this decides where the picture starts. 1.5–2 is the console norm; a game that wants to
251
- * read as "HDR on" rather than "SDR with a brighter sun" wants this over `strength`. A host may
252
- * pin it (`LECODES_HDR_PAPER_WHITE`). */
253
- get paperWhite(): number {
254
- return gl()?.getHdrPaperWhite?.() ?? hdrPaperWhiteLocal
255
- },
256
- set paperWhite(v: number) {
257
- const p = Number.isFinite(v) ? Math.min(8, Math.max(1, v)) : 1
258
- hdrPaperWhiteLocal = p
259
- gl()?.setHdrPaperWhite?.(p)
260
- },
261
232
  },
262
233
  /** Current connectivity — best-effort navigator.onLine semantics: `false` only when the platform
263
234
  * is sure there's no network. `true` on hosts that don't track it. Change events: `"online"` /
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.2.14"
7
+ export const SDK_VERSION = "2.3.1"
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 => {