ldr 0.24.1 → 0.25.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,10 +83,11 @@ 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. |
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. |
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). |
90
+ | `blendedColourHex(code)` | RGB of a blended (old dithered) code 256-511, for codes LDConfig doesn't define. |
90
91
  | `parseColourLine`, `colourSurface`, `colourMaterialKey`, `makeFinishMaterial` | Spec finish → PBR surface (metalness/roughness/clearcoat/sheen; glitter/speckle use cheap procedural grain). |
91
92
  | `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. |
92
93
  | `looseSteps` / `looseFile` / `looseSource` / `loosePartName` | A model's own type 2–5 geometry, per step, as synthetic parts named `<file>#<step>`. |
@@ -209,6 +210,8 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
209
210
  - `0 STEP`, nested submodel step sequencing, `0 ROTSTEP` (`REL`/`ADD`/`ABS`/`END`), and `0 BUFEXCHG`.
210
211
  - `!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.
211
212
  - Colours: LDConfig `!COLOUR`, edge colours, main/edge colour inheritance (16/24), and direct colours (`0x2RRGGBB`).
213
+ - `!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. Colours hard-coded inside parts (printed parts, MPD-embedded parts) always come from LDConfig, because parts are precompiled.
214
+ - 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.
212
215
  - Case-insensitive, backslash-normalized file lookup; `~Moved to` alias parts.
213
216
 
214
217
  ## Limitations
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/cli.ts
3
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/cli.ts
4
4
  import fs4 from "node:fs";
5
5
  import zlib from "node:zlib";
6
6
 
7
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/node/build-pack.ts
7
+ // ../../../../../../../tmp/ldr-build-sxbVKM/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-eJ6hMz/src/node/resolver.ts
11
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/node/resolver.ts
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
14
 
15
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/spec.ts
15
+ // ../../../../../../../tmp/ldr-build-sxbVKM/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))
@@ -705,7 +705,7 @@ function fileStepViews(f) {
705
705
  return out;
706
706
  }
707
707
 
708
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/model.ts
708
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/model.ts
709
709
  var CALLOUT_END = 999999;
710
710
  var stepKey = (steps) => steps.map((x) => String(x).padStart(6, "0")).join(".") + "~";
711
711
  function flatten(doc, opts = {}) {
@@ -724,17 +724,18 @@ function flatten(doc, opts = {}) {
724
724
  }
725
725
  return fn;
726
726
  };
