@solidrt/core 0.0.55 → 0.0.57
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 +150 -20
- package/agents/painting.md +29 -12
- package/agents/performance.md +14 -3
- package/docs/reference/detached.md +8 -5
- package/docs/reference/drawing.md +1 -1
- package/docs/reference/elements.md +3 -1
- package/docs/reference/gpu.md +12 -1
- package/docs/reference/layout.md +28 -0
- package/examples/README.md +3 -0
- package/examples/detached-positioning.tsx +3 -1
- package/examples/gamepad.tsx +94 -0
- package/examples/view-design-size.tsx +5 -0
- package/package.json +7 -5
- package/src/core.ts +15 -2
- package/src/environment.ts +87 -69
- package/src/gamepad.ts +10 -8
- package/src/gpu.ts +122 -18
- package/src/index.ts +14 -2
- package/src/input-axes.ts +180 -0
- package/src/input-gamepad-device.ts +161 -0
- package/src/input-gamepad.ts +24 -0
- package/src/input-keyboard.ts +156 -0
- package/src/input-map.ts +467 -0
- package/src/input-pointer.ts +213 -0
- package/src/input.ts +12 -0
- package/src/pan.ts +20 -5
- package/src/runtime-modules.d.ts +7 -7
- package/src/text-input.ts +43 -8
- package/src/transform.ts +52 -23
- package/src/types.d.ts +84 -16
- package/src/window.ts +54 -38
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
|
package/src/environment.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSignal, runWithOwner } from "@solidjs/signals"
|
|
2
2
|
import { on } from "srt:events"
|
|
3
3
|
import { onPointerMove } from "./core"
|
|
4
4
|
import { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } from "./window"
|
|
@@ -12,13 +12,17 @@ import { windowSize, safeArea, displayScale, windowFocused, keyboardHeight } fro
|
|
|
12
12
|
// capability derived from them can appear mid-session (e.g. the first mouse
|
|
13
13
|
// move) but never flickers away.
|
|
14
14
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
15
|
+
// Every fact below is lazily created the first time its ensure* function
|
|
16
|
+
// runs, which is usually inside a component or a computation (a memo's first
|
|
17
|
+
// read of env.inputDevices, a HUD effect's first read of pointerLocked()).
|
|
18
|
+
// Created there, the fact would belong to that reader: sticky events replay
|
|
19
|
+
// their cached value synchronously on subscribe (srt:events' on()), so the
|
|
20
|
+
// on() call writes the signal it just created inside the reader's owned
|
|
21
|
+
// scope - a render-time write, which the guard rejects - and a root or a
|
|
22
|
+
// subscription created there is disposed with the reader. The facts are
|
|
23
|
+
// app-lifetime, so each ensure* creates under no owner (runWithOwner(null)):
|
|
24
|
+
// the replay lands unowned, nothing is filed under the reader, and no
|
|
25
|
+
// `ownedWrite` opt-out is needed.
|
|
22
26
|
|
|
23
27
|
/** Connected input device classes, as reported by the runtime. */
|
|
24
28
|
export interface InputDevices {
|
|
@@ -39,58 +43,68 @@ let devicesAccessor: (() => InputDevices | undefined) | undefined
|
|
|
39
43
|
|
|
40
44
|
function ensureDevicesState() {
|
|
41
45
|
if (devicesAccessor) return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
runWithOwner(null, () => {
|
|
47
|
+
let [devices, setDevices] = createSignal<InputDevices | undefined>(undefined)
|
|
48
|
+
// Sticky: the current state replays on subscribe, so the first read already
|
|
49
|
+
// sees it on runtimes that report devices.
|
|
50
|
+
on("inputDevices", (d: InputDevices) => {
|
|
51
|
+
setDevices({ keyboard: !!d.keyboard, mouse: !!d.mouse, touch: !!d.touch, screenKeyboard: !!d.screenKeyboard })
|
|
52
|
+
})
|
|
53
|
+
devicesAccessor = devices
|
|
47
54
|
})
|
|
48
|
-
devicesAccessor = devices
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
let systemThemeAccessor: (() => SystemTheme) | undefined
|
|
52
58
|
|
|
53
59
|
function ensureSystemThemeState() {
|
|
54
60
|
if (systemThemeAccessor) return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
runWithOwner(null, () => {
|
|
62
|
+
let [theme, setTheme] = createSignal<SystemTheme>("unknown")
|
|
63
|
+
on("systemTheme", (e: { theme?: SystemTheme }) => setTheme(e.theme ?? "unknown"))
|
|
64
|
+
systemThemeAccessor = theme
|
|
65
|
+
})
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
let visibilityAccessor: (() => Visibility) | undefined
|
|
61
69
|
|
|
62
70
|
function ensureVisibilityState() {
|
|
63
71
|
if (visibilityAccessor) return
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
runWithOwner(null, () => {
|
|
73
|
+
let [visibility, setVisibility] = createSignal<Visibility>("visible")
|
|
74
|
+
// Sticky. The runtime may report the same state through several platform
|
|
75
|
+
// paths; the signal's equality check turns repeats into no-ops for
|
|
76
|
+
// reactive consumers.
|
|
77
|
+
on("visibility", (e: { state?: Visibility }) => setVisibility(e.state === "hidden" ? "hidden" : "visible"))
|
|
78
|
+
visibilityAccessor = visibility
|
|
79
|
+
})
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
let orientationAccessor: (() => Orientation) | undefined
|
|
73
83
|
|
|
74
84
|
function ensureOrientationState() {
|
|
75
85
|
if (orientationAccessor) return
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
runWithOwner(null, () => {
|
|
87
|
+
let [orientation, setOrientation] = createSignal<Orientation>("unknown")
|
|
88
|
+
on("displayOrientation", (e: { orientation?: Orientation }) => {
|
|
89
|
+
setOrientation(e.orientation ?? "unknown")
|
|
90
|
+
})
|
|
91
|
+
orientationAccessor = orientation
|
|
79
92
|
})
|
|
80
|
-
orientationAccessor = orientation
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
let textScaleAccessor: (() => number) | undefined
|
|
84
96
|
|
|
85
97
|
function ensureTextScaleState() {
|
|
86
98
|
if (textScaleAccessor) return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
runWithOwner(null, () => {
|
|
100
|
+
let [scale, setScale] = createSignal(1)
|
|
101
|
+
// Sticky, like systemTheme. Guard nonsense values: a runtime bug reporting
|
|
102
|
+
// 0 or a negative would otherwise collapse all text.
|
|
103
|
+
on("textScale", (e: { scale?: number }) => {
|
|
104
|
+
setScale(typeof e.scale === "number" && e.scale > 0 ? e.scale : 1)
|
|
105
|
+
})
|
|
106
|
+
textScaleAccessor = scale
|
|
92
107
|
})
|
|
93
|
-
textScaleAccessor = scale
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
let mouseSeenAccessor: (() => boolean) | undefined
|
|
@@ -98,50 +112,54 @@ let touchSeenAccessor: (() => boolean) | undefined
|
|
|
98
112
|
|
|
99
113
|
function ensurePointerState() {
|
|
100
114
|
if (mouseSeenAccessor) return
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
sawMouse
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
sawTouch
|
|
117
|
-
|
|
115
|
+
runWithOwner(null, () => {
|
|
116
|
+
let [mouse, setMouse] = createSignal(false)
|
|
117
|
+
let [touch, setTouch] = createSignal(false)
|
|
118
|
+
let sawMouse = false
|
|
119
|
+
let sawTouch = false
|
|
120
|
+
let unsubs: (() => void)[] = []
|
|
121
|
+
let unsubMove: () => void = null!
|
|
122
|
+
let note = (e: { pointerType?: string }) => {
|
|
123
|
+
if (e.pointerType === "mouse" && !sawMouse) {
|
|
124
|
+
sawMouse = true
|
|
125
|
+
setMouse(true)
|
|
126
|
+
// Moves have nothing left to teach: touch is learned from downs (a
|
|
127
|
+
// touch never moves without one), so drop the move subscription and
|
|
128
|
+
// with it the ambient interest bit that forces move deliveries.
|
|
129
|
+
unsubMove()
|
|
130
|
+
} else if (e.pointerType === "touch" && !sawTouch) {
|
|
131
|
+
sawTouch = true
|
|
132
|
+
setTouch(true)
|
|
133
|
+
}
|
|
134
|
+
// Both types observed: nothing left to learn, stop listening.
|
|
135
|
+
if (sawMouse && sawTouch) for (let u of unsubs) u()
|
|
118
136
|
}
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
mouseSeenAccessor = mouse
|
|
129
|
-
touchSeenAccessor = touch
|
|
137
|
+
// Downs always deliver, so the raw bus tap suffices; moves are gated when
|
|
138
|
+
// nobody listens, so they go through onPointerMove, whose subscription
|
|
139
|
+
// keeps them flowing. Unowned here, so it registers no cleanup: the
|
|
140
|
+
// returned unsubscribe is the probe's only handle, as intended.
|
|
141
|
+
unsubMove = onPointerMove(note)
|
|
142
|
+
unsubs.push(unsubMove, on("pointerDown", note))
|
|
143
|
+
mouseSeenAccessor = mouse
|
|
144
|
+
touchSeenAccessor = touch
|
|
145
|
+
})
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
let keyboardSeenAccessor: (() => boolean) | undefined
|
|
133
149
|
|
|
134
150
|
function ensureKeyboardState() {
|
|
135
151
|
if (keyboardSeenAccessor) return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
runWithOwner(null, () => {
|
|
153
|
+
let [keyboard, setKeyboard] = createSignal(false)
|
|
154
|
+
// Soft keyboards also deliver some keydowns (Backspace, Return), so this can
|
|
155
|
+
// read true on a touch-only device once the user types in a field. Only a
|
|
156
|
+
// fallback: capabilities prefer runtime-reported device presence.
|
|
157
|
+
let unsub = on("keydown", () => {
|
|
158
|
+
setKeyboard(true)
|
|
159
|
+
unsub()
|
|
160
|
+
})
|
|
161
|
+
keyboardSeenAccessor = keyboard
|
|
143
162
|
})
|
|
144
|
-
keyboardSeenAccessor = keyboard
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
/**
|
package/src/gamepad.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSignal } from "@solidjs/signals"
|
|
1
|
+
import { createSignal, runWithOwner } from "@solidjs/signals"
|
|
2
2
|
import { on } from "srt:events"
|
|
3
3
|
|
|
4
4
|
// Gamepad State: a reactive mirror of the runtime's sticky "gamepads" event.
|
|
@@ -18,7 +18,7 @@ import { on } from "srt:events"
|
|
|
18
18
|
*
|
|
19
19
|
* The snapshot is a faithful report. Note that pressing "back" (select) on a
|
|
20
20
|
* mapped pad ALSO emits the `back` event (see onBack) - it is the pad-side
|
|
21
|
-
* sibling of Android's system back, the runtime's exit-to-
|
|
21
|
+
* sibling of Android's system back, the runtime's exit-to-player gesture.
|
|
22
22
|
* Apps that bind "back" for their own controls should preventDefault that
|
|
23
23
|
* event.
|
|
24
24
|
*/
|
|
@@ -52,11 +52,13 @@ let gamepadsAccessor: (() => (GamepadState | null)[]) | undefined
|
|
|
52
52
|
*/
|
|
53
53
|
export function gamepads(): (GamepadState | null)[] {
|
|
54
54
|
if (!gamepadsAccessor) {
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
// Under no owner: the sticky event replays synchronously inside on(),
|
|
56
|
+
// and the first read is usually a tracked scope (see environment.ts).
|
|
57
|
+
runWithOwner(null, () => {
|
|
58
|
+
let [pads, setPads] = createSignal<(GamepadState | null)[]>([])
|
|
59
|
+
on("gamepads", (e: { pads?: (GamepadState | null)[] }) => setPads(e.pads ?? []))
|
|
60
|
+
gamepadsAccessor = pads
|
|
61
|
+
})
|
|
60
62
|
}
|
|
61
|
-
return gamepadsAccessor()
|
|
63
|
+
return gamepadsAccessor!()
|
|
62
64
|
}
|
package/src/gpu.ts
CHANGED
|
@@ -51,7 +51,11 @@
|
|
|
51
51
|
// and target holds 8-bit RGBA UNORM exactly as written; nothing converts to
|
|
52
52
|
// or from linear light. `filter: "linear"` averages and the `blend` modes
|
|
53
53
|
// accumulate non-linear values - the usual approximation, stated so
|
|
54
|
-
// shaders written today stay correct
|
|
54
|
+
// shaders written today stay correct. The one exception is opt-in: an
|
|
55
|
+
// "rgba8-srgb" texture decodes to linear light when a shader samples it
|
|
56
|
+
// (and "rgba16f" holds linear HDR values), which is how a linear-space
|
|
57
|
+
// renderer such as `@solidrt/3d` reads its inputs before encoding its
|
|
58
|
+
// output back into this contract.
|
|
55
59
|
|
|
56
60
|
import { createEffect, createSignal, getOwner, onCleanup, untrack } from "@solidjs/signals"
|
|
57
61
|
import * as gpu from "flux:gpu"
|
|
@@ -73,9 +77,16 @@ export type SamplerOptions = { filter?: gpu.FilterMode; wrap?: gpu.WrapMode; mip
|
|
|
73
77
|
export type { FilterMode, WrapMode, TextureBinding, TextureBindings } from "flux:gpu"
|
|
74
78
|
|
|
75
79
|
// Pixel format option for the pixel-upload creates (createTexture,
|
|
76
|
-
// createMutableTexture), fixed for the id's lifetime like
|
|
77
|
-
// "rgba8" (default)
|
|
78
|
-
//
|
|
80
|
+
// createCubeTexture, createMutableTexture), fixed for the id's lifetime like
|
|
81
|
+
// the sampler state. "rgba8" (default), "rgba8-srgb" (decodes to linear
|
|
82
|
+
// light on sample), "r8", the float data-texture formats "r32f"/"rgba32f"
|
|
83
|
+
// (Float32Array payload, nearest/texelFetch sampling only, no readback) and
|
|
84
|
+
// the HDR image format "rgba16f" (Float32Array payload packed to half float,
|
|
85
|
+
// filterable) - see TextureFormat in flux:gpu for each format's contract.
|
|
86
|
+
// Draw targets (createDrawTarget, createCubeDrawTarget) take the renderable
|
|
87
|
+
// subset: "rgba8", "rgba8-srgb", "rgba16f".
|
|
88
|
+
// "etc2-rgba8" (compressed) is a reserved future value of the same
|
|
89
|
+
// vocabulary.
|
|
79
90
|
export type TextureFormatOptions = { format?: gpu.TextureFormat }
|
|
80
91
|
export type { TextureFormat } from "flux:gpu"
|
|
81
92
|
|
|
@@ -136,7 +147,7 @@ export {
|
|
|
136
147
|
// GPU-side (exact, same size): seed a loadOp "load" accumulator, snapshot a
|
|
137
148
|
// ping-pong buffer, reset state to a known image.
|
|
138
149
|
export { copyTexture, destroyBuffer, renderTarget, setDraw } from "flux:gpu"
|
|
139
|
-
export type { BlendMode, BufferUpdate, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, InstanceAttribute, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
150
|
+
export type { BlendMode, BufferUpdate, CullMode, DrawRange, IndexBinding, IndexFormat, IndexRange, InstanceAttribute, InstanceOrder, OrderUpdate, ShaderParams, Topology, VertexAttribute } from "flux:gpu"
|
|
140
151
|
|
|
141
152
|
// The draw-list verbs, re-exported raw: entries live and die with their draw
|
|
142
153
|
// target (see createDrawTarget below), so there is no per-entry lifetime to
|
|
@@ -150,7 +161,10 @@ export type { BlendMode, BufferUpdate, CullMode, DrawRange, IndexBinding, IndexF
|
|
|
150
161
|
// params every entry reads. setDrawOrder replaces the whole
|
|
151
162
|
// list order with a full permutation of the live ids - the sorting verb
|
|
152
163
|
// (opaque front-to-back, transparent back-to-front, re-issued when the
|
|
153
|
-
// camera moves).
|
|
164
|
+
// camera moves). That orders ENTRIES; ordering the instance RECORDS within
|
|
165
|
+
// one entry is the instanceOrder creation option (see InstanceOrder), whose
|
|
166
|
+
// projected-key direction updates ride setDraw/setDrawRange as
|
|
167
|
+
// orderDirection.
|
|
154
168
|
export { addDraw, removeDraw, setDrawBuffers, setDrawOrder, setDrawParams, setDrawRange, setDrawTextures } from "flux:gpu"
|
|
155
169
|
|
|
156
170
|
// The device ceilings (max texture/target size, sampler inputs per pass,
|
|
@@ -217,8 +231,9 @@ export { captureSnapshot, readTexture } from "flux:gpu"
|
|
|
217
231
|
/**
|
|
218
232
|
* Uploads raw pixels to an immutable GPU texture and returns its id (use it
|
|
219
233
|
* as `<texture src={id} />`). `data` must be exactly `width * height` pixels
|
|
220
|
-
* at the declared format's size
|
|
221
|
-
* for "r8"
|
|
234
|
+
* at the declared format's size, in the view type matching the format
|
|
235
|
+
* (Uint8Array for "rgba8"/"rgba8-srgb"/"r8", Float32Array for "r32f"/
|
|
236
|
+
* "rgba32f"/"rgba16f"); a mismatch throws. RGBA data is uploaded verbatim and composited
|
|
222
237
|
* as premultiplied alpha, like every texture: `decodeImage` and the readback
|
|
223
238
|
* calls already deliver that, and hand-built pixels must too (`rgb * a`). For pixels you intend to mutate and
|
|
224
239
|
* re-upload, use `createMutableTexture` instead. When called inside a
|
|
@@ -230,7 +245,7 @@ export { captureSnapshot, readTexture } from "flux:gpu"
|
|
|
230
245
|
* scope.
|
|
231
246
|
*/
|
|
232
247
|
export function createTexture(
|
|
233
|
-
data: Uint8Array,
|
|
248
|
+
data: Uint8Array | Float32Array,
|
|
234
249
|
width: number,
|
|
235
250
|
height: number,
|
|
236
251
|
opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
|
|
@@ -240,18 +255,52 @@ export function createTexture(
|
|
|
240
255
|
return id
|
|
241
256
|
}
|
|
242
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Uploads a cube map: six square faces of `size * size` pixels in GL order
|
|
260
|
+
* (+X, -X, +Y, -Y, +Z, -Z), each in the view type matching the declared
|
|
261
|
+
* format like `createTexture`, and returns its id. A cube map is sampled by
|
|
262
|
+
* DIRECTION from a `uniform samplerCube` in a shader (`texture(uEnv, dir)`;
|
|
263
|
+
* `textureLod` for an explicit mip level, `mipmap: true` builds the chain
|
|
264
|
+
* from the faces) - the skybox, reflection and environment-lighting
|
|
265
|
+
* primitive. `faces` may instead be an explicit mip chain - an array of
|
|
266
|
+
* six-face arrays, level 0 first, halving down to the 1x1 level (the full
|
|
267
|
+
* chain) - uploaded as given: prefiltered environment levels, with no
|
|
268
|
+
* generated chain and so no renderability requirement on the format (an
|
|
269
|
+
* "rgba16f" chain works on every device). Explicit levels imply `mipmap:
|
|
270
|
+
* true`. Sampler-only: the id binds through `textures` like any other,
|
|
271
|
+
* but `<texture src>` cannot display it and `readTexture`, `copyTexture`,
|
|
272
|
+
* `uploadTexture` and `resizeTexture` throw (it is create-once). `wrap` has
|
|
273
|
+
* no effect - cube filtering is seamless across faces. Binding a cube map
|
|
274
|
+
* to a `sampler2D`, or a 2D texture to a `samplerCube`, throws at the bind.
|
|
275
|
+
* Note GL's cube convention: each face is what a lookup in that direction
|
|
276
|
+
* returns, the cube as seen from OUTSIDE, so a face set authored the Three
|
|
277
|
+
* way (each face as seen from inside) reads mirrored on x unless each
|
|
278
|
+
* image is mirrored at load. Freed like `createTexture` (auto-free under a
|
|
279
|
+
* reactive owner, `{ autoFree: false }` or `destroyTexture` otherwise).
|
|
280
|
+
*/
|
|
281
|
+
export function createCubeTexture(
|
|
282
|
+
faces: (Uint8Array | Float32Array)[] | (Uint8Array | Float32Array)[][],
|
|
283
|
+
size: number,
|
|
284
|
+
opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
|
|
285
|
+
): gpu.TextureId {
|
|
286
|
+
let id = gpu.createCubeTexture(faces, size, opts)
|
|
287
|
+
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
288
|
+
return id
|
|
289
|
+
}
|
|
290
|
+
|
|
243
291
|
/**
|
|
244
292
|
* Creates a GPU texture you intend to update over time: seed it with `data`,
|
|
245
293
|
* then call `uploadTexture(id, data)` (from flux:gpu) to push new pixels.
|
|
246
294
|
* `data` must hold at least `width * height` pixels at the declared format's
|
|
247
|
-
* size
|
|
248
|
-
*
|
|
295
|
+
* size, in the view type matching the format (Uint8Array for "rgba8"/
|
|
296
|
+
* "rgba8-srgb"/"r8", Float32Array for "r32f"/"rgba32f"/"rgba16f"); it may
|
|
297
|
+
* hold several frames. Like `createTexture`, the texture is freed automatically
|
|
249
298
|
* when the reactive owner is disposed (opt out with `{ autoFree: false }`);
|
|
250
299
|
* created outside a reactive scope you must call `destroyTexture` (from
|
|
251
300
|
* flux:gpu) yourself.
|
|
252
301
|
*/
|
|
253
302
|
export function createMutableTexture(
|
|
254
|
-
data: Uint8Array,
|
|
303
|
+
data: Uint8Array | Float32Array,
|
|
255
304
|
width: number,
|
|
256
305
|
height: number,
|
|
257
306
|
opts?: CreateOptions & SamplerOptions & TextureFormatOptions,
|
|
@@ -360,6 +409,8 @@ export function createShaderTarget(
|
|
|
360
409
|
/** One buffer per instance slot of the pipeline (index = the
|
|
361
410
|
* attributes' `slot`); pass this OR `instanceBuffer`, not both. */
|
|
362
411
|
instanceBuffers?: gpu.BufferId[]
|
|
412
|
+
/** Draw the instance records in key order (see InstanceOrder). */
|
|
413
|
+
instanceOrder?: gpu.InstanceOrder
|
|
363
414
|
clearColor?: [number, number, number, number]
|
|
364
415
|
render?: "auto" | "manual"
|
|
365
416
|
loadOp?: "clear" | "load"
|
|
@@ -400,6 +451,13 @@ export function createShaderTarget(
|
|
|
400
451
|
* driven later with `setTargetTextures`, same precedence and coverage
|
|
401
452
|
* rules.
|
|
402
453
|
*
|
|
454
|
+
* `format` is the color storage: "rgba8" (default), "rgba8-srgb" (encodes
|
|
455
|
+
* what a pass writes, decodes on sample) or "rgba16f" (half float, keeps
|
|
456
|
+
* the range - an HDR view or bake; where `limits.halfFloatRenderable`,
|
|
457
|
+
* else it throws). A target of either non-default format is SAMPLER-ONLY:
|
|
458
|
+
* a pass source, never displayed by a `<texture>` leaf, read back or
|
|
459
|
+
* copied (render it through a pass into an rgba8 target for those).
|
|
460
|
+
*
|
|
403
461
|
* `into` makes a sub-target: a draw target rendering into the rectangle at
|
|
404
462
|
* `x`/`y` (top-left origin) of draw target `into`'s storage, so N views or
|
|
405
463
|
* N shadow maps share one texture and ONE pass; the id is a draw target to
|
|
@@ -421,6 +479,7 @@ export function createDrawTarget(
|
|
|
421
479
|
params?: gpu.ShaderParams | null,
|
|
422
480
|
opts?: {
|
|
423
481
|
depth?: boolean | "texture"
|
|
482
|
+
format?: "rgba8" | "rgba8-srgb" | "rgba16f"
|
|
424
483
|
textures?: gpu.TextureBindings
|
|
425
484
|
clearColor?: [number, number, number, number]
|
|
426
485
|
render?: "auto" | "manual"
|
|
@@ -437,6 +496,41 @@ export function createDrawTarget(
|
|
|
437
496
|
return id
|
|
438
497
|
}
|
|
439
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Creates a cube draw target: a draw target whose output is a `size` x
|
|
501
|
+
* `size` cube map, rendered one face at a time with `renderTarget(id,
|
|
502
|
+
* face)` (0 +X, 1 -X, 2 +Y, 3 -Y, 4 +Z, 5 -Z) - the reflection-probe
|
|
503
|
+
* primitive (`@solidrt/3d`'s `scene.createReflectionProbe` drives it).
|
|
504
|
+
* Manual by contract, one depth renderbuffer for the six faces (`depth:
|
|
505
|
+
* true`), no samples or tiles; `format` rgba8, rgba8-srgb (encodes on
|
|
506
|
+
* write, decodes on sample) or rgba16f (half float, the HDR probe format,
|
|
507
|
+
* where `limits.halfFloatRenderable`); `mipmap: true` allocates the chain and
|
|
508
|
+
* `renderTarget(id, face, level)` renders one level of it (a face render
|
|
509
|
+
* without a level regenerates the chain instead) - the prefiltered
|
|
510
|
+
* environment path. The id binds to a `samplerCube`
|
|
511
|
+
* and cannot be displayed, read back, copied or resized. Each face pass
|
|
512
|
+
* inverts the front-face rule (a GL cube face is the x mirror of a 2D
|
|
513
|
+
* target's image), so render the faces through an x-mirrored projection;
|
|
514
|
+
* see flux:gpu's createCubeDrawTarget. Freed like every target.
|
|
515
|
+
*/
|
|
516
|
+
export function createCubeDrawTarget(
|
|
517
|
+
size: number,
|
|
518
|
+
params?: gpu.ShaderParams | null,
|
|
519
|
+
opts?: {
|
|
520
|
+
depth?: boolean
|
|
521
|
+
format?: "rgba8" | "rgba8-srgb" | "rgba16f"
|
|
522
|
+
textures?: gpu.TextureBindings
|
|
523
|
+
clearColor?: [number, number, number, number]
|
|
524
|
+
render?: "manual"
|
|
525
|
+
loadOp?: "clear" | "load"
|
|
526
|
+
} & CreateOptions &
|
|
527
|
+
SamplerOptions,
|
|
528
|
+
): gpu.TextureId {
|
|
529
|
+
let id = gpu.createCubeDrawTarget(size, params, opts)
|
|
530
|
+
if (opts?.autoFree !== false && getOwner()) onCleanup(() => gpu.destroyTexture(id))
|
|
531
|
+
return id
|
|
532
|
+
}
|
|
533
|
+
|
|
440
534
|
/** The reactive shader description `createShaderTextureMemo` builds from.
|
|
441
535
|
* Sampling (`filter`/`wrap`/`mipmap`) is creation-time state, so changing it rebuilds
|
|
442
536
|
* at a fresh id, like a fragment-source or sampler-binding change. */
|
|
@@ -449,14 +543,22 @@ export type ShaderSpec = {
|
|
|
449
543
|
} & SamplerOptions
|
|
450
544
|
|
|
451
545
|
// Shallow name->value equality for params/textures records; treats undefined
|
|
452
|
-
// as the empty record. A param value may be a number or a flat number
|
|
453
|
-
// (typed uniforms
|
|
454
|
-
// an `{ id, filter?, wrap? }`
|
|
455
|
-
|
|
546
|
+
// as the empty record. A param value may be a number or a flat number list
|
|
547
|
+
// (typed uniforms: a number[] or a Float32Array/Float64Array), so lists
|
|
548
|
+
// compare elementwise; a texture binding may be an `{ id, filter?, wrap? }`
|
|
549
|
+
// override, compared field by field.
|
|
550
|
+
type RecordValue = number | number[] | Float32Array | Float64Array | gpu.TextureBinding
|
|
551
|
+
function isNumberList(v: RecordValue | undefined): v is number[] | Float32Array | Float64Array {
|
|
552
|
+
return Array.isArray(v) || v instanceof Float32Array || v instanceof Float64Array
|
|
553
|
+
}
|
|
456
554
|
function sameValue(a: RecordValue | undefined, b: RecordValue | undefined): boolean {
|
|
457
555
|
if (a === b) return true
|
|
458
|
-
if (
|
|
459
|
-
|
|
556
|
+
if (isNumberList(a) && isNumberList(b)) {
|
|
557
|
+
if (a.length !== b.length) return false
|
|
558
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
|
|
559
|
+
return true
|
|
560
|
+
}
|
|
561
|
+
if (typeof a === "object" && typeof b === "object" && !isNumberList(a) && !isNumberList(b)) {
|
|
460
562
|
return a.id === b.id && a.filter === b.filter && a.wrap === b.wrap
|
|
461
563
|
}
|
|
462
564
|
return false
|
|
@@ -606,6 +708,8 @@ export function createPipelineTexture(
|
|
|
606
708
|
/** One buffer per instance slot (index = the attributes' `slot`);
|
|
607
709
|
* pass this OR `instanceBuffer`, not both. */
|
|
608
710
|
instanceBuffers?: gpu.BufferId[]
|
|
711
|
+
/** Draw the instance records in key order (see InstanceOrder). */
|
|
712
|
+
instanceOrder?: gpu.InstanceOrder
|
|
609
713
|
topology?: gpu.Topology
|
|
610
714
|
depth?: boolean
|
|
611
715
|
depthWrite?: boolean
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from "./renderer"
|
|
2
|
-
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, layoutNextLine, unitInk, getBoundingBox, getBoundingBoxViewport, snapshotTexture, onPointerMove } from "./core"
|
|
2
|
+
export { setFocus, focusedNode, startTextInput, textInputActive, getFocusables, measureText, prepareText, 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"
|
|
@@ -12,7 +12,7 @@ export { gamepads } from "./gamepad"
|
|
|
12
12
|
export type { GamepadState } from "./gamepad"
|
|
13
13
|
export { capabilities } from "./capabilities"
|
|
14
14
|
export type { Capabilities, WindowSizeClass } from "./capabilities"
|
|
15
|
-
export { createTexture } from "./gpu"
|
|
15
|
+
export { createCubeTexture, createTexture } from "./gpu"
|
|
16
16
|
export type { TextureId } from "./gpu"
|
|
17
17
|
export { createImage, decodeImage, encodeImage } from "./image"
|
|
18
18
|
export type { DecodedImage, ImageSource } from "./image"
|
|
@@ -28,6 +28,16 @@ export { createPan } from "./pan"
|
|
|
28
28
|
export type { PanAxis, PanOptions } from "./pan"
|
|
29
29
|
export { createTransform } from "./transform"
|
|
30
30
|
export type { TransformDelta, TransformOptions } from "./transform"
|
|
31
|
+
export { createInputMap, invert, scale } from "./input-map"
|
|
32
|
+
export type { ActionKind, ActionValue, ActionsDecl, Binding, DeltaSink, GestureListener, InputMap, InputSource } from "./input-map"
|
|
33
|
+
export { createAxes } from "./input-axes"
|
|
34
|
+
export type { Axes, AxesDecl, AxesHooks, AxisKind, AxisValue, Vec2 } from "./input-axes"
|
|
35
|
+
export { keyboard } from "./input-keyboard"
|
|
36
|
+
export type { KeyboardDevice, KeyboardVec2Keys } from "./input-keyboard"
|
|
37
|
+
export { gamepad } from "./input-gamepad"
|
|
38
|
+
export type { GamepadDevice } from "./input-gamepad"
|
|
39
|
+
export { createPointerFeed } from "./input-pointer"
|
|
40
|
+
export type { PointerFeed, PointerFeedOptions } from "./input-pointer"
|
|
31
41
|
export type {
|
|
32
42
|
Element,
|
|
33
43
|
LayoutProps,
|
|
@@ -91,6 +101,8 @@ export {
|
|
|
91
101
|
createRoot,
|
|
92
102
|
createStore,
|
|
93
103
|
reconcile,
|
|
104
|
+
merge,
|
|
105
|
+
omit,
|
|
94
106
|
mapArray,
|
|
95
107
|
repeat,
|
|
96
108
|
untrack,
|