@es3d/es3d 0.0.0-stage → 1.0.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.
Files changed (82) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +86 -3
  3. package/docs/REFERENCE.md +581 -0
  4. package/docs/SPEC.md +417 -0
  5. package/package.json +50 -4
  6. package/scripts/es3d-cli.mjs +377 -0
  7. package/src/cache/geometry-store.js +392 -0
  8. package/src/cache/mesh-bundle.js +123 -0
  9. package/src/cache/sha256.js +73 -0
  10. package/src/format/ast.js +238 -0
  11. package/src/format/colors.js +93 -0
  12. package/src/format/compact-parser.js +171 -0
  13. package/src/format/compact-tokenizer.js +165 -0
  14. package/src/format/errors.js +247 -0
  15. package/src/format/json.js +55 -0
  16. package/src/format/limits.js +14 -0
  17. package/src/format/parse.js +92 -0
  18. package/src/format/schema.js +617 -0
  19. package/src/format/serializer.js +333 -0
  20. package/src/format/validator.js +330 -0
  21. package/src/format/value-error.js +12 -0
  22. package/src/format/value-format.js +314 -0
  23. package/src/format/values.js +459 -0
  24. package/src/format/xml-parser.js +136 -0
  25. package/src/format/xml-tokenizer.js +237 -0
  26. package/src/geometry/contour.js +86 -0
  27. package/src/geometry/extrude.js +175 -0
  28. package/src/geometry/gear.js +85 -0
  29. package/src/geometry/geometry-cache.js +159 -0
  30. package/src/geometry/lod-error.js +178 -0
  31. package/src/geometry/mesh.js +458 -0
  32. package/src/geometry/primitives.js +299 -0
  33. package/src/geometry/registry.js +645 -0
  34. package/src/geometry/revolve.js +136 -0
  35. package/src/geometry/sweep.js +441 -0
  36. package/src/index.js +47 -0
  37. package/src/io/compression.js +67 -0
  38. package/src/io/convert.js +236 -0
  39. package/src/io/download.js +13 -0
  40. package/src/io/external-scene.js +127 -0
  41. package/src/io/file-loader.js +28 -0
  42. package/src/io/gltf.js +630 -0
  43. package/src/io/mesh-import.js +104 -0
  44. package/src/io/obj.js +352 -0
  45. package/src/io/step-brep.js +439 -0
  46. package/src/io/step-exact.js +1149 -0
  47. package/src/io/step.js +1418 -0
  48. package/src/io/stl.js +193 -0
  49. package/src/io/svg-import.js +787 -0
  50. package/src/material/materials.js +154 -0
  51. package/src/math/mat4.js +475 -0
  52. package/src/math/random.js +30 -0
  53. package/src/math/trig.js +65 -0
  54. package/src/math/vec3.js +81 -0
  55. package/src/optimize/optimizer.js +579 -0
  56. package/src/path/path-flatten.js +322 -0
  57. package/src/path/path-parser.js +332 -0
  58. package/src/path/shapes.js +129 -0
  59. package/src/path/triangulate.js +517 -0
  60. package/src/pipeline.js +74 -0
  61. package/src/render/camera.js +168 -0
  62. package/src/render/frame-builder.js +362 -0
  63. package/src/render/geometry-pool.js +149 -0
  64. package/src/render/lighting.js +149 -0
  65. package/src/render/orbit-controls.js +237 -0
  66. package/src/render/renderer.js +249 -0
  67. package/src/render/scroll-timeline.js +104 -0
  68. package/src/render/shaders/glsl.js +285 -0
  69. package/src/render/shaders/wgsl.js +252 -0
  70. package/src/render/stage.js +352 -0
  71. package/src/render/texture-cache.js +40 -0
  72. package/src/render/textures.js +223 -0
  73. package/src/render/webgl2/webgl2-backend.js +532 -0
  74. package/src/render/webgpu/webgpu-backend.js +480 -0
  75. package/src/scene/animation.js +171 -0
  76. package/src/scene/camera-constraints.js +117 -0
  77. package/src/scene/flatten.js +754 -0
  78. package/src/scene/picking.js +161 -0
  79. package/src/scene/scene-graph.js +320 -0
  80. package/src/transform/transform.js +211 -0
  81. package/src/wasm/kernels-binary.js +3 -0
  82. package/src/wasm/kernels.js +385 -0
