@solidrt/3d 0.0.50 → 0.0.52

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.
@@ -0,0 +1,95 @@
1
+ // Sprites: camera-facing quads turned in the vertex stage, so the camera
2
+ // can circle and climb while no per-frame JS touches a single sprite. Two
3
+ // billboard modes side by side: "full" glows that stay flat to the screen
4
+ // whatever the camera does, and "fixed-y" trees that only yaw to follow
5
+ // the camera and stay upright as it climbs - the classic upright sprite.
6
+ // The spinning cube is the reference solid; the sprites' `scale` is their
7
+ // world size, and their (ignored) rotation is never set.
8
+ import { createSignal, onFrame, pct, render } from "@solidrt/core"
9
+ import { createTexture } from "@solidrt/core/gpu"
10
+ import { box, Group, Mesh, PerspectiveCamera, plane, Scene, sprite, Sprite, unlit } from "@solidrt/3d"
11
+
12
+ const SIZE = 720
13
+
14
+ // A soft disc, alpha falling off from the center; premultiplied like every
15
+ // texture the engine samples.
16
+ function glow(): ReturnType<typeof createTexture> {
17
+ let n = 64
18
+ let data = new Uint8Array(n * n * 4)
19
+ for (let y = 0; y < n; y++) {
20
+ for (let x = 0; x < n; x++) {
21
+ let dx = (x + 0.5) / n - 0.5
22
+ let dy = (y + 0.5) / n - 0.5
23
+ let a = Math.max(0, 1 - Math.sqrt(dx * dx + dy * dy) * 2)
24
+ a = a * a
25
+ data.set([255 * a, 240 * a, 200 * a, 255 * a], (y * n + x) * 4)
26
+ }
27
+ }
28
+ return createTexture(data, n, n)
29
+ }
30
+
31
+ // A cutout tree: green triangle over a brown trunk, transparent elsewhere.
32
+ function tree(): ReturnType<typeof createTexture> {
33
+ let n = 64
34
+ let data = new Uint8Array(n * n * 4)
35
+ for (let y = 0; y < n; y++) {
36
+ for (let x = 0; x < n; x++) {
37
+ let u = (x + 0.5) / n
38
+ let v = (y + 0.5) / n
39
+ let inCrown = v < 0.8 && Math.abs(u - 0.5) < v * 0.55
40
+ let inTrunk = v >= 0.8 && Math.abs(u - 0.5) < 0.08
41
+ let rgb = inCrown ? [40, 140, 60] : inTrunk ? [110, 70, 40] : [0, 0, 0]
42
+ let a = inCrown || inTrunk ? 255 : 0
43
+ data.set([rgb[0]!, rgb[1]!, rgb[2]!, a], (y * n + x) * 4)
44
+ }
45
+ }
46
+ return createTexture(data, n, n)
47
+ }
48
+
49
+ function App() {
50
+ let [t, setT] = createSignal(0)
51
+ onFrame(tick => setT(tick / 1000))
52
+
53
+ // The camera circles the scene and climbs from eye level to well above
54
+ // it: the glows never change shape, the trees tilt away only as the
55
+ // view looks down on them.
56
+ let eye = () => {
57
+ let a = t() * 0.4
58
+ let elevation = 1.2 + 2.5 * (0.5 - 0.5 * Math.cos(t() * 0.5))
59
+ return [Math.sin(a) * 5, elevation, Math.cos(a) * 5] as [number, number, number]
60
+ }
61
+
62
+ let glows = sprite({ map: glow(), color: [1, 0.85, 0.5] })
63
+ let trees = sprite({ map: tree(), billboard: "fixed-y" })
64
+ let ringPositions = Array.from({ length: 8 }, (_, i) => {
65
+ let a = (i / 8) * Math.PI * 2
66
+ return [Math.cos(a) * 1.4, 0.9 + Math.sin(a * 2) * 0.3, Math.sin(a) * 1.4] as [number, number, number]
67
+ })
68
+
69
+ return (
70
+ <window>
71
+ <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
72
+ <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sprites">
73
+ <PerspectiveCamera fov={50} position={eye()} lookAt={[0, 0.6, 0]} />
74
+ <Mesh
75
+ geometry={plane({ width: 8, height: 8 })}
76
+ material={unlit({ color: [0.16, 0.17, 0.22] })}
77
+ rotation={[-Math.PI / 2, 0, 0]}
78
+ />
79
+ <Mesh geometry={box({ width: 0.7, height: 0.7, depth: 0.7 })} material={unlit({ color: [0.85, 0.3, 0.3] })} position={[0, 0.35, 0]} rotation={[0, t(), 0]} />
80
+ <Group rotation={[0, -t() * 0.3, 0]}>
81
+ {ringPositions.map(p => (
82
+ <Sprite material={glows} position={p} scale={0.5} />
83
+ ))}
84
+ </Group>
85
+ <Sprite material={trees} position={[-2.2, 0.8, -1]} scale={[1.2, 1.6, 1]} />
86
+ <Sprite material={trees} position={[2.4, 0.9, 0.5]} scale={[1.4, 1.8, 1]} />
87
+ <Sprite material={trees} position={[0.8, 0.7, -2.6]} scale={[1, 1.4, 1]} />
88
+ <Sprite material={trees} position={[-1.5, 0.6, 2.2]} scale={[0.9, 1.2, 1]} />
89
+ </Scene>
90
+ </view>
91
+ </window>
92
+ )
93
+ }
94
+
95
+ render(() => <App />)
@@ -3,13 +3,12 @@
3
3
  // over the crate's edges like real webbing - while smooth-tagged points
