@solidrt/core 0.0.67 → 0.0.68

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/AGENTS.md CHANGED
@@ -138,9 +138,9 @@ that reads exactly like Solid fallout.
138
138
  window (message, stack, a Reset button that retries the failed
139
139
  computations) and logs `Uncaught error`. `<Errored>` gives a subtree its
140
140
  own in-place fallback.
141
- An `onFrame` callback is its own boundary, event-listener style: a throw
142
- is caught and logged (`Error in onFrame callback`, repeating every
143
- frame), the subscription stays, and THE REST OF THAT CALLBACK IS
141
+ An `onFrame` (or `onBeforeRender`) callback is its own boundary,
142
+ event-listener style: a throw is caught and logged (`Error in onFrame
143
+ callback`, repeating every frame), the subscription stays, and THE REST OF THAT CALLBACK IS
144
144
  ABANDONED - what it wrote before the throw lands, what came after never
145
145
  does, frame after frame while the app keeps presenting. A partial frame
146
146
  is therefore a live possibility: every sprite drawn after the throwing
@@ -613,7 +613,17 @@ that reads exactly like Solid fallout.
613
613
  `onFrame(fn, { demand: false })` is the same hook without the standing
614
614
  frame request: it runs on every frame the runtime delivers and the app
615
615
  presents only when the body writes, for a loop that follows time but
