@solidrt/core 0.0.69 → 0.0.71
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/performance.md +6 -1
- package/examples/README.md +1 -0
- package/examples/gpu-shader-chain.tsx +135 -0
- package/package.json +3 -3
- package/src/core.ts +45 -10
- package/src/gpu.ts +122 -0
- package/src/index.ts +4 -3
- package/src/scroll.ts +12 -16
- package/src/text-input.ts +3 -3
- package/src/types.d.ts +15 -0
- package/src/warn.ts +22 -0
package/agents/performance.md
CHANGED
|
@@ -102,7 +102,12 @@ Rules, in order of leverage:
|
|
|
102
102
|
boundary node itself (or any ancestor) are hoisted out of the cache and
|
|
103
103
|
applied at composite time, so animating x/y/scale/rotate/opacity of a
|
|
104
104
|
boundary does NOT re-raster it (verified by A/B measurement - the damage
|
|
105
|
-
system classifies these as
|
|
105
|
+
system classifies these as Compose and keeps the node's own cache). One
|
|
106
|
+
exception, for crispness: a boundary holding text that comes to rest at
|
|
107
|
+
a new scale re-records once there (a snapshot boundary re-rasterizes
|
|
108
|
+
once), so its content is rasterized at the resting size instead of
|
|
109
|
+
stretched, and a scale transition with a known end rasters for that end
|
|
110
|
+
as it starts; a press scale that returns to 1 never re-rasters.
|
|
106
111
|
What DOES invalidate the cache is any paint or content change inside the
|
|
107
112
|
subtree - colors, path data, text, a Show toggling - so drive animation
|
|
108
113
|
with transforms and keep the cached content itself static. Off a boundary,
|
package/examples/README.md
CHANGED
|
@@ -51,6 +51,7 @@ for the element/prop model see `@solidrt/core/AGENTS.md`.
|
|
|
51
51
|
- `gpu-texture-blend.tsx` - compositing two shader targets as stacked `<texture>` layers with `blendMode` ("plus" additive over a base pass), the alternative to a third shader that samples both. Click toggles against `"source-over"` to show why. The tree-level counterpart to blending within one draw (`gpu-particles.tsx`); without `blend: "add"` a target's own draw runs with GL blending disabled.
|
|
52
52
|
- `gpu-raw-program.tsx` - the raw shading layer: compileShader/linkProgram/createRenderPipeline/createShaderTarget, one vertex stage shared by two programs, with and without the standard header.
|
|
53
53
|
- `gpu-pipeline.tsx` - `createPipelineTexture`: a custom vertex+fragment pair drawing an interleaved vertex buffer (position and color attributes resolved by name), a depth-tested spinning cube whose `uTime` uniform is driven through `<texture params>` exactly like a fragment shader's.
|
|
54
|
+
- `gpu-shader-chain.tsx` - a chain of targets: a plasma `createShaderTexture` bound as a cube `createPipelineTexture`'s sampler input (`textures: { uMap: plasmaId }`). Sampler bindings are live dependencies, so the app drives only the plasma's `uTime` and the cube re-renders after it every frame, in that order, with no uniform and no per-frame code of its own. The plasma shows beside the cube on the `<texture>` that carries its params.
|
|
54
55
|
- `gpu-particles.tsx` - an additive particle field: `topology: "points"` with `blend: "add"`, so overlapping gaussian splats (`gl_PointSize` from the vertex stage, `gl_PointCoord` falloff, premultiplied output) accumulate into a glowing volume instead of overwriting. The tints are typed vec3 uniforms filled from 3-number array params.
|
|
55
56
|
- `gpu-instancing.tsx` - instanced drawing: one 3-vertex triangle drawn hundreds of times via `instanceCount`, each instance placed and tinted on a phyllotaxis spiral from `gl_InstanceID` alone. `setDraw(id, { instanceCount })` merges into the draw range per frame (absent keys keep their values, like params), so the population breathes without touching the buffer.
|
|
56
57
|
- `gpu-draw-list.tsx` - `createDrawTarget`: one render target holding an ordered, mutable LIST of draws - two orbiting triangles from different programs sharing one depth buffer (the target owns the depth storage, each pipeline the test/write behavior), a third entry added and removed live via its stable `DrawId`, and `setDrawParams` as the per-object channel.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// A chain of targets: a plasma fragment pass feeding a cube pipeline that
|
|
2
|
+
// samples it. Sampler bindings are live dependencies (the model in
|
|
3
|
+
// @solidrt/core/docs/reference/gpu.md): the cube's target is bound to the
|
|
4
|
+
// plasma's through `textures`, so whenever the plasma re-renders, the cube
|
|
5
|
+
// re-renders after it in the same frame, in that order. The app therefore
|
|
6
|
+
// drives only the first pass: the plasma's uTime through the <texture>
|
|
7
|
+
// params prop, as in gpu-shader.tsx. The cube has no uniform of its own
|
|
8
|
+
// and no per-frame code; its view is fixed in the vertex shader, and its
|
|
9
|
+
// faces move because what they sample moves.
|
|
10
|
+
//
|
|
11
|
+
// The plasma is shown beside the cube because that <texture> is where its
|
|
12
|
+
// params prop lives. A source pass that is not on screen is driven the same
|
|
13
|
+
// way from any <texture> of its id. The cube's buffers and index buffer are
|
|
14
|
+
// the gpu-pipeline.tsx mesh with a UV per corner in place of a color.
|
|
15
|
+
import { render, onFrame, createSignal } from "@solidrt/core"
|
|
16
|
+
import { createBuffer, createPipelineTexture, createShaderTexture, glsl } from "@solidrt/core/gpu"
|
|
17
|
+
|
|
18
|
+
// The plasma target's side: what the cube's faces sample.
|
|
19
|
+
let PLASMA_SIZE = 256
|
|
20
|
+
// The cube target's side: what the window shows.
|
|
21
|
+
let CUBE_SIZE = 512
|
|
22
|
+
|
|
23
|
+
// Pass 1: a plasma, the only thing the app animates.
|
|
24
|
+
let PLASMA = glsl`
|
|
25
|
+
uniform float uTime;
|
|
26
|
+
void main() {
|
|
27
|
+
vec2 uv = vUV * 6.0;
|
|
28
|
+
float t = uTime;
|
|
29
|
+
float v = sin(uv.x + t) + sin(uv.y + t * 0.8) + sin((uv.x + uv.y) * 0.7 + t * 1.3)
|
|
30
|
+
+ sin(length(uv - 3.0) * 1.5 - t);
|
|
31
|
+
vec3 col = 0.5 + 0.5 * cos(v * 1.2 + vec3(0.0, 2.1, 4.2));
|
|
32
|
+
fragColor = vec4(col, 1.0);
|
|
33
|
+
}
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
// Pass 2: the cube, viewed from one fixed angle. The projection rig and the
|
|
37
|
+
// clip-y flip are gpu-pipeline.tsx's.
|
|
38
|
+
let VERTEX = glsl`
|
|
39
|
+
in vec3 aPos;
|
|
40
|
+
in vec2 aUV;
|
|
41
|
+
out vec2 vTex;
|
|
42
|
+
|
|
43
|
+
void main() {
|
|
44
|
+
float cy = cos(0.7), sy = sin(0.7);
|
|
45
|
+
float cx = cos(0.5), sx = sin(0.5);
|
|
46
|
+
mat3 rotY = mat3(cy, 0.0, -sy, 0.0, 1.0, 0.0, sy, 0.0, cy);
|
|
47
|
+
mat3 rotX = mat3(1.0, 0.0, 0.0, 0.0, cx, sx, 0.0, -sx, cx);
|
|
48
|
+
vec3 p = rotX * (rotY * aPos);
|
|
49
|
+
p.z -= 2.5;
|
|
50
|
+
float w = -p.z;
|
|
51
|
+
float f = 2.0;
|
|
52
|
+
float a = 11.0 / 9.0;
|
|
53
|
+
float b = -20.0 / 9.0;
|
|
54
|
+
gl_Position = vec4(p.x * f, -p.y * f, w * a + b, w);
|
|
55
|
+
vTex = aUV;
|
|
56
|
+
}
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
// The sampler is the chain: uMap is bound to the plasma target below.
|
|
60
|
+
let FRAGMENT = glsl`
|
|
61
|
+
in vec2 vTex;
|
|
62
|
+
uniform sampler2D uMap;
|
|
63
|
+
void main() {
|
|
64
|
+
fragColor = texture(uMap, vTex);
|
|
65
|
+
}
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
// Interleaved [pos vec3, uv vec2]: 24 vertices, 4 per face, each face the
|
|
69
|
+
// whole plasma once. Every face winds counter-clockwise seen from outside.
|
|
70
|
+
function cubeVertices(): Float32Array {
|
|
71
|
+
type Vec3 = [number, number, number]
|
|
72
|
+
let verts: number[] = []
|
|
73
|
+
let quad = (a: Vec3, b: Vec3, c: Vec3, d: Vec3) => {
|
|
74
|
+
let uvs = [[0, 1], [1, 1], [1, 0], [0, 0]]
|
|
75
|
+
for (let [i, p] of [a, b, c, d].entries()) verts.push(p[0], p[1], p[2], uvs[i]![0]!, uvs[i]![1]!)
|
|
76
|
+
}
|
|
77
|
+
let s = 0.5
|
|
78
|
+
quad([-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]) // front
|
|
79
|
+
quad([s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]) // back
|
|
80
|
+
quad([s, -s, s], [s, -s, -s], [s, s, -s], [s, s, s]) // right
|
|
81
|
+
quad([-s, -s, -s], [-s, -s, s], [-s, s, s], [-s, s, -s]) // left
|
|
82
|
+
quad([-s, s, s], [s, s, s], [s, s, -s], [-s, s, -s]) // top
|
|
83
|
+
quad([-s, -s, -s], [s, -s, -s], [s, -s, s], [-s, -s, s]) // bottom
|
|
84
|
+
return new Float32Array(verts)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Two triangles per face over its 4 shared vertices: 36 uint16 indices.
|
|
88
|
+
function cubeIndices(): Uint16Array {
|
|
89
|
+
let indices: number[] = []
|
|
90
|
+
for (let face = 0; face < 6; face++) {
|
|
91
|
+
let v = face * 4
|
|
92
|
+
indices.push(v, v + 1, v + 2, v, v + 2, v + 3)
|
|
93
|
+
}
|
|
94
|
+
return new Uint16Array(indices)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function App() {
|
|
98
|
+
let plasmaId = createShaderTexture(PLASMA, PLASMA_SIZE, PLASMA_SIZE, { uTime: 0 }, { label: "plasma" })
|
|
99
|
+
let bufferId = createBuffer(cubeVertices(), { label: "cube-verts" })
|
|
100
|
+
let indexId = createBuffer(cubeIndices(), { label: "cube-indices" })
|
|
101
|
+
// No params: nothing of the cube's is ever written. Its one input is the
|
|
102
|
+
// plasma target, and that binding is what re-renders it.
|
|
103
|
+
let cubeId = createPipelineTexture(VERTEX, FRAGMENT, CUBE_SIZE, CUBE_SIZE, null, {
|
|
104
|
+
label: "cube",
|
|
105
|
+
textures: { uMap: plasmaId },
|
|
106
|
+
buffers: [
|
|
107
|
+
{
|
|
108
|
+
attributes: [
|
|
109
|
+
{ name: "aPos", format: "float32x3" },
|
|
110
|
+
{ name: "aUV", format: "float32x2" },
|
|
111
|
+
],
|
|
112
|
+
buffer: bufferId,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
indexBuffer: indexId,
|
|
116
|
+
indexFormat: "uint16",
|
|
117
|
+
depth: true,
|
|
118
|
+
cull: "back",
|
|
119
|
+
clearColor: [0.08, 0.08, 0.12, 1],
|
|
120
|
+
})
|
|
121
|
+
let [time, setTime] = createSignal(0)
|
|
122
|
+
onFrame((tick) => setTime(tick / 1000))
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<window alignItems="center" justifyContent="center" flexDirection="row" gap={24}>
|
|
126
|
+
<texture src={cubeId} width={CUBE_SIZE} height={CUBE_SIZE} />
|
|
127
|
+
<view flexDirection="column" alignItems="center" gap={8}>
|
|
128
|
+
<texture src={plasmaId} params={{ uTime: time() }} width={PLASMA_SIZE} height={PLASMA_SIZE} />
|
|
129
|
+
<text fontSize={14} color="#888">the plasma pass, the only one driven</text>
|
|
130
|
+
</view>
|
|
131
|
+
</window>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render(() => <App />)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.71",
|
|
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.
|
|
37
|
-
"@solidrt/test": "0.0.
|
|
36
|
+
"@solidrt/flux-types": "0.0.71",
|
|
37
|
+
"@solidrt/test": "0.0.71"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@solidjs/signals": "2.0.0-rc.13",
|
package/src/core.ts
CHANGED
|
@@ -391,15 +391,42 @@ export function prepareText(text: string, options?: tree.MeasureTextOptions): tr
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
/**
|
|
394
|
-
* Makes
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
394
|
+
* Makes glyph cells ahead of their first use, off the frame: call it at
|
|
395
|
+
* startup with the type scale the app draws in (the components' Window
|
|
396
|
+
* does it for the theme's), so a screen's first paint in any of them never
|
|
397
|
+
* waits for cells. Each entry names a font as a <text> would (line height
|
|
398
|
+
* and maxLines are ignored) and warms its printable ASCII; an entry with a
|
|
399
|
+
* `text` also warms the glyphs that string shapes to in the font (a
|
|
400
|
+
* ligature, a curly quote, a letter beyond ASCII), so a headline or a
|
|
401
|
+
* label the app knows it will draw is warm too. Anything else is made at
|
|
402
|
+
* first use.
|
|
400
403
|
*/
|
|
401
|
-
export function warmText(
|
|
402
|
-
tree.warmText(
|
|
404
|
+
export function warmText(entries: tree.WarmTextEntry[]): void {
|
|
405
|
+
tree.warmText(entries)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export type TextRenderingOptions = tree.TextRenderingOptions
|
|
409
|
+
export type TextRenderingPolicy = tree.TextRenderingPolicy
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Changes how every text is rendered: the coverage-to-color mode and its
|
|
413
|
+
* gamma or contrast, and the stem darkening and hinting of the glyph cells. Each option
|
|
414
|
+
* given replaces its part of the current policy, the rest stays; every text
|
|
415
|
+
* redraws at the next frame. The defaults are what an app that says nothing
|
|
416
|
+
* gets; this is the knob for judging a policy by eye on a running app.
|
|
417
|
+
*/
|
|
418
|
+
export function setTextRendering(options: TextRenderingOptions): void {
|
|
419
|
+
tree.setTextRendering(options)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The text rendering policy as it stands, resolved for this display: the
|
|
424
|
+
* shape setTextRendering takes, with a darkening or hinting left at its
|
|
425
|
+
* default read as the value the display scale gives it. What a settings
|
|
426
|
+
* screen shows; setting it back changes nothing on this display.
|
|
427
|
+
*/
|
|
428
|
+
export function textRendering(): TextRenderingPolicy {
|
|
429
|
+
return tree.textRendering()
|
|
403
430
|
}
|
|
404
431
|
|
|
405
432
|
/** One laid-out line from layoutNextLine. */
|
|
@@ -426,6 +453,13 @@ export type TextLine = {
|
|
|
426
453
|
* every glued piece that follows, plus the last piece's ink. What must fit on
|
|
427
454
|
* the line for the unit to go on it.
|
|
428
455
|
*/
|
|
456
|
+
// Slack a line's width may be exceeded by and still fit, logical px: the
|
|
457
|
+
// width a laid-out box hands back is a float ulp off the width its text
|
|
458
|
+
// measured (the rendertree's layout::FIT_SLACK, the same value), so an
|
|
459
|
+
// exact fit wraps a word inside its own box. 1/64 px is a browser layout
|
|
460
|
+
// unit: far above any ulp drift, far below a visible overhang.
|
|
461
|
+
export const FIT_SLACK = 1 / 64
|
|
462
|
+
|
|
429
463
|
export function unitInk(units: tree.TextUnit[], index: number): number {
|
|
430
464
|
let ink = units[index]!.width
|
|
431
465
|
let advance = 0
|
|
@@ -439,7 +473,8 @@ export function unitInk(units: tree.TextUnit[], index: number): number {
|
|
|
439
473
|
/**
|
|
440
474
|
* The next line of `prepared` from unit `cursor` that fits `width`, or null
|
|
441
475
|
* when the text is used up. Greedy: units go on the line while the pen plus
|
|
442
|
-
* the unit's ink stays within `width
|
|
476
|
+
* the unit's ink stays within `width` (a hair of slack for float drift
|
|
477
|
+
* included); a hard break ends the line; a unit
|
|
443
478
|
* wider than `width` on its own goes on the line whole and overflows. Draw a
|
|
444
479
|
* line as `<d-text x y w={line.width + 1}>{prepared.text.slice(line.start, line.end)}</d-text>`
|
|
445
480
|
* with the same font options; its words are already shaped, so that is
|
|
@@ -458,7 +493,7 @@ export function layoutNextLine(prepared: tree.PreparedText, cursor: number, widt
|
|
|
458
493
|
let unit = units[i]!
|
|
459
494
|
// Glued pieces stay with the unit they continue: the whole wrap unit
|
|
460
495
|
// (this piece through the last glued one) must fit for any of it to go on.
|
|
461
|
-
if (i > cursor && !unit.glue && pen + unitInk(units, i) > width) break
|
|
496
|
+
if (i > cursor && !unit.glue && pen + unitInk(units, i) > width + FIT_SLACK) break
|
|
462
497
|
pen += unit.advance
|
|
463
498
|
if (unit.ascent > ascent) ascent = unit.ascent
|
|
464
499
|
if (unit.descent > descent) descent = unit.descent
|
package/src/gpu.ts
CHANGED
|
@@ -753,6 +753,128 @@ export function writeBuffer(id: gpu.BufferId, data: ArrayBuffer | ArrayBufferVie
|
|
|
753
753
|
gpu.writeBuffer(id, toUint8(data), byteOffset)
|
|
754
754
|
}
|
|
755
755
|
|
|
756
|
+
/**
|
|
757
|
+
* A stream of fixed-size records with a GPU buffer, a CPU mirror and a
|
|
758
|
+
* dirty range: the publish model shared by every JS-stepped population
|
|
759
|
+
* (a 2d sprite layer's records, a 3d mesh's instance streams). Write the
|
|
760
|
+
* mirror, `mark` the records written, then `publish` at the flush: the
|
|
761
|
+
* dirty range goes out as one `writeBuffer` at its byte offset, or the
|
|
762
|
+
* drawn prefix whole through the write lease when the buffer carries an
|
|
763
|
+
* instance order (the engine gathers the copy into key order, and a byte
|
|
764
|
+
* range has no stable position under a permutation). Growth replaces the
|
|
765
|
+
* buffer and the mirror (never a resize) and marks what the mirror held,
|
|
766
|
+
* so the next publish fills the replacement; the old buffer is the
|
|
767
|
+
* caller's to destroy once nothing binds it. Read `buffer` and `bytes` at
|
|
768
|
+
* use time: both are replaced by growth.
|
|
769
|
+
*/
|
|
770
|
+
export type RecordStream = {
|
|
771
|
+
/** The GPU buffer the records publish to; replaced by `grow`. */
|
|
772
|
+
readonly buffer: gpu.BufferId
|
|
773
|
+
/** The mirror, `capacity * stride` bytes; replaced by `grow`. View it as
|
|
774
|
+
* the record layout wants (a Float32Array over it for an all-float
|
|
775
|
+
* record). */
|
|
776
|
+
readonly bytes: Uint8Array<ArrayBuffer>
|
|
777
|
+
/** Bytes per record. */
|
|
778
|
+
readonly stride: number
|
|
779
|
+
/** Records the mirror and the buffer hold. */
|
|
780
|
+
readonly capacity: number
|
|
781
|
+
/** Whether any record is marked since the last publish. */
|
|
782
|
+
readonly dirty: boolean
|
|
783
|
+
/** Extend the dirty range over records [first, last). */
|
|
784
|
+
mark(first: number, last: number): void
|
|
785
|
+
/** Send what is dirty and clear the mark. `whole`: the first `limit`
|
|
786
|
+
* records go out through the lease regardless of the range (the
|
|
787
|
+
* ordered form, and a replacement buffer's first fill); otherwise the
|
|
788
|
+
* dirty range, clipped to `limit` records (the live sprites, or the
|
|
789
|
+
* capacity when records are written ahead of their count). Nothing is
|
|
790
|
+
* sent for an empty range or a zero `limit`. */
|
|
791
|
+
publish(whole: boolean, limit: number): void
|
|
792
|
+
/** Replace the buffer and the mirror with room for `capacity` records,
|
|
793
|
+
* the old contents copied over and marked whole; returns the OLD buffer
|
|
794
|
+
* for the caller to destroy once its draws point at the new one. */
|
|
795
|
+
grow(capacity: number): gpu.BufferId
|
|
796
|
+
/** Free the buffer. Safe to call more than once. */
|
|
797
|
+
destroy(): void
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Creates a record stream of `capacity` records of `stride` bytes (see
|
|
802
|
+
* {@link RecordStream}): a sized, unwritten GPU buffer and a zeroed
|
|
803
|
+
* mirror. Freed with the reactive owner like a buffer (opt out with
|
|
804
|
+
* `{ autoFree: false }` and call `destroy`).
|
|
805
|
+
*/
|
|
806
|
+
export function createRecordStream(stride: number, capacity: number, opts?: CreateOptions): RecordStream {
|
|
807
|
+
if (!(Number.isInteger(stride) && stride > 0)) throw new Error("createRecordStream: stride must be a positive integer, got " + stride)
|
|
808
|
+
if (!(Number.isInteger(capacity) && capacity > 0)) throw new Error("createRecordStream: capacity must be a positive integer, got " + capacity)
|
|
809
|
+
let buffer = createBuffer(capacity * stride, { label: opts?.label, autoFree: false })
|
|
810
|
+
let bytes = new Uint8Array(capacity * stride)
|
|
811
|
+
let dirty: [number, number] | null = null
|
|
812
|
+
let destroyed = false
|
|
813
|
+
let stream: RecordStream = {
|
|
814
|
+
get buffer() {
|
|
815
|
+
return buffer
|
|
816
|
+
},
|
|
817
|
+
get bytes() {
|
|
818
|
+
return bytes
|
|
819
|
+
},
|
|
820
|
+
stride,
|
|
821
|
+
get capacity() {
|
|
822
|
+
return bytes.length / stride
|
|
823
|
+
},
|
|
824
|
+
get dirty() {
|
|
825
|
+
return dirty !== null
|
|
826
|
+
},
|
|
827
|
+
mark(first, last) {
|
|
828
|
+
let capacity = bytes.length / stride
|
|
829
|
+
if (!Number.isInteger(first) || !Number.isInteger(last) || first < 0 || last > capacity || first > last) {
|
|
830
|
+
throw new Error("RecordStream.mark: range [" + first + ", " + last + ") is outside the stream's " + capacity + " records")
|
|
831
|
+
}
|
|
832
|
+
if (first === last) return
|
|
833
|
+
if (dirty === null) dirty = [first, last]
|
|
834
|
+
else {
|
|
835
|
+
if (first < dirty[0]) dirty[0] = first
|
|
836
|
+
if (last > dirty[1]) dirty[1] = last
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
publish(whole, limit) {
|
|
840
|
+
let range = dirty
|
|
841
|
+
dirty = null
|
|
842
|
+
if (whole) {
|
|
843
|
+
let byteLength = Math.min(limit, bytes.length / stride) * stride
|
|
844
|
+
if (byteLength <= 0) return
|
|
845
|
+
let out = beginBufferWrite(buffer)
|
|
846
|
+
new Uint8Array(out.buffer, 0, byteLength).set(bytes.subarray(0, byteLength))
|
|
847
|
+
gpu.endBufferWrite(buffer, byteLength)
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
if (range === null) return
|
|
851
|
+
let lo = range[0]
|
|
852
|
+
let hi = Math.min(range[1], limit)
|
|
853
|
+
if (hi > lo) writeBuffer(buffer, bytes.subarray(lo * stride, hi * stride), lo * stride)
|
|
854
|
+
},
|
|
855
|
+
grow(next) {
|
|
856
|
+
let previous = bytes.length / stride
|
|
857
|
+
if (!(Number.isInteger(next) && next > previous)) {
|
|
858
|
+
throw new Error("RecordStream.grow: capacity must be an integer above the current " + previous + ", got " + next)
|
|
859
|
+
}
|
|
860
|
+
let old = buffer
|
|
861
|
+
buffer = createBuffer(next * stride, { label: opts?.label, autoFree: false })
|
|
862
|
+
let grown = new Uint8Array(next * stride)
|
|
863
|
+
grown.set(bytes)
|
|
864
|
+
bytes = grown
|
|
865
|
+
dirty = [0, previous]
|
|
866
|
+
return old
|
|
867
|
+
},
|
|
868
|
+
destroy() {
|
|
869
|
+
if (destroyed) return
|
|
870
|
+
destroyed = true
|
|
871
|
+
gpu.destroyBuffer(buffer)
|
|
872
|
+
},
|
|
873
|
+
}
|
|
874
|
+
if (opts?.autoFree !== false && getOwner()) onCleanup(() => stream.destroy())
|
|
875
|
+
return stream
|
|
876
|
+
}
|
|
877
|
+
|
|
756
878
|
/**
|
|
757
879
|
* Hands an ordered instance buffer's full record set to the engine - the
|
|
758
880
|
* write-once form: the engine keeps the one CPU copy and sorts,
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, warmText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, getLayoutBox, snapshotTexture, onPointerMove } from "./core"
|
|
3
|
-
export type { BoundingBox, GlobalPointerEvent, TextLine } from "./core"
|
|
2
|
+
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, warmText, setTextRendering, textRendering, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, getLayoutBox, snapshotTexture, onPointerMove } from "./core"
|
|
3
|
+
export type { BoundingBox, GlobalPointerEvent, TextLine, TextRenderingOptions, TextRenderingPolicy } from "./core"
|
|
4
4
|
export { parseColor, mixColors, withAlpha, brightness, createLinearGradient, createRadialGradient } from "./color"
|
|
5
5
|
export type { Gradient, GradientStop } from "./color"
|
|
6
6
|
export { onFrame, frameTime, onBeforeRender, onLayout, onResize, onWindowFocus, onWindowBlur, onBack, onLink, onSuspend, onQuit, exit, background, registerProtocolHandler } from "./window"
|
|
@@ -25,6 +25,7 @@ export { Logo } from "./logo"
|
|
|
25
25
|
export type { LogoProps } from "./logo"
|
|
26
26
|
export type { SvgDocument, SvgDraw } from "./svg"
|
|
27
27
|
export { createScroll } from "./scroll"
|
|
28
|
+
export { warnOnce } from "./warn"
|
|
28
29
|
export type { Scroll, ScrollAxis, ScrollBehavior, ScrollOffset, ScrollOptions, ScrollToOptions } from "./scroll"
|
|
29
30
|
export { arena } from "./arena"
|
|
30
31
|
export type { ArenaOwner } from "./arena"
|
|
@@ -93,7 +94,7 @@ export type {
|
|
|
93
94
|
Color,
|
|
94
95
|
Pct,
|
|
95
96
|
} from "./types"
|
|
96
|
-
export type { MeasureTextOptions, PreparedText, TextRunRange, TextUnit } from "flux:rendertree"
|
|
97
|
+
export type { MeasureTextOptions, PreparedText, TextRunRange, TextUnit, WarmTextEntry } from "flux:rendertree"
|
|
97
98
|
|
|
98
99
|
// A percentage value for dimensional props (e.g. transformOrigin): `pct(50)` is
|
|
99
100
|
// half the element box. Keeps percentages a first-class branded value rather
|
package/src/scroll.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createSignal } from "@solidjs/signals"
|
|
8
8
|
import { getBoundingBox } from "./core"
|
|
9
9
|
import { onLayout } from "./window"
|
|
10
|
+
import { warnOnce } from "./warn"
|
|
10
11
|
|
|
11
12
|
export type ScrollAxis = "vertical" | "horizontal" | "both"
|
|
12
13
|
|
|
@@ -75,11 +76,9 @@ export function createScroll(
|
|
|
75
76
|
|
|
76
77
|
// A scroll viewport with no explicit main-axis size resolves to 0 in flex
|
|
77
78
|
// layout and its content silently vanishes - a classic trap (maxHeight alone
|
|
78
|
-
// does not size it either). Detect it at measure time and warn once,
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
let origin = new Error().stack ?? ""
|
|
82
|
-
let warnedCollapsed = false
|
|
79
|
+
// does not size it either). Detect it at measure time and warn once, from
|
|
80
|
+
// the component that built the scroller (warnOnce captures its stack here).
|
|
81
|
+
let warnCollapsed = warnOnce()
|
|
83
82
|
|
|
84
83
|
// Last measured overflow, refreshed each layout. scrollBy/scrollTo clamp
|
|
85
84
|
// against these between layouts; onLayout re-clamps once new sizes are known.
|
|
@@ -110,17 +109,14 @@ export function createScroll(
|
|
|
110
109
|
let vb = getBoundingBox(vp)
|
|
111
110
|
let cb = getBoundingBox(ct)
|
|
112
111
|
if (!vb || !cb) return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`Give it an explicit ${axisName} or flex; maxHeight/maxWidth alone does not size it.\n${origin}`,
|
|
122
|
-
)
|
|
123
|
-
}
|
|
112
|
+
let zeroY = canY && vb.height === 0 && cb.height > 0
|
|
113
|
+
let zeroX = canX && vb.width === 0 && cb.width > 0
|
|
114
|
+
if (zeroY || zeroX) {
|
|
115
|
+
let axisName = zeroY ? "height" : "width"
|
|
116
|
+
warnCollapsed(
|
|
117
|
+
`Scroll container resolved to ${axisName} 0, so its content is invisible. ` +
|
|
118
|
+
`Give it an explicit ${axisName} or flex; maxHeight/maxWidth alone does not size it.`,
|
|
119
|
+
)
|
|
124
120
|
}
|
|
125
121
|
maxX = Math.max(0, cb.width - vb.width)
|
|
126
122
|
maxY = Math.max(0, cb.height - vb.height)
|
package/src/text-input.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// are policy and belong to the component (the "skin") that composes these.
|
|
6
6
|
|
|
7
7
|
import { createMemo, createSignal, flush, untrack } from "@solidjs/signals"
|
|
8
|
-
import { getLayoutBox, layoutNextLine, measureText, prepareText, unitInk } from "./core"
|
|
8
|
+
import { FIT_SLACK, getLayoutBox, layoutNextLine, measureText, prepareText, unitInk } from "./core"
|
|
9
9
|
import type { MeasureTextOptions, PreparedText, TextRunRange, TextUnit } from "flux:rendertree"
|
|
10
10
|
import { onLayout } from "./window"
|
|
11
11
|
|
|
@@ -542,12 +542,12 @@ export function createTextEditorLayout(
|
|
|
542
542
|
// through as is.
|
|
543
543
|
function splitWide(prepared: PreparedText, width: number): PreparedText {
|
|
544
544
|
let all = prepared.units
|
|
545
|
-
if (!all.some((u, i) => !u.glue && unitInk(all, i) > width)) return prepared
|
|
545
|
+
if (!all.some((u, i) => !u.glue && unitInk(all, i) > width + FIT_SLACK)) return prepared
|
|
546
546
|
let wide = false
|
|
547
547
|
let units: TextUnit[] = []
|
|
548
548
|
for (let u = 0; u < all.length; u++) {
|
|
549
549
|
let unit = all[u]!
|
|
550
|
-
if (!unit.glue) wide = unitInk(all, u) > width
|
|
550
|
+
if (!unit.glue) wide = unitInk(all, u) > width + FIT_SLACK
|
|
551
551
|
let stops = unit.carets
|
|
552
552
|
if (!wide || !stops || stops.length <= 2) {
|
|
553
553
|
units.push(unit)
|
package/src/types.d.ts
CHANGED
|
@@ -103,6 +103,21 @@ export interface LayoutProps extends FlexboxProps, GridProps {
|
|
|
103
103
|
* resolves against whatever relative element is above all of them.
|
|
104
104
|
*/
|
|
105
105
|
position?: "relative" | "absolute"
|
|
106
|
+
/**
|
|
107
|
+
* CSS size containment: the element sizes as if it had no content. A
|
|
108
|
+
* texture or text measures as zero on every axis layout does not hand it,
|
|
109
|
+
* and a view's children add nothing to its box; they lay out inside
|
|
110
|
+
* whatever box it gets. The one CSS `contain` value with a meaning here.
|
|
111
|
+
* The element still needs a size from its own props or its parent (a
|
|
112
|
+
* `flexGrow` in a sized column), or it comes out empty.
|
|
113
|
+
*
|
|
114
|
+
* What takes the place of the web's `minHeight={0}` escape from the flex
|
|
115
|
+
* automatic minimum, declared on the element whose content would
|
|
116
|
+
* otherwise pin its ancestors: a fill-mode `<Scene>` or `<SpriteLayer>`
|
|
117
|
+
* leaf sets it, so the target that follows the leaf's box never feeds its
|
|
118
|
+
* pixel size back into layout.
|
|
119
|
+
*/
|
|
120
|
+
contain?: "size"
|
|
106
121
|
|
|
107
122
|
top?: Dimension
|
|
108
123
|
right?: Dimension
|
package/src/warn.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// A development warning for a misuse that shows later than the code that
|
|
2
|
+
// caused it: a layout that comes out empty, an event that never arrives. The
|
|
3
|
+
// site of the misuse is the component that built the thing, not the layout
|
|
4
|
+
// pass that notices, so the warning carries that component's stack.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A once-only dev warning bound to the place it is created: `let warn =
|
|
8
|
+
* warnOnce()` where a component builds something whose misuse surfaces
|
|
9
|
+
* later (a scroller, a fill-mode scene), `warn(message)` from the layout
|
|
10
|
+
* or event read that notices. The first call reports, with the stack of the
|
|
11
|
+
* creation site under the message (the dev server remaps the frames to
|
|
12
|
+
* .tsx); later calls are silent.
|
|
13
|
+
*/
|
|
14
|
+
export function warnOnce(): (message: string) => void {
|
|
15
|
+
let origin = new Error().stack ?? ""
|
|
16
|
+
let warned = false
|
|
17
|
+
return message => {
|
|
18
|
+
if (warned) return
|
|
19
|
+
warned = true
|
|
20
|
+
console.warn(`${message}\n${origin}`)
|
|
21
|
+
}
|
|
22
|
+
}
|