@solidrt/core 0.0.55 → 0.0.56

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
@@ -111,7 +111,7 @@ tsconfig.json - the two load-bearing lines are jsx + jsxImportSource:
111
111
  ```
112
112
 
113
113
  Peer deps @solidjs/signals and @solidjs/universal must match (currently
114
- 2.0.0-rc.4); bun resolves them from peerDependencies. On a version bump,
114
+ 2.0.0-rc.6); bun resolves them from peerDependencies. On a version bump,
115
115
  `bun install` only warns ("incorrect peer dependency") and keeps the old
116
116
  ones, and `bun update solid-js @solidjs/signals @solidjs/universal` does
117
117
  not read the pin either: it adds all three to package.json at registry
@@ -165,10 +165,19 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
165
165
  LayoutProps`, so it draws AND is laid out by Taffy. The detached variant
166
166
  (`d-rect`) is `RectProps` only - it draws but is NOT in the layout pass; you
167
167
  place it yourself with `x`/`y` (omit them and it fills the parent, which is
168
- how backgrounds work). Reach for `d-` whenever you want explicit coordinate
169
- positioning instead of layout. It is also a performance lever: for many
170
- directly-positioned, often-animating elements (e.g. hundreds of balls), `d-`
171
- skips the per-element layout that plain elements would incur.
168
+ how backgrounds work). Laid-out elements are the DEFAULT for building a
169
+ screen: structure, cards, panels, labels - anything flex/gap/padding
170
+ should place - is `<view>`/`<text>` and plain primitives. Reach for `d-`
171
+ only with a concrete reason: a background/underlay, free-form drawn
172
+ geometry (a chart's marks, a diagram, vector art), an overlay pinned at
173
+ coordinates, or as a performance lever for many directly-positioned,
174
+ often-animating elements (e.g. hundreds of balls), where `d-` skips the
175
+ per-element layout plain elements would incur. Hand-positioning what flex
176
+ would place - a card built from d-rects and d-texts at computed offsets -
177
+ forfeits wrapping, sizing and resize behavior for nothing: inside a chart
178
+ the marks and axis labels are d-*, but the card around it is a `<view>`.
179
+ A `designSize` view is NOT a reason to go detached - laid-out children
180
+ resolve in design units too.
172
181
 
173
182
  - Transform origin on a `d-view`: with `originX`/`originY` unset, scale/rotate
174
183
  pivot at the view's local (0,0) - the origin its children's coordinates are
@@ -213,6 +222,22 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
213
222
  `position:absolute` at `left:0,top:0`, or just let normal flow place it) and
214
223
  then translate it with `x`/`y`.
215
224
 
225
+ - Measuring layout: never re-derive an element's position with your own
226
+ arithmetic - the layout engine already knows, so ask it.
227
+ `getBoundingBox(ref)` returns the node's laid-out box
228
+ ({x, y, width, height}, transforms composed, relative to the nearest
229
+ `position="relative"` ancestor, else the window);
230
+ `getBoundingBoxViewport(ref)` is always window-relative (the space pointer
231
+ `clientX`/`clientY` report in). Both are snapshot reads of the last
232
+ computed layout, not reactive - read them in `onLayout(fn)` (fires after
233
+ layout, before paint) or in an event handler. The canonical shape: let
234
+ flex place the boxes, measure them in onLayout, and drive DETACHED drawing
235
+ from the result - a d-path connector between two cards, a badge pinned to
236
+ a bar. Two rules keep it one pass: write the measurement only to props
237
+ that do not affect layout (a `d` string, a d-* position, a transform), and
238
+ call `flush()` after the write so it lands in this frame's display list.
239
+ See examples/on-layout-connect.tsx.
240
+
216
241
  - Text `lineHeight` is a MULTIPLIER of fontSize (1.3-1.6 is typical), not
217
242
  pixels. A CSS-reflex value like 22 makes each line box 22x the font size:
218
243
  the text becomes blank space and the parent balloons.
