@metamesh/modeling 0.7.0 → 0.9.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 (38) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +86 -4
  3. package/dist/api/engine.d.ts +4 -0
  4. package/dist/api/geometry.d.ts +1067 -908
  5. package/dist/api/index.d.ts +203 -18
  6. package/dist/api/text.d.ts +21 -7
  7. package/dist/engine/construction-operations.js +52 -10
  8. package/dist/engine/execution.js +55 -2
  9. package/dist/engine/export-3mf.js +17 -1
  10. package/dist/engine/finalization.js +27 -18
  11. package/dist/engine/inspection-operations.js +26 -22
  12. package/dist/engine/loft-operations.js +1 -1
  13. package/dist/engine/mechanical-operations.js +5 -8
  14. package/dist/engine/mesh-view-cache.js +23 -0
  15. package/dist/engine/native/adapter.js +2 -4
  16. package/dist/engine/native/distance.js +42 -0
  17. package/dist/engine/native/status.js +16 -0
  18. package/dist/engine/operation-environment.js +2 -1
  19. package/dist/engine/section-operations.js +3 -3
  20. package/dist/geometry/heightfield.js +101 -42
  21. package/dist/geometry/index.js +3 -0
  22. package/dist/geometry/loft-simple-mesh.js +2 -3
  23. package/dist/geometry/loft-triangulation.js +12 -9
  24. package/dist/geometry/loft.js +2 -6
  25. package/dist/geometry/mechanical/standards.js +2 -3
  26. package/dist/geometry/mechanical/threads.js +2 -3
  27. package/dist/geometry/mesh-centroid.js +36 -0
  28. package/dist/geometry/mesh-solid-intersections.js +123 -0
  29. package/dist/geometry/mesh-solid.js +284 -0
  30. package/dist/geometry/profiles/cross-section.js +102 -0
  31. package/dist/geometry/svg-import.js +8 -1
  32. package/dist/geometry/sweeps/sweep-closed.js +4 -4
  33. package/dist/geometry/sweeps/sweep-mesh.js +2 -6
  34. package/dist/geometry/sweeps/sweep-open.js +4 -4
  35. package/dist/geometry/sweeps/validation.js +1 -2
  36. package/dist/model/operations.js +10 -3
  37. package/dist/text/vector-text-layout.js +46 -3
  38. package/package.json +1 -1
