ldr 0.5.1 → 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 +12 -10
- package/dist/cdn.d.ts +8 -41
- package/dist/cli.js +157 -30
- package/dist/cull.d.ts +68 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +844 -500
- package/dist/load-worker.d.ts +1 -0
- package/dist/load-worker.gen.d.ts +2 -0
- package/dist/node/index.js +970 -541
- package/dist/pack-io.d.ts +9 -0
- package/dist/parts-cdn.d.ts +64 -0
- package/dist/render.d.ts +23 -43
- package/dist/scene.d.ts +10 -1
- package/package.json +2 -2
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
|
|
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
|
[](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 / .
|
|
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 | **
|
|
171
|
-
| Datsville 100k | 100,000 | 105.7 ms | **
|
|
172
|
-
| Datsville (full) | 392,514 | 409.6 ms | **
|
|
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 (
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
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:**
|
|
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 "./
|
|
2
|
-
import {
|
|
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
|
-
|
|
5
|
-
export
|
|
6
|
-
export interface
|
|
7
|
-
|
|
8
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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(
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 <
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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";
|