616
- seldom changes the picture. `frameTime()` is the current frame's `tick`
616
+ seldom changes the picture. `onBeforeRender(fn)` is the frame's late
617
+ update: it runs after every onFrame callback and the reactive flush,
618
+ when the frame's state is final, for what must see everything the frame
619
+ wrote and write once more (a camera follow, a fit to a measured box;
620
+ Unity's LateUpdate); its signal writes are flushed before the paint
621
+ too, the way onLayout's are. `onBeforeRender(fn, {
622
+ publish: true })` runs after those, and again after a frame's onLayout
623
+ handlers: the pass where an extension hands the engine what the frame
624
+ wrote into its mirrors (a scene its record buffers and lights, a sprite
625
+ layer its style and count), so a write made anywhere in the frame's JS
626
+ is in that frame's picture, never the next. `frameTime()` is the current frame's `tick`
617
627
  read from anywhere (not reactive, requests nothing): the app's own
618
628
  clock for code with no tick in hand, where `performance.now()` would
619
629
  ignore the dev clock, `sol render` and tests. A JS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.67",
3
+ "version": "0.0.68",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -33,8 +33,8 @@
33
33
  "AGENTS.md"
34
34
  ],
35
35
  "devDependencies": {
36
- "@solidrt/flux-types": "0.0.67",
37
- "@solidrt/test": "0.0.67"
36
+ "@solidrt/flux-types": "0.0.68",
37
+ "@solidrt/test": "0.0.68"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@solidjs/signals": "2.0.0-rc.13",
package/src/core.ts CHANGED
@@ -390,6 +390,18 @@ export function prepareText(text: string, options?: tree.MeasureTextOptions): tr
390
390
  return tree.prepareText(text, options)
391
391
  }
392
392
 
393
+ /**
394
+ * Makes the glyph cells of printable ASCII for these font styles ahead of
395
+ * their first use, off the frame: call it at startup with the type scale
396
+ * the app draws in (the components' Window does it for the theme's), so a
397
+ * screen's first paint in any of them never waits for cells. Glyphs outside
398
+ * ASCII are made at first use. Each entry names a font as a <text> would;
399
+ * line height and maxLines are ignored.
400
+ */
401
+ export function warmText(styles: tree.MeasureTextOptions[]): void {
402
+ tree.warmText(styles)
403
+ }
404
+
393
405
  /** One laid-out line from layoutNextLine. */
394
406
  export type TextLine = {
395
407
  /** Unit range [from, to) into prepared.units. */
package/src/gpu.ts CHANGED
@@ -97,7 +97,7 @@ export type { TextureFormat } from "flux:gpu"
97
97
  // runtime, distinct types to the checker, so a cross-space slip like
98
98
  // destroyBuffer(textureId) fails to compile. Exported so apps can annotate
99
99
  // storage (`let ids: TextureId[]`).
100
- export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, TextureId } from "flux:gpu"
100
+ export type { BufferId, DrawId, ProgramId, RenderPipelineId, ShaderStageId, TextureId, TextureRect } from "flux:gpu"
101
101
 
102
102
  // Re-exported so callers that depend on @solidrt/core -- like @solidrt/components
103
103
  // -- need not import flux directly: destroyTexture for the manual-cleanup path
@@ -196,22 +196,11 @@ export {
196
196
  programAttributes,
197
197
  } from "flux:gpu"
198
198
 
199
- /**
200
- * Tags an inline GLSL source, returning it unchanged. Shaders small enough to
201
- * belong beside the code that uses them stay in the file; the tag is what makes
202
- * them legible there, because editors highlight GLSL inside a template literal
203
- * only when a known tag marks it (the name matters - `glsl` is the one the
204
- * grammars look for).
205
- *
206
- * Interpolated values are stringified verbatim, with no GLSL-aware formatting:
207
- * `${2}` splices in the int literal `2`, which will not assign to a float. Pass
208
- * anything that varies as a uniform instead of building it into the source.
209
- *
210
- * Raw semantics, so backslashes reach the compiler as written: the GLSL
211
- * preprocessor continues a line with a trailing `\`, which a cooked template
212
- * would reject as an invalid escape and silently pass through as `undefined`.
213
- */
214
- export let glsl = String.raw
199
+ // The tag, and the screen-size clamp both sprite renderers share: the
200
+ // GLSL function a vertex stage pastes in, its JS twin for the picker, and
201
+ // the validation of a [minScreenPx, maxScreenPx] pair. Both live in the
202
+ // pure shaders.ts so headless tests and pure modules can reach them.
203
+ export { checkScreenSize, glsl, SCREEN_SIZE_GLSL, screenSizeScale } from "./shaders.ts"
215
204
 
216
205
  // captureSnapshot renders a node to pixels and readTexture reads any
217
206
  // texture's bytes back; both resolve the same { width, height, data } shape.
@@ -293,7 +282,9 @@ export function createCubeTexture(
293
282
 
294
283
  /**
295
284
  * Creates a GPU texture you intend to update over time: seed it with `data`,
296
- * then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels.
285
+ * then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels, or
286
+ * `uploadTexture(id, cell, { x, y, width, height })` to write one rect of it
287
+ * (a runtime atlas placing a cell without re-sending the sheet).
297
288
  * `data` must hold at least `width * height` pixels at the declared format's
298
289
  * size, in the view type matching the format (Uint8Array for "rgba8"/
299
290
  * "rgba8-srgb"/"r8", Float32Array for "r32f"/"rgba32f"/"rgba16f"); it may
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export * from "./renderer"
2
- export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, getLayoutBox, snapshotTexture, onPointerMove } from "./core"
2
+ export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, warmText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, getLayoutBox, snapshotTexture, onPointerMove } from "./core"
3
3
  export type { BoundingBox, GlobalPointerEvent, TextLine } from "./core"
4
4
  export { parseColor, mixColors, withAlpha, brightness, createLinearGradient, createRadialGradient } from "./color"
5
5
  export type { Gradient, GradientStop } from "./color"
6
- export { onFrame, frameTime, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, onLink, onSuspend, onQuit, exit, background, registerProtocolHandler } from "./window"
7
- export type { BackEvent, FrameOptions, LifecycleHandler } from "./window"
6
+ export { onFrame, frameTime, onBeforeRender, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, onLink, onSuspend, onQuit, exit, background, registerProtocolHandler } from "./window"
7
+ export type { BackEvent, BeforeRenderOptions, FrameOptions, LifecycleHandler } from "./window"
8
8
  export { windowSize, safeArea, displayScale, windowFocused, keyboardHeight, lockPointer, pointerLocked } from "./window"
9
9
  export { env } from "./environment"
10
10
  export type { InputDevices, SystemTheme, Orientation, Visibility, Launch } from "./environment"
@@ -306,4 +306,12 @@ declare module "sol:test" {
306
306
  * frame runs and no app time passes.
307
307
  */
308
308
  export function capture(node: number): { width: number; height: number; data: Uint8Array }
309
+ /**
310
+ * Run one frame with a capture of `node` queued ahead of it, so the
311
+ * frame's own paint services it: fulfills, once the frame has run, with
312
+ * what that frame drew of the node (the shape of `capture`). Written
313
+ * and painted in one frame, where `capture` paints the tree afresh
314
+ * after the fact. One frame at a time, as `frame`.
315
+ */
316
+ export function painted(node: number): Promise<{ width: number; height: number; data: Uint8Array }>
309
317
  }
package/src/shaders.ts ADDED
@@ -0,0 +1,82 @@
1
+ // The GPU-free shader helpers: the `glsl` tag every shader source in the
2
+ // repo is written with, and the GLSL shared between renderers with its JS
3
+ // twins. Pure - no GPU or GUI imports - so the tests run headless on flux
4
+ // and the extensions' pure modules can import from here; gpu.ts re-exports
5
+ // all of it for apps.
6
+ //
7
+ // The screen-size clamp, shared by every renderer that draws a world-sized
8
+ // quad and lets it hold a size on screen: @solidrt/2d's sprites (the
9
+ // camera zoom is the pixels per world unit) and @solidrt/3d's sprite
10
+ // material (the projection at the quad's depth is). A quad `w x h` world
11
+ // units draws at its world size times the pixels per unit, clamped to
12
+ // [minPx, maxPx] on its smaller axis as ONE uniform scale, so its aspect
13
+ // holds. No bounds is world size; a floor alone is the marker that stays
14
+ // findable at the overview; a ceiling alone is the label that must not
15
+ // balloon; equal bounds is a constant screen size (Three's
16
+ // `sizeAttenuation: false`), where w and h carry the aspect alone. A zero
17
+ // bound is off. The GLSL and the JS below are the same function twice,
18
+ // and every picker applies the JS one to the rect it tests, so a hit is
19
+ // where the pixel is (tests/shaders.test.ts pins the pair).
20
+
21
+ /**
22
+ * Tags an inline GLSL source, returning it unchanged. Shaders small enough to
23
+ * belong beside the code that uses them stay in the file; the tag is what makes
24
+ * them legible there, because editors highlight GLSL inside a template literal
25
+ * only when a known tag marks it (the name matters - `glsl` is the one the
26
+ * grammars look for).
27
+ *
28
+ * Interpolated values are stringified verbatim, with no GLSL-aware formatting:
29
+ * `${2}` splices in the int literal `2`, which will not assign to a float. Pass
30
+ * anything that varies as a uniform instead of building it into the source.
31
+ *
32
+ * Raw semantics, so backslashes reach the compiler as written: the GLSL
33
+ * preprocessor continues a line with a trailing `\`, which a cooked template
34
+ * would reject as an invalid escape and silently pass through as `undefined`.
35
+ */
36
+ export let glsl = String.raw
37
+
38
+ /**
39
+ * The GLSL half: `screenSizeScale(size, pxPerUnit, screenPx)` returns the
40
+ * uniform scale a `size` (world units) quad takes so its smaller axis
41
+ * covers at least `screenPx.x` and at most `screenPx.y` pixels when one
42
+ * world unit is `pxPerUnit` pixels; a zero bound is off, a collapsed quad
43
+ * (zero axis) stays collapsed. Paste it before `main`, multiply the quad's
44
+ * size by it.
45
+ */
46
+ export const SCREEN_SIZE_GLSL = glsl`
47
+ float screenSizeScale(vec2 size, float pxPerUnit, vec2 screenPx) {
48
+ float smallest = min(abs(size.x), abs(size.y)) * pxPerUnit;
49
+ if (smallest <= 0.0) return 1.0;
50
+ float target = smallest;
51
+ if (screenPx.x > 0.0) target = max(target, screenPx.x);
52
+ if (screenPx.y > 0.0) target = min(target, screenPx.y);
53
+ return target / smallest;
54
+ }
55
+ `
56
+
57
+ /**
58
+ * The JS half of SCREEN_SIZE_GLSL, for the picker: the uniform scale a
59
+ * `w x h` quad draws at when one world unit is `pxPerUnit` pixels and
60
+ * its smaller axis is clamped to [minPx, maxPx] on screen. 1 with both
61
+ * bounds off, when the quad already lies within them, or when it is
62
+ * collapsed.
63
+ */
64
+ export function screenSizeScale(w: number, h: number, pxPerUnit: number, minPx: number, maxPx: number): number {
65
+ let smallest = Math.min(Math.abs(w), Math.abs(h)) * pxPerUnit
66
+ if (!(smallest > 0)) return 1
67
+ let target = smallest
68
+ if (minPx > 0) target = Math.max(target, minPx)
69
+ if (maxPx > 0) target = Math.min(target, maxPx)
70
+ return target / smallest
71
+ }
72
+
73
+ /**
74
+ * Validate a screen-size pair (throws - the dev validation policy): each
75
+ * bound finite and >= 0 (0 = off), and a ceiling never below a floor that
76
+ * is on. `site` names the caller in the message.
77
+ */
78
+ export function checkScreenSize(site: string, minPx: number, maxPx: number): void {
79
+ if (!(Number.isFinite(minPx) && minPx >= 0)) throw new Error(`${site}: minScreenPx must be a finite number >= 0, got ${minPx}`)
80
+ if (!(Number.isFinite(maxPx) && maxPx >= 0)) throw new Error(`${site}: maxScreenPx must be a finite number >= 0, got ${maxPx}`)
81
+ if (minPx > 0 && maxPx > 0 && maxPx < minPx) throw new Error(`${site}: maxScreenPx ${maxPx} is below minScreenPx ${minPx}`)
82
+ }
package/src/types.d.ts CHANGED
@@ -1053,6 +1053,14 @@ export interface PathProps extends PaintProps, PointerProps, DashProps {
1053
1053
  shadow?: ShadowProps
1054
1054
  }
1055
1055
 
1056
+ /**
1057
+ * A font role ("sans", "serif", "mono": the font the app packages for it,
1058
+ * or the runtime's own Noto) or a registered family name; an unknown
1059
+ * family falls back to sans. A character the family's font lacks is drawn
1060
+ * from the next registered font that has it, roles first; one no
1061
+ * registered font has draws as the font's missing-glyph box. Whitespace
1062
+ * and control characters never draw a box: a tab advances like a space.
1063
+ */
1056
1064
  export type FontFamily = "sans" | "serif" | "mono" | (string & {})
1057
1065
  export type FontStyle = "normal" | "italic"
1058
1066
  export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
@@ -1076,11 +1084,21 @@ export interface TextRunProps {
1076
1084
  lineHeight?: number
1077
1085
  fontStyle?: FontStyle
1078
1086
  fontWeight?: FontWeight
1087
+ /**
1088
+ * Width as a percentage of normal (CSS font-stretch: 100 normal, 75
1089
+ * condensed, 125 expanded), set on the font's width axis and clamped to
1090
+ * its range; a font without one draws at its one width. Default 100.
1091
+ */
1092
+ fontStretch?: number
1093
+ /**
1094
+ * Extra advance after every character (grapheme cluster) in pixels, the
1095
+ * last one included, as CSS letter-spacing; negative tightens. Default 0.
1096
+ */
1097
+ letterSpacing?: number
1079
1098
  /**
1080
1099
  * Underline in the run's own color, drawn straight through descenders
1081
1100
  * (no skip-ink). Position and thickness come from the font's own metrics
1082
- * unless overridden; a font Impeller resolves through the system fallback
1083
- * gets the shipped Noto values.
1101
+ * unless overridden.
1084
1102
  */
1085
1103
  textDecoration?: TextDecoration
1086
1104
  /** Pixels from the baseline to the top of the underline. */
package/src/window.ts CHANGED
@@ -422,7 +422,9 @@ export function keyboardHeight(): number {
422
422
  // write made by a handler would otherwise reach its node a frame late. The
423
423
  // drain lives here, the way runFrame drains before renderFrame, so that no
424
424
  // handler has to flush for itself. A throwing handler skips neither the
425
- // handlers after it nor the flush.
425
+ // handlers after it nor the flush. The publish pass (onBeforeRender) runs
426
+ // once more after the flush, for the same reason: what a handler hands an
427
+ // extension lands in this frame's paint too.
426
428
  let layoutHandlers: (() => void)[] = []
427
429
  let layoutSubscribed = false
428
430
 
@@ -439,6 +441,7 @@ function runLayoutHandlers() {
439
441
  } catch (err) {
440
442
  console.error("Error in reactive flush:", err)
441
443
  }
444
+ runPublish()
442
445
  }
443
446
 
444
447
  /**
@@ -462,6 +465,93 @@ export function onLayout(fn: () => void) {
462
465
  return unsubscribe
463
466
  }
464
467
 
468
+ // ------ Before render ----------------
469
+
470
+ // The last JS of a frame, in two passes. The plain handlers are the app's
471
+ // late update: they run once per frame after every onFrame callback and
472
+ // the reactive flush, so a follow or a fit reads the finished state of the
473
+ // frame (Unity's LateUpdate). The publish handlers run after them, and
474
+ // once more after the post-layout handlers' flush (the one other JS entry
475
+ // ahead of the paint): an extension hands its pending writes to the engine
476
+ // there - a scene's record bytes and lights, a layer's style and pose
477
+ // publish - so they are in the frame they were made in, the way the
478
+ // engine's own transform flush ahead of the paint is. Without this pass
479
+ // those writes land at the microtask after the whole frame closure, one
480
+ // frame after the paint that should have shown them. Each handler is its
481
+ // own boundary, listener style: a throw is logged and the rest still run.
482
+ let lateHandlers: (() => void)[] = []
483
+ let publishHandlers: (() => void)[] = []
484
+
485
+ function runHandlers(handlers: (() => void)[]) {
486
+ for (let fn of [...handlers]) {
487
+ try {
488
+ fn()
489
+ } catch (err) {
490
+ console.error("Error in onBeforeRender callback:", err)
491
+ }
492
+ }
493
+ }
494
+
495
+ // Before renderFrame(): the late pass (skipped on the bootstrap frame, which
496
+ // runs no frame callbacks either), its signal writes flushed, then the
497
+ // publish pass - the same handlers-then-flush shape as the post-layout
498
+ // entry, so a late handler's reactive write reaches its node in this frame
499
+ // and the publish pass sees it. An empty flush is a flag check.
500
+ function runBeforeRender(bootstrap: boolean) {
501
+ if (!bootstrap) {
502
+ runHandlers(lateHandlers)
503
+ try {
504
+ flush()
505
+ } catch (err) {
506
+ console.error("Error in reactive flush:", err)
507
+ }
508
+ }
509
+ runHandlers(publishHandlers)
510
+ }
511
+
512
+ // After the post-layout handlers' flush: the publish pass alone.
513
+ function runPublish() {
514
+ runHandlers(publishHandlers)
515
+ }
516
+
517
+ export interface BeforeRenderOptions {
518
+ /**
519
+ * Run in the publish pass: after every plain handler, and again after
520
+ * the `onLayout` handlers of a frame that has any. For handing pending
521
+ * state to the engine, never for deciding it.
522
+ */
523
+ publish?: boolean
524
+ }
525
+
526
+ /**
527
+ * Calls `fn` right before every frame renders: after the frame's `onFrame`
528
+ * callbacks and the reactive flush, when the frame's state is final. The
529
+ * frame's late update: a camera follow, a fit to a measured box, anything
530
+ * that must see what every frame callback wrote and write once more.
531
+ * Writes made here, to the engine or through signals, are in this frame:
532
+ * the pending reactive writes are flushed once every handler has run.
533
+ *
534
+ * `{ publish: true }` registers for the publish pass instead: after every
535
+ * plain handler, and once more after the `onLayout` handlers of a frame
536
+ * that has any. It is where an extension hands the engine what the frame
537
+ * wrote into its mirrors (a scene syncs its record buffers and lights, a
538
+ * sprite layer its style and pose publish), so a write made anywhere in
539
+ * the frame's JS is in that frame's paint. A publish handler must be
540
+ * idempotent (it may run twice per frame) and must not write app state.
541
+ *
542
+ * Returns a cleanup function; also auto-cleans within an owned scope.
543
+ */
544
+ export function onBeforeRender(fn: () => void, options?: BeforeRenderOptions) {
545
+ let handlers = options?.publish ? publishHandlers : lateHandlers
546
+ handlers.push(fn)
547
+ let unsubscribe = () => {
548
+ let i = handlers.indexOf(fn)
549
+ if (i >= 0) handlers.splice(i, 1)
550
+ }
551
+ if (getOwner()) onCleanup(unsubscribe)
552
+ return unsubscribe
553
+ }
554
+
465
555
  export function onWindowFocus(fn: () => void) {
466
556
  let unsubscribe = on("windowFocus", fn)
467
557
  if (getOwner()) onCleanup(unsubscribe)
@@ -589,6 +679,7 @@ export function attachWindow(nodeId: number) {
589
679
  } catch (err) {
590
680
  console.error("Error in reactive flush:", err)
591
681
  }
682
+ runBeforeRender(bootstrap)
592
683
  scanForOrphans(t)
593
684
  renderFrame()
594
685
  }