@@ -14,17 +14,13 @@ import { buildOpenSweepMesh } from "./sweep-open.js";
14
14
  * ```
15
15
  */
16
16
  export function sweepMesh(profile, path, options = {}) {
17
- return buildSweepMesh(profile, path, options);
18
- }
19
- /** @internal Applies host ceilings before frames, caps, or manufacturing buffers. */
20
- export function buildSweepMesh(profile, path, options = {}, allocationGuard) {
21
17
  validateSweepOptions(options);
22
18
  if (options.closed === true) {
23
- return buildClosedSweepMesh(profile, path, options, allocationGuard);
19
+ return buildClosedSweepMesh(profile, path, options);
24
20
  }
25
21
  if ((options.seamProfileOffset ?? 0) !== 0)
26
22
  throw new GeometryError("INVALID_ARGUMENT", "A seamProfileOffset requires a closed path.", {
27
23
  operation: "sweep",
28
24
  });
29
- return buildOpenSweepMesh(profile, path, options, allocationGuard);
25
+ return buildOpenSweepMesh(profile, path, options);
30
26
  }
@@ -6,23 +6,23 @@ import { computeSweepFrames, validateSweepOptions } from "./orientation.js";
6
6
  import { planSweepStations, refineSweepStations } from "./stations.js";
7
7
  import { assertSweepAllocation, normalizeSweepPath, normalizeSweepProfile, writeSweepVertex, } from "./validation.js";
8
8
  /** @internal Checks host allocation policy before frames, caps or manufacturing buffers. */
9
- export const buildOpenSweepMesh = (profile, path, options = {}, allocationGuard) => {
9
+ export const buildOpenSweepMesh = (profile, path, options = {}) => {
10
10
  const operation = "sweep";
11
11
  validateSweepOptions(options);
12
12
  profile = normalizeSweepProfile(profile, operation, options.limits);
13
13
  path = normalizeSweepPath(path, false, operation, options.limits);
14
- assertSweepAllocation(path.length, profile.length, false, operation, allocationGuard, options.limits);
14
+ assertSweepAllocation(path.length, profile.length, false, operation, options.limits);
15
15
  let scales = resolveSweepScales(options.scale, path.length);
16
16
  const twistRadians = ((options.twistDegrees ?? 0) / 180) * Math.PI;
17
17
  if (twistRadians !== 0) {
18
18
  const plan = planSweepStations([profile], path, scales, options);
19
- assertSweepAllocation(plan.stationCount, profile.length, false, operation, allocationGuard, options.limits);
19
+ assertSweepAllocation(plan.stationCount, profile.length, false, operation, options.limits);
20
20
  }
21
21
  let frames = computeSweepFrames(path, options);
22
22
  let twists;
23
23
  if (twistRadians !== 0) {
24
24
  const plan = planSweepStations([profile], path, scales, options, frames);
25
- assertSweepAllocation(plan.stationCount, profile.length, false, operation, allocationGuard, options.limits);
25
+ assertSweepAllocation(plan.stationCount, profile.length, false, operation, options.limits);
26
26
  const refined = refineSweepStations(path, scales, frames, options, plan);
27
27
  path = refined.path;
28
28
  scales = refined.scales;
@@ -87,11 +87,10 @@ export function normalizeSweepPath(path, closed, operation, limits = {}) {
87
87
  return Object.freeze(cleaned);
88
88
  }
89
89
  /** @internal Checks output allocation before creating frames or buffers. */
90
- export function assertSweepAllocation(pathCount, profileCount, closed, operation, allocationGuard, limits = {}) {
90
+ export function assertSweepAllocation(pathCount, profileCount, closed, operation, limits = {}) {
91
91
  const vertexCount = pathCount * profileCount;
92
92
  const triangleCount = (closed ? pathCount : pathCount - 1) * profileCount * 2 + (closed ? 0 : (profileCount - 2) * 2);
93
93
  assertMeshAllocationWithinLimits(vertexCount, triangleCount, (vertexCount + triangleCount) * 12, operation, resolveModelingLimits(limits));
94
- allocationGuard?.(vertexCount, triangleCount, (vertexCount + triangleCount) * 12, operation);
95
94
  }
96
95
  /** @internal Rejects coordinates that cannot be represented in manufacturing mesh buffers. */
97
96
  export function writeSweepVertex(positions, offset, point, operation) {
@@ -3,8 +3,6 @@ function operation(name) {
3
3
  const invoke = (...args) => {
4
4
  const context = currentModelContext(name);
5
5
  const method = context[name];
6
- if (typeof method !== "function")
7
- throw new Error(`Unknown modeling operation "${name}".`);
8
6
  return Reflect.apply(method, undefined, args);
9
7
  };
10
8
  return invoke;
@@ -23,7 +21,7 @@ export function modelingFunctions(resolution) {
23
21
  }
24
22
  /** Host-bound box operation. Valid only during a synchronous build. */
25
23
  export const box = /* @__PURE__ */ operation("box");
26
- /** Constructs a closed regularly sampled heightfield during the active synchronous build. */
24
+ /** Constructs a closed rectilinear heightfield during the active synchronous build. */
27
25
  export const heightfield = /* @__PURE__ */ operation("heightfield");
28
26
  /** Returns an immutable copy with the explicit native millimetre tolerance. */
29
27
  export const setTolerance =
@@ -42,6 +40,11 @@ export const union = /* @__PURE__ */ operation("union");
42
40
  export const subtract = /* @__PURE__ */ operation("subtract");
43
41
  /** Host-bound intersect operation. Valid only during a synchronous build. */
44
42
  export const intersect = /* @__PURE__ */ operation("intersect");
43
+ /** Returns intersection and difference with a same-build cutter, including empty results. */
44
+ export const split = /* @__PURE__ */ operation("split");
45
+ /** Splits a solid into the normal-side and opposite-side halves of a world-space plane. */
46
+ export const splitByPlane =
47
+ /* @__PURE__ */ operation("splitByPlane");
45
48
  /** Host-bound compose operation. Valid only during a synchronous build. */
46
49
  export const compose = /* @__PURE__ */ operation("compose");
47
50
  /** Host-bound hull operation. Valid only during a synchronous build. */
@@ -98,6 +101,10 @@ export const inspectSection =
98
101
  /** Host-bound inspectSolid operation. Valid only during a synchronous build. */
99
102
  export const inspectSolid =
100
103
  /* @__PURE__ */ operation("inspectSolid");
104
+ /** Tests a solid or filled section in the active build, including expected empty Boolean results. */
105
+ export const isEmpty = /* @__PURE__ */ operation("isEmpty");
106
+ /** Measures the minimum separation between two nonempty solids in millimetres. */
107
+ export const distance = /* @__PURE__ */ operation("distance");
101
108
  /** Host-bound components operation. Valid only during a synchronous build. */
102
109
  export const components = /* @__PURE__ */ operation("components");
103
110
  /** Host-bound simplify operation. Valid only during a synchronous build. */
@@ -22,13 +22,33 @@ function checkText(text) {
22
22
  return lines;
23
23
  }
24
24
  function normalizeControls(options, staticWeight) {
25
+ const baseline = options.baseline;
26
+ if (baseline !== undefined) {
27
+ if (typeof baseline !== "object" || baseline === null || baseline.kind !== "arc")
28
+ fail("Invalid text baseline; use an arc or omit baseline for horizontal text.");
29
+ if (!Number.isFinite(baseline.radius) || baseline.radius <= 0 || baseline.radius > 10000)
30
+ fail("Arc baseline radius must be positive and at most 10000 mm.");
31
+ if (baseline.centerAngleDegrees !== undefined && !Number.isFinite(baseline.centerAngleDegrees))
32
+ fail("Arc baseline centerAngleDegrees must be finite.");
33
+ if (baseline.direction !== undefined &&
34
+ baseline.direction !== "clockwise" &&
35
+ baseline.direction !== "counterclockwise")
36
+ fail("Invalid arc baseline direction.");
37
+ }
25
38
  const controls = {
26
39
  fontSize: options.fontSize,
27
40
  fontWeight: options.fontWeight ?? staticWeight,
28
41
  letterSpacing: options.letterSpacing ?? 0,
29
- align: options.align ?? "left",
42
+ align: options.align ?? (baseline === undefined ? "left" : "center"),
30
43
  verticalAlign: options.verticalAlign ?? "baseline",
31
44
  lineHeight: options.lineHeight,
45
+ baseline: baseline === undefined
46
+ ? undefined
47
+ : {
48
+ radius: baseline.radius,
49
+ centerAngleDegrees: (((baseline.centerAngleDegrees ?? 90) % 360) + 360) % 360,
50
+ direction: baseline.direction ?? "clockwise",
51
+ },
32
52
  };
33
53
  if (!Number.isFinite(controls.fontSize) || controls.fontSize <= 0 || controls.fontSize > 10000)
34
54
  fail("fontSize must be positive and at most 10000 mm.");
@@ -45,6 +65,8 @@ function normalizeControls(options, staticWeight) {
45
65
  controls.verticalAlign !== "center" &&
46
66
  controls.verticalAlign !== "bottom")
47
67
  fail("Invalid vertical text alignment.");
68
+ if (controls.baseline !== undefined && controls.verticalAlign !== "baseline")
69
+ fail("Arc text requires baseline vertical alignment.");
48
70
  if (controls.lineHeight !== undefined &&
49
71
  (!Number.isFinite(controls.lineHeight) || controls.lineHeight < 0.5 || controls.lineHeight > 5))
50
72
  fail("lineHeight must be between 0.5 and 5.");
@@ -189,6 +211,8 @@ function buildLayout(font, fontMetrics, lines, controls) {
189
211
  let commands = 0;
190
212
  for (let lineIndex = 0; lineIndex < shaped.length; lineIndex++) {
191
213
  const { run, advanceWidth } = shaped[lineIndex];
214
+ if (controls.baseline !== undefined && advanceWidth / controls.baseline.radius >= Math.PI * 2)
215
+ fail("Arc text must span less than one full revolution. Increase radius or reduce text size or spacing.");
192
216
  const lineX = controls.align === "center"
193
217
  ? -advanceWidth / 2
194
218
  : controls.align === "right"
@@ -197,11 +221,16 @@ function buildLayout(font, fontMetrics, lines, controls) {
197
221
  let x = 0;
198
222
  let y = 0;
199
223
  let spacingSteps = -1;
224
+ let groupCenter = 0;
200
225
  for (let index = 0; index < run.glyphs.length; index++) {
201
226
  const position = run.positions[index];
202
227
  // Zero-advance marks retain their base glyph's tracking offset.
203
- if (index === 0 || position.xAdvance !== 0)
228
+ if (index === 0 || position.xAdvance !== 0) {
204
229
  spacingSteps++;
230
+ // Keep zero-advance marks on the same rigid frame as their advancing base glyph.
231
+ groupCenter =
232
+ (x + position.xAdvance / 2) * scale + spacingSteps * controls.letterSpacing + lineX;
233
+ }
205
234
  const source = run.glyphs[index].path.commands;
206
235
  commands += source.length;
207
236
  if (commands > MAX_TEXT_COMMANDS)
@@ -209,9 +238,21 @@ function buildLayout(font, fontMetrics, lines, controls) {
209
238
  if (source.length > 0) {
210
239
  // Preserve full precision: fontkit's toSVG() rounds before scaling and reparsing it wastes work.
211
240
  const path = new SVGPathData(source.map(convertCommand)).transform(SVGPathDataTransformer.MATRIX(scale, 0, 0, scale, (x + position.xOffset) * scale + spacingSteps * controls.letterSpacing + lineX, (y + position.yOffset) * scale - lineIndex * lineHeight));
212
- const bounds = path.getBounds();
241
+ let bounds = path.getBounds();
213
242
  if (![bounds.minX, bounds.minY, bounds.maxX, bounds.maxY].every(Number.isFinite))
214
243
  fail("The font returned invalid glyph outline bounds.");
244
+ if (controls.baseline !== undefined) {
245
+ const { radius, centerAngleDegrees, direction } = controls.baseline;
246
+ const clockwise = direction === "clockwise";
247
+ if (clockwise ? radius + bounds.minY <= 0 : radius - bounds.maxY <= 0)
248
+ fail("Arc glyph outlines cross the circle's center axis. Increase the radius or reduce text size.");
249
+ const angle = (centerAngleDegrees * Math.PI) / 180 + ((clockwise ? -1 : 1) * groupCenter) / radius;
250
+ const rotation = angle + (clockwise ? -Math.PI / 2 : Math.PI / 2);
251
+ const cosine = Math.cos(rotation);
252
+ const sine = Math.sin(rotation);
253
+ path.transform(SVGPathDataTransformer.MATRIX(cosine, sine, -sine, cosine, radius * Math.cos(angle) - cosine * groupCenter, radius * Math.sin(angle) - sine * groupCenter));
254
+ bounds = path.getBounds();
255
+ }
215
256
  minX = Math.min(minX, bounds.minX);
216
257
  minY = Math.min(minY, bounds.minY);
217
258
  maxX = Math.max(maxX, bounds.maxX);
@@ -261,6 +302,8 @@ export function createVectorTextLayout(font, staticWeight) {
261
302
  return (text, options) => {
262
303
  const lines = checkText(text);
263
304
  const controls = normalizeControls(options, staticWeight);
305
+ if (controls.baseline !== undefined && lines.length !== 1)
306
+ fail("Arc text supports exactly one line.");
264
307
  const key = JSON.stringify([lines, controls]);
265
308
  if (previous?.key === key)
266
309
  return previous.layout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamesh/modeling",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Scoped solid modeling and pure geometry foundations for Node and browsers.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {