@es3d/es3d 1.0.0 → 1.0.2

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 (80) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +95 -86
  3. package/docs/SPEC.md +426 -417
  4. package/package.json +1 -1
  5. package/scripts/es3d-cli.mjs +390 -377
  6. package/src/cache/geometry-store.js +392 -392
  7. package/src/cache/mesh-bundle.js +123 -123
  8. package/src/cache/sha256.js +73 -73
  9. package/src/format/ast.js +238 -238
  10. package/src/format/colors.js +93 -93
  11. package/src/format/compact-parser.js +171 -171
  12. package/src/format/compact-tokenizer.js +165 -165
  13. package/src/format/errors.js +247 -247
  14. package/src/format/json.js +55 -55
  15. package/src/format/limits.js +14 -14
  16. package/src/format/parse.js +132 -92
  17. package/src/format/schema.js +617 -617
  18. package/src/format/serializer.js +333 -333
  19. package/src/format/validator.js +330 -330
  20. package/src/format/value-error.js +12 -12
  21. package/src/format/value-format.js +314 -314
  22. package/src/format/values.js +459 -459
  23. package/src/format/xml-parser.js +136 -136
  24. package/src/format/xml-tokenizer.js +237 -237
  25. package/src/geometry/contour.js +86 -86
  26. package/src/geometry/extrude.js +175 -175
  27. package/src/geometry/gear.js +85 -85
  28. package/src/geometry/geometry-cache.js +159 -159
  29. package/src/geometry/lod-error.js +178 -178
  30. package/src/geometry/mesh.js +458 -458
  31. package/src/geometry/primitives.js +299 -299
  32. package/src/geometry/registry.js +645 -645
  33. package/src/geometry/revolve.js +136 -136
  34. package/src/geometry/sweep.js +441 -441
  35. package/src/index.js +47 -47
  36. package/src/io/compression.js +67 -67
  37. package/src/io/convert.js +236 -236
  38. package/src/io/download.js +13 -13
  39. package/src/io/external-scene.js +127 -127
  40. package/src/io/file-loader.js +28 -28
  41. package/src/io/gltf-pack.js +1461 -0
  42. package/src/io/gltf.js +630 -630
  43. package/src/io/mesh-import.js +104 -104
  44. package/src/io/obj.js +352 -352
  45. package/src/io/step-brep.js +439 -439
  46. package/src/io/step-exact.js +1149 -1149
  47. package/src/io/step.js +1418 -1418
  48. package/src/io/stl.js +193 -193
  49. package/src/io/svg-import.js +787 -787
  50. package/src/material/materials.js +154 -154
  51. package/src/math/mat4.js +475 -475
  52. package/src/math/random.js +30 -30
  53. package/src/math/trig.js +65 -65
  54. package/src/math/vec3.js +81 -81
  55. package/src/optimize/optimizer.js +604 -579
  56. package/src/path/path-flatten.js +322 -322
  57. package/src/path/path-parser.js +332 -332
  58. package/src/path/shapes.js +129 -129
  59. package/src/path/triangulate.js +517 -517
  60. package/src/pipeline.js +74 -74
  61. package/src/render/camera.js +168 -168
  62. package/src/render/frame-builder.js +362 -362
  63. package/src/render/geometry-pool.js +149 -149
  64. package/src/render/lighting.js +149 -149
  65. package/src/render/orbit-controls.js +237 -237
  66. package/src/render/renderer.js +258 -249
  67. package/src/render/scroll-timeline.js +104 -104
  68. package/src/render/shaders/glsl.js +285 -285
  69. package/src/render/shaders/wgsl.js +252 -252
  70. package/src/render/stage.js +365 -352
  71. package/src/render/texture-cache.js +40 -40
  72. package/src/render/textures.js +223 -223
  73. package/src/render/webgl2/webgl2-backend.js +586 -532
  74. package/src/render/webgpu/webgpu-backend.js +482 -480
  75. package/src/scene/flatten.js +754 -754
  76. package/src/scene/picking.js +161 -161
  77. package/src/scene/scene-graph.js +320 -320
  78. package/src/transform/transform.js +211 -211
  79. package/src/wasm/kernels-binary.js +3 -3
  80. package/src/wasm/kernels.js +385 -385
@@ -1,439 +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
-
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
+