ldr 0.29.1 → 0.31.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 12 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 a fleet of 75 UCS Millennium Falcons (394,950 bricks) from 205 ms to 29 ms per frame, and its load time from 2.4 s to 1.0 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`.
@@ -83,7 +83,7 @@ Light the scene (or set `scene.environment`) as you would for any `MeshStandardM
83
83
  | export | description |
84
84
  |---|---|
85
85
  | `parse(text, name?, { geometryText? }) → Doc` | Parses LDraw text into files and commands. MPD-aware: `0 FILE`/`0 NOFILE` and `0 !DATA` blocks. |
86
- | `flatten(doc, options?) → PartInstance[]` | Expands submodels into placed parts. Each instance has `part`, `color`, a 12-number matrix `m`, `step`, nested `steps`, `path` and the ROTSTEP `view` of its step. `colour` (an `LDrawColor`) is set when a `0 !COLOUR` line in the model itself defines the instance's colour. |
86
+ | `flatten(doc, options?) → PartInstance[]` | Expands submodels into placed parts. Each instance has `part`, `color`, a 12-number matrix `m`, `step`, nested `steps`, `path` and the ROTSTEP `view` of its step. `colour` (an `LDrawColor`) is set when a `0 !COLOUR` line in the model itself defines the instance's colour. `colPart` / `colSource` name the part with the model's definitions in scope applied inside it (a synthetic part, `<part>#col-<hash>`; packs from `buildPack` have it only when it changes a colour, and the renderer falls back to `part`). |
87
87
  | `parseModel(text, name?, options?)` | `parse` followed by `flatten`. |
88
88
  | `sequenceSteps(instances) → { count, stepOf, views }` | Gives each instance a global build-step index, sequencing nested submodel steps. `views` holds the ROTSTEP view per global step. |
89
89
  | `parseLDConfig(text) → Map<code, LDrawColor>` | Reads a `!COLOUR` table into `{ code, name, hex, edge, alpha, luminance, finish }` (CHROME / PEARLESCENT / RUBBER / METAL / MATTE_METALLIC / MATERIAL GLITTER|SPECKLE|FABRIC). |
@@ -178,29 +178,31 @@ All numbers below were measured with the harness in [`bench/`](https://github.co
178
178
  - **Hardware and browser:** Apple **M4 Pro** MacBook Pro, Chrome with native GL (ANGLE → Metal), Bun 1.3.
179
179
  - **Frame time:** the **median synced frame time** (`gl.finish` per frame) while orbiting the camera.
180
180
  - **Load:** from fetch to the first rendered frame.
181
- - **Baseline:** three.js geometry from `LDrawLoader` merged per part and drawn with `InstancedMesh`, which is the fastest stock approach. Stock `LDrawLoader` scenes crash the tab (OOM) at 50k bricks and above, and already on Datsville 10k.
181
+ - **Baseline:** three.js geometry from `LDrawLoader` merged per part and drawn with `InstancedMesh`, which is the fastest stock approach. Stock `LDrawLoader` scenes crash the tab (OOM) at 50k bricks and above.
182
182
  - **ldr settings:** the defaults: LOD pack, distance LOD, low-detail studs, stud and underside culling, single-sided faces, no edges.
183
183
 
184
- Sources: `bench/results/latest-perf.md` (the regression gate, `bench/baselines/darwin-apple-m4-pro.json`), `latest-mac-ablation.md`, and `research.md` §7 and Round 19.
184
+ Sources: `bench/results/2026-10-18T18-10-46-518Z-falcon-vs-stock.md` (the Falcon rows), `bench/results/latest-perf.md` (the regression gate, `bench/baselines/darwin-apple-m4-pro.json`), `latest-mac-ablation.md`, and `research.md` §7 and Round 19.
185
185
 
186
186
  | model | bricks | baseline frame | **ldr frame** | baseline load | **ldr load** |
187
187
  |---|---|---|---|---|---|
188
188
  | synthetic grid | 100,021 | 24.4 ms | **2.7 ms** | 0.2 s | 0.3 s |
189
- | Datsville 10k | 10,000 | 45.9 ms | **1.9 ms** | 13.5 s | **0.36 s** |
190
- | Datsville 100k | 100,000 | 105.7 ms | **4.4 ms** | 19.5 s | **0.79 s** |
191
- | Datsville (full) | 392,514 | 409.6 ms | **12.5 ms** | 24.7 s | **1.6 s** |
189
+ | Millennium Falcon ×2 | 10,532 | 8.0 ms | 8.2 ms | 1.98 s | **0.14 s** |
190
+ | Millennium Falcon ×19 | 100,054 | 54.2 ms | **19.6 ms** | 2.04 s | **0.38 s** |
191
+ | Millennium Falcon ×75 | 394,950 | 204.7 ms | **29.2 ms** | 2.40 s | **1.03 s** |
192
192
 
193
- 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é).
193
+ The benchmark model is a fleet: the LDraw OMR model of set 10179 (UCS Millennium Falcon, 5,266 parts, 251 unique) placed 2, 19 or 75 times on a grid (`apps/demo-site/scripts/build-fleet.ts`). It is a hard case for ldr: dense, detailed ships stay large on screen, so distance LOD can't drop much and the overview still draws ~32M triangles; on two ships ldr only matches stock instancing per frame (it still loads 14× faster). Datsville, the benchmark until Round 60 (a flat town, 392,514 bricks, mostly baseplates), went from 409.6 to 12.5 ms per frame and 24.7 to 1.6 s load.
194
194
 
195
- - **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**.
196
- - **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.
195
+ How much each optimization contributed on Datsville 100k (Rounds 1–20, before the Falcon fleets), 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é).
196
+
197
+ - **Pack size:** the Falcon fleet (251 unique parts) is 24.5 MB raw and **1.65 MB zstd** with LOD meshes, at any number of ships. Decoding a zstd pack in the browser (fzstd) takes about **60 ms**.
198
+ - **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.
197
199
  - **Original demo app** (retired; rAF frame median at 2880×1800 on the M4 Pro):
198
200
  - 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.
199
201
  - Full Datsville: **1.7 → 9.9 fps**.
200
202
  - Datsville 100k: **8 → 43.5 fps**.
201
- - **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.
202
- - **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.
203
- - **Build steps:** `setStep` takes 66 ms on Datsville 100k and 29 ms on 10k (it was a full rebuild: 752 / 347 ms).
203
+ - **Conditional lines** (opt-in `cond` packs) cost **+12–14 %** frame time on Datsville 10k and 100k (M4 Pro, before Round 60), and add 7–14 % to the zstd pack size.
204
+ - **Edges:** with LOD, edges take the Falcon fleet from 19.8 → 42.2 ms (19 ships) and 29.4 → 56.3 ms (75 ships). Each cluster draws only edge segments at least 0.15 px long (`lodEdgeMinPx`) and none where bricks are sub-pixel (`lodEdgePx`), which saves little on ships that fill the screen. Far views drawn with edges get lighter (≤ 4.7% of pixels on Datsville overviews, 0.27% on the fleet); `lodEdgeMinPx: 0, lodEdgePx: 0` keeps every edge.
205
+ - **Build steps:** `setStep` takes 39 ms on the 19-ship fleet (26,619 steps); on Datsville 100k it went from a full rebuild (752 ms) to 66 ms.
204
206
  - **Visual fidelity:** pixel diffs show culling and single-sided faces are lossless (≤ 0.009 % of pixels differ). Low-detail studs only change stud silhouettes.
205
207
 
