ldr 0.6.0 → 0.8.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
@@ -102,7 +102,7 @@ Light the scene (or set `scene.environment`) as you would for any `MeshStandardM
102
102
  | `resolveModel(name)` | – | Resolves an external reference to a `Doc`, such as a model loaded separately. Return `null` to treat it as a part. |
103
103
  | `maxDepth` | `64` | Submodel nesting limit (cycle guard). |
104
104
 
105
- MPD-embedded parts, such as Studio/OMR unofficial parts or stickers, become part instances (`embedded: true`) and override library parts of the same name.
105
+ MPD-embedded parts, such as Studio/OMR unofficial parts or stickers, become part instances (`embedded: true`) and override library parts of the same name. `loadModel` asks the parts CDN for them by a hash of their definition (`embeddedKeys`), so a CDN built with `--embedded <model dir>` serves the embedded parts of those models (the demo CDN carries the ones in its 1,773 catalog models).
106
106
 
107
107
  **Packs and rendering**
108
108
 
@@ -143,6 +143,15 @@ MPD-embedded parts, such as Studio/OMR unofficial parts or stickers, become part
143
143
 
144
144
  `setStep` doesn't rebuild anything: it compacts instance buffers, and culling is step-aware (a stud covered only by a later step's part stays visible). `stats` reflect the shown step.
145
145
 
146
+ **Building instructions.** `parseModel(text, name, { callouts: true })` (or `loadModel(url, { callouts: true })`) sequences a
147
+ submodel placed N times in one step like a booklet: the first copy is built step by step (its instances get `mult = N`, the "xN"),
148
+ the other copies appear finished at the end of that step. `planStepViews(instances, pack.header.parts, stepOf, stepCount)` plans a
149
+ camera per step (`view(step)` → `{ dir, target, radius, minVis, meanVis, below, isolate }`, LDraw space): the booklet view (30° up,
150
+ 45° round) while it shows the new parts at least half visible on average, otherwise the best of 24 views from above (or from below for
151
+ parts added underneath), preferring front three-quarter views and small turns (thresholds from Agrawala et al., SIGGRAPH 2003).
152
+ `isolate` is set while a sub-assembly is being built: show it on its own with `setStep(step, isolateMask(instances, view.isolate))`.
153
+ Visibility is measured on part bounding boxes, so it needs no GPU and also runs in Node.
154
+
146
155
  ### `ldr/node` (Node / Bun only)
147
156
 
148
157
  Everything above, plus:
@@ -202,7 +211,7 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
202
211
 
203
212
  - **Transparency:** transparent colours render opaque.
204
213
  - **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.
205
- - **ROTSTEP** is reported (`stepViews`, `instance.view`), not applied. Rotating the camera per step is up to your app.
214
+ - **ROTSTEP** is reported (`stepViews`, `instance.view`), not applied; `planStepViews` takes a `fixed(step)` override if you want to honour it.
206
215
  - **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`.
207
216
  - **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.
208
217
  - **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.