@@ -269,7 +294,12 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
269
294
  - Gesture recognizers, shared by every package: `createPan` (single-pointer
270
295
  drag, axis-aware slop, per-event dx/dy) and `createTransform` (merged
271
296
  pan + pinch + rotate over the whole pointer set, Flutter-Scale style: streams
272
- `{ dx, dy, scale, rotation, x, y, pointers }` once per FRAME - positions
297
+ `{ dx, dy, scale, rotation, x, y, pointers }` once per FRAME. Frames, both
298
+ recognizers: slop and span gates are finger travel and measure in window
299
+ pixels; dx/dy are measured in the handler node's PARENT frame (the frame
300
+ its own x/y live in) so they apply 1:1 to the node or to content inside
301
+ it under any scaled ancestor (a designSize fit); the transform's focal
302
+ x/y is in the node's LOCAL frame, the zoom-about anchor. Positions
273
303
  update per event, but the cross-pointer measure waits for the
274
304
  `pointerFrame` batch terminator, when every pointer is the same age; one
275
305
  finger degrades to a plain pan, and `pointers` is how a consumer gives one-
@@ -292,13 +322,14 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
292
322
  inside JSX, a `createMemo`, or an effect's compute phase) reads it
293
323
  untracked: it silently freezes at the initial value.
294
324
  Writing a signal or store from inside an owned scope (a component body, a
295
- `createMemo`, an effect's compute phase) throws
296
- `REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader called in the component
297
- body that sets state is the classic React / Solid 1.x reflex that hits
298
- this. Move the write into an event handler, an effect's apply phase,
299
- `onSettled`, or an `untrack` block; opt in narrowly with
300
- `createSignal(v, { ownedWrite: true })` for a signal that genuinely is
301
- internal state.
325
+ `createMemo`, an effect's compute phase, a `ref` callback) throws
326
+ `REACTIVE_WRITE_IN_OWNED_SCOPE` in dev; a loader kicked off in the
327
+ component body or a ref that reports progress through a signal is the
328
+ classic reflex that hits this. `untrack` does NOT exempt a write (the
329
+ guard is owner-based, and untrack only stops tracking). Move the write
330
+ into an event handler, an effect's apply phase, or `onSettled`; opt in
331
+ narrowly with `createSignal(v, { ownedWrite: true })` for a signal that
332
+ genuinely is internal state.
302
333
  Signal writes flush on a microtask: a handler that sets a signal and
303
334
  immediately reads it back gets the OLD value. Read it in an effect, or
304
335
  call `flush()` (from @solidjs/signals) to force it through.
@@ -330,7 +361,8 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
330
361
  then bump `epoch`). See examples/stagger.tsx.
331
362
  Reach for per-frame work only for genuinely procedural motion:
332
363
  `onFrame((tick, frame, rate) => {})` is the native hook (runtime-paced,
333
- returns a cleanup, auto-cleaned inside a reactive scope); `rate` is the
364
+ returns a cleanup, auto-cleaned inside an owned scope; from an effect's
365
+ apply, return it so the loop runs only while the condition holds); `rate` is the
334
366
  display's nominal refresh rate in Hz, which a fixed-timestep loop needs
335
367
  (see @solidrt/cli agents/debugging.md on stepping); `requestAnimationFrame`
336
368
  exists as a web-standard one-shot but is not the preferred driver. A JS
@@ -340,10 +372,11 @@ latest (solid-js 1.x, off Solid 2.0 entirely). The recipe that works is
340
372
  Window state: onResize, onLayout, onWindowFocus, onWindowBlur exist, but
341
373
  prefer the reactive reads (`env`/`capabilities` above, or the accessors
342
374
  `windowSize()`, `safeArea()`, `displayScale()`, `windowFocused()`,
343
- `keyboardHeight()`, `pointerLocked()`) for reading layout and window
344
- state. For mouse look, `lockPointer(true)` enters relative mouse mode
345
- (cursor hidden and confined, positions freeze) and pointer events keep
346
- reporting motion through `movementX`/`movementY`.
375
+ `keyboardHeight()`, `pointerLocked()`) for reading window state;
376
+ onLayout's real job is the measure-then-draw pattern above. For mouse
377
+ look, `lockPointer(true)` enters relative mouse mode (cursor hidden and
378
+ confined, positions freeze) and pointer events keep reporting motion
379
+ through `movementX`/`movementY`.
347
380
 
348
381
  - `createPortal` cannot mount during the app's initial render (it throws
349
382
  "no mount target"): gate portal content behind a signal that starts false
@@ -3,12 +3,15 @@
3
3
  Read this before building any screen whose look matters: a background, a
4
4
  decoration, an effect, a chart, anything you would have reached for CSS for.
5
5
 
6
- Layout and props are half the model. There is no stylesheet: no filters, no
7
- box-shadow, no keyframes, no canvas. The visual range a web app gets from CSS
6
+ Layout and props are half the model. There is no stylesheet: no selectors, no
7
+ keyframes, no canvas. The visual range a web app gets from CSS
8
8
  comes from the tiers below instead, and reaching past tier 1 is ordinary
9
9
  app-building here, not optimization - a screen built only from view
10
10
  backgrounds and text is using a fraction of the runtime. Pick the tier the
11
- CONTENT calls for, not the one that looks safest.
11
+ CONTENT calls for, not the one that looks safest. The tiers compose per
12
+ ELEMENT, not per screen: a dashboard's cards, rows and labels stay tier-1
13
+ laid-out structure even when every card holds a tier-2 chart or a tier-3
14
+ shader. Escalate the element the content calls for, never the whole screen.
12
15
 
13
16
  1. Laid-out elements - `<view>`/`<text>`, with `<rect>` (or a filling
14
17
  `<d-rect>` child) for background, border, radius. The structure of a
@@ -47,15 +50,23 @@ no matter how complex the effect, which is why the performance model
47
50
  readable text over any fill.
48
51
  - gradient background -> a gradient `color` on a `d-rect` (gradients are
49
52
  paint values, usable anywhere a color is)
50
- - `filter: blur/grayscale/hue-rotate`, and any "make this look processed" ->
51
- a `shader` on the view (requires repaintBoundary="snapshot"), or on
52
- `<window>` for the whole frame
53
- - `box-shadow` / `text-shadow` / glow -> no shadow prop exists: draw an
54
- offset `d-*` shape under the content, or a view shader with `outset` (the
55
- transparent margin an effect bleeds into)
56
- - `backdrop-filter` -> no equivalent. A view shader sees only its own
57
- subtree's pixels, never what is behind it. Frost the whole frame with a
58
- window shader, or fake the layer with your own content
53
+ - `filter: blur/grayscale/hue-rotate/...` -> the `filter` prop on a view
54
+ (object form: `filter={{ blur: 4, grayscale: 1 }}`; hueRotate in radians).
55
+ For processing beyond the CSS filter functions, a `shader` on the view
56
+ (requires repaintBoundary="snapshot"), or on `<window>` for the whole frame
57
+ - `box-shadow` -> the `shadow` prop on the shape that IS the surface:
58
+ `<rect radius={12} color="white" shadow={{ y: 4, blur: 16, color:
59
+ "#0006" }}/>` (also on oval and path; fields are CSS box-shadow's, color
60
+ required). `text-shadow` / glow -> still no prop: draw an offset `d-*`
61
+ shape under the content, or a view shader with `outset` (the transparent
62
+ margin an effect bleeds into)
63
+ - `backdrop-filter` -> the `backdropFilter` prop on a view (same keys as
64
+ `filter`): frosted glass with real see-through semantics. It reads the
65
+ pixels already painted beneath, so it costs an offscreen capture - a
66
+ deliberate panel, not a casual style - and inside a
67
+ repaintBoundary="snapshot" subtree it sees only that boundary's own
68
+ content. A view `shader` cannot do this at all (it sees only its own
69
+ subtree's pixels)
59
70
  - CSS `transition` -> the `transition` prop: declare it on the element and
60
71
  keep writing targets; the runtime animates natively (performance rule 1)
61
72
  - `@keyframes` -> a `transition` prop when the motion is target-shaped;
@@ -63,6 +74,12 @@ no matter how complex the effect, which is why the performance model
63
74
  uniform when the animation is continuous and visual
64
75
  - `<canvas>` 2D -> `d-*` primitives (rebuild one `d-path` string per frame
65
76
  rather than animating N elements)
77
+ - leader lines, connectors, an annotation or badge hugging an element, any
78
+ drawing that must align with laid-out content -> measure, do not
79
+ calculate: read the boxes with `getBoundingBox` inside `onLayout` and
80
+ drive the `d-*` overlay from the result (core AGENTS.md "Measuring
81
+ layout", examples/on-layout-connect.tsx). Mirroring the layout math in JS
82
+ to predict where an element ends up is always wrong somewhere
66
83
  - `<canvas>` WebGL, three.js -> `createPipelineTexture`, or `@solidrt/3d`
67
84
  - video background, animated hero, particle field -> a shader texture; this
68
85
  is the case the runtime is built for
@@ -88,8 +88,11 @@ Rules, in order of leverage:
88
88
  springs need no pump at all - that is rule 1, and the runtime requests
89
89
  frames only while tracks run. For genuinely procedural per-frame motion,
90
90
  use a self-rechaining one-shot requestAnimationFrame that stops
91
- re-requesting when its work list empties. (Registering onFrame outside a
92
- component body also warns NO_OWNER_CLEANUP - it assumes a reactive owner.)
91
+ re-requesting when its work list empties. A loop that should run only
92
+ while a condition holds is an effect returning it:
93
+ `createEffect(() => active(), on => { if (!on) return; return onFrame(...) })`
94
+ - the apply phase has no owner, so onFrame registers nothing there and
95
+ the returned cleanup is the handle the effect disposes.
93
96
  5. repaintBoundary works like Flutter's: transforms and opacity on the
94
97
  boundary node itself (or any ancestor) are hoisted out of the cache and
95
98
  applied at composite time, so animating x/y/scale/rotate/opacity of a
@@ -114,7 +117,15 @@ Rules, in order of leverage:
114
117
  node - the hoisting described above.
115
118
  6. "snapshot" boundaries pay first-frame texture allocation + raster:
116
119
  creating many at once (dealing a board of 64 sprites) is a visible
117
- one-frame hiccup - pool or pre-warm if that moment matters.
120
+ one-frame hiccup - pool or pre-warm if that moment matters. The texture
121
+ is sized by the view's box: the layout box on a view, the inherited
122
+ frame on a d-view (so a d-view directly under the window snapshots the
123
+ whole window - box it tighter when the content is small). Confirm a
124
+ boundary actually engaged rather than trusting the prop: get_render_tree
125
+ props reads
126
+ repaintBoundary back, get_stats' nodesPainted drops when the fenced
127
+ subtree stops being walked, and the dev overlay's BND/SNP lines count
128
+ boundary reuse per frame.
118
129
  7. Shading pixels the app already drew is a different mechanism from rule 2's
119
130
  generated textures, and both forms are a `shader` prop taking a linked
120
131
  program from compileShader/linkProgram (@solidrt/core/gpu), not a
@@ -21,11 +21,14 @@ because there is nothing in the layout to reflow. The same content as a
21
21
  laid-out `<rect>` with `left={x()}` would put the layout engine on the path of
22
22
  every frame.
23
23
 
24
- **Reach for the detached form first for anything that moves at animation
25
- frequency**, and for content authored in fixed design units - a chart, a
26
- diagram, an svg drawing, a particle field. Reach for the layout form when you
27
- want the element to participate in a flex or grid arrangement, which is most
28
- static UI.
24
+ **The layout form is the default: most of a screen - structure, panels,
25
+ labels, anything a flex or grid arrangement should place - is laid-out
26
+ elements.** Reach for the detached form for anything that moves at animation
27
+ frequency, and for free-form geometry drawn at coordinates - a chart's marks,
28
+ a diagram, an svg drawing, a particle field. A `designSize` view is not such
29
+ a reason: laid-out children resolve in design units too, so even a
30
+ fixed-design screen is built from laid-out elements, with detached geometry
31
+ only where content is genuinely drawn rather than arranged.
29
32
 
30
33
  ## What changes
31
34
 
@@ -91,7 +91,7 @@ for every pixel source in the engine.
91
91
  ## Logo
92
92
 
93
93
  The SolidRT brand mark, the same seven-segment puzzle the scaffold's welcome
94
- screen and the launcher draw, as a component: a square `view` of `size`
94
+ screen and the player draw, as a component: a square `view` of `size`
95
95
  pixels with the segments as gradient-filled `d-path`s.
96
96
 
97
97
  {{ decl packages/core/src/logo.tsx LogoProps }}
@@ -51,6 +51,8 @@ Three of those props are worth knowing before you need them:
51
51
  reflow.
52
52
  - `repaintBoundary` retains the subtree's display list, and in its
53
53
  `"snapshot"` forms its rasterized pixels too. It is the lever for putting
54
- heavy static content next to content that changes every frame.
54
+ heavy static content next to content that changes every frame. On a
55
+ `d-view` the snapshot boxes the inherited frame: the nearest laid-out
56
+ ancestor's box, or the design size under a `designSize` view.
55
57
  - `clipRadius` rounds the clip, and only does anything when `overflow` is
56
58
  non-visible.
@@ -59,6 +59,8 @@ the dev tooling's GPU inventory and in engine log messages.
59
59
 
60
60
  {{ decl packages/core/src/gpu.ts createTexture }}
61
61
 
62
+ {{ decl packages/core/src/gpu.ts createCubeTexture }}
63
+
62
64
  {{ decl packages/core/src/gpu.ts createMutableTexture }}
63
65
 
64
66
  {{ decl packages/core/src/gpu.ts createShaderTexture }}
@@ -147,6 +149,14 @@ clamped target in one consumer. The texture's own state stays what
147
149
  `<texture>` paints and what every other binding uses. `mipmap` is not
148
150
  overridable: the chain either exists on the id or it does not.
149
151
 
152
+ A cube map (`createCubeTexture`) is the same id currency with a different
153
+ sampler: declare `uniform samplerCube` and look it up by direction. The
154
+ declared sampler type is what a binding is checked against - a cube map on
155
+ a `sampler2D` throws, as does a 2D texture on a `samplerCube` - and a cube
156
+ map is sampler-only: nothing displays, reads back, copies or re-uploads it.
157
+ `wrap` does not apply to it (cube filtering is seamless across faces);
158
+ `filter`, `mipmap` and `anisotropy` do.
159
+
150
160
  ## Blending
151
161
 
152
162
  Combining passes is a render-tree job: stack `<texture>` elements and set
@@ -202,6 +212,7 @@ instead - for a UI subtree, `snapshotTexture`.
202
212
  ## Limits
203
213
 
204
214
  `limits` holds the device ceilings queried at startup: maximum texture and
205
- target size, sampler inputs per pass, vertex attributes per pipeline. Creates
215
+ target size, cube map face size, sampler inputs per pass, vertex attributes
216
+ per pipeline. Creates
206
217
  and binds validate against them and throw naming the limit; read them to
207
218
  size within the device instead.
@@ -42,3 +42,31 @@ wrap around.
42
42
  ## Grid
43
43
 
44
44
  {{ decl packages/core/src/types.d.ts GridProps }}
45
+
46
+ ## Measuring
47
+
48
+ Positions come from the engine, not from arithmetic mirrored in app code.
49
+ `getBoundingBox(node)` returns the box from the most recently computed layout
50
+ as `{ x, y, width, height }`, transforms composed, relative to the nearest
51
+ `position="relative"` ancestor (falling back to the window), or `null` before
52
+ the first layout. `getBoundingBoxViewport(node)` is the same box always
53
+ window-relative, the space pointer `clientX`/`clientY` report in. Both are
54
+ snapshot reads, not reactive.
55
+
56
+ The moment to read them is `onLayout(fn)`: it fires after layout is computed
57
+ but before paint, so a measurement can still shape what the current frame
58
+ draws - the pattern for a connector between two laid-out cards, or an
59
+ annotation pinned to a bar the layout placed. Write the result to something
60
+ that does not affect layout (detached geometry, a `d` string, a transform)
61
+ and call `flush()` so the write lands before the display list is built;
62
+ writing a layout prop from `onLayout` costs an extra layout pass.
63
+
64
+ ```tsx
65
+ onLayout(() => {
66
+ let a = getBoundingBox(boxA)
67
+ let b = getBoundingBox(boxB)
68
+ if (!a || !b) return
69
+ setD(`M ${a.x + a.width / 2} ${a.y + a.height / 2} L ${b.x} ${b.y}`)
70
+ flush()
71
+ })
72
+ ```
@@ -2,7 +2,9 @@
2
2
  // you place it yourself with x/y. Because a detached node does not participate in
3
3
  // layout, moving it (animating x/y) does not reflow its siblings - which makes
4
4
  // d- elements the right choice for overlays, badges, and anything that moves
5
- // independently. The rule: a detached node can only contain other detached
5
+ // independently. They are the exception, not the default: screen structure
6
+ // (panels, cards, labels) stays laid out with <view>/<text>; a d- subtree is
7
+ // for drawn or moving content. The rule: a detached node can only contain other detached
6
8
  // nodes. Everything under a d- element must itself be a d- element (here d-rect +
7
9
  // d-text) - nesting a plain <view> or <text> inside a d- element is an error.
8
10
  import { render } from "@solidrt/core"
@@ -30,6 +30,11 @@
30
30
  // geometry - diagrams, slides, dashboards, game boards, emulator screens - fit
31
31
  // one design space instead.
32
32
  //
33
+ // Design space changes the units, not the element choice: laid-out views and
34
+ // text stay the default under a designSize (fact 3). The d-* scene below is
35
+ // free-form drawn geometry; panels, cards and labels would still be built
36
+ // from laid-out elements, as the flex row at the bottom shows.
37
+ //
33
38
  // Resize the window and watch: the scene keeps its aspect ratio, everything
34
39
  // including text scales together, and the readout reports the same design
35
40
  // coordinates for the same spot on the scene at any window size.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidrt/core",
3
- "version": "0.0.55",
3
+ "version": "0.0.56",
4
4
  "license": "MIT",
5
5
  "funding": "https://github.com/sponsors/wellawaretech",
6
6
  "author": "Antoine van Wel",
@@ -17,6 +17,7 @@
17
17
  "./microphone": "./src/microphone.ts",
18
18
  "./speech-recognition": "./src/speech-recognition.ts",
19
19
  "./text-input": "./src/text-input.ts",
20
+ "./video": "./src/video.ts",
20
21
  "./jsx-runtime": "./jsx-runtime.d.ts",
21
22
  "./jsx-runtime-dev": "./jsx-runtime.d.ts"
22
23
  },
@@ -29,11 +30,11 @@
29
30
  "AGENTS.md"
30
31
  ],
31
32
  "devDependencies": {
32
- "@solidrt/flux-types": "0.0.55"
33
+ "@solidrt/flux-types": "0.0.56"
33
34
  },
34
35
  "peerDependencies": {
35
- "@solidjs/signals": "2.0.0-rc.4",
36
- "@solidjs/universal": "2.0.0-rc.4",
37
- "solid-js": "2.0.0-rc.4"
36
+ "@solidjs/signals": "2.0.0-rc.6",
37
+ "@solidjs/universal": "2.0.0-rc.6",
38
+ "solid-js": "2.0.0-rc.6"
38
39
  }
39
40
  }
package/src/core.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as tree from "flux:rendertree"
2
2
  import type { TextureId } from "flux:gpu"
3
- import { createSignal, onCleanup } from "@solidjs/signals"
3
+ import { createSignal, getOwner, onCleanup } from "@solidjs/signals"
4
4
  import { on } from "srt:events"
5
5
 
6
6
  let handlers = new Map<number, Map<string, Function>>()
@@ -93,7 +93,7 @@ export function onPointerMove(fn: (e: GlobalPointerEvent) => void): () => void {
93
93
  if (interestRoot != null) syncInterest(interestRoot)
94
94
  }
95
95
  }
96
- onCleanup(cleanup)
96
+ if (getOwner()) onCleanup(cleanup)
97
97
  return cleanup
98
98
  }
99
99
 
@@ -326,6 +326,19 @@ export function getBoundingBoxViewport(node: { id: number }): BoundingBox | null
326
326
  return tree.getBoundingBoxViewport(node.id)
327
327
  }
328
328
 
329
+ /**
330
+ * The node's laid-out box in its parent's frame: layout output before any
331
+ * transform composes (DOM offsetLeft/offsetTop/offsetWidth/offsetHeight
332
+ * semantics). The untransformed companion to getBoundingBox: pointer events
333
+ * report localX/localY in this box's units, whatever designSize fits or
334
+ * transforms sit on the ancestor chain. A snapshot read like getBoundingBox
335
+ * (call it inside `onLayout` or an event handler); `null` for detached nodes
336
+ * and before the first layout.
337
+ */
338
+ export function getLayoutBox(node: { id: number }): BoundingBox | null {
339
+ return tree.getLayoutBox(node.id)
340
+ }
341
+
329
342
  /**
330
343
  * The texture id of a snapshot boundary's retained rasterization
331
344
  * (`repaintBoundary="snapshot"`): the subtree's pixels as a live texture any