206
208
  Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun run bench` (see [`bench/README.md`](https://github.com/EvanBacon/ldr/blob/main/bench/README.md)).
@@ -213,7 +215,7 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
213
215
  - `0 STEP`, nested submodel step sequencing, `0 ROTSTEP` (`REL`/`ADD`/`ABS`/`END`), and `0 BUFEXCHG`.
214
216
  - `!TEXMAP` `START`/`NEXT`/`FALLBACK`/`END`, with `PLANAR`, `CYLINDRICAL` and `SPHERICAL` projections (orientation matches LDView), `GLOSSMAP` (alpha = gloss amount, same UVs), and model-level `!TEXMAP` around part references.
215
217
  - Colours: LDConfig `!COLOUR`, edge colours, main/edge colour inheritance (16/24), and direct colours (`0x2RRGGBB`).
216
- - `!COLOUR` lines in a model follow the spec's scope rule. A definition applies from its line to the end of its file and to the files that file references, and it never leaks back to the parent. It sets the instance colour and finish. `!COLOUR` lines inside a part (an MPD-embedded part and its subparts) follow the same rule when the part is compiled, and a model file's own loose geometry sees that file's definitions: its triangles and studs take the part's own definitions (RGB; baked part colours are opaque, as for LDConfig codes). A model's definitions don't reach the colours hard-coded inside the parts it places: those come from LDConfig or the part's own definitions, because parts are precompiled.
218
+ - `!COLOUR` lines in a model follow the spec's scope rule. A definition applies from its line to the end of its file and to the files that file references, and it never leaks back to the parent. It sets the instance colour and finish. `!COLOUR` lines inside a part (an MPD-embedded part and its subparts) follow the same rule when the part is compiled, and a model file's own loose geometry sees that file's definitions: its triangles and studs take the part's own definitions (RGB; baked part colours are opaque, as for LDConfig codes). A model's definitions also reach the colours hard-coded inside the parts it places ("passed to subfiles"): `buildPack` compiles a variant of such a part with those definitions applied (only where one changes a colour the part uses), and the renderer draws it; the part's own later definitions still win inside it. Parts from the hosted parts CDN (`loadModel` / `prepareModel`) don't have these variants yet, so there the colours inside parts come from LDConfig or the part's own definitions.
217
219
  - Named colour codes: a `!COLOUR` CODE may be an identifier instead of a number (`0 !COLOUR My_Red CODE MyRed VALUE #FF0000 EDGE #333333`, then `1 myred ...`), which the spec allows outside strict LDraw compatibility. Identifiers are case-insensitive and follow the same scope rule. `parseColor` turns one into a stable negative code (`namedColorCode`, `isNamedColor`), and `colorToken` turns it back into the identifier. A named code with no definition in scope draws the fallback colour.
218
220
  - Blended colour codes 256-511 that LDConfig doesn't define (used by old models, e.g. 382 in Datsville) render as the spec's average of two ldraw.exe colours instead of the fallback grey.
219
221
  - Case-insensitive, backslash-normalized file lookup; `~Moved to` alias parts.
@@ -221,7 +223,7 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
221
223
  ## Limitations
222
224
 
223
225
  - **Finishes:** `!COLOUR` CHROME / PEARLESCENT / RUBBER / METAL / MATTE_METALLIC map to MeshStandard/Physical PBR (same numbers as three.js `LDrawLoader`). MATERIAL GLITTER / SPECKLE use a procedural grain approximation; FABRIC uses sheen. ALPHA sets opacity; LUMINANCE drives emissive.
224
- - **Transparency:** transparent colours (`ALPHA < 255`) render with opacity and `depthWrite: false` (one material bucket per finish × opacity class).
226
+ - **Transparency:** transparent colours (`ALPHA < 255`) render with their opacity and `depthWrite: false` (one material bucket per finish × opacity class), from `ldr pack` packs and, since 0.31, from the parts CDN (`loadModel`), whose index now carries the colour materials. Studs and undersides behind a transparent part are never culled. Transparent batches are sorted per batch (InstancedMesh), not per instance or triangle, so overlapping transparent parts can blend in the wrong order.
225
227
  - **Opt-in pack features:** conditional lines need packs built with `cond`. Printed parts show their images only from `textures` packs (the embedded PNGs dominate the pack size); default packs draw the FALLBACK geometry.
226
228
  - **ROTSTEP** is reported (`stepViews`, `instance.view`), not applied; `planStepViews` takes a `fixed(step)` override if you want to honour it.
227
229
  - **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`.
@@ -232,5 +234,5 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
232
234
 
233
235
  - **Code:** MIT © Evan Bacon.
234
236
  - **LDraw parts:** packs you build contain geometry from the **LDraw.org Parts Library**, which is licensed **CC BY 2.0** (newer files CC BY 4.0, per file header). If you ship packs, credit "LDraw.org Parts Library (CC BY 2.0)" in your app. See <https://www.ldraw.org/legal-info>.
235
- - **Datsville:** the benchmark model is by [mjhorvath/Datsville](https://github.com/mjhorvath/Datsville) (CC BY-SA 4.0). It is used for benchmarking only and is not redistributed.
237
+ - **Millennium Falcon:** the benchmark fleets place the [LDraw OMR model of set 10179](https://library.ldraw.org/omr/sets/320) by Roland Dahl [RolandD] (CC BY 2.0) N times.
236
238
  - **Trademarks:** LEGO® is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project; ldr is not affiliated with the LEGO Group. LDraw™ is a trademark of the Estate of James Jessiman.
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/cli.ts
3
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/cli.ts
4
4
  import fs4 from "node:fs";
5
5
  import zlib from "node:zlib";
6
6
 
7
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/node/build-pack.ts
7
+ // ../../../../../../../tmp/ldr-build-dLNbNv/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-Fyp0fs/src/node/resolver.ts
11
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/node/resolver.ts
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
14
 
15
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/spec.ts
15
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/spec.ts
16
16
  var norm = (s) => s.trim().toLowerCase().replace(/\\/g, "/").replace(/\/+/g, "/");
17
17
  var COLOUR_ID_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
18
18
  var namedCodes = new Map;
@@ -723,9 +723,35 @@ function fileStepViews(f) {
723
723
  return out;
724
724
  }
725
725
 
726
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/model.ts
726
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/model.ts
727
727
  var CALLOUT_END = 999999;
728
728
  var stepKey = (steps) => steps.map((x) => String(x).padStart(6, "0")).join(".") + "~";
729
+ var defLine = new WeakMap;
730
+ var wrapMemo = new WeakMap;
731
+ function scopeWrapper(part, scope) {
732
+ let w = wrapMemo.get(scope);
733
+ if (!w) {
734
+ const lines = [...scope.values()].map((d) => {
735
+ const t = defLine.get(d) ?? "";
736
+ const k = t.indexOf("!COLOUR");
737
+ return k < 0 ? "" : "0 " + t.slice(k).trim().replace(/\s+/g, " ");
738
+ }).filter(Boolean);
739
+ w = { head: lines.join(`
740
+ `), parts: new Map };
741
+ wrapMemo.set(scope, w);
742
+ }
743
+ let r = w.parts.get(part);
744
+ if (!r) {
745
+ const n = norm(part), source = `0 ldr: ${n} placed in a model's !COLOUR scope
746
+ 0 BFC CERTIFY CCW
747
+ ${w.head}
748
+ 1 16 0 0 0 1 0 0 0 1 0 0 0 1 ${n}
749
+ `;
750
+ r = { name: `${n}#col-${hash32(source)}`, source };
751
+ w.parts.set(part, r);
752
+ }
753
+ return r;
754
+ }
729
755
  function flatten(doc, opts = {}) {
730
756
  const out = [];
731
757
  const maxDepth = opts.maxDepth ?? 64;
@@ -770,7 +796,7 @@ function flatten(doc, opts = {}) {
770
796
  const views = fileStepViews(f);
771
797
  const ls = loose ? looseSteps(f) : [];
772
798
  let li = 0;
773
- const emit = (part, col, mm, sp, k, embedded, tex, cd) => {
799
+ const emit = (part, col, mm, sp, k, embedded, tex, cd, sc) => {
774
800
  if (fixed)
775
801
  sp = fixed;
776
802
  const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
@@ -789,6 +815,10 @@ function flatten(doc, opts = {}) {
789
815
  const w = texWrapper(part, tex);
790
816
  i.texPart = w.name;
791
817
  i.texSource = w.source;
818
+ } else if (sc) {
819
+ const w = scopeWrapper(part, sc);
820
+ i.colPart = w.name;
821
+ i.colSource = w.source;
792
822
  }
793
823
  const v = views[k];
794
824
  if (v)
@@ -799,7 +829,7 @@ function flatten(doc, opts = {}) {
799
829
  const flushLoose = (upTo) => {
800
830
  while (li < ls.length && ls[li] <= upTo) {
801
831
  const k = ls[li++];
802
- emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null, colour).source = lazy(f, k);
832
+ emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null, colour, scope0).source = lazy(f, k);
803
833
  }
804
834
  };
805
835
  for (const c0 of f.cmds) {
@@ -815,8 +845,10 @@ function flatten(doc, opts = {}) {
815
845
  if (c.t === 0) {
816
846
  if (c.meta === "!COLOUR") {
817
847
  const def = parseColourLine(c.text);
818
- if (def && def.code !== 16 && def.code !== 24)
848
+ if (def && def.code !== 16 && def.code !== 24) {
849
+ defLine.set(def, c.text);
819
850
  (scope = new Map(scope ?? [])).set(def.code, def);
851
+ }
820
852
  continue;
821
853
  }
822
854
  if (tx && c.geom?.t === 1)
@@ -837,7 +869,7 @@ function flatten(doc, opts = {}) {
837
869
  sub(d, c.file, mm, col, sp, lt, cd);
838
870
  continue;
839
871
  }
840
- emit(c.file, col, mm, sp, s, true, lt, cd);
872
+ emit(c.file, col, mm, sp, s, true, lt, cd, scope);
841
873
  continue;
842
874
  }
843
875
  const ext = opts.resolveModel?.(c.file);
@@ -845,7 +877,7 @@ function flatten(doc, opts = {}) {
845
877
  sub(ext, c.file, mm, col, sp, lt, cd);
846
878
  continue;
847
879
  }
848
- emit(c.file, col, mm, sp, s, false, lt, cd);
880
+ emit(c.file, col, mm, sp, s, false, lt, cd, scope);
849
881
  }
850
882
  flushLoose(Infinity);
851
883
  if (groups) {
@@ -1106,7 +1138,7 @@ function blendedColourHex(code) {
1106
1138
  return "#" + [0, 1, 2].map((k) => (a[k] + b[k] >> 1).toString(16).padStart(2, "0")).join("").toUpperCase();
1107
1139
  }
1108
1140
 
1109
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/node/resolver.ts
1141
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/node/resolver.ts
1110
1142
  var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
1111
1143
  var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
1112
1144
  var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
@@ -1192,6 +1224,53 @@ class Library {
1192
1224
  isPart(key) {
1193
1225
  return key.startsWith("parts/") && !key.startsWith("parts/s/");
1194
1226
  }
1227
+ #codes = new Map;
1228
+ #ldconfig;
1229
+ colourCodes(key) {
1230
+ let s = this.#codes.get(key);
1231
+ if (s)
1232
+ return s;
1233
+ s = new Set;
1234
+ this.#codes.set(key, s);
1235
+ for (const line of this.read(key).split(/\r?\n/)) {
1236
+ const t = line.trim(), c = t[0];
1237
+ if (c !== "1" && c !== "3" && c !== "4")
1238
+ continue;
1239
+ const tok = t.split(/\s+/);
1240
+ if (tok.length < 2)
1241
+ continue;
1242
+ const code = parseColor(tok[1]);
1243
+ if (code !== 16 && code !== 24 && !Number.isNaN(code))
1244
+ s.add(code);
1245
+ if (c === "1") {
1246
+ const r = parseType1(t);
1247
+ const ck = r && this.resolve(r.name);
1248
+ if (ck)
1249
+ for (const x of this.colourCodes(ck))
1250
+ s.add(x);
1251
+ }
1252
+ }
1253
+ return s;
1254
+ }
1255
+ scopeChanges(key, wrapperSource) {
1256
+ if (!this.#ldconfig) {
1257
+ const f = path.join(this.root, "LDConfig.ldr");
1258
+ this.#ldconfig = fs.existsSync(f) ? parseLDConfig(fs.readFileSync(f, "latin1")) : new Map;
1259
+ }
1260
+ const used = this.colourCodes(key);
1261
+ for (const line of wrapperSource.split(`
1262
+ `)) {
1263
+ if (!line.startsWith("0 !COLOUR"))
1264
+ continue;
1265
+ const d = parseColourLine(line);
1266
+ if (!d || !used.has(d.code))
1267
+ continue;
1268
+ const g = this.#ldconfig.get(d.code);
1269
+ if (!g || g.hex.toUpperCase() !== d.hex.toUpperCase())
1270
+ return true;
1271
+ }
1272
+ return false;
1273
+ }
1195
1274
  fileStats(key) {
1196
1275
  if (this.stats.has(key))
1197
1276
  return this.stats.get(key);
@@ -1331,6 +1410,7 @@ function flatten2(model, lib, onInstance, opts = {}) {
1331
1410
  lib.addVirtual(n, t);
1332
1411
  const doc = modelDoc(model);
1333
1412
  const at = new Map;
1413
+ const changes = new Map;
1334
1414
  for (const i of flatten(doc, opts)) {
1335
1415
  let key;
1336
1416
  if (i.embedded)
@@ -1351,6 +1431,15 @@ function flatten2(model, lib, onInstance, opts = {}) {
1351
1431
  r.texRef = i.texPart;
1352
1432
  r.texPart = lib.addVirtual(i.texPart, i.texSource);
1353
1433
  }
1434
+ if (i.colPart) {
1435
+ let ch = changes.get(i.colPart);
1436
+ if (ch === undefined)
1437
+ changes.set(i.colPart, ch = lib.scopeChanges(key, i.colSource));
1438
+ if (ch) {
1439
+ r.colRef = i.colPart;
1440
+ r.colPart = lib.addVirtual(i.colPart, i.colSource);
1441
+ }
1442
+ }
1354
1443
  onInstance(r);
1355
1444
  }
1356
1445
  return { missing };
@@ -1376,11 +1465,11 @@ function collectDeps(lib, refs) {
1376
1465
  return out;
1377
1466
  }
1378
1467
 
1379
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/node/compile.ts
1468
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/node/compile.ts
1380
1469
  import fs2 from "node:fs";
1381
1470
  import path2 from "node:path";
1382
1471
 
1383
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/node/lod.ts
1472
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/node/lod.ts
1384
1473
  import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer";
1385
1474
  await Promise.all([MeshoptSimplifier.ready, MeshoptEncoder.ready]);
1386
1475
  function reorderMesh(P, N, C, I) {
@@ -1572,7 +1661,7 @@ function lodChain(P, C, I) {
1572
1661
  return out;
1573
1662
  }
1574
1663
 
1575
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/node/compile.ts
1664
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/node/compile.ts
1576
1665
  function loadColors(root) {
1577
1666
  const map = new Map;
1578
1667
  for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
@@ -2287,7 +2376,7 @@ function encodePack(parts, studKinds, weld, extra, options = {}) {
2287
2376
  return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
2288
2377
  }
2289
2378
 
2290
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/node/build-pack.ts
2379
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/node/build-pack.ts
2291
2380
  function packCompilerOpts(o) {
2292
2381
  return { studs: o.studs ?? "low", prims: o.primitives ?? "full", weld: o.weld ?? true, separateStuds: true, cond: !!o.cond, tex: !!o.textures };
2293
2382
  }
@@ -2307,6 +2396,8 @@ function buildPack(o) {
2307
2396
  refs.set(i.ref, i.part);
2308
2397
  if (i.texRef)
2309
2398
  refs.set(i.texRef, i.texPart);
2399
+ if (i.colRef)
2400
+ refs.set(i.colRef, i.colPart);
2310
2401
  }, { textures, looseGeometry: o.looseGeometry });
2311
2402
  for (const k of r.missing.keys())
2312
2403
  missing.add(k);
@@ -2358,7 +2449,7 @@ function buildPack(o) {
2358
2449
  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 } : {} };
2359
2450
  }
2360
2451
 
2361
- // ../../../../../../../tmp/ldr-build-Fyp0fs/src/cli.ts
2452
+ // ../../../../../../../tmp/ldr-build-dLNbNv/src/cli.ts
2362
2453
  var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
2363
2454
 
2364
2455
  Usage:
package/dist/cull.d.ts CHANGED
@@ -51,7 +51,8 @@ export type CullResult = {
51
51
  export declare const STEP_NEVER = 2147483647;
52
52
  /** Visit every stud (instance order, then part stud order) with its world matrix `w` (scratch; copy to keep). */
53
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. */
54
+ /** `steps` (optional, per instance): also compute when each stud / underside becomes hidden as the model is built step by step.
55
+ * Transparent instances hide nothing (studs inside their hollow stay visible through them) and keep their own undersides. */
55
56
  export declare function cullCore(insts: Inst[], H: any, steps?: ArrayLike<number>): CullResult;
56
57
  /** Scene instances and per-instance step ranks as `createLdrObject` builds them (shared with the load worker). */
57
58
  export declare function sceneInstances(instances: PartInstance[], H: any, useTex: boolean): {