727
- const walk = (d, fname, m, color, path, stepPath, depth, texIn, fixed = null, subPath = [], subMats = []) => {
727
+ const walk = (d, fname, m, color, path, stepPath, depth, texIn, fixed = null, subPath = [], subMats = [], colour = undefined, scope0 = null) => {
728
728
  const f = d.files.get(fname);
729
729
  if (!f || depth > maxDepth)
730
730
  return;
731
+ let scope = scope0;
731
732
  let s = 0;
732
733
  const subId = depth > 0 ? ++subs : 0;
733
734
  const sp0 = co && subId ? [...subPath, subId] : subPath;
734
735
  const sm0 = co && subId ? [...subMats, m] : subMats;
735
736
  const groups = co && !fixed ? new Map : null;
736
737
  let ordStep = -1, ord = 0;
737
- const sub = (sd, file, mm, col, sp, lt) => {
738
+ const sub = (sd, file, mm, col, sp, lt, cd) => {
738
739
  if (ordStep !== s) {
739
740
  ordStep = s;
740
741
  ord = 0;
@@ -742,19 +743,21 @@ function flatten(doc, opts = {}) {
742
743
  const g = groups?.get(s + "|" + file);
743
744
  if (g) {
744
745
  g.n++;
745
- return walk(sd, sd === d ? file : sd.main, mm, col, path + "/" + file, [...sp, CALLOUT_END], depth + 1, lt, [...sp, CALLOUT_END], sp0, sm0);
746
+ return walk(sd, sd === d ? file : sd.main, mm, col, path + "/" + file, [...sp, CALLOUT_END], depth + 1, lt, [...sp, CALLOUT_END], sp0, sm0, cd, scope);
746
747
  }
747
748
  const spo = [...sp, ord++], from = out.length;
748
- walk(sd, sd === d ? file : sd.main, mm, col, path + "/" + file, spo, depth + 1, lt, fixed, sp0, sm0);
749
+ walk(sd, sd === d ? file : sd.main, mm, col, path + "/" + file, spo, depth + 1, lt, fixed, sp0, sm0, cd, scope);
749
750
  groups?.set(s + "|" + file, { n: 1, from, to: out.length });
750
751
  };
751
752
  const views = fileStepViews(f);
752
753
  const ls = loose ? looseSteps(f) : [];
753
754
  let li = 0;
754
- const emit = (part, col, mm, sp, k, embedded, tex) => {
755
+ const emit = (part, col, mm, sp, k, embedded, tex, cd) => {
755
756
  if (fixed)
756
757
  sp = fixed;
757
758
  const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
759
+ if (cd)
760
+ i.colour = cd;
758
761
  if (subId) {
759
762
  i.sub = subId;
760
763
  if (co) {
@@ -778,7 +781,7 @@ function flatten(doc, opts = {}) {
778
781
  const flushLoose = (upTo) => {
779
782
  while (li < ls.length && ls[li] <= upTo) {
780
783
  const k = ls[li++];
781
- emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null).source = lazy(f, k);
784
+ emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null, colour).source = lazy(f, k);
782
785
  }
783
786
  };
784
787
  for (const c0 of f.cmds) {
@@ -792,6 +795,12 @@ function flatten(doc, opts = {}) {
792
795
  continue;
793
796
  }
794
797
  if (c.t === 0) {
798
+ if (c.meta === "!COLOUR") {
799
+ const def = parseColourLine(c.text);
800
+ if (def && def.code !== 16 && def.code !== 24)
801
+ (scope = new Map(scope ?? [])).set(def.code, def);
802
+ continue;
803
+ }
795
804
  if (tx && c.geom?.t === 1)
796
805
  c = c.geom;
797
806
  else
@@ -801,24 +810,24 @@ function flatten(doc, opts = {}) {
801
810
  continue;
802
811
  if (tx && c.fallback)
803
812
  continue;
804
- const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
813
+ const col = c.color === 16 ? color : c.color, cd = c.color === 16 ? colour : scope?.get(c.color), mm = mul(m, c.m), sp = [...stepPath, s];
805
814
  const tex = tx ? c.tex ?? texIn : null;
806
815
  const local = tex ? invert(c.m) : null;
807
816
  const lt = tex && local ? transformTexMap(tex, local) : null;
808
817
  if (d.files.has(c.file)) {
809
818
  if (fileKind(d, c.file) === "model") {
810
- sub(d, c.file, mm, col, sp, lt);
819
+ sub(d, c.file, mm, col, sp, lt, cd);
811
820
  continue;
812
821
  }
813
- emit(c.file, col, mm, sp, s, true, lt);
822
+ emit(c.file, col, mm, sp, s, true, lt, cd);
814
823
  continue;
815
824
  }
816
825
  const ext = opts.resolveModel?.(c.file);
817
826
  if (ext) {
818
- sub(ext, c.file, mm, col, sp, lt);
827
+ sub(ext, c.file, mm, col, sp, lt, cd);
819
828
  continue;
820
829
  }
821
- emit(c.file, col, mm, sp, s, false, lt);
830
+ emit(c.file, col, mm, sp, s, false, lt, cd);
822
831
  }
823
832
  flushLoose(Infinity);
824
833
  if (groups) {
@@ -1054,8 +1063,32 @@ function parseLDConfig(text) {
1054
1063
  }
1055
1064
  return out;
1056
1065
  }
1066
+ var LDRAW_EXE_16 = [
1067
+ [51, 51, 51],
1068
+ [0, 51, 178],
1069
+ [0, 127, 51],
1070
+ [0, 181, 166],
1071
+ [204, 0, 0],
1072
+ [255, 51, 153],
1073
+ [102, 51, 0],
1074
+ [153, 153, 153],
1075
+ [102, 102, 88],
1076
+ [0, 128, 255],
1077
+ [51, 255, 102],
1078
+ [171, 253, 249],
1079
+ [255, 0, 0],
1080
+ [255, 176, 204],
1081
+ [255, 229, 0],
1082
+ [255, 255, 255]
1083
+ ];
1084
+ function blendedColourHex(code) {
1085
+ if (!Number.isInteger(code) || code < 256 || code > 511)
1086
+ return null;
1087
+ const a = LDRAW_EXE_16[code - 256 >> 4], b = LDRAW_EXE_16[code - 256 & 15];
1088
+ return "#" + [0, 1, 2].map((k) => (a[k] + b[k] >> 1).toString(16).padStart(2, "0")).join("").toUpperCase();
1089
+ }
1057
1090
 
1058
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/node/resolver.ts
1091
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/node/resolver.ts
1059
1092
  var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
1060
1093
  var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
1061
1094
  var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
@@ -1325,11 +1358,11 @@ function collectDeps(lib, refs) {
1325
1358
  return out;
1326
1359
  }
1327
1360
 
1328
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/node/compile.ts
1361
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/node/compile.ts
1329
1362
  import fs2 from "node:fs";
1330
1363
  import path2 from "node:path";
1331
1364
 
1332
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/node/lod.ts
1365
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/node/lod.ts
1333
1366
  import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer";
1334
1367
  await Promise.all([MeshoptSimplifier.ready, MeshoptEncoder.ready]);
1335
1368
  function reorderMesh(P, N, C, I) {
@@ -1521,7 +1554,7 @@ function lodChain(P, C, I) {
1521
1554
  return out;
1522
1555
  }
1523
1556
 
1524
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/node/compile.ts
1557
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/node/compile.ts
1525
1558
  function loadColors(root) {
1526
1559
  const map = new Map;
1527
1560
  for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
@@ -1533,6 +1566,13 @@ function loadColors(root) {
1533
1566
  }
1534
1567
  return map;
1535
1568
  }
1569
+ var blendedRgb = (code) => {
1570
+ const h = blendedColourHex(+code);
1571
+ if (!h)
1572
+ return null;
1573
+ const v = parseInt(h.slice(1), 16);
1574
+ return [(v >> 16) / 255, (v >> 8 & 255) / 255, (v & 255) / 255];
1575
+ };
1536
1576
  var det3 = (m) => m[3] * (m[7] * m[11] - m[8] * m[10]) - m[4] * (m[6] * m[11] - m[8] * m[9]) + m[5] * (m[6] * m[10] - m[7] * m[9]);
1537
1577
 
1538
1578
  class Compiler {
@@ -1570,7 +1610,7 @@ class Compiler {
1570
1610
  if (this.cache.has(ck))
1571
1611
  return this.cache.get(ck);
1572
1612
  const out = { body: { pos: [], col: [] }, under: { pos: [], col: [] }, edges: [], condLines: 0, cond: this.opts.cond ? [] : undefined, condUnder: this.opts.cond ? [] : undefined, studs: [], bbox: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity], fullBottom: false };
1573
- const colorOf = (code) => code === "16" ? [1, 1, 1, 0] : [...this.colors.get(code) || (code.startsWith("0x2") ? [parseInt(code.slice(3, 5), 16) / 255, parseInt(code.slice(5, 7), 16) / 255, parseInt(code.slice(7, 9), 16) / 255] : [1, 0, 1]), 1];
1613
+ const colorOf = (code) => code === "16" ? [1, 1, 1, 0] : [...this.colors.get(code) || (code.startsWith("0x2") ? [parseInt(code.slice(3, 5), 16) / 255, parseInt(code.slice(5, 7), 16) / 255, parseInt(code.slice(7, 9), 16) / 255] : blendedRgb(code) ?? [1, 0, 1]), 1];
1574
1614
  const walk = (fk, M, color, invert, target, depth) => {
1575
1615
  if (depth > 40)
1576
1616
  return;
@@ -2151,7 +2191,7 @@ function encodePack(parts, studKinds, weld, extra, options = {}) {
2151
2191
  return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
2152
2192
  }
2153
2193
 
2154
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/node/build-pack.ts
2194
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/node/build-pack.ts
2155
2195
  function buildPack(o) {
2156
2196
  const lib = new Library(o.ldrawRoot);
2157
2197
  const refs = new Map;
@@ -2219,7 +2259,7 @@ function buildPack(o) {
2219
2259
  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 } : {} };
2220
2260
  }
2221
2261
 
2222
- // ../../../../../../../tmp/ldr-build-eJ6hMz/src/cli.ts
2262
+ // ../../../../../../../tmp/ldr-build-sxbVKM/src/cli.ts
2223
2263
  var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
2224
2264
 
2225
2265
  Usage:
package/dist/cull.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PartInstance } from './model.js';
1
+ import type { PartInstance, LDrawColor } from './model.js';
2
2
  export type Inst = {
3
3
  part: string;
4
4
  color: string;
@@ -63,6 +63,7 @@ export declare function sceneInstances(instances: PartInstance[], H: any, useTex
63
63
  }[];
64
64
  stepOf: number[];
65
65
  nSteps: number;
66
+ localColours: Map<string, LDrawColor>;
66
67
  };
67
68
  /** Culling for `createLdrObject(instances, pack, { textures })`, computable off the main thread and passed back as `precomputedCull`. */
68
69
  export declare function precomputeCull(instances: PartInstance[], H: any, useTex?: boolean): CullResult;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { parse, parseColor, isDirectColor, mul, I as IDENTITY, norm, smoothNormals, parseTexMap, texmapUV, texmapTriangleUV, transformTexMap, resolveTexture, fileKind, looseSteps, looseSource, looseFile, loosePartName, texWrapper, invert, hash32, colorToken, rotStepMatrix, stepViewMatrix, fileStepViews } from "./spec.js";
2
2
  export type { M as Matrix12, Doc, LFile, Cmd, RotStep, BufExchg, TexMap, TexMapCmd, TexMapMethod, TexAnno, DataBlock, PartMesh, FileKind, StepView } from "./spec.js";
3
- export { flatten, parseModel, sequenceSteps, stepKey, parseLDConfig, parseColourLine, colourSurface, colourMaterialKey, CALLOUT_END } from "./model.js";
3
+ export { flatten, parseModel, sequenceSteps, stepKey, parseLDConfig, parseColourLine, colourSurface, colourMaterialKey, blendedColourHex, CALLOUT_END } from "./model.js";
4
4
  export { planStepViews, isolateMask, viewDir, instanceBoxes, MIN_VIS, REORIENT } from "./instructions.js";
5
5
  export type { StepCameraView, StepCameraOptions, Vec3 } from "./instructions.js";
6
6
  export type { PartInstance, FlattenOptions, LDrawColor, LDrawFinish, LDrawFinishType, LDrawMaterialGrain, ColourSurface } from "./model.js";