ldr 0.29.0 → 0.30.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
@@ -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). |
@@ -213,7 +213,7 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
213
213
  - `0 STEP`, nested submodel step sequencing, `0 ROTSTEP` (`REL`/`ADD`/`ABS`/`END`), and `0 BUFEXCHG`.
214
214
  - `!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
215
  - 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.
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 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
217
  - 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
218
  - 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
219
  - Case-insensitive, backslash-normalized file lookup; `~Moved to` alias parts.
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../../../../../../tmp/ldr-build-JOxLfU/src/cli.ts
3
+ // ../../../../../../../tmp/ldr-build-P0dzRz/src/cli.ts
4
4
  import fs4 from "node:fs";
5
5
  import zlib from "node:zlib";
6
6
 
7
- // ../../../../../../../tmp/ldr-build-JOxLfU/src/node/build-pack.ts
7
+ // ../../../../../../../tmp/ldr-build-P0dzRz/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-JOxLfU/src/node/resolver.ts
11
+ // ../../../../../../../tmp/ldr-build-P0dzRz/src/node/resolver.ts
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
14
 
15
- // ../../../../../../../tmp/ldr-build-JOxLfU/src/spec.ts
15
+ // ../../../../../../../tmp/ldr-build-P0dzRz/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-JOxLfU/src/model.ts
726
+ // ../../../../../../../tmp/ldr-build-P0dzRz/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-JOxLfU/src/node/resolver.ts
1141
+ // ../../../../../../../tmp/ldr-build-P0dzRz/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-JOxLfU/src/node/compile.ts
1468
+ // ../../../../../../../tmp/ldr-build-P0dzRz/src/node/compile.ts
1380
1469
  import fs2 from "node:fs";
1381
1470
  import path2 from "node:path";
1382
1471
 
1383
- // ../../../../../../../tmp/ldr-build-JOxLfU/src/node/lod.ts
1472
+ // ../../../../../../../tmp/ldr-build-P0dzRz/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-JOxLfU/src/node/compile.ts
1664
+ // ../../../../../../../tmp/ldr-build-P0dzRz/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-JOxLfU/src/node/build-pack.ts
2379
+ // ../../../../../../../tmp/ldr-build-P0dzRz/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-JOxLfU/src/cli.ts
2452
+ // ../../../../../../../tmp/ldr-build-P0dzRz/src/cli.ts
2362
2453
  var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
2363
2454
 
2364
2455
  Usage: