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 +18 -16
- package/dist/cli.js +107 -16
- package/dist/cull.d.ts +2 -1
- package/dist/index.js +71 -25
- package/dist/load-worker.gen.d.ts +1 -1
- package/dist/model.d.ts +17 -0
- package/dist/node/index.js +142 -33
- package/dist/node/library.d.ts +2 -1
- package/dist/node/resolver.d.ts +12 -1
- package/dist/parts-cdn.d.ts +2 -0
- package/dist/spec.d.ts +2 -0
- package/package.json +1 -1
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
|
|
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
|
[](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
|
|
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
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
203
|
-
- **Build steps:** `setStep` takes
|
|
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
|
|
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
|
-
- **
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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): {
|