@@ -0,0 +1,439 @@
1
+ // The exact geometry of a STEP boundary representation: every face with its
2
+ // surface (plane, cylinder, cone, sphere, torus, linear extrusion, or other)
3
+ // and its loops of edges with their curves (line, circle, ellipse, B-spline,
4
+ // polyline). Nothing is sampled here; step-exact.js decides what each face
5
+ // becomes in ES3D.
6
+ //
7
+ // B-spline curves that are in fact straight or circular (CAD tools write
8
+ // circles as rational quadratic B-splines, and lines as degree-1 splines)
9
+ // are recognised as such, so the faces they bound keep exact edges.
10
+
11
+ import { has, part, coordsOf, directionOf, placementFrame, shellEntries, isFaceEntity, labelOf } from './step.js';
12
+
13
+ const TAU = Math.PI * 2;
14
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
15
+ const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
16
+ const mul = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
17
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
18
+ const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
19
+ const len = (a) => Math.hypot(a[0], a[1], a[2]);
20
+ const unit = (a) => mul(a, 1 / (len(a) || 1));
21
+
22
+ const refs = (args) => args.filter((a) => a && a.ref);
23
+ const nums = (args) => args.filter((a) => typeof a === 'number');
24
+ const lists = (args) => args.filter((a) => Array.isArray(a));
25
+ const bools = (args) => args.filter((a) => typeof a === 'boolean');
26
+ const get = (entities, ref) => (ref && ref.ref ? entities.get(ref.ref) : null);
27
+
28
+ /** Radians per unit of plane angle in the file's representation context (1, or π/180 for degrees). */
29
+ export function angleFactor(entities) {
30
+ for (const e of entities.values()) {
31
+ const ctx = part(e, 'GLOBAL_UNIT_ASSIGNED_CONTEXT');
32
+ if (!ctx) continue;
33
+ for (const list of lists(ctx.args)) {
34
+ for (const r of list) {
35
+ const u = get(entities, r);
36
+ if (!u || !has(u, 'PLANE_ANGLE_UNIT')) continue;
37
+ const conv = part(u, 'CONVERSION_BASED_UNIT');
38
+ const name = conv && conv.args.find((a) => typeof a === 'string');
39
+ return name && /deg/i.test(name) ? Math.PI / 180 : 1;
40
+ }
41
+ }
42
+ }
43
+ return 1;
44
+ }
45
+
46
+ // --- B-spline curves -------------------------------------------------------------------
47
+
48
+ function findSpan(n, p, t, U) {
49
+ if (t >= U[n + 1]) return n;
50
+ if (t <= U[p]) return p;
51
+ let lo = p, hi = n + 1;
52
+ let mid = (lo + hi) >> 1;
53
+ while (t < U[mid] || t >= U[mid + 1]) {
54
+ if (t < U[mid]) hi = mid;
55
+ else lo = mid;
56
+ mid = (lo + hi) >> 1;
57
+ }
58
+ return mid;
59
+ }
60
+
61
+ /** A point of a (rational) B-spline curve at parameter t (de Boor in homogeneous coordinates). */
62
+ export function evalSpline(c, t) {
63
+ const { degree: p, points: P, knots: U, weights: W } = c;
64
+ const n = P.length - 1;
65
+ const k = findSpan(n, p, t, U);
66
+ const d = [];
67
+ for (let j = 0; j <= p; j++) {
68
+ const i = k - p + j;
69
+ const w = W ? W[i] : 1;
70
+ d.push([P[i][0] * w, P[i][1] * w, P[i][2] * w, w]);
71
+ }
72
+ for (let r = 1; r <= p; r++) {
73
+ for (let j = p; j >= r; j--) {
74
+ const i = k - p + j;
75
+ const a = (t - U[i]) / (U[i + p - r + 1] - U[i] || 1);
76
+ for (let q = 0; q < 4; q++) d[j][q] = (1 - a) * d[j - 1][q] + a * d[j][q];
77
+ }
78
+ }
79
+ const h = d[p];
80
+ return [h[0] / h[3], h[1] / h[3], h[2] / h[3]];
81
+ }
82
+
83
+ function splineFrom(entities, e) {
84
+ // the simple form keeps everything in one part; the complex form spreads it over several
85
+ let degree, points, mults, knotValues, weights = null;
86
+ const simple = part(e, 'B_SPLINE_CURVE_WITH_KNOTS');
87
+ const base = part(e, 'B_SPLINE_CURVE');
88
+ if (simple && !base) {
89
+ const a = simple.args;
90
+ degree = nums(a)[0];
91
+ const ls = lists(a);
92
+ points = ls[0];
93
+ mults = ls[1];
94
+ knotValues = ls[2];
95
+ } else if (base) {
96
+ degree = nums(base.args)[0];
97
+ points = lists(base.args)[0];
98
+ const kn = simple && lists(simple.args);
99
+ if (!kn) return null;
100
+ mults = kn[0];
101
+ knotValues = kn[1];
102
+ const rat = part(e, 'RATIONAL_B_SPLINE_CURVE');
103
+ if (rat) weights = lists(rat.args)[0];
104
+ } else return null;
105
+ if (!Number.isInteger(degree) || !points || !mults || !knotValues) return null;
106
+ const P = points.map((r) => coordsOf(entities, r));
107
+ if (P.some((p) => !p)) return null;
108
+ const knots = [];
109
+ mults.forEach((m, i) => {
110
+ for (let k = 0; k < m; k++) knots.push(knotValues[i]);
111
+ });
112
+ if (knots.length !== P.length + degree + 1) return null;
113
+ return { kind: 'bspline', degree, points: P, knots, weights: weights && weights.length === P.length ? weights : null };
114
+ }
115
+
116
+ /** The circle through three points, or null when they are on one line. */
117
+ function circleThrough(a, b, c) {
118
+ const ab = sub(b, a);
119
+ const ac = sub(c, a);
120
+ const n = cross(ab, ac);
121
+ const nn = dot(n, n);
122
+ if (nn < 1e-24 * dot(ab, ab) * dot(ac, ac)) return null;
123
+ const centre = add(a, mul(add(mul(cross(n, ab), dot(ac, ac)), mul(cross(ac, n), dot(ab, ab))), 1 / (2 * nn)));
124
+ return { centre, normal: unit(n), radius: len(sub(a, centre)) };
125
+ }
126
+
127
+ /** A B-spline that is really a line or a circular arc becomes one; tol is a length. */
128
+ function simplifySpline(c, tol) {
129
+ const t0 = c.knots[c.degree];
130
+ const t1 = c.knots[c.knots.length - c.degree - 1];
131
+ const samples = [];
132
+ const N = Math.max(16, c.points.length * 4);
133
+ for (let i = 0; i <= N; i++) samples.push(evalSpline(c, t0 + ((t1 - t0) * i) / N));
134
+ const a = samples[0];
135
+ const b = samples[N];
136
+ const span = len(sub(b, a));
137
+ const dir = span > tol ? unit(sub(b, a)) : null;
138
+ if (dir && samples.every((p) => len(cross(sub(p, a), dir)) <= tol)) return { kind: 'line', point: a, dir };
139
+ const circle = circleThrough(a, samples[N >> 1], span > tol ? b : samples[N >> 2]);
140
+ if (circle && samples.every((p) => Math.abs(len(sub(p, circle.centre)) - circle.radius) <= tol && Math.abs(dot(sub(p, circle.centre), circle.normal)) <= tol)) {
141
+ const x = unit(sub(a, circle.centre));
142
+ // orient the circle so the spline runs counter-clockwise about its axis
143
+ let z = circle.normal;
144
+ const probe = sub(samples[1], circle.centre);
145
+ if (dot(cross(x, probe), z) < 0) z = mul(z, -1);
146
+ return { kind: 'circle', frame: { origin: circle.centre, x, y: cross(z, x), z }, radius: circle.radius, fromSpline: true };
147
+ }
148
+ return c;
149
+ }
150
+
151
+ /**
152
+ * The 3D curve of a curve entity, unwrapping surface curves and trimmed curves.
153
+ * @returns {{kind: string, ...}}
154
+ */
155
+ export function readCurve(entities, ref, tol = 1e-6, depth = 0) {
156
+ const e = get(entities, ref);
157
+ if (!e || depth > 6) return { kind: 'other' };
158
+ for (const wrapper of ['SURFACE_CURVE', 'SEAM_CURVE', 'INTERSECTION_CURVE', 'TRIMMED_CURVE', 'BOUNDED_SURFACE_CURVE']) {
159
+ const w = part(e, wrapper);
160
+ if (w && refs(w.args).length) return { ...readCurve(entities, refs(w.args)[0], tol, depth + 1), seam: wrapper === 'SEAM_CURVE' };
161
+ }
162
+ const line = part(e, 'LINE');
163
+ if (line) {
164
+ const [pRef, vRef] = refs(line.args);
165
+ const point = coordsOf(entities, pRef);
166
+ const vec = get(entities, vRef);
167
+ const dir = vec && directionOf(entities, refs(part(vec, 'VECTOR') ? part(vec, 'VECTOR').args : [])[0]);
168
+ if (point && dir) return { kind: 'line', point, dir };
169
+ return { kind: 'other' };
170
+ }
171
+ const circle = part(e, 'CIRCLE');
172
+ if (circle) {
173
+ const frame = placementFrame(entities, refs(circle.args)[0]);
174
+ const radius = nums(circle.args)[0];
175
+ return frame && radius > 0 ? { kind: 'circle', frame, radius } : { kind: 'other' };
176
+ }
177
+ const ellipse = part(e, 'ELLIPSE');
178
+ if (ellipse) {
179
+ const frame = placementFrame(entities, refs(ellipse.args)[0]);
180
+ const [a, b] = nums(ellipse.args);
181
+ return frame && a > 0 && b > 0 ? { kind: 'ellipse', frame, a, b } : { kind: 'other' };
182
+ }
183
+ const poly = part(e, 'POLYLINE');
184
+ if (poly) {
185
+ const pts = (lists(poly.args)[0] || []).map((r) => coordsOf(entities, r));
186
+ return pts.length >= 2 && pts.every(Boolean) ? { kind: 'polyline', points: pts } : { kind: 'other' };
187
+ }
188
+ if (part(e, 'B_SPLINE_CURVE_WITH_KNOTS') || part(e, 'B_SPLINE_CURVE')) {
189
+ const s = splineFrom(entities, e);
190
+ return s ? simplifySpline(s, tol) : { kind: 'other' };
191
+ }
192
+ return { kind: 'other' };
193
+ }
194
+
195
+ // --- surfaces ------------------------------------------------------------------------
196
+
197
+ /** The surface of a face: kind, frame and its sizes. */
198
+ export function readSurface(entities, ref, angle, tol) {
199
+ const e = get(entities, ref);
200
+ if (!e) return { kind: 'other' };
201
+ const frameOf = (p) => placementFrame(entities, refs(p.args)[0]);
202
+ let p;
203
+ if ((p = part(e, 'PLANE'))) {
204
+ const frame = frameOf(p);
205
+ return frame ? { kind: 'plane', frame } : { kind: 'other' };
206
+ }
207
+ if ((p = part(e, 'CYLINDRICAL_SURFACE'))) {
208
+ const frame = frameOf(p);
209
+ const radius = nums(p.args)[0];
210
+ return frame && radius > 0 ? { kind: 'cylinder', frame, radius } : { kind: 'other' };
211
+ }
212
+ if ((p = part(e, 'CONICAL_SURFACE'))) {
213
+ const frame = frameOf(p);
214
+ const [radius, semi] = nums(p.args);
215
+ return frame && radius >= 0 && semi > 0 ? { kind: 'cone', frame, radius, semiAngle: semi * angle } : { kind: 'other' };
216
+ }
217
+ if ((p = part(e, 'SPHERICAL_SURFACE'))) {
218
+ const frame = frameOf(p);
219
+ const radius = nums(p.args)[0];
220
+ return frame && radius > 0 ? { kind: 'sphere', frame, radius } : { kind: 'other' };
221
+ }
222
+ if ((p = part(e, 'TOROIDAL_SURFACE'))) {
223
+ const frame = frameOf(p);
224
+ const [major, minor] = nums(p.args);
225
+ return frame && major > 0 && minor > 0 ? { kind: 'torus', frame, major, minor } : { kind: 'other' };
226
+ }
227
+ if ((p = part(e, 'SURFACE_OF_LINEAR_EXTRUSION'))) {
228
+ const [cRef, vRef] = refs(p.args);
229
+ const curve = readCurve(entities, cRef, tol);
230
+ const vec = get(entities, vRef);
231
+ const vp = vec && part(vec, 'VECTOR');
232
+ const dir = vp && directionOf(entities, refs(vp.args)[0]);
233
+ return dir ? { kind: 'extrusion', curve, dir } : { kind: 'other' };
234
+ }
235
+ if (part(e, 'B_SPLINE_SURFACE_WITH_KNOTS') || part(e, 'B_SPLINE_SURFACE')) {
236
+ const sp = splineSurfaceFrom(entities, e);
237
+ if (sp) return sp;
238
+ }
239
+ const names = e.parts.map((x) => x.name);
240
+ return { kind: 'other', type: names.find((n) => n.includes('SURFACE')) || names[0] };
241
+ }
242
+
243
+ function expandKnots(mults, values) {
244
+ const out = [];
245
+ mults.forEach((m, i) => {
246
+ for (let k = 0; k < m; k++) out.push(values[i]);
247
+ });
248
+ return out;
249
+ }
250
+
251
+ function splineSurfaceFrom(entities, e) {
252
+ let du, dv, grid, um, vm, uk, vk, weights = null;
253
+ const simple = part(e, 'B_SPLINE_SURFACE_WITH_KNOTS');
254
+ const base = part(e, 'B_SPLINE_SURFACE');
255
+ if (simple && !base) {
256
+ [du, dv] = nums(simple.args);
257
+ const ls = lists(simple.args);
258
+ [grid, um, vm, uk, vk] = ls;
259
+ } else if (base && simple) {
260
+ [du, dv] = nums(base.args);
261
+ grid = lists(base.args)[0];
262
+ [um, vm, uk, vk] = lists(simple.args);
263
+ const rat = part(e, 'RATIONAL_B_SPLINE_SURFACE');
264
+ if (rat) weights = lists(rat.args)[0];
265
+ } else return null;
266
+ if (!grid || !um || !vm || !uk || !vk) return null;
267
+ const P = grid.map((row) => row.map((r) => coordsOf(entities, r)));
268
+ if (P.some((row) => row.some((p) => !p))) return null;
269
+ const U = expandKnots(um, uk);
270
+ const V = expandKnots(vm, vk);
271
+ if (U.length !== P.length + du + 1 || V.length !== P[0].length + dv + 1) return null;
272
+ return { kind: 'bspline', du, dv, points: P, U, V, weights: weights && weights.length === P.length ? weights : null };
273
+ }
274
+
275
+ /** A point of a (rational) B-spline surface at (u, v). */
276
+ export function evalSplineSurface(s, u, v) {
277
+ // a curve in u through the points each row of v makes
278
+ const rows = s.points.map((row, i) => {
279
+ const w = s.weights ? s.weights[i] : null;
280
+ const curve = { degree: s.dv, points: row.map((p, j) => (w ? [p[0] * w[j], p[1] * w[j], p[2] * w[j]] : p)), knots: s.V, weights: null };
281
+ const pt = evalSpline(curve, v);
282
+ const weight = w ? evalSpline({ degree: s.dv, points: w.map((x) => [x, 0, 0]), knots: s.V, weights: null }, v)[0] : 1;
283
+ return [pt, weight];
284
+ });
285
+ const hp = evalSpline({ degree: s.du, points: rows.map(([p]) => p), knots: s.U, weights: null }, u);
286
+ if (!s.weights) return hp;
287
+ const hw = evalSpline({ degree: s.du, points: rows.map(([, w]) => [w, 0, 0]), knots: s.U, weights: null }, u)[0];
288
+ return [hp[0] / hw, hp[1] / hw, hp[2] / hw];
289
+ }
290
+
291
+ // --- edges, loops, faces ---------------------------------------------------------------
292
+
293
+ /**
294
+ * One edge of a loop, in the order the loop walks it.
295
+ * @returns {{curve: object, start: number[], end: number[], forward: boolean}|null}
296
+ * forward: the loop walks the edge in the curve's own direction
297
+ */
298
+ function readEdge(entities, item, tol) {
299
+ let e = get(entities, item);
300
+ if (!e) return null;
301
+ let orientation = true;
302
+ const oe = part(e, 'ORIENTED_EDGE');
303
+ if (oe) {
304
+ if (bools(oe.args).pop() === false) orientation = false;
305
+ e = get(entities, refs(oe.args).pop());
306
+ if (!e) return null;
307
+ }
308
+ const ec = part(e, 'EDGE_CURVE');
309
+ if (!ec) return null;
310
+ const [v0, v1, cRef] = refs(ec.args);
311
+ const sameSense = bools(ec.args).pop() !== false;
312
+ const a = coordsOf(entities, v0);
313
+ const b = coordsOf(entities, v1);
314
+ if (!a || !b) return null;
315
+ const curve = readCurve(entities, cRef, tol);
316
+ return orientation ? { curve, start: a, end: b, forward: sameSense } : { curve, start: b, end: a, forward: !sameSense };
317
+ }
318
+
319
+ function readLoop(entities, bound, tol) {
320
+ const b = part(bound, 'FACE_OUTER_BOUND') || part(bound, 'FACE_BOUND');
321
+ if (!b) return null;
322
+ const loop = get(entities, refs(b.args)[0]);
323
+ const orientation = bools(b.args).pop() !== false;
324
+ if (!loop) return null;
325
+ const outer = !!part(bound, 'FACE_OUTER_BOUND');
326
+ if (part(loop, 'VERTEX_LOOP')) {
327
+ const p = coordsOf(entities, refs(part(loop, 'VERTEX_LOOP').args)[0]);
328
+ return p ? { outer, vertex: p, edges: [] } : null;
329
+ }
330
+ if (part(loop, 'POLY_LOOP')) {
331
+ const pts = (lists(part(loop, 'POLY_LOOP').args)[0] || []).map((r) => coordsOf(entities, r));
332
+ if (pts.length < 3 || pts.some((p) => !p)) return null;
333
+ const ring = orientation ? pts : pts.slice().reverse();
334
+ return { outer, edges: ring.map((p, i) => ({ curve: { kind: 'line', point: p, dir: unit(sub(ring[(i + 1) % ring.length], p)) }, start: p, end: ring[(i + 1) % ring.length], forward: true })) };
335
+ }
336
+ const el = part(loop, 'EDGE_LOOP');
337
+ if (!el) return null;
338
+ const edges = [];
339
+ for (const item of lists(el.args)[0] || []) {
340
+ const edge = readEdge(entities, item, tol);
341
+ if (!edge) return null;
342
+ edges.push(edge);
343
+ }
344
+ if (!orientation) {
345
+ edges.reverse();
346
+ for (const ed of edges) {
347
+ [ed.start, ed.end] = [ed.end, ed.start];
348
+ ed.forward = !ed.forward;
349
+ }
350
+ }
351
+ return { outer, edges };
352
+ }
353
+
354
+ /**
355
+ * A face: its surface, whether its outward side is the surface's normal side
356
+ * (`sense`), and its loops (the first one the outer bound when the file says so).
357
+ */
358
+ function readFace(entities, entry, angle, tol) {
359
+ const f = entry.face;
360
+ const fp = part(f, 'ADVANCED_FACE') || part(f, 'FACE_SURFACE') || part(f, 'FACE');
361
+ const bounds = (lists(fp.args)[0] || []).map((r) => get(entities, r)).filter(Boolean);
362
+ const surfRef = refs(fp.args).find((r) => !bounds.some((b) => b.id === r.ref));
363
+ const surface = surfRef ? readSurface(entities, surfRef, angle, tol) : { kind: 'unbounded' };
364
+ const sameSense = bools(fp.args).pop() !== false;
365
+ const loops = [];
366
+ for (const b of bounds) {
367
+ const loop = readLoop(entities, b, tol);
368
+ if (!loop) return { id: f.id, surface: { kind: 'other' }, loops: [], sense: true, broken: true };
369
+ loops.push(loop);
370
+ }
371
+ loops.sort((a, b) => (b.outer ? 1 : 0) - (a.outer ? 1 : 0));
372
+ // a faceted face names no surface: its plane is the one its largest loop turns around
373
+ if (surface.kind === 'unbounded') return { id: f.id, surface: planeOfLoops(loops) || { kind: 'other' }, sense: !entry.flip, loops };
374
+ return { id: f.id, surface, sense: sameSense !== entry.flip, loops };
375
+ }
376
+
377
+ function planeOfLoops(loops) {
378
+ let best = null;
379
+ let bestLen = 0;
380
+ for (const loop of loops) {
381
+ if (!loop.edges.length || loop.edges.some((e) => e.curve.kind !== 'line')) return null;
382
+ const pts = loop.edges.map((e) => e.start);
383
+ const n = [0, 0, 0];
384
+ for (let i = 0; i < pts.length; i++) {
385
+ const a = pts[i];
386
+ const b = pts[(i + 1) % pts.length];
387
+ n[0] += (a[1] - b[1]) * (a[2] + b[2]);
388
+ n[1] += (a[2] - b[2]) * (a[0] + b[0]);
389
+ n[2] += (a[0] - b[0]) * (a[1] + b[1]);
390
+ }
391
+ if (len(n) > bestLen) {
392
+ bestLen = len(n);
393
+ best = { pts, n };
394
+ }
395
+ }
396
+ if (!best || bestLen === 0) return null;
397
+ const z = unit(best.n);
398
+ const d = sub(best.pts[1], best.pts[0]);
399
+ const x = unit(sub(d, mul(z, dot(d, z))));
400
+ return { kind: 'plane', frame: { origin: best.pts[0], x, y: cross(z, x), z } };
401
+ }
402
+
403
+ /** Largest coordinate in the file's points, for tolerances. */
404
+ function extentOf(entities) {
405
+ let m = 0;
406
+ for (const e of entities.values()) {
407
+ const cp = part(e, 'CARTESIAN_POINT');
408
+ if (!cp) continue;
409
+ const l = lists(cp.args)[0];
410
+ if (l) for (const v of l) if (typeof v === 'number') m = Math.max(m, Math.abs(v));
411
+ }
412
+ return m || 1;
413
+ }
414
+
415
+ /**
416
+ * Shells of the B-rep, each with its faces in exact form.
417
+ * @returns {{shells: {id: number, name: string, closed: boolean, faces: object[]}[], tol: number}}
418
+ */
419
+ export function readBrep(entities) {
420
+ const tol = extentOf(entities) * 1e-7;
421
+ const angle = angleFactor(entities);
422
+ const shells = [];
423
+ const used = new Set();
424
+ for (const e of entities.values()) {
425
+ const kind = ['CLOSED_SHELL', 'OPEN_SHELL', 'CONNECTED_FACE_SET'].find((k) => has(e, k));
426
+ if (!kind) continue;
427
+ const faces = [];
428
+ for (const entry of shellEntries(entities, e)) {
429
+ used.add(entry.face.id);
430
+ faces.push(readFace(entities, entry, angle, tol));
431
+ }
432
+ if (faces.length) shells.push({ id: e.id, name: labelOf(e), closed: kind === 'CLOSED_SHELL', faces });
433
+ }
434
+ const loose = [];
435
+ for (const e of entities.values()) if (isFaceEntity(e) && !used.has(e.id)) loose.push(readFace(entities, { face: e, flip: false }, angle, tol));
436
+ if (loose.length) shells.push({ id: 0, name: '', closed: false, faces: loose });
437
+ return { shells, tol };
438
+ }
439
+