ldr 0.11.1 → 0.13.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
@@ -86,7 +86,8 @@ Light the scene (or set `scene.environment`) as you would for any `MeshStandardM
86
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
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
- | `parseLDConfig(text) → Map<code, LDrawColor>` | Reads a `!COLOUR` table into `{ code, name, hex, edge, alpha }` entries. |
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
+ | `parseColourLine`, `colourSurface`, `colourMaterialKey`, `makeFinishMaterial` | Spec finish → PBR surface (metalness/roughness/clearcoat/sheen; glitter/speckle use cheap procedural grain). |
90
91
  | `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
92
  | `looseSteps` / `looseFile` / `looseSource` / `loosePartName` | A model's own type 2–5 geometry, per step, as synthetic parts named `<file>#<step>`. |
92
93
  | `rotStepMatrix` / `stepViewMatrix` / `fileStepViews` | ROTSTEP views, following MLCad: `REL`, `ADD`, `ABS`, `END`. A ROTSTEP ends its step. |
@@ -209,7 +210,8 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
209
210
 
210
211
  ## Limitations
211
212
 
212
- - **Transparency:** transparent colours render opaque.
213
+ - **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.
214
+ - **Transparency:** transparent colours (`ALPHA < 255`) render with opacity and `depthWrite: false` (one material bucket per finish × opacity class).
213
215
  - **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.
214
216
  - **ROTSTEP** is reported (`stepViews`, `instance.view`), not applied; `planStepViews` takes a `fixed(step)` override if you want to honour it.
