@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/src/node.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import * as spatial from "flux:spatial"
10
10
  import type { NodeId, NodeMotionSpec, NodeTransition } from "flux:spatial"
11
- import { on } from "srt:events"
11
+ import { on } from "sol:events"
12
12
  import { createMutableTexture, destroyTexture } from "@solidrt/core/gpu"
13
13
  import type { ShaderParams, TextureId } from "@solidrt/core/gpu"
14
14
  import type { PointerEvent as ElementPointerEvent, WheelEvent as ElementWheelEvent } from "@solidrt/core"
@@ -273,6 +273,11 @@ export type SceneEventBase = {
273
273
  * (a view's own pixels under a view leaf). */
274
274
  x: number
275
275
  y: number
276
+ /** When the event happened, ms: the element event's `timeStamp`. */
277
+ timeStamp: number
278
+ /** The element event's `predicted`: a move the pointer was never
279
+ * reported at. Draw with it, do not measure with it. */
280
+ predicted: boolean
276
281
  pointerId: number
277
282
  pointerType: string
278
283
  button?: number
@@ -771,7 +776,7 @@ export type { TransformUpdate } from "./math.ts"
771
776
  * orchestrator is always an ancestor). Clearing cancels running tracks in
772
777
  * place (the node keeps its mid-flight transform) and later writes snap. Each natural settle calls the node's
773
778
  * `onTransitionEnd` with the component (the raw "spatialTransitionEnd"
774
- * engine event on srt:events stays for flux:spatial consumers; it carries
779
+ * engine event on sol:events stays for flux:spatial consumers; it carries
775
780
  * the core id, `_node`).
776
781
  */
777
782
  export function setTransition(node: SceneNode, transition: NodeTransition | string | null): void {
@@ -19,7 +19,7 @@
19
19
  // dispatch.ts is the same model one dimension down.
20
20
  //
21
21
  // Pure BY DESIGN (types only, nothing with GPU imports) so
22
- // checks/dispatch-check.ts drives it headless with a fake pick.
22
+ // tests/dispatch.test.ts drives it headless with a fake pick.
23
23
 
24
24
  import type { PointerEvent as ElementPointerEvent, PointerFeed } from "@solidrt/core"
25
25
  import type { NodePointerEvent, SceneNode, ScenePointerListener } from "./node.ts"
@@ -58,6 +58,8 @@ type InternalEvent = {
58
58
  distance: number | null
59
59
  x: number
60
60
  y: number
61
+ timeStamp: number
62
+ predicted: boolean
61
63
  pointerId: number
62
64
  pointerType: string
63
65
  button?: number
@@ -95,8 +97,6 @@ export type PointerInputDeps = {
95
97
  root: Scene | ViewHandle
96
98
  /** The root's listeners, in registration order. */
97
99
  listeners: Set<ScenePointerListener>
98
- /** The clock for tap repeats (default performance.now; checks inject). */
99
- now?: () => number
100
100
  }
101
101
 
102
102
  export type PointerInput = {
@@ -108,7 +108,6 @@ export type PointerInput = {
108
108
  }
109
109
 
110
110
  export function makePointerInput(deps: PointerInputDeps): PointerInput {
111
- let now = deps.now ?? (() => performance.now())
112
111
  let presses = new Map<number, Press>()
113
112
  let hover = new Map<number, Target>()
114
113
  let lastTap: { time: number; cx: number; cy: number; target: Target | null; count: number } | null = null
@@ -170,6 +169,8 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
170
169
  distance: hit === null ? null : hit.distance,
171
170
  x,
172
171
  y,
172
+ timeStamp: e.timeStamp,
173
+ predicted: e.predicted,
173
174
  pointerId: e.pointerId,
174
175
  pointerType: e.pointerType,
175
176
  button: e.button,
@@ -245,7 +246,7 @@ export function makePointerInput(deps: PointerInputDeps): PointerInput {
245
246
  if (press.moved || !press.alone) return
246
247
  let release = nearest(x, y)
247
248
  if (press.target === null ? release !== null : release === null || !sameHit(release, press.target)) return
248
- let time = now()
249
+ let time = e.timeStamp
249
250
  let repeat = lastTap !== null && time - lastTap.time <= TAP_INTERVAL_MS && sameTarget(lastTap.target, press.target) && Math.hypot(e.clientX - lastTap.cx, e.clientY - lastTap.cy) <= TAP_REPEAT_SLOP
250
251
  let count = repeat ? lastTap!.count + 1 : 1
251
252
  lastTap = { time, cx: e.clientX, cy: e.clientY, target: press.target, count }
package/src/scene.ts CHANGED
@@ -635,7 +635,7 @@ export type ReflectionProbeOptions = {
635
635
  export type ReflectionProbe = {
636
636
  /** The cube map: what `environment={{ cube }}` and `background={{
637
637
  * cube }}` take. The prefiltered chain (roughness 0 sharp at level 0,
638
- * blurred below, the same rule as a baked .srte), or with `prefilter:
638
+ * blurred below, the same rule as a baked .sol3e), or with `prefilter:
639
639
  * false` the sharp faces alone - then `standard` reflects it at its
640
640
  * roughness 0 look whatever its roughness. */
641
641
  cube: TextureId
package/src/splat-data.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  // @solidrt/3d/splat - the splat DATA side of the package without the
2
2
  // runtime: the capture parsers (.ply, .splat, gunzipped .spz), the
3
- // covariance bake and the .srts container, for `srt tool 3d/splat` and any
3
+ // covariance bake and the .sol3s container, for `sol tool 3d/splat` and any
4
4
  // app bake script under bun. The package root imports `flux:*` and so
5
5
  // loads only on the runtime; this entry imports nothing that does (the
6
- // model-data rule; tests/splat-data.test.ts imports it under bun and fails
7
- // the moment a runtime import creeps into the chain).
6
+ // model-data rule; tests/splat-data.test.ts imports it on the bare flux
7
+ // binary and fails the moment a gui or sol: import creeps into the chain).
8
8
  //
9
9
  // import { encodeSplat, parseSplat } from "@solidrt/3d/splat"
10
- // writeFileSync("assets/scan.srts", encodeSplat(parseSplat(bytes)))
10
+ // writeFileSync("assets/scan.sol3s", encodeSplat(parseSplat(bytes)))
11
11
  //
12
- // loadSplat / createSplatMesh on the runtime read what it writes: a .srts
12
+ // loadSplat / createSplatMesh on the runtime read what it writes: a .sol3s
13
13
  // record IS the texel data the splat shader fetches (two rgba32ui texels
14
14
  // per splat, SPLAT_ATTRIBUTES as a byte layout) and the record the core
15
15
  // sorts by depth, so runtime loading is a header parse plus byte views -
@@ -113,8 +113,8 @@ const SPZ_COLOR_SCALE = 0.15
113
113
  const SPZ_SCALE_OFFSET = 10
114
114
  const SPZ_SCALE_STEP = 16
115
115
 
116
- /** "SRTS" read as a little-endian u32. */
117
- const MAGIC = 0x53545253
116
+ /** "SOLS" read as a little-endian u32. */
117
+ const MAGIC = 0x534c4f53
118
118
  // Version 2: the 32-byte two-texel record, importance-sorted, y-up unless
119
119
  // the bake kept orientation, plus the optional SH texel block; the header
120
120
  // carries count, shDegree, bounds and both block ranges. (Version 1 was
@@ -164,10 +164,10 @@ let colorByte = (v: number) => Math.round(Math.min(1, Math.max(0, v)) * 255)
164
164
  * Parse a captured splat cloud - a trainer's .ply, the de-facto .splat
165
165
  * interchange, or a gunzipped .spz (Niantic; the container is
166
166
  * gzip-compressed, and this entry stays decompression-free, so hand it
167
- * the inflated bytes - `srt tool 3d/splat` does) - into baked SplatData:
167
+ * the inflated bytes - `sol tool 3d/splat` does) - into baked SplatData:
168
168
  * covariance precomputed, y-up by default, importance-sorted (size times
169
169
  * opacity), bounds measured. Runs anywhere (bun, the runtime); for a
170
- * capture of real size run it at pack time and ship encodeSplat's .srts.
170
+ * capture of real size run it at pack time and ship encodeSplat's .sol3s.
171
171
  */
172
172
  export function parseSplat(bytes: Uint8Array, opts?: ParseSplatOptions): SplatData {
173
173
  let format = opts?.format ?? sniffFormat(bytes)
@@ -185,7 +185,7 @@ function sniffFormat(bytes: Uint8Array): SplatFormat {
185
185
  if (dv.getUint32(0, true) === SPZ_MAGIC) return "spz"
186
186
  }
187
187
  if (bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) {
188
- throw new Error("parseSplat: gzip-compressed input (.spz files are); gunzip the bytes first - srt tool 3d/splat does")
188
+ throw new Error("parseSplat: gzip-compressed input (.spz files are); gunzip the bytes first - sol tool 3d/splat does")
189
189
  }
190
190
  if (bytes.length > 0 && bytes.length % SPLAT_IN_BYTES === 0) return "splat"
191
191
  throw new Error("parseSplat: unrecognized input - not a .ply, not a gunzipped .spz, and not whole 32-byte .splat records")
@@ -537,8 +537,8 @@ function shBytesOf(count: number, shDegree: number): number {
537
537
  }
538
538
 
539
539
  /**
540
- * Serialize a splat cloud into the .srts container, all little-endian:
541
- * "SRTS" u32 | version u32 | jsonLength u32 | json (padded to 4) |
540
+ * Serialize a splat cloud into the .sol3s container, all little-endian:
541
+ * "SOLS" u32 | version u32 | jsonLength u32 | json (padded to 4) |
542
542
  * payload. The JSON header carries count, shDegree, bounds and the byte
543
543
  * range of the record block and (above degree 0) the SH block; the
544
544
  * payload is both blocks verbatim.
@@ -574,7 +574,7 @@ export function encodeSplat(data: SplatData): Uint8Array {
574
574
  }
575
575
 
576
576
  /**
577
- * Read a .srts container back into SplatData. The records and the SH
577
+ * Read a .sol3s container back into SplatData. The records and the SH
578
578
  * block are VIEWS onto `bytes` (copied once only when the input is not
579
579
  * 4-aligned), so the bytes must outlive the data - which is the point:
580
580
  * loading a bake is a header parse, and the views upload as-is.
@@ -585,7 +585,7 @@ export function decodeSplat(bytes: Uint8Array): SplatData {
585
585
  let dv = new DataView(bytes.buffer, bytes.byteOffset, 12)
586
586
  if (dv.getUint32(0, true) !== MAGIC) throw new Error("decodeSplat: not a splat file (bad magic)")
587
587
  let version = dv.getUint32(4, true)
588
- if (version !== VERSION) throw new Error("decodeSplat: version " + version + ", expected " + VERSION + " - re-bake with `srt tool 3d/splat`")
588
+ if (version !== VERSION) throw new Error("decodeSplat: version " + version + ", expected " + VERSION + " - re-bake with `sol tool 3d/splat`")
589
589
  let jsonLength = dv.getUint32(8, true)
590
590
  let header: Header = JSON.parse(new TextDecoder().decode(bytes.subarray(12, 12 + jsonLength)))
591
591
  let payload = 12 + jsonLength + ((4 - (jsonLength % 4)) % 4)
package/src/splat.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // The splat RUNTIME side: the stock splat material and createSplatMesh
2
2
  // over a baked SplatData (parse and bake live in ./splat-data.ts, the
3
- // runtime-free `@solidrt/3d/splat` entry; `srt tool 3d/splat` writes the
4
- // .srts this loads). A splat cloud here is an ordinary record mesh in the
3
+ // runtime-free `@solidrt/3d/splat` entry; `sol tool 3d/splat` writes the
4
+ // .sol3s this loads). A splat cloud here is an ordinary record mesh in the
5
5
  // INDEXED form - the records live in an rgba32ui data texture the vertex
6
6
  // stage fetches by id, SPLAT_GROUP splats share one drawn instance (a
7
7
  // merged-quad geometry; the instancing frontend was the measured wall),
@@ -393,7 +393,7 @@ export function createSplatMesh(data: SplatData, opts?: SplatMeshOptions): Splat
393
393
  return mesh
394
394
  }
395
395
 
396
- /** Read a baked .srts splat cloud (`srt tool 3d/splat`): no parsing, the
396
+ /** Read a baked .sol3s splat cloud (`sol tool 3d/splat`): no parsing, the
397
397
  * records and the SH block view the file's bytes and upload as-is at
398
398
  * createSplatMesh. */
399
399
  export async function loadSplat(path: string): Promise<SplatData> {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /// <reference path="./node-env.d.ts" />
3
3
 
4
- // srt tool 3d/environment: bake a Radiance .hdr panorama into a .srte
4
+ // sol tool 3d/environment: bake a Radiance .hdr panorama into a .sol3e
5
5
  // environment file - the six cube faces and their GGX-prefiltered mip
6
6
  // chain in linear float, the exact chain loadEnvironment uploads as an
7
7
  // explicit rgba16f cube (no runtime conversion, no generated mipmaps, no
@@ -10,7 +10,7 @@
10
10
  // at runtime every time the sky changes. Put the output under assets/ so
11
11
  // it ships with the app.
12
12
  //
13
- // srt tool 3d/environment <in.hdr> [-o <out.srte>] [--size <edge>]
13
+ // sol tool 3d/environment <in.hdr> [-o <out.sol3e>] [--size <edge>]
14
14
  //
15
15
  // --size is the cube's face edge: 128 (default) is Unity's probe default
16
16
  // and enough for every surface short of a mirror; 256 (Godot's radiance
@@ -28,7 +28,7 @@ const MAX_SIZE = 1024
28
28
 
29
29
  function usage(error?: string): never {
30
30
  if (error) console.error(error)
31
- console.log("Usage: srt tool 3d/environment <in.hdr> [-o <out.srte>] [--size <edge>]")
31
+ console.log("Usage: sol tool 3d/environment <in.hdr> [-o <out.sol3e>] [--size <edge>]")
32
32
  process.exit(error ? 1 : 0)
33
33
  }
34
34
 
@@ -54,7 +54,7 @@ for (let i = 0; i < args.length; i++) {
54
54
  else usage("Unexpected argument " + arg)
55
55
  }
56
56
  if (input === undefined) usage("Missing input file")
57
- if (output === undefined) output = basename(input, extname(input)) + ".srte"
57
+ if (output === undefined) output = basename(input, extname(input)) + ".sol3e"
58
58
 
59
59
  let started = performance.now()
60
60
  let panorama = decodeHdr(new Uint8Array(readFileSync(input)))
@@ -0,0 +1,206 @@
1
+ // sol tool 3d/model: bake a glTF into a .sol3m model file - the parse
2
+ // (src/gltf.ts) run once here, the result written in the exact layout
3
+ // loadModel views without any per-vertex work at runtime. Same parser,
4
+ // same subset, same result as loadGltf; this only moves the cost to build
5
+ // time. Put the output under assets/ so it ships with the app.
6
+ //
7
+ // The images are baked too: each PNG or JPEG becomes a KTX2 compressed
8
+ // texture with its mip chain, which the device turns into its own block
9
+ // format at load (a quarter of the GPU memory of the decoded image). One
10
+ // file for every platform, so the tool takes no platform. How each image
11
+ // is compressed, and whether, is the app's: the `textures` group of the
12
+ // `solidrt` key in the package.json of the project the tool runs in, over
13
+ // the defaults (@solidrt/core/textures) - per kind of map (color, normal,
14
+ // data) and per file, an image named by its path from the project root
15
+ // and an embedded one by `<model path>#<its name>`. --compress and
16
+ // --no-compress stand in for the app's own `compress` for one run.
17
+ //
18
+ // A flux tool (the file name says so): the texture encoder is the
19
+ // runtime's, flux:image. The model side comes from the runtime-free
20
+ // `@solidrt/3d/model` entry (src/model-data.ts), the same surface an
21
+ // app's own bake script uses under bun.
22
+ //
23
+ // sol tool 3d/model <in.gltf|in.glb> [-o <out.sol3m>] [--compress|--no-compress]
24
+
25
+ import { file, glob } from "flux:fs"
26
+ import { decodeImage, encodeTexture } from "flux:image"
27
+ import { basename, dirname, extname, join, matchesGlob, relative } from "flux:path"
28
+ import { argv, exit } from "flux:process"
29
+ import { isKtx2, textureSettingFor, textureSettings, unmatchedTextureFiles, unmatchedTextureNames } from "@solidrt/core/textures"
30
+ import type { TextureKind, TextureSetting, TextureSettings, TextureSource } from "@solidrt/core/textures"
31
+ import { encodeModel, gltfExternalUris, layoutStride, modelImageUses, parseGltf } from "../src/model-data.ts"
32
+ import type { ModelData, ModelImage } from "../src/model-data.ts"
33
+
34
+ // The project's package.json, where the texture settings live.
35
+ const PROJECT_CONFIG = "package.json"
36
+
37
+ // How many images are decoded and encoded at once: what the runtime
38
+ // encodes at a time. More would only queue, each with its decoded pixels.
39
+ const IMAGES_IN_FLIGHT = 4
40
+
41
+ // The project the tool runs in: its root is the cwd, which the patterns
42
+ // of the texture settings are written from.
43
+ const PROJECT_ROOT = "."
44
+
45
+ const USAGE = "Usage: sol tool 3d/model <in.gltf|in.glb> [-o <out.sol3m>] [--compress|--no-compress]"
46
+
47
+ function usage(error?: string): never {
48
+ if (error) console.error(error)
49
+ console.log(USAGE)
50
+ exit(error ? 1 : 0)
51
+ }
52
+
53
+ function kib(bytes: number): string {
54
+ return (bytes / 1024).toFixed(0) + " KiB"
55
+ }
56
+
57
+ function images(count: number): string {
58
+ return count === 1 ? "1 image" : count + " images"
59
+ }
60
+
61
+ // The texture settings of the project the tool runs in; the defaults where
62
+ // it has no package.json. `compress` is the run's own word, when it has
63
+ // one.
64
+ async function projectTextureSettings(compress: boolean | undefined): Promise<TextureSettings> {
65
+ let config = file(PROJECT_CONFIG)
66
+ return textureSettings((await config.exists()) ? await config.json() : undefined, { compress })
67
+ }
68
+
69
+ // What an image is to the texture settings: its file as the path from the
70
+ // project root, or for an embedded image the model's file and its name.
71
+ function imageSource(image: ModelImage, model: string): TextureSource {
72
+ if (image.uri === undefined) return { file: relative(PROJECT_ROOT, model), name: image.name }
73
+ return { file: relative(PROJECT_ROOT, join(dirname(model), decodeURIComponent(image.uri))) }
74
+ }
75
+
76
+ // The entries of the settings that name nothing, which is a misspelled
77
+ // pattern: a file the project does not have, a name the model does not.
78
+ async function unmatchedPatterns(data: ModelData, model: string, settings: TextureSettings): Promise<string[]> {
79
+ let files = await unmatchedTextureFiles(settings, (pattern) => glob(pattern, { cwd: PROJECT_ROOT }))
80
+ let embedded = data.images.filter((image) => image.uri === undefined).map((image) => image.name)
81
+ return [...files, ...unmatchedTextureNames(settings, relative(PROJECT_ROOT, model), embedded, matchesGlob)]
82
+ }
83
+
84
+ // The images of one kind compressed the same way, and the ones kept.
85
+ type Group = { kind: TextureKind; setting: TextureSetting; images: number; from: number; to: number }
86
+ type Baked = { groups: Group[]; kept: { images: number; bytes: number } }
87
+
88
+ // The images as KTX2, in place, each as the settings say for it and in
89
+ // the color space createModel samples it in (modelImageUses, the rule the
90
+ // two share), mipmapped for repeat wrap. IMAGES_IN_FLIGHT at a time, each
91
+ // decoded when its turn comes. An image the settings leave alone is kept,
92
+ // and so is one that is a KTX2 already.
93
+ async function bakeImages(data: ModelData, model: string, settings: TextureSettings): Promise<Baked> {
94
+ let baked: Baked = { groups: [], kept: { images: 0, bytes: 0 } }
95
+ let groupOf = (kind: TextureKind, setting: TextureSetting): Group => {
96
+ let group = baked.groups.find((g) => g.kind === kind && g.setting.codec === setting.codec && g.setting.quality === setting.quality)
97
+ if (group === undefined) {
98
+ group = { kind, setting, images: 0, from: 0, to: 0 }
99
+ baked.groups.push(group)
100
+ }
101
+ return group
102
+ }
103
+ let uses = modelImageUses(data)
104
+ let next = 0
105
+ let lane = async () => {
106
+ while (next < data.images.length) {
107
+ let i = next++
108
+ let image = data.images[i]!
109
+ let source = image.bytes
110
+ let { kind, srgb } = uses[i]!
111
+ let setting = textureSettingFor(settings, kind, imageSource(image, model), matchesGlob)
112
+ if (!setting.compress || isKtx2(source)) {
113
+ baked.kept.images++
114
+ baked.kept.bytes += source.byteLength
115
+ continue
116
+ }
117
+ image.bytes = await encodeTexture(decodeImage(source), {
118
+ codec: setting.codec,
119
+ quality: setting.quality,
120
+ srgb,
121
+ mipmap: true,
122
+ wrap: "repeat",
123
+ })
124
+ let group = groupOf(kind, setting)
125
+ group.images++
126
+ group.from += source.byteLength
127
+ group.to += image.bytes.byteLength
128
+ }
129
+ }
130
+ await Promise.all(Array.from({ length: IMAGES_IN_FLIGHT }, lane))
131
+ let order: TextureKind[] = ["color", "normal", "data"]
132
+ baked.groups.sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind))
133
+ return baked
134
+ }
135
+
136
+ let input: string | undefined
137
+ let output: string | undefined
138
+ let compress: boolean | undefined
139
+ for (let i = 0; i < argv.length; i++) {
140
+ let arg = argv[i]!
141
+ if (arg === "--help" || arg === "-h") usage()
142
+ else if (arg === "-o" || arg === "--output") {
143
+ output = argv[++i]
144
+ if (output === undefined) usage("Missing value for " + arg)
145
+ } else if (arg === "--compress") compress = true
146
+ else if (arg === "--no-compress") compress = false
147
+ else if (arg.startsWith("-")) usage("Unknown option " + arg)
148
+ else if (input === undefined) input = arg
149
+ else usage("Unexpected argument " + arg)
150
+ }
151
+ if (input === undefined) usage("Missing input file")
152
+ if (output === undefined) output = basename(input, extname(input)) + ".sol3m"
153
+
154
+ // The parse is synchronous, so the external files it will open are read
155
+ // ahead of it (the same prefetch loadGltf does).
156
+ let dir = dirname(input)
157
+ let bytes = await file(input).bytes()
158
+ let files = new Map<string, Uint8Array>()
159
+ for (let uri of gltfExternalUris(bytes)) {
160
+ if (!files.has(uri)) files.set(uri, await file(join(dir, decodeURIComponent(uri))).bytes())
161
+ }
162
+
163
+ let started = performance.now()
164
+ let data = parseGltf(bytes, (uri) => files.get(uri)!)
165
+ let parsed = performance.now() - started
166
+
167
+ // Read and checked before the encode, so a mistake in the settings costs
168
+ // no bake.
169
+ let settings: TextureSettings
170
+ let unmatched: string[]
171
+ try {
172
+ settings = await projectTextureSettings(compress)
173
+ unmatched = await unmatchedPatterns(data, input, settings)
174
+ } catch (e) {
175
+ console.error(`The texture settings in ${PROJECT_CONFIG} cannot be used: ${e instanceof Error ? e.message : String(e)}`)
176
+ exit(1)
177
+ }
178
+ if (unmatched.length > 0) {
179
+ console.error(`The texture settings in ${PROJECT_CONFIG} (solidrt.textures.files) name nothing with:`)
180
+ for (let pattern of unmatched) console.error(` ${pattern}`)
181
+ console.error("A pattern is a path from the project root; after a model's path, #name picks an image embedded in it.")
182
+ exit(1)
183
+ }
184
+ started = performance.now()
185
+ let baked = await bakeImages(data, input, settings)
186
+ let bakeMs = performance.now() - started
187
+
188
+ let encoded = encodeModel(data)
189
+ await file(output).write(encoded)
190
+
191
+ let triangles = data.parts.reduce((n, p) => n + p.geometry.indices.length / 3, 0)
192
+ let vertices = data.parts.reduce((n, p) => n + p.geometry.vertices.byteLength / layoutStride(p.geometry.layout), 0)
193
+ let targets = data.parts.reduce((n, p) => n + (p.geometry.morphs?.names.length ?? 0), 0)
194
+ console.log(
195
+ `${output}: ${data.nodes.length} nodes, ${data.parts.length} parts, ${vertices} vertices, ${triangles} triangles, ` +
196
+ (targets > 0 ? `${targets} morph targets, ` : "") +
197
+ `${data.clips.length} clips, ${data.materials.length} materials, ${data.images.length} images, ${kib(encoded.byteLength)} ` +
198
+ `(parsed in ${parsed.toFixed(0)} ms)`,
199
+ )
200
+ if (baked.groups.length > 0) {
201
+ console.log(`Images as KTX2 (encoded in ${(bakeMs / 1000).toFixed(1)} s):`)
202
+ for (let group of baked.groups) {
203
+ console.log(` ${group.kind}: ${images(group.images)}, ${group.setting.codec} at quality ${group.setting.quality}, ${kib(group.from)} to ${kib(group.to)}`)
204
+ }
205
+ }
206
+ if (baked.kept.images > 0) console.log(`Kept as they are: ${images(baked.kept.images)}, ${kib(baked.kept.bytes)}`)
@@ -1,4 +1,4 @@
1
- // The node surface the tools use, declared here so `srt check
1
+ // The node surface the tools use, declared here so `sol check
2
2
  // packages/3d/tools/<name>.ts` typechecks them on the package's flux-typed
3
3
  // program (the nearest tsconfig is the repo root's). The tools run under
4
4
  // bun, but bun's own types cannot join this program: their globals
package/tools/splat.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /// <reference path="./node-env.d.ts" />
3
3
 
4
- // srt tool 3d/splat: bake a captured splat cloud into a .srts file - the
4
+ // sol tool 3d/splat: bake a captured splat cloud into a .sol3s file - the
5
5
  // parse, covariance bake and SH packing (src/splat-data.ts) run once here
6
6
  // under bun, the result written in the exact texel layout createSplatMesh
7
7
  // uploads and the engine sorts, without any per-splat work at runtime.
@@ -9,7 +9,7 @@
9
9
  // Everything here comes from the runtime-free `@solidrt/3d/splat` entry,
10
10
  // the same surface an app's own bake script uses.
11
11
  //
12
- // srt tool 3d/splat <in.ply|in.splat|in.spz> [-o <out.srts>] [--sh <0..3>] [--keep-orientation]
12
+ // sol tool 3d/splat <in.ply|in.splat|in.spz> [-o <out.sol3s>] [--sh <0..3>] [--keep-orientation]
13
13
  //
14
14
  // Inputs: a trainer's .ply, the de-facto .splat interchange, or Niantic's
15
15
  // .spz (gunzipped here; version 2 or 3). A y-down capture (.ply, .splat)
@@ -25,7 +25,7 @@ import { encodeSplat, parseSplat } from "../src/splat-data.ts"
25
25
 
26
26
  function usage(error?: string): never {
27
27
  if (error) console.error(error)
28
- console.log("Usage: srt tool 3d/splat <in.ply|in.splat|in.spz> [-o <out.srts>] [--sh <0..3>] [--keep-orientation]")
28
+ console.log("Usage: sol tool 3d/splat <in.ply|in.splat|in.spz> [-o <out.sol3s>] [--sh <0..3>] [--keep-orientation]")
29
29
  process.exit(error ? 1 : 0)
30
30
  }
31
31
 
@@ -51,7 +51,7 @@ for (let i = 0; i < args.length; i++) {
51
51
  else usage("Unexpected argument " + arg)
52
52
  }
53
53
  if (input === undefined) usage("Missing input file")
54
- if (output === undefined) output = basename(input, extname(input)) + ".srts"
54
+ if (output === undefined) output = basename(input, extname(input)) + ".sol3s"
55
55
 
56
56
  let bytes = new Uint8Array(readFileSync(input))
57
57
  // .spz is gzip-compressed; the parser takes the inflated bytes.
package/tools/model.ts DELETED
@@ -1,56 +0,0 @@
1
- #!/usr/bin/env bun
2
- /// <reference path="./node-env.d.ts" />
3
-
4
- // srt tool 3d/model: bake a glTF into a .srtm model file - the parse
5
- // (src/gltf.ts) run once here under bun, the result written in the exact
6
- // layout loadModel views without any per-vertex work at runtime. Same
7
- // parser, same subset, same result as loadGltf; this only moves the cost
8
- // to build time. Put the output under assets/ so it ships with the app.
9
- // Everything here comes from the runtime-free `@solidrt/3d/model` entry
10
- // (src/model-data.ts), the same surface an app's own bake script uses.
11
- //
12
- // srt tool 3d/model <in.gltf|in.glb> [-o <out.srtm>]
13
-
14
- import { readFileSync, writeFileSync } from "node:fs"
15
- import { basename, dirname, extname, join } from "node:path"
16
- import { encodeModel, layoutStride, parseGltf } from "../src/model-data.ts"
17
-
18
- function usage(error?: string): never {
19
- if (error) console.error(error)
20
- console.log("Usage: srt tool 3d/model <in.gltf|in.glb> [-o <out.srtm>]")
21
- process.exit(error ? 1 : 0)
22
- }
23
-
24
- let input: string | undefined
25
- let output: string | undefined
26
- let args = process.argv.slice(2)
27
- for (let i = 0; i < args.length; i++) {
28
- let arg = args[i]!
29
- if (arg === "--help" || arg === "-h") usage()
30
- else if (arg === "-o" || arg === "--output") {
31
- output = args[++i]
32
- if (output === undefined) usage("Missing value for " + arg)
33
- } else if (arg.startsWith("-")) usage("Unknown option " + arg)
34
- else if (input === undefined) input = arg
35
- else usage("Unexpected argument " + arg)
36
- }
37
- if (input === undefined) usage("Missing input file")
38
- if (output === undefined) output = basename(input, extname(input)) + ".srtm"
39
-
40
- let dir = dirname(input)
41
- let bytes = new Uint8Array(readFileSync(input))
42
- let started = performance.now()
43
- let data = parseGltf(bytes, (uri) => new Uint8Array(readFileSync(join(dir, decodeURIComponent(uri)))))
44
- let parsed = performance.now() - started
45
- let encoded = encodeModel(data)
46
- writeFileSync(output, encoded)
47
-
48
- let triangles = data.parts.reduce((n, p) => n + p.geometry.indices.length / 3, 0)
49
- let vertices = data.parts.reduce((n, p) => n + p.geometry.vertices.byteLength / layoutStride(p.geometry.layout), 0)
50
- let targets = data.parts.reduce((n, p) => n + (p.geometry.morphs?.names.length ?? 0), 0)
51
- console.log(
52
- `${output}: ${data.nodes.length} nodes, ${data.parts.length} parts, ${vertices} vertices, ${triangles} triangles, ` +
53
- (targets > 0 ? `${targets} morph targets, ` : "") +
54
- `${data.clips.length} clips, ${data.materials.length} materials, ${data.images.length} images, ${(encoded.byteLength / 1024).toFixed(0)} KiB ` +
55
- `(parsed in ${parsed.toFixed(0)} ms)`,
56
- )