4
4
  // share averaged normals, so the helix sweeps into ONE continuous coil
5
5
  // (per-segment boxes with unmitred gaps are exactly what this replaces).
6
- // tube() is the round-profile shorthand. Lit materials on purpose:
6
+ // tube() is the round-profile shorthand. lit() materials on purpose:
7
7
  // creased vs smooth joints differ only in normals, which unlit color
8
8
  // would hide.
9
9
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
10
- import { box, Group, Mesh, PerspectiveCamera, plane, roundRect, Scene, shaderMaterial, sweep, tube, unlit } from "@solidrt/3d"
10
+ import { box, DirectionalLight, Group, HemisphereLight, lit, Mesh, PerspectiveCamera, plane, roundRect, Scene, sweep, tube, unlit } from "@solidrt/3d"
11
11
  import type { SweepPath } from "@solidrt/3d"
12
- import { HEMISPHERE, LAMBERT, LIT_VERTEX } from "@solidrt/3d/glsl"
13
12
 
14
13
  const SIZE = 720
15
14
 
@@ -17,23 +16,6 @@ function App() {
17
16
  let [spin, setSpin] = createSignal(0)
18
17
  onFrame(tick => setSpin(tick / 4000))
19
18
 
20
- // One hemisphere + lambert look per color (a material instance is the
21
- // pipeline handle, so each look is created once and shared).
22
- let lit = (r: number, g: number, b: number) =>
23
- shaderMaterial({
24
- vertex: LIT_VERTEX,
25
- fragment: `
26
- in vec3 vNormal;
27
- ${HEMISPHERE}
28
- ${LAMBERT}
29
- void main() {
30
- vec3 n = normalize(vNormal);
31
- vec3 l = normalize(vec3(0.5, 0.8, 0.4));
32
- vec3 base = vec3(${r}, ${g}, ${b});
33
- fragColor = vec4(base * (hemisphere(n, vec3(0.45), vec3(0.22)) + 0.8 * lambert(n, l)), 1.0);
34
- }`,
35
- })
36
-
37
19
  // The strap hugs the crate at half its thickness; every bend is a bare
38
20
  // (sharp) point, so each fold creases exactly on a crate edge.
39
21
  let o = 0.013
@@ -45,7 +27,7 @@ function App() {
45
27
  [-0.3 + o, o, 0],
46
28
  [0.15, o, 0],
47
29
  ]
48
- let strap = sweep(roundRect(0.3, 0.026, 0.008), strapPath, "strap")
30
+ let strap = sweep(roundRect(0.3, 0.026, 0.008), strapPath, { label: "strap" })
49
31
 
50
32
  // A smooth-tagged helix: one continuous tube, not a stack of segments.
51
33
  let coilPath: SweepPath = []
@@ -53,22 +35,24 @@ function App() {
53
35
  let a = (i / 60) * Math.PI * 5
54
36
  coilPath.push({ p: [0.85 + Math.cos(a) * 0.35, 0.055 + i * 0.0095, Math.sin(a) * 0.35], smooth: true })
55
37
  }
56
- let coil = tube(coilPath, 0.05, 12, "coil")
38
+ let coil = tube(coilPath, { radius: 0.05, radialSegments: 12, label: "coil" })
57
39
 
58
40
  return (
59
41
  <window>
60
- <view width={pct(100)} height={pct(100)} viewBox={[SIZE, SIZE]}>
42
+ <view width={pct(100)} height={pct(100)} designSize={[SIZE, SIZE]}>
61
43
  <Scene width={SIZE} height={SIZE} clearColor={[0.07, 0.07, 0.1, 1]} label="sweep-paths">
62
44
  <PerspectiveCamera fov={55} position={[0, 1.9, 3.9]} lookAt={[0, 0.35, 0]} />
45
+ <HemisphereLight sky={[0.45, 0.45, 0.45]} ground={[0.22, 0.22, 0.22]} />
46
+ <DirectionalLight direction={[-0.5, -0.8, -0.4]} intensity={0.8} />
63
47
  <Mesh
64
- geometry={plane(6, 6, "floor")}
48
+ geometry={plane({ width: 6, height: 6, label: "floor" })}
65
49
  material={unlit({ color: [0.16, 0.17, 0.22] })}
66
50
  rotation={[-Math.PI / 2, 0, 0]}
67
51
  />
68
52
  <Group rotation={[0, spin(), 0]}>
69
- <Mesh geometry={box(1, 0.6, 0.8)} material={lit(0.55, 0.42, 0.28)} position={[-0.8, 0.3, 0]} />
70
- <Mesh geometry={strap} material={lit(0.9, 0.55, 0.2)} />
71
- <Mesh geometry={coil} material={lit(0.45, 0.6, 0.8)} />
53
+ <Mesh geometry={box({ width: 1, height: 0.6, depth: 0.8 })} material={lit({ color: [0.55, 0.42, 0.28] })} position={[-0.8, 0.3, 0]} />
54
+ <Mesh geometry={strap} material={lit({ color: [0.9, 0.55, 0.2] })} />
55
+ <Mesh geometry={coil} material={lit({ color: [0.45, 0.6, 0.8] })} />
72
56
  </Group>
73
57
  </Scene>
74
58
  </view>
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@solidrt/3d",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "license": "MIT",
5
+ "funding": "https://github.com/sponsors/wellawaretech",
5
6
  "author": "Antoine van Wel",
6
7
  "type": "module",
7
8
  "main": "src/index.ts",
@@ -13,10 +14,11 @@
13
14
  "files": [
14
15
  "src/",
15
16
  "examples/",
17
+ "demos/",
16
18
  "AGENTS.md"
17
19
  ],
18
20
  "peerDependencies": {
19
- "@solidjs/signals": "2.0.0-rc.0",
20
- "@solidrt/core": "0.0.50"
21
+ "@solidjs/signals": "2.0.0-rc.1",
22
+ "@solidrt/core": "0.0.52"
21
23
  }
22
24
  }
@@ -10,11 +10,20 @@ import { createContext, createEffect, onCleanup, untrack, useContext } from "@so
10
10
  import type { Element, ParentComponent, TextureId, VoidComponent } from "@solidrt/core"
11
11
  import {
12
12
  add,
13
+ createDirectionalLight,
13
14
  createGroup,
15
+ createHemisphereLight,
16
+ createInstancedMesh,
14
17
  createMesh,
15
18
  createScene,
19
+ createSprite,
20
+ disposeInstances,
16
21
  remove,
17
22
  setGeometry,
23
+ setInstanceCount,
24
+ setInstances,
25
+ setLight,
26
+ setCastShadow,
18
27
  setMaterial,
19
28
  setMeshParams,
20
29
  setRenderOrder,
@@ -22,7 +31,7 @@ import {
22
31
  setVisible,
23
32
  } from "./scene.ts"
24
33
  import type { ShaderParams } from "@solidrt/core/gpu"
25
- import type { Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent } from "./scene.ts"
34
+ import type { DirectionalLight as DirectionalLightNode, HemisphereLight as HemisphereLightNode, InstancedMesh as InstancedMeshNode, Mesh as MeshNode, Scene as SceneHandle, SceneNode, ScenePointerEvent, ShadowOptions } from "./scene.ts"
26
35
  import type { Geometry } from "./geometry.ts"
27
36
  import type { Material } from "./material.ts"
28
37
  import type { Quat, Vec3 } from "./math.ts"
@@ -99,6 +108,9 @@ export type SceneProps = {
99
108
  * `scene.background = color` is `clearColor` here. */
100
109
  background?: string
101
110
  label?: string
111
+ /** Multisample count (1, 2, 4 or 8; default 1): anti-aliased mesh edges.
112
+ * Fixed at creation. */
113
+ samples?: 1 | 2 | 4 | 8
102
114
  ref?: (scene: SceneHandle) => void
103
115
  /**
104
116
  * Compose the output yourself: called once (untracked) with the scene's
@@ -127,7 +139,7 @@ export type SceneProps = {
127
139
  */
128
140
  export let Scene: ParentComponent<SceneProps> = props => {
129
141
  let scene = untrack(() =>
130
- createScene(props.width, props.height, { clearColor: props.clearColor, label: props.label }),
142
+ createScene(props.width, props.height, { clearColor: props.clearColor, label: props.label, samples: props.samples }),
131
143
  )
132
144
  createEffect(
133
145
  () => [props.width, props.height] as const,
@@ -182,9 +194,34 @@ export type MeshProps = TransformProps & PointerEventProps & {
182
194
  params?: ShaderParams
183
195
  /** Explicit draw-order key (setRenderOrder as a prop); default 0. */
184
196
  renderOrder?: number
197
+ /** Draw into the scene's shadow map (setCastShadow as a prop); default
198
+ * false. Needs a `castShadow` DirectionalLight to show. */
199
+ castShadow?: boolean
185
200
  ref?: (mesh: MeshNode) => void
186
201
  }
187
202
 
203
+ // The mesh-side props Mesh and Sprite share (Sprite has no geometry).
204
+ function syncMesh(mesh: MeshNode, props: SpriteProps): void {
205
+ createEffect(
206
+ () => props.material,
207
+ m => setMaterial(mesh, m),
208
+ { defer: true },
209
+ )
210
+ createEffect(
211
+ () => props.params,
212
+ p => {
213
+ if (p !== undefined) setMeshParams(mesh, p)
214
+ },
215
+ )
216
+ createEffect(
217
+ () => props.renderOrder,
218
+ o => setRenderOrder(mesh, o ?? 0),
219
+ )
220
+ syncNode(mesh, props)
221
+ untrack(() => props.ref)?.(mesh)
222
+ onCleanup(() => remove(mesh))
223
+ }
224
+
188
225
  /** One draw entry: geometry drawn with a material at a transform. */
189
226
  export let Mesh: VoidComponent<MeshProps> = props => {
190
227
  let ctx = useContext(SceneContext)
@@ -195,6 +232,81 @@ export let Mesh: VoidComponent<MeshProps> = props => {
195
232
  g => setGeometry(mesh, g),
196
233
  { defer: true },
197
234
  )
235
+ createEffect(
236
+ () => props.castShadow,
237
+ c => setCastShadow(mesh, c === true),
238
+ )
239
+ syncMesh(mesh, props)
240
+ return null
241
+ }
242
+
243
+ export type SpriteProps = TransformProps & PointerEventProps & {
244
+ /** A `sprite()` material (any material draws, only a sprite one turns). */
245
+ material: Material
246
+ /** Per-mesh uniforms, merge semantics - as on Mesh. */
247
+ params?: ShaderParams
248
+ /** Explicit draw-order key (setRenderOrder as a prop); default 0. */
249
+ renderOrder?: number
250
+ ref?: (mesh: MeshNode) => void
251
+ }
252
+
253
+ /** A camera-facing unit quad (createSprite as a component): no geometry
254
+ * prop, `scale` is its world size, rotation is ignored. */
255
+ export let Sprite: VoidComponent<SpriteProps> = props => {
256
+ let ctx = useContext(SceneContext)
257
+ let mesh = untrack(() => createSprite(props.material))
258
+ add(ctx.parent, mesh)
259
+ syncMesh(mesh, props)
260
+ return null
261
+ }
262
+
263
+ export type InstancedMeshProps = TransformProps & PointerEventProps & {
264
+ geometry: Geometry
265
+ /** Must declare instanceAttributes (shaderMaterialClass). */
266
+ material: Material
267
+ /** Interleaved per-instance records (stride = the material's instance
268
+ * attributes summed). Reactive; a later array larger than the buffer
269
+ * grows it (capacity doubles into a replacement buffer). */
270
+ records: Float32Array
271
+ /** How many records draw; default all of the latest `records`. */
272
+ count?: number
273
+ /** LOCAL bounds covering every instance ([minX..maxZ]), fixed at
274
+ * creation. Without them the mesh has no picking leaf, so pointer events
275
+ * never target it. */
276
+ bounds?: ArrayLike<number>
277
+ /** Per-mesh uniforms, merge semantics - as on Mesh. */
278
+ params?: ShaderParams
279
+ /** Explicit draw-order key (setRenderOrder as a prop); default 0. */
280
+ renderOrder?: number
281
+ ref?: (mesh: InstancedMeshNode) => void
282
+ }
283
+
284
+ /** One draw entry covering N instances: geometry repeated per record of
285
+ * `records` (createInstancedMesh as a component). The record buffer is
286
+ * component-owned and freed on unmount. */
287
+ export let InstancedMesh: VoidComponent<InstancedMeshProps> = props => {
288
+ let ctx = useContext(SceneContext)
289
+ let mesh = untrack(() =>
290
+ createInstancedMesh(props.geometry, props.material, props.records, props.count, { bounds: props.bounds }),
291
+ )
292
+ add(ctx.parent, mesh)
293
+ createEffect(
294
+ () => props.records,
295
+ r => setInstances(mesh, r, untrack(() => props.count)),
296
+ { defer: true },
297
+ )
298
+ createEffect(
299
+ () => props.count,
300
+ c => {
301
+ if (c !== undefined) setInstanceCount(mesh, c)
302
+ },
303
+ { defer: true },
304
+ )
305
+ createEffect(
306
+ () => props.geometry,
307
+ g => setGeometry(mesh, g),
308
+ { defer: true },
309
+ )
198
310
  createEffect(
199
311
  () => props.material,
200
312
  m => setMaterial(mesh, m),
@@ -212,7 +324,7 @@ export let Mesh: VoidComponent<MeshProps> = props => {
212
324
  )
213
325
  syncNode(mesh, props)
214
326
  untrack(() => props.ref)?.(mesh)
215
- onCleanup(() => remove(mesh))
327
+ onCleanup(() => disposeInstances(mesh))
216
328
  return null
217
329
  }
218
330
 
@@ -240,3 +352,59 @@ export let PerspectiveCamera: VoidComponent<PerspectiveCameraProps> = props => {
240
352
  )
241
353
  return null
242
354
  }
355
+
356
+ export type HemisphereLightProps = { sky?: Vec3; ground?: Vec3; intensity?: number; ref?: (light: HemisphereLightNode) => void }
357
+
358
+ /** The scene's ambient term as a node (createHemisphereLight); one per
359
+ * scene, the last mounted wins. */
360
+ export let HemisphereLight: VoidComponent<HemisphereLightProps> = props => {
361
+ let ctx = useContext(SceneContext)
362
+ let light = untrack(() => createHemisphereLight({ sky: props.sky, ground: props.ground, intensity: props.intensity }))
363
+ add(ctx.parent, light)
364
+ createEffect(
365
+ () => [props.sky, props.ground, props.intensity] as const,
366
+ ([sky, ground, intensity]) => setLight(light, { sky, ground, intensity }),
367
+ )
368
+ untrack(() => props.ref)?.(light)
369
+ onCleanup(() => remove(light))
370
+ return null
371
+ }
372
+
373
+ export type DirectionalLightProps = TransformProps & {
374
+ /** Travel direction in the node's local space; default [0, -1, 0]. */
375
+ direction?: Vec3
376
+ color?: Vec3
377
+ intensity?: number
378
+ /** Render a shadow map from this light (any directional light may;
379
+ * each is a pass). Its shadow camera sits at the light's WORLD
380
+ * position, so give a casting light a `position` above the scene. */
381
+ castShadow?: boolean
382
+ /** Shadow-map options (mapSize, bias, normalBias, camera frustum),
383
+ * merged key by key. */
384
+ shadow?: ShadowOptions
385
+ ref?: (light: DirectionalLightNode) => void
386
+ }
387
+
388
+ /** A directional light node (createDirectionalLight): a parent Group's
389
+ * rotation turns it; up to MAX_LIGHTS per scene, in mount order. */
390
+ export let DirectionalLight: VoidComponent<DirectionalLightProps> = props => {
391
+ let ctx = useContext(SceneContext)
392
+ let light = untrack(() =>
393
+ createDirectionalLight({
394
+ direction: props.direction,
395
+ color: props.color,
396
+ intensity: props.intensity,
397
+ castShadow: props.castShadow,
398
+ shadow: props.shadow,
399
+ }),
400
+ )
401
+ add(ctx.parent, light)
402
+ syncNode(light, props)
403
+ createEffect(
404
+ () => [props.direction, props.color, props.intensity, props.castShadow, props.shadow] as const,
405
+ ([direction, color, intensity, castShadow, shadow]) => setLight(light, { direction, color, intensity, castShadow, shadow }),
406
+ )
407
+ untrack(() => props.ref)?.(light)
408
+ onCleanup(() => remove(light))
409
+ return null
410
+ }
@@ -0,0 +1,97 @@
1
+ // Geometry on the GPU: the lazy buffer step for geometry.ts's data. Buffers
2
+ // (and the picking shape - the spatial core's own copy of positions, UVs
3
+ // and indices for the triangle narrowphase, one per geometry however many
4
+ // meshes share it) are created on first acquire and shared by every mesh
5
+ // and scene drawing the geometry; each draw entry holds one reference, and
6
+ // the buffers are
7
+ // freed when the last reference is released - deferred to a microtask, so
8
+ // a same-tick entry rebuild (a material swap, a geometry that comes right
9
+ // back) keeps its upload. The handles and the reference count live in a
10
+ // map private to this module, keeping Geometry itself plain data.
11
+ // disposeGeometry frees immediately, the explicit override; either way the
12
+ // geometry stays usable - fresh buffers are created on next acquire.
13
+
14
+ import { createBuffer, destroyBuffer } from "@solidrt/core/gpu"
15
+ import type { BufferId, IndexFormat } from "@solidrt/core/gpu"
16
+ import { createShape, destroyShape } from "flux:spatial"
17
+ import type { ShapeId } from "flux:spatial"
18
+ import { layoutStride } from "./geometry.ts"
19
+ import type { Geometry } from "./geometry.ts"
20
+
21
+ /** An acquired reference to a geometry's GPU buffers: what a draw entry
22
+ * binds, and the token releaseGeometryBuffers takes - releasing the exact
23
+ * acquisition keeps the pairing correct however the caller's geometry
24
+ * fields have moved since. */
25
+ export type GeometryBuffers = {
26
+ buffer: BufferId
27
+ index: BufferId
28
+ indexFormat: IndexFormat
29
+ /** The picking shape (positions at 0, uv at 6 of every layout). */
30
+ shape: ShapeId
31
+ }
32
+
33
+ type GpuEntry = GeometryBuffers & { geometry: Geometry; refs: number }
34
+
35
+ let entries = new WeakMap<Geometry, GpuEntry>()
36
+
37
+ /** The geometry's GPU buffers, created on first use, plus the index format
38
+ * the draw entry must bind them with. Takes a reference - pair every
39
+ * acquire with a releaseGeometryBuffers of the returned token when the
40
+ * entry built from it goes. */
41
+ export function acquireGeometryBuffers(geometry: Geometry): GeometryBuffers {
42
+ let entry = entries.get(geometry)
43
+ if (entry === undefined) {
44
+ entry = {
45
+ geometry,
46
+ buffer: createBuffer(geometry.vertices, {
47
+ autoFree: false,
48
+ label: geometry.label ? geometry.label + "-verts" : undefined,
49
+ }),
50
+ index: createBuffer(geometry.indices, {
51
+ autoFree: false,
52
+ label: geometry.label ? geometry.label + "-indices" : undefined,
53
+ }),
54
+ indexFormat: geometry.indices instanceof Uint32Array ? "uint32" : "uint16",
55
+ shape: createShape(geometry.vertices, layoutStride(geometry.layout), 0, 6, geometry.indices),
56
+ refs: 0,
57
+ }
58
+ entries.set(geometry, entry)
59
+ }
60
+ entry.refs++
61
+ return entry
62
+ }
63
+
64
+ /** Release one acquire. At zero references the buffers are freed at the
65
+ * end of the microtask; an acquire before then keeps them, so a detach and
66
+ * re-attach in one tick never re-uploads. A token orphaned by an explicit
67
+ * disposeGeometry releases against the orphan, never against a successor's
68
+ * fresh buffers. */
69
+ export function releaseGeometryBuffers(acquired: GeometryBuffers): void {
70
+ let entry = acquired as GpuEntry
71
+ if (entry.refs === 0) return
72
+ entry.refs--
73
+ if (entry.refs > 0) return
74
+ queueMicrotask(() => {
75
+ if (entries.get(entry.geometry) !== entry || entry.refs > 0) return
76
+ entries.delete(entry.geometry)
77
+ destroyBuffer(entry.buffer)
78
+ destroyBuffer(entry.index)
79
+ destroyShape(entry.shape)
80
+ })
81
+ }
82
+
83
+ /**
84
+ * Free the geometry's GPU buffers now, held references or not - the
85
+ * explicit override for geometry an app is done with for good. Draw
86
+ * entries created from them hold their own reference, so destruction order
87
+ * is safe; the geometry can be used again afterwards (fresh buffers are
88
+ * created on next use).
89
+ */
90
+ export function disposeGeometry(geometry: Geometry): void {
91
+ let entry = entries.get(geometry)
92
+ if (entry === undefined) return
93
+ entries.delete(geometry)
94
+ destroyBuffer(entry.buffer)
95
+ destroyBuffer(entry.index)
96
+ destroyShape(entry.shape)
97
+ }