ldr 0.3.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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { parse, parseColor, isDirectColor, mul, I as IDENTITY, norm, smoothNormals, parseTexMap, texmapUV, texmapTriangleUV, transformTexMap, resolveTexture } from "./spec.js";
2
- export type { M as Matrix12, Doc, LFile, Cmd, RotStep, BufExchg, TexMap, TexMapCmd, TexMapMethod, TexAnno, DataBlock, PartMesh } from "./spec.js";
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
+ 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
5
  export { loadPack, decodePack, buildLdrScene, makeMaterial, createInstancedEdges, computeCulling, conditionalLines, createInstancedConditionalEdges, conditionalLineVisible } from "./render.js";
package/dist/index.js CHANGED
@@ -6,6 +6,17 @@ function parseColor(tok) {
6
6
  return +tok;
7
7
  }
8
8
  var MOVED_RE = /^~?\s*moved\s+to\s+(.+)$/i;
9
+ var SPECIAL0 = new Set(["FILE", "NOFILE", "!DATA", "!:", "STEP", "BFC", "ROTSTEP", "BUFEXCHG", "!TEXMAP"]);
10
+ var PART_ORG = new Set(["part", "subpart", "primitive", "8_primitive", "48_primitive", "shortcut", "helper"]);
11
+ var MODEL_ORG = new Set(["model", "submodel", "configuration"]);
12
+ var ORG_RE = /^(?:!?LDRAW_ORG|Unofficial|Official\s+LCAD)\s+(\S+)/i;
13
+ function orgType(text) {
14
+ const m = ORG_RE.exec(text);
15
+ if (!m)
16
+ return;
17
+ const t = m[1].toLowerCase().replace(/^unofficial_/, "");
18
+ return PART_ORG.has(t) || MODEL_ORG.has(t) ? t : undefined;
19
+ }
9
20
  function texNames(rest) {
10
21
  const out = [];
11
22
  let i = 0;
@@ -91,7 +102,8 @@ function geomLine(tk) {
91
102
  return { t: 4, color, p: nums.slice(0, 12) };
92
103
  return { t: 5, color, p: nums.slice(0, 12) };
93
104
  }
94
- function parse(src, name = "main.ldr") {
105
+ function parse(src, name = "main.ldr", popts = {}) {
106
+ const gtext = !!popts.geometryText;
95
107
  const files = new Map;
96
108
  let cur = null;
97
109
  let main = "";
@@ -107,6 +119,7 @@ function parse(src, name = "main.ldr") {
107
119
  (data ??= new Map).set(norm(dataCur.name), { name: dataCur.name, bytes, ...png ? { mime: "image/png" } : {} });
108
120
  dataCur = null;
109
121
  };
122
+ let inHeader = true;
110
123
  const start = (n) => {
111
124
  closeData();
112
125
  cur = { name: norm(n), cmds: [] };
@@ -115,6 +128,7 @@ function parse(src, name = "main.ldr") {
115
128
  main = cur.name;
116
129
  texStack = [];
117
130
  texNext = null;
131
+ inHeader = true;
118
132
  };
119
133
  let skip = false, sawBlock = false;
120
134
  const firstBlock = () => {
@@ -144,6 +158,53 @@ function parse(src, name = "main.ldr") {
144
158
  const line = raw.trim();
145
159
  if (!line)
146
160
  continue;
161
+ if (gtext && line.charCodeAt(0) >= 50 && line.charCodeAt(0) <= 53 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
162
+ if (dataCur || skip)
163
+ continue;
164
+ if (!cur)
165
+ start(name);
166
+ const sp = line.slice(2).trimStart();
167
+ const e = sp.search(/\s/);
168
+ const g = { t: line.charCodeAt(0) - 48, color: parseColor(e < 0 ? sp : sp.slice(0, e)), p: [], src: line };
169
+ annotate(g, false);
170
+ cur.cmds.push(g);
171
+ inHeader = false;
172
+ continue;
173
+ }
174
+ if (line.charCodeAt(0) === 48 && (line.charCodeAt(1) === 32 || line.charCodeAt(1) === 9)) {
175
+ let a = 2;
176
+ while (line.charCodeAt(a) === 32 || line.charCodeAt(a) === 9)
177
+ a++;
178
+ let b = a;
179
+ while (b < line.length && line.charCodeAt(b) !== 32 && line.charCodeAt(b) !== 9)
180
+ b++;
181
+ const kw = line.slice(a, b);
182
+ if (!SPECIAL0.has(kw)) {
183
+ if (skip || dataCur)
184
+ continue;
185
+ if (!cur)
186
+ start(name);
187
+ const text = line.slice(a);
188
+ const c = { t: 0, text };
189
+ if (kw.charCodeAt(0) === 33)
190
+ c.meta = kw;
191
+ texNext = null;
192
+ if (inHeader && !cur.org) {
193
+ const o = orgType(text);
194
+ if (o)
195
+ cur.org = o;
196
+ }
197
+ const moved = text.match(MOVED_RE);
198
+ if (moved && !cur.movedTo) {
199
+ let target = moved[1].trim().replace(/\\/g, "/");
200
+ if (!/\.(dat|ldr|mpd)$/i.test(target))
201
+ target += ".dat";
202
+ cur.movedTo = norm(target);
203
+ }
204
+ cur.cmds.push(c);
205
+ continue;
206
+ }
207
+ }
147
208
  const tk = line.split(/\s+/);
148
209
  const t = +tk[0];
149
210
  if (t === 0) {
@@ -186,6 +247,9 @@ function parse(src, name = "main.ldr") {
186
247
  c.bfc = tk.slice(2).join(" ");
187
248
  else if (tk[1] === "ROTSTEP") {
188
249
  c.meta = "ROTSTEP";
250
+ c.step = true;
251
+ texStack = [];
252
+ texNext = null;
189
253
  if ((tk[2] ?? "").toUpperCase() === "END")
190
254
  c.rotstep = { x: 0, y: 0, z: 0, mode: "END" };
191
255
  else {
@@ -198,6 +262,11 @@ function parse(src, name = "main.ldr") {
198
262
  c.bufexchg = { op: op === "RETRIEVE" ? "RETRIEVE" : "STORE", buf: tk[3] ?? "A" };
199
263
  } else if (tk[1]?.startsWith("!"))
200
264
  c.meta = tk[1];
265
+ if (inHeader && !cur.org) {
266
+ const o = orgType(text);
267
+ if (o)
268
+ cur.org = o;
269
+ }
201
270
  if (tk[1] === "!TEXMAP") {
202
271
  const op = (tk[2] ?? "").toUpperCase();
203
272
  texNext = null;
@@ -248,6 +317,7 @@ function parse(src, name = "main.ldr") {
248
317
  continue;
249
318
  annotate(g, false);
250
319
  cur.cmds.push(g);
320
+ inHeader = false;
251
321
  }
252
322
  closeData();
253
323
  return data ? { main, files, data } : { main, files };
@@ -427,34 +497,249 @@ function smoothNormals(pos, edges, cond, creaseDeg = 35) {
427
497
  }
428
498
  return out;
429
499
  }
500
+ var kindMemo = new WeakMap;
501
+ var SUBREF_RE = /^(s|8|48)\//;
502
+ function fileKind(doc, name) {
503
+ const f = doc.files.get(norm(name));
504
+ if (!f)
505
+ return "part";
506
+ let k = kindMemo.get(f);
507
+ if (k)
508
+ return k;
509
+ if (f.org)
510
+ k = PART_ORG.has(f.org) ? "part" : "model";
511
+ else if (f.name === doc.main)
512
+ k = "model";
513
+ else
514
+ k = f.cmds.some((c) => c.t >= 2 || c.t === 0 && !!c.geom || c.t === 1 && SUBREF_RE.test(c.file)) ? "part" : "model";
515
+ kindMemo.set(f, k);
516
+ return k;
517
+ }
518
+ var looseMemo = new WeakMap;
519
+ function looseSteps(f) {
520
+ let r = looseMemo.get(f);
521
+ if (r)
522
+ return r;
523
+ r = [];
524
+ let s = 0;
525
+ for (const c of f.cmds) {
526
+ if (c.t === 0 && c.step) {
527
+ s++;
528
+ continue;
529
+ }
530
+ if ((c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) && r[r.length - 1] !== s)
531
+ r.push(s);
532
+ }
533
+ looseMemo.set(f, r);
534
+ return r;
535
+ }
536
+ var colorToken = (c) => c > 16777215 ? "0x" + c.toString(16).toUpperCase() : String(c);
537
+ var lineOf = (c) => c.t === 0 ? "0 " + c.text : c.t === 1 ? `1 ${colorToken(c.color)} ${c.m.join(" ")} ${c.file}` : c.src ? c.src : `${c.t} ${colorToken(c.color)} ${c.p.join(" ")}`;
538
+ var loosePartName = (file, step) => `${norm(file)}#${step}`;
539
+ function looseFile(f, step) {
540
+ const out = [];
541
+ let s = 0;
542
+ const isNext = (c) => !!c && c.t === 0 && c.texmap?.op === "NEXT";
543
+ for (const c of f.cmds) {
544
+ if (c.t === 0) {
545
+ if (c.step) {
546
+ s++;
547
+ continue;
548
+ }
549
+ if (c.bfc !== undefined) {
550
+ out.push(c);
551
+ continue;
552
+ }
553
+ if (s !== step)
554
+ continue;
555
+ if (c.texmap)
556
+ out.push(c);
557
+ else if (c.geom) {
558
+ if (c.geom.t >= 2)
559
+ out.push(c);
560
+ else if (isNext(out[out.length - 1]))
561
+ out.pop();
562
+ }
563
+ continue;
564
+ }
565
+ if (s !== step)
566
+ continue;
567
+ if (c.t === 1) {
568
+ if (isNext(out[out.length - 1]))
569
+ out.pop();
570
+ continue;
571
+ }
572
+ out.push(c);
573
+ }
574
+ return { name: loosePartName(f.name, step), cmds: out };
575
+ }
576
+ function looseSource(f, step) {
577
+ return [`0 ldr: own geometry of ${f.name}, step ${step}`, ...looseFile(f, step).cmds.map(lineOf)].join(`
578
+ `) + `
579
+ `;
580
+ }
581
+ function hash32(s) {
582
+ let h = 2166136261;
583
+ for (let i = 0;i < s.length; i++) {
584
+ h ^= s.charCodeAt(i);
585
+ h = Math.imul(h, 16777619);
586
+ }
587
+ return (h >>> 0).toString(16).padStart(8, "0");
588
+ }
589
+ function invert(m) {
590
+ const [x, y, z, a, b, c, d, e, f, g, h, i] = m;
591
+ const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
592
+ const det = a * A + b * B + c * C;
593
+ if (!Number.isFinite(det) || Math.abs(det) < 0.000000000001)
594
+ return null;
595
+ const k = 1 / det;
596
+ const r = [A * k, -(b * i - c * h) * k, (b * f - c * e) * k, B * k, (a * i - c * g) * k, -(a * f - c * d) * k, C * k, -(a * h - b * g) * k, (a * e - b * d) * k];
597
+ return [-(r[0] * x + r[1] * y + r[2] * z), -(r[3] * x + r[4] * y + r[5] * z), -(r[6] * x + r[7] * y + r[8] * z), ...r];
598
+ }
599
+ var num = (x) => String(+x.toFixed(6) || 0);
600
+ var quote = (s) => '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + '"';
601
+ function texWrapper(part, map) {
602
+ const v = [...map.p1, ...map.p2, ...map.p3, ...map.method !== "PLANAR" ? [map.a ?? 360] : [], ...map.method === "SPHERICAL" ? [map.b ?? 180] : []].map(num).join(" ");
603
+ const source = `0 ldr: ${part} with a model-level !TEXMAP
604
+ 0 BFC CERTIFY CCW
605
+ 0 !TEXMAP START ${map.method} ${v} ${quote(map.texture)}${map.glossmap ? " GLOSSMAP " + quote(map.glossmap) : ""}
606
+ 1 16 0 0 0 1 0 0 0 1 0 0 0 1 ${norm(part)}
607
+ 0 !TEXMAP END
608
+ `;
609
+ return { name: `${norm(part)}#tex-${hash32(source)}`, source };
610
+ }
611
+ var mul3 = (a, b) => {
612
+ const r = new Array(9);
613
+ for (let i = 0;i < 3; i++)
614
+ for (let j = 0;j < 3; j++)
615
+ r[i * 3 + j] = a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
616
+ return r;
617
+ };
618
+ function rotStepMatrix(x, y, z) {
619
+ const D = Math.PI / 180, s1 = Math.sin(x * D), c1 = Math.cos(x * D), s2 = Math.sin(y * D), c2 = Math.cos(y * D), s3 = Math.sin(z * D), c3 = Math.cos(z * D);
620
+ return [c2 * c3, -c2 * s3, s2, c1 * s3 + s1 * s2 * c3, c1 * c3 - s1 * s2 * s3, -s1 * c2, s1 * s3 - c1 * s2 * c3, s1 * c3 + c1 * s2 * s3, c1 * c2];
621
+ }
622
+ function stepViewMatrix(view, defaultView = [1, 0, 0, 0, 1, 0, 0, 0, 1]) {
623
+ return !view ? [...defaultView] : view.abs ? [...view.m] : mul3(defaultView, view.m);
624
+ }
625
+ var viewMemo = new WeakMap;
626
+ function fileStepViews(f) {
627
+ const hit = viewMemo.get(f);
628
+ if (hit)
629
+ return hit;
630
+ const out = [];
631
+ let cur = null;
632
+ for (const c of f.cmds) {
633
+ if (c.t !== 0 || !c.step)
634
+ continue;
635
+ const r = c.rotstep;
636
+ if (r) {
637
+ if (r.mode === "END")
638
+ cur = null;
639
+ else {
640
+ const R = rotStepMatrix(r.x, r.y, r.z);
641
+ cur = r.mode === "ABS" ? { abs: true, m: R, rotstep: r } : r.mode === "ADD" && cur ? { abs: cur.abs, m: mul3(cur.m, R), rotstep: r } : { abs: false, m: R, rotstep: r };
642
+ }
643
+ }
644
+ out.push(cur);
645
+ }
646
+ out.push(cur);
647
+ viewMemo.set(f, out);
648
+ return out;
649
+ }
430
650
  // src/model.ts
431
651
  function flatten(doc, opts = {}) {
432
652
  const out = [];
433
653
  const maxDepth = opts.maxDepth ?? 64;
434
- const walk = (d, fname, m, color, path, stepPath, depth) => {
654
+ const loose = opts.looseGeometry !== false, tx = !!opts.textures;
655
+ const src = new Map;
656
+ const lazy = (f, k) => {
657
+ const key = f.name + "#" + k;
658
+ let fn = src.get(key);
659
+ if (!fn) {
660
+ let s;
661
+ fn = Object.assign(() => s ??= looseSource(f, k), { file: f, step: k });
662
+ src.set(key, fn);
663
+ }
664
+ return fn;
665
+ };
666
+ const walk = (d, fname, m, color, path, stepPath, depth, texIn) => {
435
667
  const f = d.files.get(fname);
436
668
  if (!f || depth > maxDepth)
437
669
  return;
438
670
  let s = 0;
439
- for (const c of f.cmds) {
440
- if (c.t === 0 && c.step)
671
+ const views = fileStepViews(f);
672
+ const ls = loose ? looseSteps(f) : [];
673
+ let li = 0;
674
+ const emit = (part, col, mm, sp, k, embedded, tex) => {
675
+ const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
676
+ if (embedded)
677
+ i.embedded = true;
678
+ if (tex) {
679
+ const w = texWrapper(part, tex);
680
+ i.texPart = w.name;
681
+ i.texSource = w.source;
682
+ }
683
+ const v = views[k];
684
+ if (v)
685
+ i.view = v;
686
+ out.push(i);
687
+ return i;
688
+ };
689
+ const flushLoose = (upTo) => {
690
+ while (li < ls.length && ls[li] <= upTo) {
691
+ const k = ls[li++];
692
+ emit(loosePartName(f.name, k), color, m, [...stepPath, k], k, false, null).source = lazy(f, k);
693
+ }
694
+ };
695
+ for (const c0 of f.cmds) {
696
+ if (c0.t === 0 && c0.step) {
441
697
  s++;
698
+ continue;
699
+ }
700
+ let c = c0;
701
+ if (c.t >= 2 || c.t === 0 && c.geom && c.geom.t >= 2) {
702
+ flushLoose(s);
703
+ continue;
704
+ }
705
+ if (c.t === 0) {
706
+ if (tx && c.geom?.t === 1)
707
+ c = c.geom;
708
+ else
709
+ continue;
710
+ }
442
711
  if (c.t !== 1)
443
712
  continue;
713
+ if (tx && c.fallback)
714
+ continue;
444
715
  const col = c.color === 16 ? color : c.color, mm = mul(m, c.m), sp = [...stepPath, s];
716
+ const tex = tx ? c.tex ?? texIn : null;
717
+ const local = tex ? invert(c.m) : null;
718
+ const lt = tex && local ? transformTexMap(tex, local) : null;
445
719
  if (d.files.has(c.file)) {
446
- walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1);
720
+ if (fileKind(d, c.file) === "model") {
721
+ walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1, lt);
722
+ continue;
723
+ }
724
+ emit(c.file, col, mm, sp, s, true, lt);
447
725
  continue;
448
726
  }
449
727
  const ext = opts.resolveModel?.(c.file);
450
728
  if (ext) {
451
- walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1);
729
+ walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1, lt);
452
730
  continue;
453
731
  }
454
- out.push({ part: c.file, color: col, m: mm, step: sp[0], steps: sp, path });
732
+ emit(c.file, col, mm, sp, s, false, lt);
455
733
  }
734
+ flushLoose(Infinity);
456
735
  };
457
- walk(doc, doc.main, I, 16, doc.main, [], 0);
736
+ if (fileKind(doc, doc.main) === "part") {
737
+ const f = doc.files.get(doc.main);
738
+ if (f)
739
+ out.push({ part: doc.main, color: 16, m: [...I], step: 0, steps: [0], localStep: 0, path: doc.main, embedded: true });
740
+ return out;
741
+ }
742
+ walk(doc, doc.main, I, 16, doc.main, [], 0, null);
458
743
  return out;
459
744
  }
460
745
  function parseModel(text, name = "main.ldr", opts) {
@@ -465,8 +750,14 @@ function sequenceSteps(insts) {
465
750
  const uniq = [...new Set(insts.map(key))].sort();
466
751
  const rank = new Map(uniq.map((k, j) => [k, j]));
467
752
  const stepOf = new Int32Array(insts.length);
468
- insts.forEach((i, k) => stepOf[k] = rank.get(key(i)));
469
- return { count: uniq.length, stepOf };
753
+ const views = new Array(uniq.length).fill(null);
754
+ insts.forEach((i, k) => {
755
+ const r = rank.get(key(i));
756
+ stepOf[k] = r;
757
+ if (i.view)
758
+ views[r] = i.view;
759
+ });
760
+ return { count: uniq.length, stepOf, views };
470
761
  }
471
762
  function parseLDConfig(text) {
472
763
  const out = new Map;
@@ -830,7 +1121,7 @@ function buildLdrScene(insts, pack, P, color, mark) {
830
1121
  mark("culling");
831
1122
  const edgeObjs = [];
832
1123
  const condObjs = [];
833
- const wantCond = P.cond !== false && P.edges === "idx";
1124
+ const wantCond = P.cond !== false && (P.edges === "idx" || P.edges === true);
834
1125
  let condMat = null;
835
1126
  const cmat = () => condMat ??= condLineMat();
836
1127
  const condCache = new Map;
@@ -1098,11 +1389,18 @@ function toPlastic(old) {
1098
1389
  function createLdrObject(instances, pack, options = {}) {
1099
1390
  const o = { edges: true, conditionalLines: true, cullStuds: true, cullUndersides: true, side: "front", material: "plastic", ...options };
1100
1391
  const color = colorFn(o.colors, pack, new THREE2.Color(o.fallbackColor ?? 10528169));
1101
- const all = instances.map((i) => ({ part: i.part, color: String(i.color), m: i.m, steps: i.steps }));
1392
+ const useTex = o.textures !== false;
1393
+ const pick = (i) => useTex && i.texPart && pack.header.parts[i.texPart] ? i.texPart : i.part;
1394
+ const all = instances.map((i) => ({ part: pick(i), color: String(i.color), m: i.m, steps: i.steps }));
1102
1395
  const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
1103
1396
  const uniq = [...new Set(all.map(key))].sort();
1104
1397
  const rank = new Map(uniq.map((k, j) => [k, j]));
1105
1398
  const stepOf = all.map((i) => rank.get(key(i)));
1399
+ const stepViews = new Array(uniq.length).fill(null);
1400
+ instances.forEach((i, k) => {
1401
+ if (i.view)
1402
+ stepViews[stepOf[k]] = i.view;
1403
+ });
1106
1404
  const missing = [...new Set(all.filter((i) => !pack.header.parts[i.part]).map((i) => i.part))];
1107
1405
  const object = new THREE2.Group;
1108
1406
  object.name = "ldr";
@@ -1160,6 +1458,7 @@ function createLdrObject(instances, pack, options = {}) {
1160
1458
  object,
1161
1459
  stats,
1162
1460
  stepCount: uniq.length,
1461
+ stepViews,
1163
1462
  setStep: (s) => build(s),
1164
1463
  setEdges: (v) => {
1165
1464
  edges = v;
@@ -1211,6 +1510,7 @@ async function decompressPack(data) {
1211
1510
  export {
1212
1511
  I as IDENTITY,
1213
1512
  buildLdrScene,
1513
+ colorToken,
1214
1514
  computeCulling,
1215
1515
  conditionalLineVisible,
1216
1516
  conditionalLines,
@@ -1219,9 +1519,17 @@ export {
1219
1519
  createLdrObject,
1220
1520
  decodePack,
1221
1521
  decompressPack,
1522
+ fileKind,
1523
+ fileStepViews,
1222
1524
  flatten,
1525
+ hash32,
1526
+ invert,
1223
1527
  isDirectColor,
1224
1528
  loadPack,
1529
+ looseFile,
1530
+ loosePartName,
1531
+ looseSource,
1532
+ looseSteps,
1225
1533
  makeMaterial,
1226
1534
  mul,
1227
1535
  norm,
@@ -1231,8 +1539,11 @@ export {
1231
1539
  parseModel,
1232
1540
  parseTexMap,
1233
1541
  resolveTexture,
1542
+ rotStepMatrix,
1234
1543
  sequenceSteps,
1235
1544
  smoothNormals,
1545
+ stepViewMatrix,
1546
+ texWrapper,
1236
1547
  texmapTriangleUV,
1237
1548
  texmapUV,
1238
1549
  transformTexMap
package/dist/model.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { parse, norm, type Doc, type M } from "./spec.js";
1
+ import { parse, norm, type Doc, type M, type LFile, type StepView } from "./spec.js";
2
2
  /** A placed part. `color` is an LDraw colour code (or direct colour 0x2RRGGBB); `m` is the 12-number LDraw matrix `x y z a b c d e f g h i`. */
3
3
  export interface PartInstance {
4
4
  part: string;
@@ -8,28 +8,55 @@ export interface PartInstance {
8
8
  step: number;
9
9
  /** Step path through nested submodels, e.g. [3, 1] = top step 3, submodel step 1. */
10
10
  steps: number[];
11
+ /** Step within the file that places this instance (the last entry of `steps`). */
12
+ localStep: number;
11
13
  /** Submodel path, e.g. "main.ldr/wheel.ldr". */
12
14
  path: string;
15
+ /** The part is defined inside the document (an MPD-embedded part, see `fileKind`), not taken from the library. */
16
+ embedded?: boolean;
17
+ /** Set for synthetic parts (a model file's own type 2-5 lines, named `<file>#<step>`): returns the part's LDraw source
18
+ * (`source.file` / `source.step` give the model file and step, see `looseFile`). */
19
+ source?: (() => string) & {
20
+ file?: LFile;
21
+ step?: number;
22
+ };
23
+ /** Only with `flatten(doc, { textures: true })`: the part is wrapped by a model-level `!TEXMAP`. `texPart` names the synthetic
24
+ * textured part (`<part>#tex-<hash>`, in packs built with `textures: true`) and `texSource` is its LDraw source; `part` stays the plain part. */
25
+ texPart?: string;
26
+ texSource?: string;
27
+ /** View rotation of this instance's step from `0 ROTSTEP` (absent = default view). See `fileStepViews`. */
28
+ view?: StepView;
13
29
  }
14
30
  export interface FlattenOptions {
15
31
  /** Resolve an external reference that is not a submodel of this document (e.g. a model file you loaded separately). Return null to treat it as a part. */
16
32
  resolveModel?: (name: string) => Doc | null;
17
33
  /** Max submodel nesting (cycle guard). Default 64. */
18
34
  maxDepth?: number;
35
+ /** Emit a model file's own type 2-5 lines as synthetic parts `<file>#<step>` (LDraw allows geometry in any file). Default true. */
36
+ looseGeometry?: boolean;
37
+ /** TEXMAP-aware view of the model: references wrapped by a model-level `!TEXMAP` get `texPart`, `0 !:` type-1 lines are
38
+ * included and `!TEXMAP FALLBACK` references are skipped. Use with packs built with `textures: true`. Default false. */
39
+ textures?: boolean;
19
40
  }
20
- /** Flatten a parsed LDraw document into part instances. Submodels (`0 FILE` blocks in an MPD) are expanded; everything else is treated as a part. */
41
+ /**
42
+ * Flatten a parsed LDraw document into part instances. Model files (`0 FILE` blocks in an MPD that `fileKind` calls models) are
43
+ * expanded; MPD-embedded parts and library references become instances. A model file's own geometry becomes synthetic parts.
44
+ */
21
45
  export declare function flatten(doc: Doc, opts?: FlattenOptions): PartInstance[];
22
46
  /** Parse LDraw text (.ldr/.mpd/.dat) and flatten it in one call. */
23
47
  export declare function parseModel(text: string, name?: string, opts?: FlattenOptions): PartInstance[];
24
48
  /**
25
49
  * Assign each instance a global sequential build-step index, ordering by the full nested step path
26
- * (so submodel steps become their own steps). Returns the number of steps.
50
+ * (so submodel steps become their own steps). Returns the number of steps and, per global step, its ROTSTEP view
51
+ * (null = default view; the view of the innermost file's step, see `fileStepViews`).
27
52
  */
28
53
  export declare function sequenceSteps<T extends {
29
54
  steps: number[];
55
+ view?: StepView;
30
56
  }>(insts: T[]): {
31
57
  count: number;
32
58
  stepOf: Int32Array;
59
+ views: (StepView | null)[];
33
60
  };
34
61
  /** Colour table entry. */
35
62
  export interface LDrawColor {
@@ -22,6 +22,9 @@ export interface BuildPackOptions {
22
22
  /** Keep textured (`!TEXMAP`) parts' TEXMAP-aware geometry + UVs and embed their PNGs (pack v2 "tex"). Off by default;
23
23
  * packs without textured parts are byte-identical either way. Textures resolve from model `!DATA` blocks, then `textures/` in the library. */
24
24
  textures?: boolean;
25
+ /** Render a model file's own type 2-5 lines (as synthetic parts `<file>#<step>`). Default true (LDraw allows geometry in any file);
26
+ * false reproduces packs from ldr <= 0.2.0, which dropped it. */
27
+ looseGeometry?: boolean;
25
28
  /** Embed the LDConfig colour table (code → hex) in the pack header. Default true. */
26
29
  colors?: boolean;
27
30
  onProgress?: (done: number, total: number) => void;
@@ -31,5 +34,7 @@ export interface BuildPackResult {
31
34
  parts: number;
32
35
  studKinds: number;
33
36
  missing: string[];
37
+ /** e.g. two models embedding different files under the same name (the first one is used). */
38
+ warnings?: string[];
34
39
  }
35
40
  export declare function buildPack(o: BuildPackOptions): BuildPackResult;