@@ -212,4 +221,4 @@ Reproduce: `cd bench && bun run setup && bun run perf` (regression gate) or `bun
212
221
  - **Code:** MIT © Evan Bacon.
213
222
  - **LDraw parts:** packs you build contain geometry from the **LDraw.org Parts Library**, which is licensed **CC BY 2.0** (newer files CC BY 4.0, per file header). If you ship packs, credit "LDraw.org Parts Library (CC BY 2.0)" in your app. See <https://www.ldraw.org/legal-info>.
214
223
  - **Datsville:** the benchmark model is by [mjhorvath/Datsville](https://github.com/mjhorvath/Datsville) (CC BY-SA 4.0). It is used for benchmarking only and is not redistributed.
215
- - **Trademarks:** LEGO® is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project. LDraw™ is a trademark of the Estate of James Jessiman.
224
+ - **Trademarks:** LEGO® is a trademark of the LEGO Group, which does not sponsor, authorize or endorse this project; ldr is not affiliated with the LEGO Group. LDraw™ is a trademark of the Estate of James Jessiman.
package/dist/cdn.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface LoadedModel extends LdrObject {
24
24
  /**
25
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
26
26
  * parts CDN and builds an instanced `LdrObject`. No LDraw library or parts pack needed.
27
- * Limits (v1): parts defined inside the model (MPD-embedded parts) and loose geometry in model files are skipped (listed in `stats.missing`).
27
+ * MPD-embedded parts are fetched by content key (`emb/<hash>.dat`, see embedded.ts): a CDN built with `--embedded` serves the ones in
28
+ * known models. Embedded parts the CDN lacks, and loose geometry in model files, are skipped (listed in `stats.missing`).
28
29
  */
29
30
  export declare function loadModel(source: string, o?: LoadModelOptions): Promise<LoadedModel>;
package/dist/cli.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/cli.ts
3
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/cli.ts
4
4
  import fs4 from "node:fs";
5
5
  import zlib from "node:zlib";
6
6
 
7
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/build-pack.ts
7
+ // ../../../../../../../tmp/ldr-build-eZnTdy/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-NB5kkP/src/node/resolver.ts
11
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/node/resolver.ts
12
12
  import fs from "node:fs";
13
13
  import path from "node:path";
14
14
 
15
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/spec.ts
15
+ // ../../../../../../../tmp/ldr-build-eZnTdy/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))
@@ -469,6 +469,7 @@ function transformTexMap(map, m) {
469
469
  const x = (q) => [m[3] * q[0] + m[4] * q[1] + m[5] * q[2] + m[0], m[6] * q[0] + m[7] * q[1] + m[8] * q[2] + m[1], m[9] * q[0] + m[10] * q[1] + m[11] * q[2] + m[2]];
470
470
  return { ...map, p1: x(map.p1), p2: x(map.p2), p3: x(map.p3) };
471
471
  }
472
+ var STUB_OVERRIDES = { "32531.dat": "32531a.dat" };
472
473
  function mul(a, b) {
473
474
  const [ax, ay, az, a0, a1, a2, a3, a4, a5, a6, a7, a8] = a, [bx, by, bz, b0, b1, b2, b3, b4, b5, b6, b7, b8] = b;
474
475
  return [
@@ -704,12 +705,15 @@ function fileStepViews(f) {
704
705
  return out;
705
706
  }
706
707
 
707
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/model.ts
708
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/model.ts
709
+ var CALLOUT_END = 999999;
710
+ var stepKey = (steps) => steps.map((x) => String(x).padStart(6, "0")).join(".") + "~";
708
711
  function flatten(doc, opts = {}) {
709
712
  const out = [];
710
713
  const maxDepth = opts.maxDepth ?? 64;
711
- const loose = opts.looseGeometry !== false, tx = !!opts.textures;
714
+ const loose = opts.looseGeometry !== false, tx = !!opts.textures, co = !!opts.callouts;
712
715
  const src = new Map;
716
+ let subs = 0;
713
717
  const lazy = (f, k) => {
714
718
  const key = f.name + "#" + k;
715
719
  let fn = src.get(key);
@@ -720,16 +724,44 @@ function flatten(doc, opts = {}) {
720
724
  }
721
725
  return fn;
722
726
  };
723
- const walk = (d, fname, m, color, path, stepPath, depth, texIn) => {
727
+ const walk = (d, fname, m, color, path, stepPath, depth, texIn, fixed = null, subPath = [], subMats = []) => {
724
728
  const f = d.files.get(fname);
725
729
  if (!f || depth > maxDepth)
726
730
  return;
727
731
  let s = 0;
732
+ const subId = depth > 0 ? ++subs : 0;
733
+ const sp0 = co && subId ? [...subPath, subId] : subPath;
734
+ const sm0 = co && subId ? [...subMats, m] : subMats;
735
+ const groups = co && !fixed ? new Map : null;
736
+ let ordStep = -1, ord = 0;
737
+ const sub = (sd, file, mm, col, sp, lt) => {
738
+ if (ordStep !== s) {
739
+ ordStep = s;
740
+ ord = 0;
741
+ }
742
+ const g = groups?.get(s + "|" + file);
743
+ if (g) {
744
+ 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
+ }
747
+ 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
+ groups?.set(s + "|" + file, { n: 1, from, to: out.length });
750
+ };
728
751
  const views = fileStepViews(f);
729
752
  const ls = loose ? looseSteps(f) : [];
730
753
  let li = 0;
731
754
  const emit = (part, col, mm, sp, k, embedded, tex) => {
755
+ if (fixed)
756
+ sp = fixed;
732
757
  const i = { part, color: col, m: mm, step: sp[0], steps: sp, localStep: k, path };
758
+ if (subId) {
759
+ i.sub = subId;
760
+ if (co) {
761
+ i.subPath = sp0;
762
+ i.subMats = sm0;
763
+ }
764
+ }
733
765
  if (embedded)
734
766
  i.embedded = true;
735
767
  if (tex) {
@@ -775,7 +807,7 @@ function flatten(doc, opts = {}) {
775
807
  const lt = tex && local ? transformTexMap(tex, local) : null;
776
808
  if (d.files.has(c.file)) {
777
809
  if (fileKind(d, c.file) === "model") {
778
- walk(d, c.file, mm, col, path + "/" + c.file, sp, depth + 1, lt);
810
+ sub(d, c.file, mm, col, sp, lt);
779
811
  continue;
780
812
  }
781
813
  emit(c.file, col, mm, sp, s, true, lt);
@@ -783,12 +815,18 @@ function flatten(doc, opts = {}) {
783
815
  }
784
816
  const ext = opts.resolveModel?.(c.file);
785
817
  if (ext) {
786
- walk(ext, ext.main, mm, col, path + "/" + c.file, sp, depth + 1, lt);
818
+ sub(ext, c.file, mm, col, sp, lt);
787
819
  continue;
788
820
  }
789
821
  emit(c.file, col, mm, sp, s, false, lt);
790
822
  }
791
823
  flushLoose(Infinity);
824
+ if (groups) {
825
+ for (const g of groups.values())
826
+ if (g.n > 1)
827
+ for (let j = g.from;j < g.to; j++)
828
+ out[j].mult ??= g.n;
829
+ }
792
830
  };
793
831
  if (fileKind(doc, doc.main) === "part") {
794
832
  const f = doc.files.get(doc.main);
@@ -803,7 +841,7 @@ function parseModel(text, name = "main.ldr", opts) {
803
841
  return flatten(parse(text, name, { geometryText: true }), opts);
804
842
  }
805
843
  function sequenceSteps(insts) {
806
- const key = (i) => i.steps.map((x) => String(x).padStart(6, "0")).join(".");
844
+ const key = (i) => stepKey(i.steps);
807
845
  const uniq = [...new Set(insts.map(key))].sort();
808
846
  const rank = new Map(uniq.map((k, j) => [k, j]));
809
847
  const stepOf = new Int32Array(insts.length);
@@ -826,7 +864,7 @@ function parseLDConfig(text) {
826
864
  return out;
827
865
  }
828
866
 
829
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/resolver.ts
867
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/node/resolver.ts
830
868
  var TOP_STUD_RE = /^(stud|studa|stud2|stud2a|stud6|stud6a|stud10|stud15|stud2s|stud2s2|stud-logo\d*|stud2-logo\d*)\.dat$/i;
831
869
  var UNDER_STUD_RE = /^(stud3|stud3a|stud4.*|stud12|stud16.*|stud18a)\.dat$/i;
832
870
  var norm2 = (ref) => ref.trim().replace(/\\/g, "/").toLowerCase();
@@ -896,7 +934,7 @@ class Library {
896
934
  return key;
897
935
  }
898
936
  resolve(ref) {
899
- const n = norm2(ref);
937
+ const n0 = norm2(ref), n = STUB_OVERRIDES[n0] ?? n0;
900
938
  if (this.virtual.size && this.virtual.has(n))
901
939
  return "mpd/" + n;
902
940
  for (const pre of ["parts/", "p/", "models/"])
@@ -1096,11 +1134,11 @@ function collectDeps(lib, refs) {
1096
1134
  return out;
1097
1135
  }
1098
1136
 
1099
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/compile.ts
1137
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/node/compile.ts
1100
1138
  import fs2 from "node:fs";
1101
1139
  import path2 from "node:path";
1102
1140
 
1103
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/lod.ts
1141
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/node/lod.ts
1104
1142
  import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer";
1105
1143
  await Promise.all([MeshoptSimplifier.ready, MeshoptEncoder.ready]);
1106
1144
  function reorderMesh(P, N, C, I) {
@@ -1292,7 +1330,7 @@ function lodChain(P, C, I) {
1292
1330
  return out;
1293
1331
  }
1294
1332
 
1295
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/compile.ts
1333
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/node/compile.ts
1296
1334
  function loadColors(root) {
1297
1335
  const map = new Map;
1298
1336
  for (const l of fs2.readFileSync(path2.join(root, "LDConfig.ldr"), "latin1").split(/\r?\n/)) {
@@ -1875,7 +1913,7 @@ function encodePack(parts, studKinds, weld, extra, options = {}) {
1875
1913
  return Buffer.concat([head, ...chunks.map((c) => new Uint8Array(c.buffer, c.byteOffset, c.byteLength))]);
1876
1914
  }
1877
1915
 
1878
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/node/build-pack.ts
1916
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/node/build-pack.ts
1879
1917
  function buildPack(o) {
1880
1918
  const lib = new Library(o.ldrawRoot);
1881
1919
  const refs = new Map;
@@ -1938,7 +1976,7 @@ function buildPack(o) {
1938
1976
  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 } : {} };
1939
1977
  }
1940
1978
 
1941
- // ../../../../../../../tmp/ldr-build-NB5kkP/src/cli.ts
1979
+ // ../../../../../../../tmp/ldr-build-eZnTdy/src/cli.ts
1942
1980
  var HELP = `ldr pack — build a parts pack for the ldr three.js renderer