215
217
  - **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`.
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/cli.ts
3
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/cli.ts
4
4
  import fs4 from "node:fs";
5
5
  import zlib from "node:zlib";
6
6
 
7
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/node/build-pack.ts
7
+ // ../../../../../../../tmp/ldr-build-TjmzYL/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-ClpPxU/src/node/resolver.ts
11
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/node/resolver.ts
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
14
 
15
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/spec.ts
15
+ // ../../../../../../../tmp/ldr-build-TjmzYL/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-ClpPxU/src/model.ts
708
+ // ../../../../../../../tmp/ldr-build-TjmzYL/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 = {}) {
@@ -854,17 +854,208 @@ function sequenceSteps(insts) {
854
854
  });
855
855
  return { count: uniq.length, stepOf, views };
856
856
  }
857
+ var FINISH_WORDS = new Set(["CHROME", "PEARLESCENT", "RUBBER", "MATTE_METALLIC", "METAL", "MATERIAL"]);
858
+ function parseHexColor(tok) {
859
+ if (!tok)
860
+ return null;
861
+ if (tok.startsWith("#") && /^#[0-9A-Fa-f]{6}$/.test(tok))
862
+ return tok.toUpperCase();
863
+ if (/^0x[0-9A-Fa-f]{6}$/i.test(tok))
864
+ return ("#" + tok.slice(2)).toUpperCase();
865
+ return null;
866
+ }
867
+ function parseGrain(tokens, i) {
868
+ const grain = { value: "#FFFFFF", fraction: 0.5 };
869
+ while (i < tokens.length) {
870
+ const t = tokens[i].toUpperCase();
871
+ const n = tokens[i + 1];
872
+ if (t === "VALUE") {
873
+ const h = parseHexColor(n);
874
+ if (h)
875
+ grain.value = h;
876
+ i += 2;
877
+ } else if (t === "ALPHA") {
878
+ grain.alpha = +n;
879
+ i += 2;
880
+ } else if (t === "LUMINANCE") {
881
+ grain.luminance = +n;
882
+ i += 2;
883
+ } else if (t === "FRACTION") {
884
+ grain.fraction = +n;
885
+ i += 2;
886
+ } else if (t === "VFRACTION") {
887
+ grain.vfraction = +n;
888
+ i += 2;
889
+ } else if (t === "SIZE") {
890
+ grain.size = +n;
891
+ i += 2;
892
+ } else if (t === "MINSIZE") {
893
+ grain.minsize = +n;
894
+ i += 2;
895
+ } else if (t === "MAXSIZE") {
896
+ grain.maxsize = +n;
897
+ i += 2;
898
+ } else
899
+ break;
900
+ }
901
+ return { grain, next: i };
902
+ }
903
+ function parseColourLine(line) {
904
+ const idx = line.indexOf("!COLOUR");
905
+ if (idx < 0)
906
+ return null;
907
+ const tokens = line.slice(idx + "!COLOUR".length).trim().split(/\s+/).filter(Boolean);
908
+ if (tokens.length < 2)
909
+ return null;
910
+ const name = tokens[0];
911
+ let code = -1, hex = "#FF00FF", edge = "#333333", alpha = 255, luminance = 0;
912
+ let finish = { type: "default" };
913
+ let i = 1;
914
+ while (i < tokens.length) {
915
+ const t = tokens[i].toUpperCase();
916
+ const n = tokens[i + 1];
917
+ if (t === "CODE") {
918
+ code = +n;
919
+ i += 2;
920
+ } else if (t === "VALUE") {
921
+ const h = parseHexColor(n);
922
+ if (h)
923
+ hex = h;
924
+ i += 2;
925
+ } else if (t === "EDGE") {
926
+ const h = parseHexColor(n);
927
+ edge = h ?? n;
928
+ i += 2;
929
+ } else if (t === "ALPHA") {
930
+ alpha = +n;
931
+ i += 2;
932
+ } else if (t === "LUMINANCE") {
933
+ luminance = +n;
934
+ i += 2;
935
+ } else if (t === "CHROME") {
936
+ finish = { type: "chrome" };
937
+ i += 1;
938
+ } else if (t === "PEARLESCENT") {
939
+ finish = { type: "pearlescent" };
940
+ i += 1;
941
+ } else if (t === "RUBBER") {
942
+ finish = { type: "rubber" };
943
+ i += 1;
944
+ } else if (t === "MATTE_METALLIC") {
945
+ finish = { type: "matte_metallic" };
946
+ i += 1;
947
+ } else if (t === "METAL") {
948
+ finish = { type: "metal" };
949
+ i += 1;
950
+ } else if (t === "MATERIAL") {
951
+ const kind = (tokens[i + 1] || "").toUpperCase();
952
+ if (kind === "GLITTER") {
953
+ const { grain, next } = parseGrain(tokens, i + 2);
954
+ finish = { type: "glitter", material: grain };
955
+ i = next;
956
+ } else if (kind === "SPECKLE") {
957
+ const { grain, next } = parseGrain(tokens, i + 2);
958
+ finish = { type: "speckle", material: grain };
959
+ i = next;
960
+ } else if (kind === "FABRIC") {
961
+ const sub = (tokens[i + 2] || "").toUpperCase();
962
+ const fab = ["VELVET", "CANVAS", "STRING", "FUR"].includes(sub) ? sub.toLowerCase() : undefined;
963
+ finish = { type: "fabric", kind: fab };
964
+ i += fab ? 3 : 2;
965
+ } else {
966
+ i = tokens.length;
967
+ }
968
+ } else if (FINISH_WORDS.has(t)) {
969
+ i += 1;
970
+ } else
971
+ i += 1;
972
+ }
973
+ if (code < 0 || !hex)
974
+ return null;
975
+ return { code, name, hex, edge, alpha: Number.isFinite(alpha) ? alpha : 255, luminance: Number.isFinite(luminance) ? luminance : 0, finish };
976
+ }
977
+ function colourSurface(c) {
978
+ const opacity = Math.max(0, Math.min(1, (c.alpha ?? 255) / 255));
979
+ const transparent = opacity < 1;
980
+ const emissive = Math.max(0, Math.min(1, (c.luminance ?? 0) / 255));
981
+ const base = {
982
+ opacity,
983
+ transparent,
984
+ depthWrite: !transparent,
985
+ emissive,
986
+ clearcoat: 0,
987
+ clearcoatRoughness: 0,
988
+ sheen: 0,
989
+ sheenRoughness: 0
990
+ };
991
+ const fin = c.finish?.type ?? "default";
992
+ switch (fin) {
993
+ case "chrome":
994
+ return { ...base, metalness: 1, roughness: 0, finish: "chrome" };
995
+ case "pearlescent":
996
+ return { ...base, metalness: 0.25, roughness: 0.3, clearcoat: 0.5, clearcoatRoughness: 0.25, sheen: 0.4, sheenRoughness: 0.35, finish: "pearlescent" };
997
+ case "rubber":
998
+ return { ...base, metalness: 0, roughness: 0.9, finish: "rubber" };
999
+ case "matte_metallic":
1000
+ return { ...base, metalness: 0.4, roughness: 0.8, finish: "matte_metallic" };
1001
+ case "metal":
1002
+ return { ...base, metalness: 0.85, roughness: 0.2, finish: "metal" };
1003
+ case "glitter": {
1004
+ const m = c.finish.material;
1005
+ const size = m.size ?? (m.minsize != null && m.maxsize != null ? (m.minsize + m.maxsize) / 2 : 1);
1006
+ return {
1007
+ ...base,
1008
+ metalness: 0.35,
1009
+ roughness: 0.25,
1010
+ clearcoat: 1,
1011
+ clearcoatRoughness: 0.08,
1012
+ particleHex: m.value,
1013
+ particleFraction: m.fraction,
1014
+ particleSize: size,
1015
+ finish: "glitter"
1016
+ };
1017
+ }
1018
+ case "speckle": {
1019
+ const m = c.finish.material;
1020
+ const size = m.size ?? (m.minsize != null && m.maxsize != null ? (m.minsize + m.maxsize) / 2 : 1);
1021
+ return {
1022
+ ...base,
1023
+ metalness: 0.2,
1024
+ roughness: 0.55,
1025
+ particleHex: m.value,
1026
+ particleFraction: m.fraction,
1027
+ particleSize: size,
1028
+ finish: "speckle"
1029
+ };
1030
+ }
1031
+ case "fabric":
1032
+ return { ...base, metalness: 0, roughness: 0.85, sheen: 0.65, sheenRoughness: 0.75, finish: "fabric" };
1033
+ default:
1034
+ return { ...base, metalness: 0, roughness: 0.45, finish: "default" };
1035
+ }
1036
+ }
1037
+ function colourMaterialKey(c) {
1038
+ const s = colourSurface(c);
1039
+ const opac = !s.transparent ? "op" : s.opacity >= 0.75 ? "a200" : s.opacity >= 0.4 ? "a128" : `a${Math.round(s.opacity * 255)}`;
1040
+ const lum = s.emissive > 0 ? `L${Math.round(s.emissive * 16)}` : "";
1041
+ let k = `${s.finish}:${opac}${lum}`;
1042
+ if (s.particleHex)
1043
+ k += `:${s.particleHex}:${(s.particleFraction ?? 0).toFixed(2)}`;
1044
+ return k;
1045
+ }
857
1046
  function parseLDConfig(text) {
858
1047
  const out = new Map;
859
1048
  for (const l of text.split(/\r?\n/)) {
860
- const m = /!COLOUR\s+(\S+)\s+CODE\s+(\d+)\s+VALUE\s+(#[0-9A-Fa-f]{6})\s+EDGE\s+(\S+)(?:.*?ALPHA\s+(\d+))?/.exec(l);
861
- if (m)
862
- out.set(+m[2], { code: +m[2], name: m[1], hex: m[3].toUpperCase(), edge: m[4], alpha: m[5] ? +m[5] : 255 });
1049
+ if (!l.includes("!COLOUR"))
1050
+ continue;
1051
+ const c = parseColourLine(l);
1052
+ if (c)
1053
+ out.set(c.code, c);
863
1054
  }
864
1055
  return out;
865
1056
  }
866
1057
 
867
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/node/resolver.ts
1058
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/node/resolver.ts
868
1059
  var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
869
1060
  var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
870
1061
  var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
@@ -1134,11 +1325,11 @@ function collectDeps(lib, refs) {
1134
1325
  return out;
1135
1326
  }
1136
1327
 
1137
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/node/compile.ts
1328
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/node/compile.ts
1138
1329
  import fs2 from "node:fs";
1139
1330
  import path2 from "node:path";
1140
1331
 
1141
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/node/lod.ts
1332
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/node/lod.ts
1142
1333
  import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer";
1143
1334
  await Promise.all([MeshoptSimplifier.ready, MeshoptEncoder.ready]);
1144
1335
  function reorderMesh(P, N, C, I) {
@@ -1330,7 +1521,7 @@ function lodChain(P, C, I) {
1330
1521
  return out;
1331
1522
  }
1332
1523
 
1333
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/node/compile.ts
1524
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/node/compile.ts
1334
1525
  function loadColors(root) {
1335
1526
  const map = new Map;
1336
1527
  for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
@@ -1913,7 +2104,7 @@ function encodePack(parts, studKinds, weld, extra, options = {}) {
1913
2104
  return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
1914
2105
  }
1915
2106
 
1916
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/node/build-pack.ts
2107
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/node/build-pack.ts
1917
2108
  function buildPack(o) {
1918
2109
  const lib = new Library(o.ldrawRoot);
1919
2110
  const refs = new Map;
@@ -1965,8 +2156,13 @@ function buildPack(o) {
1965
2156
  let extra;
1966
2157
  if (o.colors !== false) {
1967
2158
  const cfg = path3.join(o.ldrawRoot, "LDConfig.ldr");
1968
- if (fs3.existsSync(cfg))
1969
- extra = { colors: Object.fromEntries([...parseLDConfig(fs3.readFileSync(cfg, "latin1"))].map(([k, v]) => [k, v.hex])) };
2159
+ if (fs3.existsSync(cfg)) {
2160
+ const table = [...parseLDConfig(fs3.readFileSync(cfg, "latin1"))];
2161
+ extra = {
2162
+ colors: Object.fromEntries(table.map(([k, v]) => [k, v.hex])),
2163
+ colorTable: Object.fromEntries(table.map(([k, v]) => [k, { name: v.name, hex: v.hex, edge: v.edge, alpha: v.alpha, luminance: v.luminance, finish: v.finish }]))
2164
+ };
2165
+ }
1970
2166
  }
1971
2167
  const texture = (n) => {
1972
2168
  const k = norm2(n), b = data.get(k) ?? data.get(k.replace(/^textures\//, "")) ?? data.get("textures/" + k);
@@ -1976,7 +2172,7 @@ function buildPack(o) {
1976
2172
  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 } : {} };
1977
2173
  }
1978
2174
 
1979
- // ../../../../../../../tmp/ldr-build-ClpPxU/src/cli.ts
2175
+ // ../../../../../../../tmp/ldr-build-TjmzYL/src/cli.ts
1980
2176
  var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
1981
2177
 
1982
2178
  Usage:
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
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, CALLOUT_END } from "./model.js";
3
+ export { flatten, parseModel, sequenceSteps, stepKey, parseLDConfig, parseColourLine, colourSurface, colourMaterialKey, 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
- export type { PartInstance, FlattenOptions, LDrawColor } from "./model.js";
7
- export { loadPack, decodePack, buildLdrScene, makeMaterial, createInstancedEdges, computeCulling, cullCore, forEachStud, conditionalLines, createInstancedConditionalEdges, conditionalLineVisible, warmPackGeometry } from "./render.js";
6
+ export type { PartInstance, FlattenOptions, LDrawColor, LDrawFinish, LDrawFinishType, LDrawMaterialGrain, ColourSurface } from "./model.js";
7
+ export { loadPack, decodePack, buildLdrScene, makeMaterial, makeFinishMaterial, createInstancedEdges, computeCulling, cullCore, forEachStud, conditionalLines, createInstancedConditionalEdges, conditionalLineVisible, warmPackGeometry } from "./render.js";
8
8
  export { precomputeCull, sceneInstances } from "./cull.js";
9
9
  export type { Pack, LdrParams, Inst, CullResult } from "./render.js";
10
10
  export { createLdrObject } from "./scene.js";