ldr 0.4.0 → 0.4.1

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.
Files changed (2) hide show
  1. package/README.md +157 -75
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # ldr
2
2
 
3
- High-performance LEGO® (LDraw) rendering for [three.js](https://threejs.org). Renders 100k-brick models at interactive frame rates by precompiling parts into a compact binary **parts pack** and drawing them with instancing, hidden-stud/tube culling, low-detail studs, single-sided faces and indexed edge lines.
3
+ **Render 100,000-brick LEGO® (LDraw) models in three.js at interactive frame rates.**
4
4
 
5
- - Spec-compliant LDraw reader: `.ldr`, `.mpd` (submodels), `.dat`; BFC, `STEP`, `ROTSTEP`, `BUFEXCHG`, direct colours, `~Moved to` aliases.
6
- - `createLdrObject()` gives you a `THREE.Group` to drop into any scene, plus a build-steps API.
7
- - `ldr pack` CLI builds parts packs from your LDraw library (zstd/gzip compressed for serving).
8
- - ESM, TypeScript types, tree-shakeable (`sideEffects: false`); `three` is a peer dependency.
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, low-detail studs, single-sided (BFC) faces and indexed edge lines. On an Apple M4 Pro that takes Datsville (392,514 bricks) from 410 ms to 97 ms per frame, and its load time from 24.7 s to 3.4 s.
6
+
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
+ - **Spec-compliant LDraw reader:** `.ldr`, `.mpd` and `.dat`, including MPD-embedded parts, geometry drawn directly in models, BFC, `STEP`/`ROTSTEP`, `!TEXMAP` and `!DATA`.
9
+ - **`createLdrObject()`** returns a `THREE.Group` you can drop into any scene, plus a build-steps API.
10
+ - **`ldr pack` CLI and `buildPack()`** build parts packs from your LDraw library, compressed with zstd or gzip for serving.
11
+ - **Tree-shakeable** (`sideEffects: false`). The browser entry is `ldr`; the filesystem tools are in `ldr/node`.
9
12
 
10
13
  ## Install
11
14
 
@@ -13,15 +16,41 @@ High-performance LEGO® (LDraw) rendering for [three.js](https://threejs.org). R
13
16
  bun add ldr three # or: npm i ldr three
14
17
  ```
15
18
 
19
+ You also need the LDraw parts library, which is not bundled, to *build* packs. Download `complete.zip` from <https://library.ldraw.org> and unzip it to an `ldraw/` folder containing `LDConfig.ldr`, `parts/` and `p/`. Apps only need the pack at runtime, not the library.
20
+
16
21
  ## Quickstart
17
22
 
18
- 1. **Build a parts pack** for your model(s) (one-time, at build time):
23
+ ### 1. Build a parts pack (CLI, once at build time)
19
24
 
20
25
  ```bash
21
26
  bunx ldr pack --ldraw ./ldraw -o public/car.pack.zst models/car.ldr
22
27
  ```
23
28
 
24
- 2. **Render it:**
29
+ ```
30
+ ldr pack --ldraw <dir> -o <out.bin|.zst|.gz> [model.ldr ...] [--part 3001.dat ...] [--all-parts]
31
+ [--studs low|full] [--prims full|low] [--no-weld] [--no-colors]
32
+ [--cond] [--textures] [--no-loose-geometry]
33
+ ```
34
+
35
+ The pack holds only the parts your models reference, plus the LDConfig colour table. A `.zst` or `.gz` extension compresses the output.
36
+
37
+ ### 2. …or build it from code (Node / Bun)
38
+
39
+ ```ts
40
+ import { buildPack } from "ldr/node";
41
+
42
+ const { data, parts, missing } = buildPack({
43
+ ldrawRoot: "./ldraw",
44
+ models: ["models/car.ldr"], // paths, or { name, text }
45
+ studs: "low", // default
46
+ cond: false, // conditional lines (opt-in)
47
+ textures: false, // printed/sticker parts (opt-in)
48
+ });
49
+ await Bun.write("public/car.pack.bin", data); // raw; compress with zstd/gzip for serving
50
+ console.log(`${parts} parts`, missing.length ? `missing: ${missing}` : "");
51
+ ```
52
+
53
+ ### 3. Render it (browser)
25
54
 
26
55
  ```ts
27
56
  import * as THREE from "three";
@@ -29,97 +58,150 @@ import { parseModel, loadPack, createLdrObject } from "ldr";
29
58
 
30
59
  const [text, pack] = await Promise.all([
31
60
  fetch("/car.ldr").then((r) => r.text()),
32
- loadPack("/car.pack.zst"), // .zst / .gz / .br / raw .bin
61
+ loadPack("/car.pack.zst"), // .zst / .gz / .br / raw .bin
33
62
  ]);
63
+
34
64
  const model = createLdrObject(parseModel(text, "car.ldr"), pack, {
35
65
  edges: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic",
36
66
  });
37
- scene.add(model.object); // +Y up, 1 unit = 1 LDU (scale option available)
67
+ scene.add(model.object); // +Y up, 1 unit = 1 LDU (see `scale`)
38
68
 
39
- // build steps
40
- model.setStep(0); // show step 0 only … model.stepCount steps total
41
- model.setStep(null); // show everything
69
+ // Build steps
70
+ model.setStep(0); // show step 0 … model.stepCount - 1
71
+ model.setStep(null); // show everything
72
+ const view = model.stepViews[3]; // ROTSTEP view for step 3 (or null)
42
73
  ```
43
74
 
44
- Add lights (or `scene.environment`) as for any `MeshStandardMaterial`/`MeshPhysicalMaterial`.
75
+ Light the scene (or set `scene.environment`) as you would for any `MeshStandardMaterial` or `MeshPhysicalMaterial`.
45
76
 
46
- ## Getting parts
77
+ ## API reference
47
78
 
48
- The LDraw parts library is **not bundled** in this package. Download it from <https://library.ldraw.org> (`complete.zip`, ~90 MB unzipped to an `ldraw/` folder containing `LDConfig.ldr`, `parts/`, `p/`).
49
- The LDraw library is licensed **CC BY 2.0** — if you ship packs built from it, credit "LDraw.org Parts Library" (CC BY 2.0) in your app.
50
-
51
- Packs contain only the parts you ask for, so they're small: a 10k-brick Datsville section with 80 unique parts and the 100k section with 290 unique parts are 0.27 MB and 1.1 MB with zstd (1.3 / 3.3 MB gzip).
52
-
53
- ```bash
54
- ldr pack --ldraw <dir> -o <out.bin|.zst|.gz> [model.ldr ...] [--part 3001.dat ...] [--all-parts]
55
- [--studs low|full] [--prims full|low] [--no-weld] [--no-colors] [--cond] [--textures] [--no-loose-geometry]
56
- ```
57
-
58
- Or from code (Node/Bun):
59
-
60
- ```ts
61
- import { buildPack } from "ldr/node";
62
- const { data, parts, missing } = buildPack({ ldrawRoot: "./ldraw", models: ["car.ldr"], studs: "low" });
63
- await Bun.write("car.pack.bin", data);
64
- ```
65
-
66
- The pack embeds the LDConfig colour table by default, so no extra colour file is needed at runtime.
79
+ ### `ldr` (browser-safe)
67
80
 
68
- ## API
81
+ **Parsing and model**
69
82
 
70
- ### `ldr` (browser-safe)
83
+ | export | description |
84
+ |---|---|
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. |
87
+ | `parseModel(text, name?, options?)` | `parse` followed by `flatten`. |
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
+ | `parseLDConfig(text) → Map<code, LDrawColor>` | Reads a `!COLOUR` table into `{ code, name, hex, edge, alpha }` entries. |
90
+ | `fileKind(doc, name)` | Decides whether an MPD file is a part or a model: by its header type, otherwise by whether it has its own geometry. |
91
+ | `looseSteps` / `looseFile` / `looseSource` / `loosePartName` | A model's own type 2–5 geometry, per step, as synthetic parts named `<file>#<step>`. |
92
+ | `rotStepMatrix` / `stepViewMatrix` / `fileStepViews` | ROTSTEP views, following MLCad: `REL`, `ADD`, `ABS`, `END`. A ROTSTEP ends its step. |
93
+ | `parseTexMap` / `texmapUV` / `texmapTriangleUV` / `transformTexMap` / `resolveTexture` / `texWrapper` | `!TEXMAP` support (`PLANAR`, `CYLINDRICAL`, `SPHERICAL`). UV conventions follow LDView: v = 0 is the image's top row. |
94
+ | `parseColor`, `isDirectColor`, `colorToken`, `mul`, `invert`, `IDENTITY`, `norm`, `smoothNormals`, `hash32` | Low-level helpers. |
95
+
96
+ `FlattenOptions`:
97
+
98
+ | option | default | description |
99
+ |---|---|---|
100
+ | `looseGeometry` | `true` | Emits a model file's own type 2–5 lines as synthetic parts `<file>#<step>`. LDraw allows geometry in any file. |
101
+ | `textures` | `false` | TEXMAP-aware view of the model. References wrapped by a model-level `!TEXMAP` get a `texPart`, and `!TEXMAP FALLBACK` references are skipped. Use with `textures: true` packs. |
102
+ | `resolveModel(name)` | – | Resolves an external reference to a `Doc`, such as a model loaded separately. Return `null` to treat it as a part. |
103
+ | `maxDepth` | `64` | Submodel nesting limit (cycle guard). |
104
+
105
+ MPD-embedded parts, such as Studio/OMR unofficial parts or stickers, become part instances (`embedded: true`) and override library parts of the same name.
106
+
107
+ **Packs and rendering**
71
108
 
72
109
  | export | description |
73
110
  |---|---|
74
- | `parse(text, name?) → Doc` | Parse LDraw text into files/commands (MPD-aware). |
75
- | `flatten(doc, { resolveModel?, looseGeometry?, textures? }) → PartInstance[]` | Expand submodels into placed parts with `color`, 12-number matrix `m`, `step`, nested `steps`, `path`. MPD-embedded parts (e.g. Studio/OMR unofficial parts) are emitted as parts (`embedded: true`, see `fileKind`); a model file's own type 2-5 lines become synthetic parts `<file>#<step>` (`looseGeometry`, default `true`); with `textures: true`, parts wrapped by a model-level `!TEXMAP` get `texPart` (a synthetic textured part, `texWrapper`). Instances carry their step's ROTSTEP `view`. |
76
- | `parseModel(text, name?, opts?)` | `parse` + `flatten`. |
77
- | `sequenceSteps(instances)` | Global build-step index per instance (nested submodel steps sequenced), plus `views`: the ROTSTEP view per global step. |
78
- | `fileKind(doc, name)` / `looseSource` / `loosePartName` / `texWrapper` / `fileStepViews` / `rotStepMatrix` / `stepViewMatrix` | Spec helpers: part vs model rule for MPD files (header type; else own geometry), the text of a model's loose geometry per step, model-level TEXMAP wrapper parts, ROTSTEP views (MLCad: `REL`/`ADD`/`ABS`/`END`, ROTSTEP ends the step). |
79
- | `parseLDConfig(text)` | `!COLOUR` table → `Map<code, {name, hex, edge, alpha}>`. |
80
- | `loadPack(url) → Promise<Pack>` | Fetch + decompress (by extension) + decode a pack. |
81
- | `decompressPack(bytes)` / `decodePack(buffer)` | Same, from bytes you already have (zstd/gzip auto-detected). |
82
- | `createLdrObject(instances, pack, options?) → LdrObject` | High-level three.js object. |
83
- | `createInstancedEdges(pos, instancedMesh, material?)` | Welded, indexed edge lines sharing an InstancedMesh's matrices (for custom batching). |
84
- | `conditionalLines(pack, entry, which?)` / `createInstancedConditionalEdges(cond, instancedMesh, material?)` | Type-5 conditional lines from a `--cond` pack (stride 12: p0, p1, c0, c1), drawn by a vertex shader only where the control points fall on the same side of the projected line. |
85
- | `makeTexturedMaterial(side, texture, wrapU?)` / `packTexture(pack, i)` / `texmapUV` / `texmapTriangleUV` | Printed / sticker parts from a `--textures` pack: a `!TEXMAP` UV material (image alpha blends over the part colour, untextured outside [0,1]) and the decoded embedded PNG. UV conventions follow LDView (v = 0 is the image's top row). |
111
+ | `loadPack(url) → Promise<Pack>` | Fetches, decompresses and decodes a pack. zstd and gzip are detected automatically. |
112
+ | `decompressPack(bytes)` / `decodePack(buffer)` | Same, from bytes you already have. |
113
+ | `createLdrObject(instances, pack, options?) → LdrObject` | High-level three.js object (see below). |
114
+ | `createInstancedEdges(pos, instancedMesh, material?)` | Welded, indexed edge lines that share an `InstancedMesh`'s instance matrices. |
115
+ | `conditionalLines(pack, entry, which?)` / `createInstancedConditionalEdges(cond, instancedMesh, material?)` / `conditionalLineVisible` | Type-5 conditional lines from a `cond` pack. A vertex shader keeps a segment only where both control points project to the same side of the line. |
116
+ | `makeTexturedMaterial(side, texture, wrapU?)` / `packTexture(pack, i)` | Printed and sticker parts from a `textures` pack: a UV material and the decoded embedded PNG. |
86
117
  | `buildLdrScene`, `computeCulling`, `makeMaterial` | Low-level renderer used by the benchmarks. |
87
118
 
88
- `LdrObjectOptions`: `edges` (default `true`), `cullStuds` (`true`), `cullUndersides` (`true`), `side` (`"front"` \| `"double"`, default `"front"`), `material` (`"plastic"` \| `"standard"`), `colors` (Map / record / function override), `fallbackColor`, `ldrawSpace` (keep -Y up), `scale`, `step`, `conditionalLines` (default `true`; only has an effect with packs built with `--cond`), `conditionalStudLines` (default `true`; `false` skips stud silhouettes, ~half the conditional-line GPU cost but most of their visible effect on stud-heavy scenes), `textures` (default `true`; only has an effect with packs built with `--textures`; `object.texturesReady` resolves when images are decoded).
89
-
90
- `LdrObject`: `object`, `stats` (`instances`, `drawn`, `missing[]`, `studsHidden/Total`, `undersHidden/Total`, `buildMs`), `stepCount`, `stepViews` (ROTSTEP view per step, or `null`), `setStep(n | null)`, `setEdges(on)`, `setCulling(studs, undersides)`, `setSide(side)`, `getBox()`, `dispose()`.
119
+ `LdrObjectOptions`:
91
120
 
92
- ### `ldr/node` (Node/Bun only)
121
+ | option | default | description |
122
+ |---|---|---|
123
+ | `edges` | `true` | LDraw type-2 edge lines, indexed and sharing each part's instance matrices. |
124
+ | `cullStuds` | `true` | Hides studs fully covered by a part above. |
125
+ | `cullUndersides` | `true` | Hides undersides (tubes) sitting fully on studs below. |
126
+ | `side` | `"front"` | `"front"` draws single-sided BFC faces; `"double"` draws both sides. Uncertified parts are always double-sided. |
127
+ | `material` | `"plastic"` | `"plastic"` is `MeshPhysicalMaterial` with clearcoat; `"standard"` is `MeshStandardMaterial`, the cheapest option. |
128
+ | `conditionalLines` | `true` | Conditional (silhouette) lines. Only takes effect with `cond` packs, and follows `edges`. |
129
+ | `conditionalStudLines` | `true` | Conditional lines on studs. `false` roughly halves the conditional-line cost on large scenes. |
130
+ | `textures` | `true` | Renders `!TEXMAP` images from `textures` packs; `false` draws the FALLBACK geometry. `object.texturesReady` resolves once the images are decoded. |
131
+ | `colors` | pack table | Colour override: a `Map`, a record, or `(code) => color`. |
132
+ | `fallbackColor` | `#A0A5A9` | Colour used for unknown codes. |
133
+ | `ldrawSpace` | `false` | `true` keeps native LDraw space (-Y up). |
134
+ | `scale` | `1` | Uniform scale (1 = 1 LDU; 1 stud = 20 LDU). |
135
+ | `step` | all | Initial build step (inclusive, 0-based). |
93
136
 
94
- Everything above, plus `buildPack(options)`, `Library` (filesystem LDraw resolver: `where`, `flatten`, `mesh`, `colors`), `Compiler`, `encodePack`, `writePack`.
137
+ `LdrObject`: `object`, `stats` (`instances`, `drawn`, `missing[]`, `studsHidden/Total`, `undersHidden/Total`, `buildMs`, `condBatches`, `texBatches`, `textures`), `stepCount`, `stepViews`, `texturesReady`, `setStep(n | null)`, `setEdges(on)`, `setConditionalLines(on)`, `setCulling(studs, undersides)`, `setSide(side)`, `getBox(target?)`, `dispose()`.
95
138
 
96
- ## Performance
139
+ ### `ldr/node` (Node / Bun only)
97
140
 
98
- Measured with the benchmark harness in [`bench/`](https://github.com/EvanBacon/ldr/tree/main/bench) (Chrome, synced frame time while orbiting). Apple M4 Pro, native GL (Metal via ANGLE):
99
-
100
- | model | renderer | load | frame | tris/frame | draw calls |
101
- |---|---|---|---|---|---|
102
- | Datsville 100k bricks | three.js `LDrawLoader`-geometry instancing | 19.5 s | 105.7 ms (9 fps) | 142.7M | 290 |
103
- | Datsville 100k bricks | **ldr** (low studs, culling, single-sided) | **2.2 s** | **30.1 ms (33 fps)** | 59.1M | 506 |
104
- | Datsville 10k bricks | three.js instancing | 13.5 s | 45.9 ms | 60.5M | 80 |
105
- | Datsville 10k bricks | **ldr** (same) | **1.0 s** | **10.9 ms (92 fps)** | 21.0M | 131 |
106
- | 100k-brick grid | **ldr** + edges | 0.67 s | 15.7 ms (64 fps) | 4.0M | 24 |
107
- | 10k-brick grid | `LDrawLoader` | 6.6 s | 61.5 ms | 2.6M | 29,671 |
108
- | 10k-brick grid | **ldr** + edges | 0.15 s | 2.2 ms | 0.4M | 24 |
141
+ Everything above, plus:
109
142
 
110
- **Demo app** (`demo/`, rAF fps on the M4 Pro with big-model defaults — shadows/AO/edges off above 50k bricks): Datsville 100k at **43.5 fps** (1.8 s load), full Datsville (392k bricks) at **9.9 fps**.
111
-
112
- Visual-diff studies show culling and single-sided faces are lossless (≤0.009% pixels); low-detail studs change only stud silhouettes. Stud culling hides ~39–51% of studs on real models.
143
+ | export | description |
144
+ |---|---|
145
+ | `buildPack(options) → { data, parts, studKinds, missing, warnings? }` | Builds a pack. Options: `ldrawRoot`, `models`, `parts`, `allParts`, `studs` (`"low"` \| `"full"`), `primitives` (`"full"` \| `"low"`), `weld` (`true`), `cond` (`false`), `textures` (`false`), `looseGeometry` (`true`), `colors` (`true`), `onProgress`. |
146
+ | `Library(root, unofficialRoot?)` | Filesystem LDraw resolver: `where`, `flatten`, `mesh`, `colors`, `texture`, `register`. Handles case-insensitive lookup and `~Moved to` aliases. |
147
+ | `Compiler`, `encodePack`, `writePack`, `loadColors` | Lower-level pack compiler. |
148
+ | `ResolverLibrary`, `splitModel`, `flattenWithLibrary`, `collectDeps` | The resolver that `buildPack` uses. |
113
149
 
114
- ## Notes & limitations
150
+ ## Benchmarks
115
151
 
116
- - Transparent colours render opaque. Conditional (type-5) lines are drawn only from packs built with `--cond` (pack v2, opt-in: +8–14% zstd size on Datsville); default packs carry type-2 edges only. Printed parts show their `!TEXMAP` images only from packs built with `--textures` (pack v2, opt-in; the PNGs dominate the size); default packs draw the parts' FALLBACK geometry. Model-level `!TEXMAP` around part references renders from `--textures` packs (one synthetic part per distinct map). Parts embedded in an MPD (unofficial/custom parts, stickers) render as parts and override library parts of the same name; geometry drawn directly in a model file is packed as synthetic parts (`--no-loose-geometry` to skip it).
117
- - ROTSTEP is reported (`stepViews`, `instance.view`), not applied: rotating the camera per step is up to the app.
118
- - `setStep` rebuilds batches (fast for instruction-sized models; culling is recomputed for the partial build).
119
- - Coordinates: LDraw units (1 stud = 20 LDU); the object is rotated to +Y up unless `ldrawSpace: true`.
152
+ All numbers below were measured with the harness in [`bench/`](https://github.com/EvanBacon/ldr/tree/main/bench/) and are taken from the result files there.
120
153
 
121
- LEGO® is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project.
154
+ - **Hardware and browser:** Apple **M4 Pro** MacBook Pro, Chrome with native GL (ANGLE → Metal), Bun 1.3.
155
+ - **Frame time:** the **median synced frame time** (`gl.finish` per frame) while orbiting the camera.
156
+ - **Load:** from fetch to the first rendered frame.
157
+ - **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.
158
+ - **ldr settings:** low-detail studs, welded pack, stud and underside culling, single-sided faces, no edges.
122
159
 
123
- ## License
160
+ Sources: `bench/results/latest-mac-ablation.md`, `latest-mac.md`, and `research.md` §7.
124
161
 
125
- MIT (code). Parts data you build from the LDraw library is CC BY 2.0 (LDraw.org).
162
+ | model | bricks | baseline frame | **ldr frame** | baseline load | **ldr load** |
163
+ |---|---|---|---|---|---|
164
+ | synthetic grid | 100,021 | 24.4 ms | **3.6 ms** | 0.2 s | 0.6 s |
165
+ | Datsville 10k | 10,000 | 45.9 ms | **10.9 ms** | 13.5 s | **1.0 s** |
166
+ | Datsville 100k | 100,000 | 105.7 ms | **30.1 ms** | 19.5 s | **2.2 s** |
167
+ | Datsville (full) | 392,514 | 409.6 ms | **97.2 ms** | 24.7 s | **3.4 s** |
168
+
169
+ 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.
170
+
171
+ - **Pack size:** Datsville 100k (290 unique parts) is 22.4 MB raw, 3.13 MB gzip and **1.08 MB zstd**. Decoding the zstd pack in the browser (fzstd) takes **58 ms**.
172
+ - **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.
173
+ - **Demo app** (`demo/`; rAF frame median at 2880×1800 on the M4 Pro):
174
+ - 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.
175
+ - Full Datsville: **1.7 → 9.9 fps**.
176
+ - Datsville 100k: **8 → 43.5 fps**.
177
+ - **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.
178
+ - **Edges:** turning `edges` on costs more than that on dense scenes: Datsville 100k goes from 30.1 to 98.2 ms per frame. For very large models, consider `edges: false`.
179
+ - **Visual fidelity:** pixel diffs show culling and single-sided faces are lossless (≤ 0.009 % of pixels differ). Low-detail studs only change stud silhouettes.
180
+
181
+ Reproduce: `cd bench && bun run setup && bun run bench` (see [`bench/README.md`](https://github.com/EvanBacon/ldr/blob/main/bench/README.md)).
182
+
183
+ ## LDraw spec compliance
184
+
185
+ - Line types 0–5. BFC: `CERTIFY`, `CCW`/`CW`, `INVERTNEXT`, `NOCLIP`, inverted matrices.
186
+ - MPD: `0 FILE` / `0 NOFILE`, and `0 !DATA` (base64 PNGs). An MPD file is a part or a model by its `!LDRAW_ORG` type, otherwise by whether it has its own geometry. Embedded parts override library parts.
187
+ - Geometry drawn directly in model files (type 2–5 lines outside parts), on by default.
188
+ - `0 STEP`, nested submodel step sequencing, `0 ROTSTEP` (`REL`/`ADD`/`ABS`/`END`), and `0 BUFEXCHG`.
189
+ - `!TEXMAP` `START`/`NEXT`/`FALLBACK`/`END`, with `PLANAR`, `CYLINDRICAL` and `SPHERICAL` projections (orientation matches LDView), and model-level `!TEXMAP` around part references.
190
+ - Colours: LDConfig `!COLOUR`, edge colours, main/edge colour inheritance (16/24), and direct colours (`0x2RRGGBB`).
191
+ - Case-insensitive, backslash-normalized file lookup; `~Moved to` alias parts.
192
+
193
+ ## Limitations
194
+
195
+ - **Transparency:** transparent colours render opaque.
196
+ - **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.
197
+ - **ROTSTEP** is reported (`stepViews`, `instance.view`), not applied. Rotating the camera per step is up to your app.
198
+ - **`setStep`** rebuilds batches. That is fast for instruction-sized models, and culling is recomputed for the partial build.
199
+ - **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`.
200
+ - **Occlusion culling and LOD:** there is no occlusion culling and no distance LOD yet. Only three.js frustum culling applies, per batch.
201
+
202
+ ## Attribution and license
203
+
204
+ - **Code:** MIT © Evan Bacon.
205
+ - **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>.
206
+ - **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.
207
+ - **Trademarks:** LEGO® is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project. LDraw™ is a trademark of the Estate of James Jessiman.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldr",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "High-performance LEGO (LDraw) rendering for three.js: instanced parts packs, hidden stud/tube culling, low-detail studs, indexed edges, build steps.",
5
5
  "license": "MIT",
6
6
  "author": "Evan Bacon",