1943
1981
 
1944
1982
  Usage:
@@ -0,0 +1,2 @@
1
+ /** CDN name for each embedded part in `names` (normalized file names inside `text`): `emb/<hash>.dat`. */
2
+ export declare function embeddedKeys(text: string, names: Iterable<string>, fallback?: string): Map<string, string>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
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, parseLDConfig } from "./model.js";
3
+ export { flatten, parseModel, sequenceSteps, stepKey, parseLDConfig, CALLOUT_END } from "./model.js";
4
+ export { planStepViews, isolateMask, viewDir, instanceBoxes, MIN_VIS, REORIENT } from "./instructions.js";
5
+ export type { StepCameraView, StepCameraOptions, Vec3 } from "./instructions.js";
4
6
  export type { PartInstance, FlattenOptions, LDrawColor } from "./model.js";
5
7
  export { loadPack, decodePack, buildLdrScene, makeMaterial, createInstancedEdges, computeCulling, cullCore, forEachStud, conditionalLines, createInstancedConditionalEdges, conditionalLineVisible, warmPackGeometry } from "./render.js";
6
8
  export { precomputeCull, sceneInstances } from "./cull.js";
@@ -10,3 +12,4 @@ export type { LdrObject, LdrObjectOptions, LdrStats, ColorSource } from "./scene
10
12
  export { decompressPack } from "./pack-io.js";
11
13
  export { loadModel, loadParts, loadPartsIndex, mergePacks, prepareModel, DEFAULT_PARTS_CDN } from "./cdn.js";
12
14
  export type { LoadModelOptions, LoadedModel, LoadPartsOptions, PartsCdnIndex, PrepareOptions, PreparedModel } from "./cdn.js";
15
+ export { embeddedKeys } from "./embedded.js";