ldr 0.5.0 → 0.6.0

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Render 100,000-brick LEGO® (LDraw) models in three.js at interactive frame rates.**
4
4
 
5
- `ldr` compiles the parts a model uses into a compact binary **parts pack** ahead of time. At runtime it draws them with GPU instancing, hidden-stud and underside culling, distance LOD (simplified meshes, sub-pixel studs dropped, per-cluster frustum culling), single-sided (BFC) faces and indexed edge lines. On an Apple M4 Pro that takes Datsville (392,514 bricks) from 410 ms to 27 ms per frame, and its load time from 24.7 s to 1.6 s.
5
+ `ldr` compiles the parts a model uses into a compact binary **parts pack** ahead of time. At runtime it draws them with GPU instancing, hidden-stud and underside culling, distance LOD (simplified meshes, sub-pixel studs dropped, per-cluster frustum culling), single-sided (BFC) faces and indexed edge lines. On an Apple M4 Pro that takes Datsville (392,514 bricks) from 410 ms to 12 ms per frame, and its load time from 24.7 s to 1.6 s.
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/ldr.svg)](https://www.npmjs.com/package/ldr) · MIT · ESM + TypeScript · `three` is a peer dependency
8
8
  - **Spec-compliant LDraw reader:** `.ldr`, `.mpd` and `.dat`, including MPD-embedded parts, geometry drawn directly in models, BFC, `STEP`/`ROTSTEP`, `!TEXMAP` and `!DATA`.
@@ -58,7 +58,7 @@ import { parseModel, loadPack, createLdrObject } from "ldr";
58
58
 
59
59
  const [text, pack] = await Promise.all([
60
60
  fetch("/car.ldr").then((r) => r.text()),
61
- loadPack("/car.pack.zst"), // .zst / .gz / .br / raw .bin
61
+ loadPack("/car.pack.zst"), // .zst / .gz / raw .bin (detected from the bytes)
62
62
  ]);
63
63
 
64
64
  const model = createLdrObject(parseModel(text, "car.ldr"), pack, {
@@ -136,6 +136,8 @@ MPD-embedded parts, such as Studio/OMR unofficial parts or stickers, become part
136
136
  | `lod` | `true` | Distance LOD, re-selected for each camera you render with. Batches are split into spatial clusters (off-screen ones are frustum-culled). Each cluster draws the coarsest simplified mesh (from `lod` packs) whose error stays under `lodPx`, drops studs whose 12-LDU disc is under `lodPx`, and hides edges where a stud spans less than `lodEdgePx`. |
137
137
  | `lodPx` | `0.5` | LOD: maximum screen-space error in pixels. |
138
138
  | `lodEdgePx` | `0.5` | LOD: draw edge and conditional lines only where one stud (20 LDU) spans at least this many pixels. `0` = always. |
139
+ | `lodEdgeMinPx` | `0.15` | LOD: draw only edge segments at least this many pixels long on screen. `0` = every segment. |
140
+ | `lodUnder` | `true` | LOD: skip undersides that can't be seen (camera behind every bottom face of a cluster's parts, or the body drawn as its closed bounding box). Lossless in the fidelity checks. |
139
141
 
140
142
  `LdrObject`: `object`, `stats` (`instances`, `drawn`, `missing[]`, `studsHidden/Total`, `undersHidden/Total`, `buildMs`, `condBatches`, `texBatches`, `textures`, `clusters`), `stepCount`, `stepViews`, `texturesReady`, `setStep(n | null)`, `setEdges(on)`, `setConditionalLines(on)`, `setCulling(studs, undersides)`, `setSide(side)`, `setLod(on)`, `getBox(target?)`, `dispose()`.
141
143
 
@@ -167,20 +169,20 @@ Sources: `bench/results/latest-perf.md` (the regression gate, `bench/baselines/d
167
169
  | model | bricks | baseline frame | **ldr frame** | baseline load | **ldr load** |
168
170
  |---|---|---|---|---|---|
169
171
  | synthetic grid | 100,021 | 24.4 ms | **2.7 ms** | 0.2 s | 0.3 s |
170
- | Datsville 10k | 10,000 | 45.9 ms | **2.2 ms** | 13.5 s | **0.34 s** |
171
- | Datsville 100k | 100,000 | 105.7 ms | **6.2 ms** | 19.5 s | **0.79 s** |
172
- | Datsville (full) | 392,514 | 409.6 ms | **27.2 ms** | 24.7 s | **1.6 s** |
172
+ | Datsville 10k | 10,000 | 45.9 ms | **1.9 ms** | 13.5 s | **0.36 s** |
173
+ | Datsville 100k | 100,000 | 105.7 ms | **4.4 ms** | 19.5 s | **0.79 s** |
174
+ | Datsville (full) | 392,514 | 409.6 ms | **12.5 ms** | 24.7 s | **1.6 s** |
173
175
 
174
- How much each optimization contributes on Datsville 100k, in ms per frame: welding 108.1 → 64.6, stud culling → 50.8, underside culling → 41.7, low-detail studs → 31.0, single-sided faces → 30.1. Triangles per frame drop from 142.7M to 59.1M. Distance LOD (Round 19) then takes it to 6.2 ms and 6.6M triangles; ≤ 1.6% of pixels differ from full detail (mostly sub-pixel stud moiré).
176
+ How much each optimization contributes on Datsville 100k, in ms per frame: welding 108.1 → 64.6, stud culling → 50.8, underside culling → 41.7, low-detail studs → 31.0, single-sided faces → 30.1. Triangles per frame drop from 142.7M to 59.1M. Distance LOD (Rounds 19–20: simplified and bounding-box levels with a silhouette-coverage error, sub-pixel studs dropped, undersides skipped where they can't be seen) then takes it to 4.4 ms and 4.0M triangles; ≤ 1.5% of pixels differ from full detail (mostly sub-pixel stud moiré).
175
177
 
176
- - **Pack size:** Datsville 100k (290 unique parts) is 25.8 MB raw, 3.98 MB gzip and **1.49 MB zstd** with LOD meshes (22.4 MB / 1.08 MB zstd with `lod: false`). Decoding a zstd pack in the browser (fzstd) takes about **60 ms**.
178
+ - **Pack size:** Datsville 100k (290 unique parts) is about 26 MB raw and **1.46 MB zstd** with LOD meshes (22.4 MB / 1.08 MB zstd with `lod: false`). Decoding a zstd pack in the browser (fzstd) takes about **60 ms**.
177
179
  - **Stock `LDrawLoader`:** loading the scene directly crashes the renderer (OOM) on the 50k, 100k and 250k grids and on Datsville 10k. The 10k grid loads in 6.6 s and draws at 61.5 ms per frame (29,671 draw calls), against 0.15 s and 2.2 ms with ldr + edges.
178
- - **Demo app** (`demo/`; rAF frame median at 2880×1800 on the M4 Pro):
180
+ - **Original demo app** (retired; rAF frame median at 2880×1800 on the M4 Pro):
179
181
  - These comparisons are between demo settings. "Before" has every effect on (shadows, AO, screen-space edges, PBR/IBL); "after" is the demo's big-model defaults, which turn those effects off above 50k bricks.
180
182
  - Full Datsville: **1.7 → 9.9 fps**.
181
183
  - Datsville 100k: **8 → 43.5 fps**.
182
184
  - **Conditional lines** (opt-in `cond` packs) cost **+12–14 %** frame time on Datsville 10k and 100k (M4 Pro), and add 7–14 % to the zstd pack size.
183
- - **Edges:** with LOD, edges cost Datsville 100k 6.2 → 9.2 ms and full Datsville nothing measurable (27.2 ms), because edges are hidden where bricks are sub-pixel (`lodEdgePx`). Without edge LOD they cost 35 ms (100k) and 153 ms (full).
185
+ - **Edges:** with LOD, edges cost Datsville 10k 1.9 → 3.3 ms and 100k 4.4 → 5.2 ms, and full Datsville nothing measurable, because each cluster draws only edge segments at least 0.15 px long (`lodEdgeMinPx`) and none where bricks are sub-pixel (`lodEdgePx`). Far views drawn with edges get lighter (≤ 4.7% of pixels on Datsville overviews); `lodEdgeMinPx: 0, lodEdgePx: 0` keeps every edge.
184
186
  - **Build steps:** `setStep` takes 66 ms on Datsville 100k and 29 ms on 10k (it was a full rebuild: 752 / 347 ms).
185
187
  - **Visual fidelity:** pixel diffs show culling and single-sided faces are lossless (≤ 0.009 % of pixels differ). Low-detail studs only change stud silhouettes.
186
188
 
@@ -203,7 +205,7 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
203
205
  - **ROTSTEP** is reported (`stepViews`, `instance.view`), not applied. Rotating the camera per step is up to your app.
204
206
  - **Packs are static:** they hold only the parts they were built for. A model that uses parts missing from the pack lists them in `stats.missing`.
205
207
  - **Occlusion culling:** beyond hidden studs and undersides there is no occlusion culling (bricks enclosed by other bricks are still drawn). Frustum culling and LOD apply per spatial cluster.
206
- - **LOD and edges:** at the default `lodEdgePx` edges disappear where bricks are a few pixels wide, which visibly lightens far-away views drawn with edges (4–7% of pixels on Datsville overviews). Set `lodEdgePx: 0` to keep every edge (much slower on large models), or `lod: false` to turn LOD off entirely.
208
+ - **LOD and edges:** edge LOD drops sub-pixel edge segments and hides edges where bricks are a fraction of a pixel, which visibly lightens far-away views drawn with edges (up to 4.7% of pixels on Datsville overviews). Set `lodEdgeMinPx: 0, lodEdgePx: 0` to keep every edge (much slower on large models), or `lod: false` to turn LOD off entirely.
207
209
 
208
210
  ## Attribution and license
209
211
 
package/dist/cdn.d.ts CHANGED
@@ -1,45 +1,11 @@
1
- import { type Pack } from "./render.js";
2
- import { type PartInstance } from "./model.js";
1
+ import { type Pack } from "./pack-io.js";
2
+ import type { PartInstance } from "./model.js";
3
3
  import { type LdrObject, type LdrObjectOptions } from "./scene.js";
4
- /** Default parts CDN (LDraw library parts, CC BY 2.0 / 4.0, compiled by ldr; hosted with the demo site). */
5
- export declare const DEFAULT_PARTS_CDN = "https://ldr-demo.vercel.app/cdn/v1/";
6
- export interface PartsCdnIndex {
7
- format: string;
8
- release?: string;
9
- license?: string;
10
- archives: string[];
11
- bytes: number;
12
- colors: Record<string, string>;
13
- parts: Record<string, [number, number, number]>;
14
- }
15
- export interface LoadPartsOptions {
16
- /** Base URL of the parts CDN (ending in /). Default `DEFAULT_PARTS_CDN`. */
17
- cdn?: string;
18
- /** Merge two byte ranges into one request when the gap between them is at most this many bytes. Default 64 KB. */
19
- gap?: number;
20
- /** Max parallel Range requests. Default 6. */
21
- concurrency?: number;
22
- fetch?: typeof fetch;
23
- }
24
- /** Fetch (and cache) a parts CDN index. */
25
- export declare function loadPartsIndex(cdn?: string, f?: typeof fetch): Promise<PartsCdnIndex>;
26
- /** Merge several packs into one (parts and stud kinds; the first pack's definition wins on name clashes). */
27
- export declare function mergePacks(packs: Pack[], extra?: Record<string, unknown>): Pack;
28
- /**
29
- * Fetch the given parts from a parts CDN and return them as one Pack (with the LDConfig colour table).
30
- * Parts the CDN does not have are listed in `missing`.
31
- */
32
- export declare function loadParts(names: Iterable<string>, o?: LoadPartsOptions): Promise<{
33
- pack: Pack;
34
- missing: string[];
35
- requests: number;
36
- bytes: number;
37
- }>;
38
- export interface LoadModelOptions extends LdrObjectOptions, LoadPartsOptions {
39
- /** Treat `source` as LDraw text instead of a URL. */
40
- text?: boolean;
41
- /** File name used for the main model (MPD resolution). Default: last URL segment. */
42
- name?: string;
4
+ import { type PrepareOptions } from "./parts-cdn.js";
5
+ export { DEFAULT_PARTS_CDN, loadPartsIndex, loadParts, mergePacks, prepareModel, type PartsCdnIndex, type LoadPartsOptions, type PrepareOptions, type PreparedModel } from "./parts-cdn.js";
6
+ export interface LoadModelOptions extends LdrObjectOptions, PrepareOptions {
7
+ /** Run parsing / part inflation / pack merging in a Web Worker (default true when available; ignored with a custom `fetch`). */
8
+ worker?: boolean;
43
9
  }
44
10
  export interface LoadedModel extends LdrObject {
45
11
  pack: Pack; /** The flattened model (part instances). */
@@ -53,6 +19,7 @@ export interface LoadedModel extends LdrObject {
53
19
  };
54
20
  requests: number;
55
21
  bytes: number;
22
+ /** Where the prepare step ran. */ ranIn: "worker" | "main";
56
23
  }
57
24
  /**
58
25
  * One call from an `.ldr` / `.mpd` URL (or text) to a three.js object: fetches the model, fetches just the parts it uses from the
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/cli.ts
3
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/cli.ts
4
4
  import fs4 from "node:fs";
5
5
  import zlib from "node:zlib";
6
6
 
7
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/node/build-pack.ts
7
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/build-pack.ts
8
8
  import fs3 from "node:fs";
9
9
  import path3 from "node:path";
10
10
 
11
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/node/resolver.ts
11
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/resolver.ts
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
14
 
15
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/spec.ts
15
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/spec.ts
16
16
  var norm = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
17
17
  function parseColor(tok) {
18
18
  if (/^0x/i.test(tok))
@@ -100,6 +100,36 @@ var b64 = (s) => {
100
100
  out[i] = bin.charCodeAt(i);
101
101
  return out;
102
102
  };
103
+ function type1Fast(line) {
104
+ const n = line.length;
105
+ let i = 1;
106
+ const ws = (c) => c === 32 || c === 9;
107
+ const next = () => {
108
+ while (i < n && ws(line.charCodeAt(i)))
109
+ i++;
110
+ const a = i;
111
+ while (i < n && !ws(line.charCodeAt(i)))
112
+ i++;
113
+ return a < i ? line.slice(a, i) : null;
114
+ };
115
+ const ct = next();
116
+ if (ct === null)
117
+ return null;
118
+ const m = new Array(12);
119
+ for (let k = 0;k < 12; k++) {
120
+ const t = next();
121
+ if (t === null)
122
+ return null;
123
+ m[k] = +t;
124
+ }
125
+ while (i < n && ws(line.charCodeAt(i)))
126
+ i++;
127
+ if (i >= n)
128
+ return null;
129
+ const rest = line.slice(i);
130
+ const file = /[ \t]{2,}|\t/.test(rest) ? rest.split(/\s+/).join(" ") : rest;
131
+ return { t: 1, color: parseColor(ct), m, file: norm(file) };
132
+ }
103
133
  function geomLine(tk) {
104
134
  const t = +tk[0];
105
135
  if (!(t >= 1 && t <= 5) || tk.length < 2)
@@ -168,7 +198,8 @@ function parse(src, name = "main.ldr", popts = {}) {
168
198
  } else
169
199
  c.tex = top.map;
170
200
  };
171
- for (const raw of src.split(/\r?\n/)) {
201
+ for (const raw of src.split(`
202
+ `)) {
172
203
  const line = raw.trim();
173
204
  if (!line)
174
205
  continue;
@@ -219,6 +250,17 @@ function parse(src, name = "main.ldr", popts = {}) {
219
250
  continue;
220
251
  }
221
252
  }
253
+ if (line.charCodeAt(0) === 49 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9) && !dataCur && !skip) {
254
+ const g = type1Fast(line);
255
+ if (g) {
256
+ if (!cur)
257
+ start(name);
258
+ annotate(g, false);
259
+ cur.cmds.push(g);
260
+ inHeader = false;
261
+ continue;
262
+ }
263
+ }
222
264
  const tk = line.split(/\s+/);
223
265
  const t = +tk[0];
224
266
  if (t === 0) {
@@ -662,7 +704,7 @@ function fileStepViews(f) {
662
704
  return out;
663
705
  }
664
706
 
665
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/model.ts
707
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/model.ts
666
708
  function flatten(doc, opts = {}) {
667
709
  const out = [];
668
710
  const maxDepth = opts.maxDepth ?? 64;
@@ -758,7 +800,7 @@ function flatten(doc, opts = {}) {
758
800
  return out;
759
801
  }
760
802
  function parseModel(text, name = "main.ldr", opts) {
761
- return flatten(parse(text, name), opts);
803
+ return flatten(parse(text, name, { geometryText: true }), opts);
762
804
  }
763
805
  function sequenceSteps(insts) {
764
806
  const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
@@ -784,7 +826,7 @@ function parseLDConfig(text) {
784
826
  return out;
785
827
  }
786
828
 
787
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/node/resolver.ts
829
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/resolver.ts
788
830
  var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
789
831
  var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
790
832
  var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
@@ -1054,11 +1096,11 @@ function collectDeps(lib, refs) {
1054
1096
  return out;
1055
1097
  }
1056
1098
 
1057
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/node/compile.ts
1099
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/compile.ts
1058
1100
  import fs2 from "node:fs";
1059
1101
  import path2 from "node:path";
1060
1102
 
1061
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/node/lod.ts
1103
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/lod.ts
1062
1104
  import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer";
1063
1105
  await Promise.all([MeshoptSimplifier.ready, MeshoptEncoder.ready]);
1064
1106
  function reorderMesh(P, N, C, I) {
@@ -1081,8 +1123,10 @@ function reorderMesh(P, N, C, I) {
1081
1123
  }
1082
1124
  return { P: P2, N: N2, C: C2, I: idx };
1083
1125
  }
1084
- var TARGETS = [0.25, 0.08];
1126
+ var TARGETS = [0.25, 0.08, 0.025];
1085
1127
  var MIN_TRIS = 64;
1128
+ var MIN_BOX_TRIS = 24;
1129
+ var COVERAGE_WEIGHT = Number(process.env.LDR_LOD_COVERAGE ?? 0.5);
1086
1130
  function flatMesh(tris) {
1087
1131
  const map = new Map;
1088
1132
  const P = [], N = [], C = [], I = [];
@@ -1112,9 +1156,62 @@ function flatMesh(tris) {
1112
1156
  }
1113
1157
  return { P: new Float32Array(P), N: new Float32Array(N), C: new Float32Array(C), I: new Uint32Array(I) };
1114
1158
  }
1159
+ var G = 64;
1160
+ function silhouettes(P, I, box) {
1161
+ const masks = [0, 1, 2].map(() => new Uint8Array(G * G));
1162
+ for (let ax = 0;ax < 3; ax++) {
1163
+ const u = (ax + 1) % 3, v = (ax + 2) % 3;
1164
+ const m = masks[ax];
1165
+ const su = (box.hi[u] - box.lo[u]) / G || 1, sv = (box.hi[v] - box.lo[v]) / G || 1;
1166
+ for (let t = 0;t < I.length; t += 3) {
1167
+ const p = [I[t], I[t + 1], I[t + 2]].map((k) => [(P[k * 3 + u] - box.lo[u]) / su, (P[k * 3 + v] - box.lo[v]) / sv]);
1168
+ const area = (p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]);
1169
+ if (Math.abs(area) < 0.000000000001)
1170
+ continue;
1171
+ const x0 = Math.max(0, Math.floor(Math.min(p[0][0], p[1][0], p[2][0]))), x1 = Math.min(G - 1, Math.ceil(Math.max(p[0][0], p[1][0], p[2][0])));
1172
+ const y0 = Math.max(0, Math.floor(Math.min(p[0][1], p[1][1], p[2][1]))), y1 = Math.min(G - 1, Math.ceil(Math.max(p[0][1], p[1][1], p[2][1])));
1173
+ for (let y = y0;y <= y1; y++)
1174
+ for (let x = x0;x <= x1; x++) {
1175
+ if (m[y * G + x])
1176
+ continue;
1177
+ const cx = x + 0.5, cy = y + 0.5;
1178
+ let inside = true;
1179
+ for (let e = 0;e < 3 && inside; e++) {
1180
+ const a0 = p[e], a1 = p[(e + 1) % 3];
1181
+ const w = (a1[0] - a0[0]) * (cy - a0[1]) - (cx - a0[0]) * (a1[1] - a0[1]);
1182
+ if (w * area < 0)
1183
+ inside = false;
1184
+ }
1185
+ if (inside)
1186
+ m[y * G + x] = 1;
1187
+ }
1188
+ }
1189
+ }
1190
+ return masks;
1191
+ }
1192
+ function silhouetteDiff(a, b) {
1193
+ let worst = 0;
1194
+ for (let ax = 0;ax < 3; ax++) {
1195
+ let d = 0;
1196
+ for (let i = 0;i < G * G; i++)
1197
+ d += a[ax][i] ^ b[ax][i];
1198
+ worst = Math.max(worst, d / (G * G));
1199
+ }
1200
+ return worst;
1201
+ }
1202
+ function boxMesh(box, col) {
1203
+ const [x0, y0, z0] = box.lo, [x1, y1, z1] = box.hi;
1204
+ const c = [[x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0], [x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]];
1205
+ const quads = [[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4], [3, 7, 6, 2], [0, 4, 7, 3], [1, 2, 6, 5]];
1206
+ const p = new Float32Array(c.flat());
1207
+ const idx = [];
1208
+ for (const q of quads)
1209
+ idx.push(q[0], q[1], q[2], q[0], q[2], q[3]);
1210
+ return flatMesh([{ p, i: Uint32Array.from(idx), col }]);
1211
+ }
1115
1212
  function lodChain(P, C, I) {
1116
1213
  const nTris = I.length / 3;
1117
- if (nTris < MIN_TRIS)
1214
+ if (nTris < MIN_BOX_TRIS)
1118
1215
  return [];
1119
1216
  const groups = new Map;
1120
1217
  for (let t = 0;t < I.length; t += 3) {
@@ -1145,27 +1242,57 @@ function lodChain(P, C, I) {
1145
1242
  });
1146
1243
  const out = [];
1147
1244
  let prevTris = nTris;
1148
- for (let li = 0;li < TARGETS.length; li++) {
1149
- let err = 0;
1150
- const parts = [];
1151
- for (const g of welded) {
1152
- const target = Math.max(3, Math.floor(g.i.length * TARGETS[li] / 3) * 3);
1153
- const [idx, e] = MeshoptSimplifier.simplify(g.i, g.p, 3, target, 1e9, li ? ["ErrorAbsolute", "Prune"] : ["ErrorAbsolute"]);
1154
- err = Math.max(err, e);
1155
- if (idx.length)
1156
- parts.push({ p: g.p, i: idx, col: g.col });
1245
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
1246
+ for (let v = 0;v < P.length; v += 3)
1247
+ for (let k = 0;k < 3; k++) {
1248
+ lo[k] = Math.min(lo[k], P[v + k]);
1249
+ hi[k] = Math.max(hi[k], P[v + k]);
1157
1250
  }
1158
- const m = flatMesh(parts);
1159
- const t = m.I.length / 3;
1160
- if (t > prevTris * 0.7 || !t)
1161
- continue;
1162
- out.push({ ...m, err: +Math.max(err, 0.001).toFixed(3) });
1163
- prevTris = t;
1251
+ const box = { lo, hi };
1252
+ const diag = Math.hypot(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]);
1253
+ const S0 = silhouettes(P, I, box);
1254
+ const coverageErr = (m) => COVERAGE_WEIGHT * silhouetteDiff(S0, silhouettes(m.P, m.I, box)) * diag;
1255
+ for (let li = 0;li < (nTris >= MIN_TRIS ? TARGETS.length : 0); li++) {
1256
+ let best = null;
1257
+ for (const sloppy of [false, true]) {
1258
+ let err = 0;
1259
+ const parts = [];
1260
+ for (const g of welded) {
1261
+ const target = Math.max(3, Math.floor(g.i.length * TARGETS[li] / 3) * 3);
1262
+ const [idx, e] = sloppy ? MeshoptSimplifier.simplifySloppy(g.i, g.p, 3, null, target, 1e9) : MeshoptSimplifier.simplify(g.i, g.p, 3, target, 1e9, li ? ["ErrorAbsolute", "Prune"] : ["ErrorAbsolute"]);
1263
+ err = Math.max(err, sloppy ? e * MeshoptSimplifier.getScale(g.p, 3) : e);
1264
+ if (idx.length)
1265
+ parts.push({ p: g.p, i: idx, col: g.col });
1266
+ }
1267
+ const m = flatMesh(parts);
1268
+ const t = m.I.length / 3;
1269
+ if (!t || t > prevTris * 0.7)
1270
+ continue;
1271
+ const e = +Math.max(err, coverageErr(m), 0.001).toFixed(3);
1272
+ if (!best || e < best.err)
1273
+ best = { ...m, err: e };
1274
+ }
1275
+ if (best) {
1276
+ out.push(best);
1277
+ prevTris = best.I.length / 3;
1278
+ }
1279
+ }
1280
+ if (prevTris * 0.7 >= 12) {
1281
+ const main = welded.reduce((a, g) => g.i.length > a.i.length ? g : a);
1282
+ const m = boxMesh(box, main.col);
1283
+ let inner = 0;
1284
+ for (let v = 0;v < P.length; v += 3) {
1285
+ let d = Infinity;
1286
+ for (let k = 0;k < 3; k++)
1287
+ d = Math.min(d, P[v + k] - lo[k], hi[k] - P[v + k]);
1288
+ inner = Math.max(inner, d);
1289
+ }
1290
+ out.push({ ...m, err: +Math.max(inner, coverageErr(m) / COVERAGE_WEIGHT, out.length ? out[out.length - 1].err : 0, 0.001).toFixed(3) });
1164
1291
  }
1165
1292
  return out;
1166
1293
  }
1167
1294
 
1168
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/node/compile.ts
1295
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/compile.ts
1169
1296
  function loadColors(root) {
1170
1297
  const map = new Map;
1171
1298
  for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
@@ -1748,7 +1875,7 @@ function encodePack(parts, studKinds, weld, extra, options = {}) {
1748
1875
  return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
1749
1876
  }
1750
1877
 
1751
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/node/build-pack.ts
1878
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/build-pack.ts
1752
1879
  function buildPack(o) {
1753
1880
  const lib = new Library(o.ldrawRoot);
1754
1881
  const refs = new Map;
@@ -1811,7 +1938,7 @@ function buildPack(o) {
1811
1938
  return { data: encodePack(parts, kinds, comp.opts.weld, extra, { texture, lod: o.lod !== false, reorder: !!o.reorder }), parts: parts.size, studKinds: kinds.size, missing: [...missing], ...warnings.length ? { warnings } : {} };
1812
1939
  }
1813
1940
 
1814
- // ../../../../../../../tmp/ldr-build-qIdKSv/src/cli.ts
1941
+ // ../../../../../../../tmp/ldr-build-NB5kkP/src/cli.ts
1815
1942
  var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
1816
1943
 
1817
1944
  Usage:
package/dist/cull.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { PartInstance } from './model.js';
2
+ export type Inst = {
3
+ part: string;
4
+ color: string;
5
+ m: number[];
6
+ };
7
+ export declare const upright: (m: ArrayLike<number>, o?: number) => boolean;
8
+ /** Per-part data used by culling and stud placement, prepared once per pack header. */
9
+ export type PrepPart = {
10
+ nStuds: number;
11
+ studM: Float64Array;
12
+ kind: string[];
13
+ color: string[];
14
+ mask: Uint8Array | null;
15
+ nx: number;
16
+ nz: number;
17
+ };
18
+ export declare function prepPart(H: any, part: string): PrepPart | null;
19
+ /** w = a * b for LDraw 12-number matrices (x y z a b c d e f g h i); same arithmetic as the instance flattener. */
20
+ export declare function mul12To(a: ArrayLike<number>, b: ArrayLike<number>, bo: number, w: Float64Array, ao?: number): void;
21
+ export declare const det12: (m: ArrayLike<number>) => number;
22
+ /** Open-addressing hash table from int32 triples to int32 values (-1 = absent); grows as needed. */
23
+ export declare class Int3Table {
24
+ a: Int32Array;
25
+ b: Int32Array;
26
+ c: Int32Array;
27
+ vals: Int32Array;
28
+ used: Uint8Array;
29
+ mask: number;
30
+ size: number;
31
+ constructor(cap?: number);
32
+ private slot;
33
+ get(x: number, y: number, z: number): number;
34
+ /** Sets (x, y, z) -> v and returns the previous value (-1 if none). */
35
+ put(x: number, y: number, z: number, v: number): number;
36
+ private grow;
37
+ }
38
+ export declare const inRange: (x: number, y: number, z: number) => boolean;
39
+ /** Culling result in flat arrays. Studs are enumerated in instance order, then each part's `studs` order (the order `forEachStud` visits). */
40
+ export type CullResult = {
41
+ nStuds: number;
42
+ studHidden: Uint8Array;
43
+ studUp: Uint8Array;
44
+ underHidden: Uint8Array;
45
+ studsHidden: number;
46
+ /** With `steps`: the first build step at which each stud / underside is hidden (STEP_NEVER = never), for step-aware culling. */
47
+ studHideStep?: Int32Array;
48
+ underHideStep?: Int32Array;
49
+ };
50
+ /** `studHideStep` / `underHideStep` value for "never hidden". */
51
+ export declare const STEP_NEVER = 2147483647;
52
+ /** Visit every stud (instance order, then part stud order) with its world matrix `w` (scratch; copy to keep). */
53
+ export declare function forEachStud(insts: Inst[], H: any, fn: (j: number, inst: number, w: Float64Array, kind: string, color: string, stud: number) => void): void;
54
+ /** `steps` (optional, per instance): also compute when each stud / underside becomes hidden as the model is built step by step. */
55
+ export declare function cullCore(insts: Inst[], H: any, steps?: ArrayLike<number>): CullResult;
56
+ /** Scene instances and per-instance step ranks as `createLdrObject` builds them (shared with the load worker). */
57
+ export declare function sceneInstances(instances: PartInstance[], H: any, useTex: boolean): {
58
+ all: {
59
+ part: string;
60
+ color: string;
61
+ m: import("./spec.js").M;
62
+ steps: number[];
63
+ }[];
64
+ stepOf: number[];
65
+ nSteps: number;
66
+ };
67
+ /** Culling for `createLdrObject(instances, pack, { textures })`, computable off the main thread and passed back as `precomputedCull`. */
68
+ export declare function precomputeCull(instances: PartInstance[], H: any, useTex?: boolean): CullResult;
package/dist/index.d.ts CHANGED
@@ -2,10 +2,11 @@ export { parse, parseColor, isDirectColor, mul, I as IDENTITY, norm, smoothNorma
2
2
  export type { M as Matrix12, Doc, LFile, Cmd, RotStep, BufExchg, TexMap, TexMapCmd, TexMapMethod, TexAnno, DataBlock, PartMesh, FileKind, StepView } from "./spec.js";
3
3
  export { flatten, parseModel, sequenceSteps, parseLDConfig } from "./model.js";
4
4
  export type { PartInstance, FlattenOptions, LDrawColor } from "./model.js";
5
- export { loadPack, decodePack, buildLdrScene, makeMaterial, createInstancedEdges, computeCulling, cullCore, forEachStud, conditionalLines, createInstancedConditionalEdges, conditionalLineVisible } from "./render.js";
5
+ export { loadPack, decodePack, buildLdrScene, makeMaterial, createInstancedEdges, computeCulling, cullCore, forEachStud, conditionalLines, createInstancedConditionalEdges, conditionalLineVisible, warmPackGeometry } from "./render.js";
6
+ export { precomputeCull, sceneInstances } from "./cull.js";
6
7
  export type { Pack, LdrParams, Inst, CullResult } from "./render.js";
7
8
  export { createLdrObject } from "./scene.js";
8
9
  export type { LdrObject, LdrObjectOptions, LdrStats, ColorSource } from "./scene.js";
9
10
  export { decompressPack } from "./pack-io.js";
10
- export { loadModel, loadParts, loadPartsIndex, mergePacks, DEFAULT_PARTS_CDN } from "./cdn.js";
11
- export type { LoadModelOptions, LoadedModel, LoadPartsOptions, PartsCdnIndex } from "./cdn.js";
11
+ export { loadModel, loadParts, loadPartsIndex, mergePacks, prepareModel, DEFAULT_PARTS_CDN } from "./cdn.js";
12
+ export type { LoadModelOptions, LoadedModel, LoadPartsOptions, PartsCdnIndex, PrepareOptions, PreparedModel } from "./cdn.js";