@solidrt/3d 0.0.64 → 0.0.66

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
@@ -217,7 +217,7 @@ displacement - and `cullMargin` (world units, Godot's
217
217
  `extra_cull_margin`) for bounded displacement such as wind. Sprites
218
218
  cull by their quad's reach at any facing. A SKINNED part is culled by
219
219
  the union of its joints' boxes (the bake computes each joint's
220
- influence box in joint space, `ModelSkin.jointBounds`, .srtm VERSION
220
+ influence box in joint space, `ModelSkin.jointBounds`, .sol3m VERSION
221
221
  5; the joint nodes carry them as culling-only bounds, outside the
222
222
  picking index), so the box follows the pose with no per-frame JS -
223
223
  Unity's bone bounds, Godot's per-bone AABBs; no `updateWhenOffscreen`
@@ -409,7 +409,7 @@ that pop in and shrink away on tap, the shelf cascading out and back
409
409
  through `<Show>`. Each natural settle calls the node's `onTransitionEnd`
410
410
  (plain field like the pointer handlers) with `{ component }` - never on
411
411
  a cancel, snap, leave or exit; the raw "spatialTransitionEnd" engine
412
- event (srt:events, carrying the CORE node id `_node`) stays for
412
+ event (sol:events, carrying the CORE node id `_node`) stays for
413
413
  flux:spatial consumers.
414
414
 
415
415
  ### Geometry layout, indices and topology
@@ -495,7 +495,7 @@ so the mesh picks and culls where it is now drawn (the box follows at
495
495
  the next flush, the triangle index is rebuilt by the next query - a
496
496
  deforming mesh needs no re-attach). transformGeometry and
497
497
  the edge builders share extra streams with their source; merge
498
- concatenates every stream; the `.srtm` container refuses streams (run
498
+ concatenates every stream; the `.sol3m` container refuses streams (run
499
499
  time data). `setDrawRange(mesh, first, count?)` draws indices `[first,
500
500
  first + count)` of the mesh's geometry (Three's `setDrawRange`, on the
501
501
  mesh because the scene owns the entries), applied to every entry of the
@@ -550,25 +550,25 @@ are Solid-free and
550
550
  GPU-free BY DESIGN so they can be checked headless and run under bun:
551
551
  `src/model-data.ts` re-exports exactly this set as the published
552
552
  `@solidrt/3d/model` entry (the bake tool and app bake scripts), and
553
- `tests/model-data.test.ts` imports it under `bun test`, so a runtime
554
- import creeping into any of them fails there; keep them that way.
553
+ `tests/model-data.test.ts` imports it under `sol test`, on the bare flux
554
+ binary, so a gui or `sol:` import creeping into any of them fails there
555
+ (a headless `flux:` import would not); keep them that way.
555
556
  `src/splat-data.ts` is its own published entry the same way
556
557
  (`@solidrt/3d/splat`, guarded by `tests/splat-data.test.ts`).
557
- The rigs under `checks/`
558
- (`geometry-check`, `sweep-check`, `pick-check`, `dispatch-check`,
559
- `gltf-check`) run on
560
- flux from the repo root: `bunx srt bundle -f --stdout
561
- packages/3d/checks/<name>.ts | target/release/flux -`. Run the ones
562
- touching what you changed. `raycast-check.tsx` and
563
- `collision-check.tsx` are the exceptions: they assert the documented
564
- picking contract (triangle accuracy, the box tier, pick/raycast
565
- parity, layer masks, the `{ meshes }` filter) and collision contract
566
- (exact sweep times, the surface rule, the slide filter, layers and
567
- meshes on overlap/sweep, moveAndSlide's landing) against a real scene,
568
- so they run on the playback client instead:
569
- `bunx srt render packages/3d/checks/<name>.tsx --project --duration 3
570
- --size 128x128`. Run them whenever a doc edit touches picking or
571
- collision claims - two copies of this contract have drifted before.
558
+ The tests under `tests/` run with `bun run sol test packages/3d` from the
559
+ repo root, or one file, or `--filter <text>` for the tests whose name
560
+ contains it: the pure modules on the bare flux binary, and the files
561
+ that need a GPU (`.test.tsx`, importing `@solidrt/test`) on the
562
+ headless dev client. Run them after a change. `raycast.test.tsx` and
563
+ `collision.test.tsx` assert the documented picking contract (triangle
564
+ accuracy, the box tier, pick/raycast parity, layer masks, the
565
+ `{ meshes }` filter) and collision contract (exact sweep times, the
566
+ surface rule, the slide filter, layers and meshes on overlap/sweep,
567
+ moveAndSlide's landing) against a real scene; run them whenever a doc
568
+ edit touches picking or collision claims - two copies of this contract
569
+ have drifted before. `fog.test.tsx` reads the fog example's pixels
570
+ through its debug commands, and `camera-components.test.tsx` drives
571
+ `<OrbitCamera>` and `<FirstPersonCamera>` with a real drag and key.
572
572
 
573
573
  ## Components
574
574
 
@@ -810,7 +810,7 @@ driving - the predicate every camera control shares) are reactive
810
810
  (HUD-safe); the pose is plain state via `pose()`/`set()` (also the
811
811
  debug-command shape). It drives position and target only; fov/near/far
812
812
  stay on scene.setCamera (or the Scene `camera` prop). The auto-orbit
813
- pauses while a gesture is open. checks/orbit-check.ts pins every stage
813
+ pauses while a gesture is open. tests/orbit.test.ts pins every stage
814
814
  headless.
815
815
 
816
816
  In a component tree, skip the wiring: `<OrbitCamera input={input}
@@ -868,7 +868,7 @@ the shot's recording target, so an `<OrbitCamera>` or
868
868
  it, the container runs the blend on frames while one is in flight. A
869
869
  shot's picks and raycasts (`anchor`, `occlusion`) go through the
870
870
  owner's actual camera, which is the shot's own only while it is live
871
- and at rest. Pinned in checks/orbit-check.ts; probes/shots-3d-probe.tsx
871
+ and at rest. Pinned in tests/orbit.test.ts; probes/shots-3d-probe.tsx
872
872
  is the live guard.
873
873
 
874
874
  ### First-person camera
@@ -931,8 +931,8 @@ swap with it), `moveSpeed`/`lookSpeed` follow their props, and a pitch
931
931
  clamp change re-clamps at once. Like the orbit control it drives
932
932
  position and yaw/pitch only: fov/near/far stay on scene.setCamera (or
933
933
  the Scene `camera` prop), so a walker in a scene bigger than the default
934
- `far` of 100 sets one there. checks/orbit-check.ts and
935
- checks/first-person-check.ts pin both controls headless (they import
934
+ `far` of 100 sets one there. tests/orbit.test.ts and
935
+ tests/first-person.test.ts pin both controls headless (they import
936
936
  `@solidrt/core/input` only).
937
937
 
938
938
  ### Overlay projection
@@ -1084,7 +1084,9 @@ repeats within 300 ms and 20 px on the same target (a double tap is
1084
1084
  tap. Wheel walks like a move with `deltaX/deltaY` (a node stopping it
1085
1085
  keeps the zoom out). Every event carries `native`, the leaf's element
1086
1086
  event, for core's recognizers, and the element fields (pointerId,
1087
- pointerType, button, modifiers) plus `x`/`y` in scene pixels
1087
+ pointerType, button, modifiers, `timeStamp`, and `predicted`: a move to
1088
+ draw with, not to measure with - core's AGENTS.md has both) plus `x`/`y`
1089
+ in scene pixels
1088
1090
  (screenRay's input - a drag plane is one intersection away). Root
1089
1091
  listeners all run, in registration order (the root is the last stop,
1090
1092
  nothing is left to claim). Wiring: the built-in `<Scene>` leaf carries
@@ -1098,7 +1100,7 @@ natural size, under any ancestor transforms or design-size fits (the hit
1098
1100
  test undoes them; localX/localY arrive in the leaf's layout frame). A
1099
1101
  leaf laid out at a different size (the supersampling pattern) uses
1100
1102
  `scene.handlersFor(() => ({ width, height }))` with its layout size.
1101
- checks/dispatch-check.ts pins the walk, claiming, capture, hover, wheel
1103
+ tests/dispatch.test.ts pins the walk, claiming, capture, hover, wheel
1102
1104
  and tap rules headless; examples/pick.tsx is the live guard.
1103
1105
 
1104
1106
  ### Geometry generators
@@ -1662,10 +1664,10 @@ specular, `envIrradiance` - the fully rough sample along the normal, as
1662
1664
  Three's getIBLIrradiance and Godot read it - added to the hemisphere
1663
1665
  for the diffuse) and every `phong({ reflectivity })` material mirrors,
1664
1666
  typically the skybox's own cube turned with it. The cube to use is a
1665
- BAKED one: `bunx srt tool 3d/environment sky.hdr -o assets/sky.srte`
1667
+ BAKED one: `bun run sol tool 3d/environment sky.hdr -o assets/sky.sol3e`
1666
1668
  turns an equirectangular Radiance .hdr (Poly Haven's are CC0) into the
1667
1669
  six faces plus the GGX-prefiltered mip chain in linear float, and
1668
- `await loadEnvironment("assets/sky.srte")` uploads it as an explicit
1670
+ `await loadEnvironment("assets/sky.sol3e")` uploads it as an explicit
1669
1671
  "rgba16f" chain (createCubeTexture's array-of-levels form: no generated
1670
1672
  mipmaps, so no half-float render support needed - it works on every
1671
1673
  device; created after an await, so not auto-freed). Unity convolves at
@@ -1680,7 +1682,7 @@ is a coarse average - fine for a sky gradient, wrong for a photograph.
1680
1682
  A 128 environment (2 MiB, the default) lights any surface; for a
1681
1683
  mirror-finish showpiece bake at 256, and for a crisp backdrop pair it
1682
1684
  with a separate hi-res LDR skybox (a 2k panorama through equirectToCube)
1683
- as `background` while the .srte stays the `environment`. Scene-level like Three's
1685
+ as `background` while the .sol3e stays the `environment`. Scene-level like Three's
1684
1686
  `scene.environment`, Unity's environment reflections and Godot's
1685
1687
  sky-lit reflections: ONE `uEnv` samplerCube bound on every target the
1686
1688
  scene draws into (a 1x1 black placeholder while unset) and one
@@ -1748,7 +1750,7 @@ one), then the PREFILTER: the faces convolved on the GPU into a second,
1748
1750
  `mipmap: true` cube target level by level (`renderTarget(chain, face,
1749
1751
  level)`, one small pass each - 48 at 128 - the bake tool's GGX
1750
1752
  importance sampling as a fragment, `createPrefilter` in environment.ts,
1751
- with the same roughness-to-level rule as a .srte chain), so `standard`
1753
+ with the same roughness-to-level rule as a .sol3e chain), so `standard`
1752
1754
  blurs a probe by roughness exactly like a baked environment; `prefilter:
1753
1755
  false` skips it and hands out the sharp faces (Three's CubeCamera: a
1754
1756
  mirror at every roughness). COST: the chain's passes are tiny, but a
@@ -1806,7 +1808,7 @@ clamp (`repeat` also wraps vertically and bleeds the poles). An HDR
1806
1808
  panorama uploaded as rgba16f converts into a half-float cube (sharp:
1807
1809
  a skybox, or `mipmap: true` for the box chain); its PREFILTERED form is
1808
1810
  the bake tool above, whose CPU pipeline (`src/environment-bake.ts`:
1809
- decodeHdr, panoramaToCube, prefilterCube, the .srte encode/decode) is
1811
+ decodeHdr, panoramaToCube, prefilterCube, the .sol3e encode/decode) is
1810
1812
  pure TypeScript and bun-tested - there is no runtime .hdr decoder.
1811
1813
 
1812
1814
  ### Fog
@@ -2225,7 +2227,9 @@ index into `images`, `doubleSided`, `transparent` = alphaMode BLEND,
2225
2227
  `alphaMode` as written and `alphaCutoff`, spec default 0.5, the
2226
2228
  normal and emissive slots, `metalness`/`roughness` factors and the
2227
2229
  packed `metalnessRoughnessMap` - standard's inputs), `images`
2228
- (the encoded PNG/JPEG bytes, undecoded) and `bounds` (world-space rest
2230
+ (each `{ bytes, name, uri? }`: the encoded PNG, JPEG or KTX2 file,
2231
+ undecoded; its name in the document, else the uri of its file, else
2232
+ "image<i>"; the `uri` when it is a file of its own) and `bounds` (world-space rest
2229
2233
  pose: a part's box through its node, a skinned part's per-joint boxes
2230
2234
  through the joints' rest transforms - where the skin places it, armature
2231
2235
  scale included; conservative under rotation). External
@@ -2273,7 +2277,8 @@ its true roots; a hierarchy reaching a node twice (a cycle) throws.
2273
2277
  #### createModel
2274
2278
 
2275
2279
  `createModel(data, { material?, label?, autoFree? })` - uploads the images (repeat
2276
- wrap, mipmapped, 4x anisotropic), makes one material per glTF material (default `standard`
2280
+ wrap, mipmapped, 4x anisotropic; each texture is labeled `<label>-<image name>`, so the
2281
+ `/gpu` inventory names the image), makes one material per glTF material (default `standard`
2277
2282
  with the file's color, maps, normal scale, metalness/roughness and
2278
2283
  packed map, emissive and transparency - the glTF material model, so a
2279
2284
  scene showing a model wants an `environment` (a glTF metal in a scene
@@ -2319,16 +2324,120 @@ for the loaders that scope is the caller's at the call, captured before
2319
2324
  the await, so a model loaded inside a memo goes with the memo's rerun
2320
2325
  and one loaded in a component with its unmount. A load that settles
2321
2326
  after its scope is gone (unmounted mid-load) is freed on arrival and
2322
- resolves already disposed - nothing owns it any more. `loadModel` reads the baked `.srtm` written by `srt tool
2323
- 3d/model <in.gltf|glb> -o assets/<name>.srtm`: the same parse run once
2324
- under bun, stored in the GPU layout, so loading is views onto the file's
2325
- bytes plus the image decodes. Numbers from a 32k-vertex, 6-texture model
2327
+ resolves already disposed - nothing owns it any more. `loadModel` reads the baked `.sol3m` written by `sol tool
2328
+ 3d/model <in.gltf|glb> -o assets/<name>.sol3m`: the same parse run once
2329
+ at build time, stored in the GPU layout, so loading is views onto the
2330
+ file's bytes plus the image decodes. Numbers from a 32k-vertex, 6-texture model
2326
2331
  on a release client: `parseGltf` 124 ms on flux (22 ms under bun) against
2327
2332
  40 ms for the whole baked load - the runtime parse is fine for small
2328
2333
  models and a binary import (`import bytes from "./x.glb" with { type:
2329
2334
  "binary" }` then `createModel(parseGltf(bytes))`, see
2330
2335
  `examples/model.tsx`); bake anything big.
2331
2336
 
2337
+ #### Compressed textures
2338
+
2339
+ `sol tool 3d/model <in> -o assets/<name>.sol3m` bakes the images
2340
+ too: each becomes a KTX2 compressed texture with its mip chain. The file is the
2341
+ same for every platform; at load each device turns the images into the
2342
+ block format its GPU samples (BC7 on desktops, ETC2 elsewhere), which
2343
+ holds them in a quarter of the memory of the decoded image. Measured on
2344
+ the Khronos Sponza sample (69 images): 363 MB of textures as PNG/JPEG,
2345
+ 91 MB compressed. The encode is slow (a minute for that
2346
+ model) and runs once, at bake time. Block compression is lossy: judge
2347
+ the result on your own textures. What it saves is GPU memory; the file
2348
+ shrinks for color maps and can grow for data maps.
2349
+
2350
+ How the images are compressed, and whether, is the app's to say, in the
2351
+ `textures` group of the `solidrt` key in its package.json (the project
2352
+ the tool runs in, whose root is where the tool is run). These are the
2353
+ defaults; name only what differs:
2354
+
2355
+ ```json
2356
+ "solidrt": {
2357
+ "textures": {
2358
+ "compress": true,
2359
+ "color": { "codec": "etc1s", "quality": 0.75 },
2360
+ "normal": { "codec": "uastc", "quality": 0.9 },
2361
+ "data": { "codec": "uastc", "quality": 0.9 },
2362
+ "files": []
2363
+ }
2364
+ }
2365
+ ```
2366
+
2367
+ Three levels, the narrower one winning: the app (`compress`), a kind
2368
+ of map (`compress`, `codec`, `quality`), and single files:
2369
+
2370
+ ```json
2371
+ "files": [
2372
+ { "match": "assets/sponza/textures/lion_*.png", "codec": "uastc", "quality": 0.95 },
2373
+ { "match": ["assets/ui/**", "assets/hero.glb#lut*"], "compress": false }
2374
+ ]
2375
+ ```
2376
+
2377
+ - `match` is a glob pattern, or a list of them, against the image's
2378
+ path FROM THE PROJECT ROOT, written with "/": `*` inside a segment,
2379
+ `**` across segments, `?`, `[a-z]`, `[!a-z]` (`matchesGlob` of
2380
+ flux:path; no `{a,b}`, list the patterns). Not the uri the glTF
2381
+ writes: two models with a `textures/base.png` each are two files.
2382
+ - An image embedded in the model (a .glb) has no file of its own: the
2383
+ model's path names every image in it, and `#name` after it picks
2384
+ the ones of that name (the glTF image's `name`, "image<i>" without
2385
+ one).
2386
+ - An entry sets `compress`, `codec`, `quality` or several, over what
2387
+ the image's kind says. Where several entries name one image, a
2388
+ later entry wins, field by field.
2389
+ - `"compress": false` keeps the image as its PNG or JPEG. That is for
2390
+ what must come back exact (a lookup table, a distance field) and
2391
+ for sharp art and text, which block compression damages.
2392
+ - A pattern that names nothing FAILS the bake, before anything is
2393
+ encoded: no file of the project matches it, or the baked model has
2394
+ no embedded image of that name. A misspelled pattern would
2395
+ otherwise do nothing and say nothing.
2396
+
2397
+ `--compress` and `--no-compress` stand in for the app's `compress` for
2398
+ one run (a quick bake while the model is still changing); what a kind
2399
+ or an entry says about itself still holds.
2400
+
2401
+ The kinds are core's and say what the texels are (`@solidrt/core/textures`).
2402
+ This package maps a material's slots onto them: base color and emissive
2403
+ maps are `color`, normal maps are `normal`, metallic-roughness maps
2404
+ (occlusion rides in the same image) are `data`.
2405
+ An image several slots share takes the most demanding kind, normal over
2406
+ color over data, and stays sRGB when one of them is a color slot;
2407
+ `modelImageUses(data)` returns that per image, the rule the loader and
2408
+ the bake both follow, for a bake script of your own.
2409
+ `codec` and `quality` are `encodeTexture`'s: "etc1s" is small (a
2410
+ quarter of a JPEG), "uastc" is accurate and large (often larger than
2411
+ the JPEG it replaces), `quality` is 0..1. A normal map steers the
2412
+ lighting and shows its errors, and a metallic-roughness map packs
2413
+ unrelated values into its channels, which the small codec handles
2414
+ badly, so both take the accurate one (what the Khronos KTX guide
2415
+ recommends). Set `data` to "etc1s" when the download matters more than
2416
+ roughness detail: on the Khronos Sponza sample that is 9 MB less for
2417
+ about 2 dB in the views that glossy surfaces fill. An unknown key, codec or field fails the bake naming the key,
2418
+ before anything is encoded, and the bake prints how many images it
2419
+ compressed with which codec and quality, and how many it kept. The color space (sRGB for `color`), the mip chain and the
2420
+ wrap are not settings: they follow how `createModel` samples. The
2421
+ settings are the app's, not this package's: an app's own bake script,
2422
+ or another extension's, reads the same group with
2423
+ `textureSettings(packageJson)` from `@solidrt/core/textures` and asks
2424
+ `textureSettingFor(settings, kind, { file, name }, matchesGlob)` per
2425
+ texture (core's AGENTS.md has the rest).
2426
+
2427
+ `loadModel` and `loadGltf` handle such a model as they are, and
2428
+ `loadGltf` reads a glTF that carries KTX2 images itself
2429
+ (`KHR_texture_basisu`). Only the primitives show the step: transcoding
2430
+ is asynchronous and `createModel` is not, so bytes obtained another way
2431
+ go through `transcodeModelImages` between the two:
2432
+
2433
+ ```ts
2434
+ let data = await transcodeModelImages(decodeModel(bytes))
2435
+ let model = createModel(data)
2436
+ ```
2437
+
2438
+ `createModel` on data whose KTX2 images were not transcoded throws and
2439
+ names the function. Data without KTX2 images needs no such step.
2440
+
2332
2441
  #### Baking your own geometry
2333
2442
 
2334
2443
  Geometry an app GENERATES (a terrain, a city block, a hundred thousand
@@ -2347,12 +2456,12 @@ import type { ModelData } from "@solidrt/3d/model"
2347
2456
  // takes `placements`), materials as glTF-shaped records, app data in
2348
2457
  // `extras` (JSON) and `blobs` (named bytes, each an aligned block).
2349
2458
  let data: ModelData = { nodes, parts, skins: [], clips: [], materials, images: [], bounds, extras: { spawn }, blobs: { grid } }
2350
- await Bun.write("assets/level.srtm", encodeModel(data))
2459
+ await Bun.write("assets/level.sol3m", encodeModel(data))
2351
2460
  ```
2352
2461
 
2353
- `loadModel("assets/level.srtm")` then builds it like any authored model;
2462
+ `loadModel("assets/level.sol3m")` then builds it like any authored model;
2354
2463
  `model.extras` and `model.blobs` hand the app data back (blobs are views
2355
- onto the loaded bytes). `srt tool 3d/model` is the same entry applied
2464
+ onto the loaded bytes). `sol tool 3d/model` is the same entry applied
2356
2465
  to a glTF, so the two bakes share one code path.
2357
2466
 
2358
2467
  ### Async loading
@@ -2370,7 +2479,7 @@ PENDING_ASYNC_UNTRACKED_READ, and any element the component builds before
2370
2479
  the suspending read is orphaned on the boundary's retry and never freed
2371
2480
  (the dev leak sentinel reports it) - so the suspending component creates
2372
2481
  no elements of its own. Async here means the file read: the parse and
2373
- createModel run synchronously on main. Bake anything big to .srtm; when
2482
+ createModel run synchronously on main. Bake anything big to .sol3m; when
2374
2483
  a source glTF must be parsed at runtime, do the parse in an isolate
2375
2484
  (parseGltf's result is plain data and copies across) and keep
2376
2485
  createModel on main.
@@ -2443,10 +2552,10 @@ rigged one; linear and premultiplied as glTF stores it) and the default
2443
2552
  material takes `vertexColors: true` for such parts - the callback's
2444
2553
  fourth argument says so, and a material shared by painted and unpainted
2445
2554
  parts is made once per variant, like the skinned split.
2446
- A `.srtm` baked before the material records carried the PBR fields
2555
+ A `.sol3m` baked before the material records carried the PBR fields
2447
2556
  (file version 3), or before the vertex formats took the WebGPU spelling
2448
2557
  and byte counts (file version 5), is rejected by loadModel - re-bake
2449
- with `srt tool 3d/model`. A baked part keeps a quantized export's
2558
+ with `sol tool 3d/model`. A baked part keeps a quantized export's
2450
2559
  bytes: u8 colors, u8/u16 joints and normalized weights, u16 uvs land in
2451
2560
  their own formats, positions and normals as floats.
2452
2561
 
@@ -2586,10 +2695,10 @@ mesh (JS-written records, no nodes) cannot morph: rejected at add().
2586
2695
 
2587
2696
  Not in the subset, dropped: tangents and further UV sets; samplers are
2588
2697
  ignored (every texture repeats); additive blending draws as base color.
2589
- The follow-ups are filed in okf/backlog/3d-model-loader.md. The `.srtm`
2590
- container is VERSION 8 (node table in the header, node-local vertices,
2591
- skins, clips, packed morph targets); older bakes are rejected - re-bake
2592
- with `srt tool 3d/model`.
2698
+ The follow-ups are filed in okf/backlog/3d-model-loader.md. The `.sol3m`
2699
+ container is VERSION 11 (node table in the header, node-local vertices,
2700
+ skins, clips, packed morph targets, app data, images under their
2701
+ names); older bakes are rejected - re-bake with `sol tool 3d/model`.
2593
2702
 
2594
2703
  ## Splats
2595
2704
 
@@ -2606,12 +2715,12 @@ successors) as ordinary content, the model-loading split repeated:
2606
2715
  times opacity, so the first n are the scene at n), y-down captures
2607
2716
  (.ply, .splat) stood up to y-up (positions and covariances rotated
2608
2717
  together; `keepOrientation` opts out, `.spz` is y-up already), bounds
2609
- measured. `encodeSplat`/`decodeSplat` round-trip the `.srts` container
2718
+ measured. `encodeSplat`/`decodeSplat` round-trip the `.sol3s` container
2610
2719
  (the record and SH blocks, next bullet); decode is a header parse
2611
- plus byte VIEWS, nothing per-splat. `srt tool 3d/splat <in> [-o
2612
- out.srts] [--sh 0..3] [--keep-orientation]` is the parse and bake
2720
+ plus byte VIEWS, nothing per-splat. `sol tool 3d/splat <in> [-o
2721
+ out.sol3s] [--sh 0..3] [--keep-orientation]` is the parse and bake
2613
2722
  under bun (tools/splat.ts); an app bake script uses the same entry.
2614
- - The `.srts` record (version 2) is 32 bytes = two rgba32ui texels:
2723
+ - The `.sol3s` record (version 2) is 32 bytes = two rgba32ui texels:
2615
2724
  center float32x3 + sRGB color and opacity unorm8x4, then the
2616
2725
  covariance upper triangle as six float16 + a spare word
2617
2726
  (SPLAT_ATTRIBUTES as a byte layout, SPLAT_RECORD_TEXELS). `--sh 1..3`
@@ -2622,7 +2731,7 @@ successors) as ordinary content, the model-loading split repeated:
2622
2731
  rgb interleaved), 32/48/96 bytes per splat of texture, so the
2623
2732
  default is 0 and a bake opts in. The y-up flip negates the
2624
2733
  coefficients whose basis is odd in (y, z).
2625
- - On the runtime, `loadSplat(path)` reads a `.srts` (fetch + decode,
2734
+ - On the runtime, `loadSplat(path)` reads a `.sol3s` (fetch + decode,
2626
2735
  the loadModel shape) and `createSplatMesh(data, { count?, material?,
2627
2736
  label? })` / `<SplatMesh>` shows it: a RecordMesh in the INDEXED form
2628
2737
  (see "Instance order") - the records handed to the engine as the
@@ -2719,7 +2828,7 @@ successors) as ordinary content, the model-loading split repeated:
2719
2828
  ambient probe, this does not). glTF's default metallic factor is 1,
2720
2829
  so an untextured asset is all metal, and createModel's default is
2721
2830
  `standard`: give a model scene an `environment` (loadEnvironment's
2722
- baked .srte, or the skybox's cube), or pass `phong` as the material.
2831
+ baked .sol3e, or the skybox's cube), or pass `phong` as the material.
2723
2832
  - Light intensities are the same numbers for `phong` and `standard`: 1
2724
2833
  lights a white matte surface to 1 face-on. A Three scene's intensities
2725
2834
  are a factor pi larger for the same look; divide when porting.
@@ -2792,7 +2901,7 @@ successors) as ordinary content, the model-loading split repeated:
2792
2901
  - A generated cube chain (`mipmap: true` from six faces) is a box
2793
2902
  filter, not the GGX convolution the roughness-to-level rule assumes:
2794
2903
  rough reflections read too sharp and the diffuse `envIrradiance` is a
2795
- 4x4 average. Bake with `srt tool 3d/environment` for anything
2904
+ 4x4 average. Bake with `sol tool 3d/environment` for anything
2796
2905
  photographed; the JS sky gradients in the examples get away with it.
2797
2906
  - The background covers the whole target with depth off, drawn first: it
2798
2907
  REPLACES the clearColor visually (the clear still runs; you just never
@@ -1,6 +1,6 @@
1
1
  # @solidrt/3d examples
2
2
 
3
- One concept per file; run with `bunx srt run <file>` from an app that
3
+ One concept per file; run with `bun run sol run <file>` from an app that
4
4
  depends on `@solidrt/3d` (or in-repo from the package directory).
5
5
 
6
6
  - `scene-basic.tsx` - the whole v1 surface: a `<Scene>` composited as a
@@ -57,8 +57,8 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
57
57
  the environment as tinted reflections, the dielectric row the same
58
58
  sky as a faint face-on gloss. Drag to look around.
59
59
  - `environment.tsx` - a baked HDR environment lighting the scene ALONE
60
- (no lights, no hemisphere): `loadEnvironment` reads the .srte that
61
- `bunx srt tool 3d/environment <panorama>.hdr -o assets/environment.srte`
60
+ (no lights, no hemisphere): `loadEnvironment` reads the .sol3e that
61
+ `bun run sol tool 3d/environment <panorama>.hdr -o assets/environment.sol3e`
62
62
  bakes from any equirectangular .hdr (Poly Haven's are CC0; the asset
63
63
  is not committed, bake one first), used as background and environment
64
64
  with ACES tone mapping. A metal row and a red dielectric row, roughness
@@ -200,4 +200,4 @@ depends on `@solidrt/3d` (or in-repo from the package directory).
200
200
  stays above the boundary - the two rules that avoid
201
201
  PENDING_ASYNC_UNTRACKED_READ and the suspend-retry element leak. Needs
202
202
  the asset baked into the running app:
203
- `bunx srt tool 3d/model examples/model.glb -o assets/model.srtm`.
203
+ `bun run sol tool 3d/model examples/model.glb -o assets/model.sol3m`.
@@ -22,7 +22,7 @@
22
22
  // plane, so the far pillars sink into the horizon instead of clipping
23
23
  // (`examples/fog.tsx` is the fog tour).
24
24
  import { createSignal, flush, onFrame, pct, render } from "@solidrt/core"
25
- import { registerDebug } from "srt:dev"
25
+ import { registerDebug } from "sol:dev"
26
26
  import { box, DirectionalLight, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene, sphere } from "@solidrt/3d"
27
27
  import type { Geometry, Vec3 } from "@solidrt/3d"
28
28
 
@@ -30,7 +30,7 @@ import {
30
30
  sphere,
31
31
  } from "@solidrt/3d"
32
32
  import type { FirstPersonCameraHandle, MeshNode, MoveResult, SceneHandle, Vec3, Volume } from "@solidrt/3d"
33
- import { registerDebug } from "srt:dev"
33
+ import { registerDebug } from "sol:dev"
34
34
 
35
35
  // Layer bit the level geometry adds to its drawn layer: what every
36
36
  // collision query selects, so pickups (drawn only) never block the walker.
@@ -14,7 +14,7 @@
14
14
  import { createSignal, For, pct, render, Show } from "@solidrt/core"
15
15
  import { box, Group, Mesh, PerspectiveCamera, plane, Scene, unlit } from "@solidrt/3d"
16
16
  import type { NodeTransition, SceneHandle, SceneTapEvent, Vec3 } from "@solidrt/3d"
17
- import { registerDebug } from "srt:dev"
17
+ import { registerDebug } from "sol:dev"
18
18
 
19
19
  const COUNT = 5
20
20
  // Crate spacing along x, and the pause before a tapped-away crate is
@@ -13,7 +13,7 @@ import { createInputMap, createPointerFeed, createSignal, gamepad, keyboard, loc
13
13
  import type { PointerEvent } from "@solidrt/core"
14
14
  import { box, cylinder, DirectionalLight, FirstPersonCamera, firstPersonActions, firstPersonBindings, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene } from "@solidrt/3d"
15
15
  import type { FirstPersonCameraHandle, Vec3 } from "@solidrt/3d"
16
- import { registerDebug } from "srt:dev"
16
+ import { registerDebug } from "sol:dev"
17
17
 
18
18
  // Half extent of the walkable courtyard in world units; the walls stand
19
19
  // just outside it.
@@ -18,7 +18,7 @@
18
18
  import { createInputMap, createPointerFeed, createSignal, flush, For, gamepad, pct, render } from "@solidrt/core"
19
19
  import { box, DirectionalLight, HemisphereLight, Instance, InstancedMesh, phong, Mesh, OrbitCamera, orbitActions, orbitBindings, plane, Scene, worldPosition } from "@solidrt/3d"
20
20
  import type { InstanceNode, NodeTransition, SceneHandle, Vec3 } from "@solidrt/3d"
21
- import { registerDebug } from "srt:dev"
21
+ import { registerDebug } from "sol:dev"
22
22
 
23
23
  type Tint = [number, number, number, number]
24
24
 
package/examples/fog.tsx CHANGED
@@ -16,11 +16,12 @@
16
16
  // command sets the mode and its knobs (`{ mode: "linear" | "exp2" |
17
17
  // "height" | "off", near, far, density, height, falloff }`) and returns
18
18
  // the state; `pan` parks the camera (`{ t: seconds }`), so a capture
19
- // repeats.
19
+ // repeats; `suns` is the two suns' world positions and `project`
20
+ // (`{ point }`) is where a world point is on screen, for a pixel probe.
20
21
  import { createSignal, flush, onFrame, pct, render } from "@solidrt/core"
21
- import { registerDebug } from "srt:dev"
22
+ import { registerDebug } from "sol:dev"
22
23
  import { cone, cylinder, DirectionalLight, HemisphereLight, phong, Mesh, PerspectiveCamera, plane, Scene, sphere, unlit } from "@solidrt/3d"
23
- import type { FogOptions, Vec3 } from "@solidrt/3d"
24
+ import type { FogOptions, SceneHandle, Vec3 } from "@solidrt/3d"
24
25
 
25
26
  const FAR = 400
26
27
  // The sky, shared by the clear and the fog so the horizon has no band.
@@ -36,6 +37,10 @@ const LINEAR_FAR = FAR
36
37
  const DENSITY = 0.006
37
38
  const HEIGHT = 10
38
39
  const HEIGHT_FALLOFF = 0.12
40
+ // The two suns, side by side on the far ridge: the left one opts out of
41
+ // the fog, the right one fogs like everything else.
42
+ const SUN_LIT: Vec3 = [-30, 100, -220]
43
+ const SUN_FOGGED: Vec3 = [30, 100, -220]
39
44
 
40
45
  type Mode = "linear" | "exp2" | "height" | "off"
41
46
  const MODES: Mode[] = ["linear", "exp2", "height", "off"]
@@ -48,6 +53,7 @@ let [height, setHeight] = createSignal(HEIGHT)
48
53
  let [falloff, setFalloff] = createSignal(HEIGHT_FALLOFF)
49
54
  let [time, setTime] = createSignal(0)
50
55
  let parked: number | null = null
56
+ let scene: SceneHandle | null = null
51
57
 
52
58
  let num = (v: unknown, set: (n: number) => void) => {
53
59
  if (typeof v === "number") set(v)
@@ -73,6 +79,13 @@ registerDebug("pan", (args?: Record<string, unknown>) => {
73
79
  flush()
74
80
  return { t: time(), parked: parked !== null }
75
81
  })
82
+ registerDebug("suns", () => ({ lit: SUN_LIT, fogged: SUN_FOGGED }))
83
+ registerDebug("project", (args?: Record<string, unknown>) => {
84
+ if (scene === null) throw new Error("project: the scene is not mounted")
85
+ let point = args?.point
86
+ if (!Array.isArray(point) || point.length !== 3 || !point.every(v => typeof v === "number")) throw new Error("project: pass { point: [x, y, z] }")
87
+ return scene.project(point as Vec3)
88
+ })
76
89
 
77
90
  function fog(): FogOptions | undefined {
78
91
  switch (mode()) {
@@ -148,7 +161,7 @@ function App() {
148
161
  return (
149
162
  <window>
150
163
  <view width={pct(100)} height={pct(100)} onPointerDown={() => setMode(m => MODES[(MODES.indexOf(m) + 1) % MODES.length] ?? "linear")}>
151
- <Scene clearColor={[SKY[0], SKY[1], SKY[2], 1]} fog={fog()} samples={4} label="fog">
164
+ <Scene ref={s => (scene = s)} clearColor={[SKY[0], SKY[1], SKY[2], 1]} fog={fog()} samples={4} label="fog">
152
165
  <PerspectiveCamera fov={55} near={0.5} far={FAR} position={EYE} lookAt={lookAt()} />
153
166
  <HemisphereLight sky={[0.55, 0.62, 0.75]} ground={[0.28, 0.24, 0.2]} />
154
167
  <DirectionalLight color={[0.9, 0.85, 0.75]} rotation={[-0.9, 0.6, 0]} />
@@ -162,8 +175,8 @@ function App() {
162
175
  <Mesh geometry={crown} material={needles} position={[p.position[0], p.position[1] + p.size * 0.3 + p.size * 0.5, p.position[2]]} scale={[p.size * 0.9, p.size, p.size * 0.9]} />
163
176
  </>
164
177
  ))}
165
- <Mesh geometry={sun} material={sunLit} position={[-30, 100, -220]} />
166
- <Mesh geometry={sun} material={sunFogged} position={[30, 100, -220]} />
178
+ <Mesh geometry={sun} material={sunLit} position={SUN_LIT} />
179
+ <Mesh geometry={sun} material={sunFogged} position={SUN_FOGGED} />
167
180
  </Scene>
168
181
  </view>
169
182
  </window>
package/examples/lod.tsx CHANGED
@@ -21,7 +21,7 @@
21
21
  import { onFrame, pct, render } from "@solidrt/core"
22
22
  import { addInstance, cone, cylinder, DirectionalLight, HemisphereLight, icosahedron, InstancedLod, phong, Lod, Mesh, mergeGeometries, plane, Scene, transformGeometry, useScene } from "@solidrt/3d"
23
23
  import type { InstancedMeshNode } from "@solidrt/3d"
24
- import { registerDebug } from "srt:dev"
24
+ import { registerDebug } from "sol:dev"
25
25
 
26
26
  const TREES = 1000
27
27
  // Half the side of the square field the trees scatter over.
@@ -3,7 +3,7 @@
3
3
  // bundle - the shape every app with real content ends up needing. Bake
4
4
  // the asset once and drop it in your app's assets tree:
5
5
  //
6
- // bunx srt tool 3d/model examples/model.glb -o assets/model.srtm
6
+ // bun run sol tool 3d/model examples/model.glb -o assets/model.sol3m
7
7
  //
8
8
  // (loadGltf("assets/model.glb") has the exact same async shape; the bake
9
9
  // just removes the runtime parse.)
@@ -51,7 +51,7 @@ function Rover(props: { turn: () => number }) {
51
51
  // The async value: a memo returning a Promise. Its read below suspends
52
52
  // until the file is read and the model built, then resumes under the
53
53
  // <Loading> boundary in App.
54
- let loaded = createMemo(() => loadModel("assets/model.srtm", { label: "rover" }))
54
+ let loaded = createMemo(() => loadModel("assets/model.sol3m", { label: "rover" }))
55
55
 
56
56
  // Everything derived from the model - framing, mounting - happens in
57
57
  // this memo, AFTER the suspending read, so nothing here runs or builds
@@ -4,7 +4,7 @@
4
4
  // with parseGltf and turned into a Group of meshes by createModel. The
5
5
  // glb rides in the bundle as a binary import; a model under assets/ goes
6
6
  // through loadGltf (same parser, read with flux:fs) or, baked once with
7
- // `srt tool 3d/model`, through loadModel (no parsing at all). Every part
7
+ // `sol tool 3d/model`, through loadModel (no parsing at all). Every part
8
8
  // keeps its node name and is an ordinary mesh: pointer events, transforms
9
9
  // and materials work per part. The materials are createModel's default
10
10
  // `standard` from the file's PBR factors, so the scene carries an
@@ -20,7 +20,7 @@
20
20
  // shadows morph too.
21
21
 
22
22
  import { createSignal, onCleanup, onFrame, pct, render } from "@solidrt/core"
23
- import { registerDebug } from "srt:dev"
23
+ import { registerDebug } from "sol:dev"
24
24
  import { DirectionalLight, geometryAttribute, geometryVertexCount, getMorphNames, getMorphWeights, HemisphereLight, Instance, InstancedMesh, phong, Mesh, PerspectiveCamera, plane, Scene, setMorphWeights, sphere, withMorphTargets } from "@solidrt/3d"
25
25
  import type { InstanceNode, MeshNode } from "@solidrt/3d"
26
26
 
@@ -17,7 +17,7 @@
17
17
  // `helpers` debug command ({ on?: boolean }) toggles them.
18
18
 
19
19
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
20
- import { registerDebug } from "srt:dev"
20
+ import { registerDebug } from "sol:dev"
21
21
  import { computeVertexNormals, cylinder, DirectionalLight, fillAttribute, geometryAttribute, geometryVertexCount, Group, HemisphereLight, phong, Mesh, normalsHelper, PerspectiveCamera, Scene, sphere, toNonIndexed, unlit, updateVertices, withNormals } from "@solidrt/3d"
22
22
 
23
23
  // The crease angles in degrees: flat, the default, everything smoothed.
package/examples/pick.tsx CHANGED
@@ -24,7 +24,7 @@
24
24
  import { createInputMap, createPointerFeed, createSignal, displayScale, pct, render } from "@solidrt/core"
25
25
  import { box, cone, getRotation, Group, Mesh, OrbitCamera, orbitActions, orbitBindings, PerspectiveCamera, plane, Scene, setMeshParams, setTransform, sphere, torus, unlit, useScene } from "@solidrt/3d"
26
26
  import type { Geometry, MeshNode, NodePointerEvent, NodeTapEvent, NodeWheelEvent, OrbitCameraHandle, SceneHandle, Vec3 } from "@solidrt/3d"
27
- import { registerDebug } from "srt:dev"
27
+ import { registerDebug } from "sol:dev"
28
28
 
29
29
  type Color = [number, number, number]
30
30
 
@@ -18,7 +18,7 @@
18
18
  import { createSignal, onFrame, pct, render } from "@solidrt/core"
19
19
  import { createTexture } from "@solidrt/core/gpu"
20
20
  import type { TextureId } from "@solidrt/core/gpu"
21
- import { registerDebug } from "srt:dev"
21
+ import { registerDebug } from "sol:dev"
22
22
  import { add, axesHelper, box3Helper, createModel, DirectionalLight, edgesGeometry, equirectToCube, gridHelper, Group, Mesh, parseGltf, PerspectiveCamera, Scene, setGeometry, setMaterial, unlit, wireframeGeometry } from "@solidrt/3d"
23
23
  import type { SceneNode } from "@solidrt/3d"
24
24
  import modelBytes from "./model.glb" with { type: "binary" }