@metamesh/modeling 0.6.0 → 0.7.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.
@@ -1,10 +1,22 @@
1
1
  /** Limits apply before parsing or shaping untrusted font data. */
2
2
  export declare const MAX_VECTOR_FONT_BYTES: number;
3
3
 
4
- /** Parses bounded font bytes using optional fontkit and WOFF2 peers; no font fetching occurs here. */
4
+ /**
5
+ * Parses bounded font bytes using optional fontkit and WOFF2 peers; no font fetching occurs here.
6
+ *
7
+ * @example Prepare a label before building geometry
8
+ * ```ts
9
+ * async function prepareLabel(fontBytes: Uint8Array) {
10
+ * const font = await prepareVectorFont(fontBytes);
11
+ * return font.layout("MetaMesh", { fontSize: 8 });
12
+ * }
13
+ * ```
14
+ */
5
15
  export declare function prepareVectorFont(bytes: Uint8Array, options?: VectorFontOptions): Promise<VectorFont>;
6
16
 
7
- /** A prepared font owns its parsed bytes; keep handles local to their Worker or invocation. */
17
+ /**
18
+ * A prepared font owns its parsed bytes; keep handles local to their Worker or invocation.
19
+ */
8
20
  export declare interface VectorFont {
9
21
  /** Shape and position up to 512 UTF-16 units and 16 lines; supports empty or whitespace text. */
10
22
  readonly layout: (text: string, options: VectorTextLayoutOptions) => VectorTextLayout;
@@ -28,7 +40,9 @@ export declare interface VectorTextBounds {
28
40
  readonly max: readonly [number, number];
29
41
  }
30
42
 
31
- /** Immutable vector layout with no browser, font parser, or kernel ownership. */
43
+ /**
44
+ * Immutable vector layout with no browser, font parser, or kernel ownership.
45
+ */
32
46
  export declare interface VectorTextLayout {
33
47
  /** Nonzero-filled path data in millimetres, positive Y upward; empty for text with no ink. */
34
48
  readonly paths: readonly string[];
@@ -36,7 +50,9 @@ export declare interface VectorTextLayout {
36
50
  readonly metrics: VectorTextMetrics;
37
51
  }
38
52
 
39
- /** Horizontal multiline text, bounded to 512 UTF-16 units and 16 lines without wrapping. */
53
+ /**
54
+ * Horizontal multiline text, bounded to 512 UTF-16 units and 16 lines without wrapping.
55
+ */
40
56
  export declare interface VectorTextLayoutOptions extends VectorTextOptions {
41
57
  /** Place each line's advance at the origin; defaults to left. Ink overhang is preserved. */
42
58
  readonly align?: "left" | "center" | "right";
@@ -61,7 +61,18 @@ function engineFromModule(module, engineLimits) {
61
61
  },
62
62
  });
63
63
  }
64
- /** Initializes one host kernel and returns a lifecycle-safe engine. No generator initializes WASM. */
64
+ /**
65
+ * Initializes one host kernel and returns a lifecycle-safe engine. No generator initializes WASM.
66
+ *
67
+ * @example Build a box in a host application
68
+ * ```ts
69
+ * import { box } from "@metamesh/modeling";
70
+ * import { createModelEngine } from "@metamesh/modeling/engine";
71
+ *
72
+ * const engine = await createModelEngine();
73
+ * const snapshot = engine.build(() => box({ size: [30, 20, 8] }));
74
+ * ```
75
+ */
65
76
  export async function createModelEngine(options = {}) {
66
77
  const limits = resolveModelingLimits(options.limits);
67
78
  const module = await createManifoldModule({
@@ -224,7 +224,8 @@ function sampleCurve(curve, options, createWriter) {
224
224
  return curveFailure("INVALID_ARGUMENT", "Unsupported curve kind.");
225
225
  }
226
226
  }
227
- /** Samples a spatial curve, preserving endpoints and directed unit tangents.
227
+ /**
228
+ * Samples a spatial curve, preserving endpoints and directed unit tangents.
228
229
  * @remarks Exact counts use uniform curve parameters, not equal physical distances. Adaptive cubics
229
230
  * bound the full control hull. Full-circle arcs repeat their endpoint; closed polylines do not.
230
231
  * @example
@@ -45,6 +45,14 @@ export function preflightHeightfield(options, limits = {}) {
45
45
  * Builds a closed indexed solid from a regularly sampled top, matched boundary walls, and flat base.
46
46
  * Heights are evaluated once per grid point after allocation preflight. Physical XY sizes and all
47
47
  * output coordinates must fit Float32 storage. Shared wall vertices prevent cracks at the base.
48
+ *
49
+ * @example Sample a closed wavy mesh
50
+ * ```ts
51
+ * heightfieldMesh({
52
+ * size: [30, 20], segments: [24, 16],
53
+ * height: ([x, y]) => 3 + Math.cos(x / 5) * Math.cos(y / 5),
54
+ * });
55
+ * ```
48
56
  */
49
57
  export function heightfieldMesh(options, limits = {}) {
50
58
  const facts = preflightHeightfield(options, limits);
@@ -14,6 +14,13 @@ export { triangulateLoop2 } from "./loft-triangulation.js";
14
14
  * Both ends use ear-clipped caps, supporting concave profiles without center fans.
15
15
  * Intermediate slices interpolate placed world vertices, never frame axes.
16
16
  * Transformed coordinates must fit finite Float32 mesh storage.
17
+ *
18
+ * @example Loft with explicit point correspondence
19
+ * ```ts
20
+ * const bottom = rectangleLoop({ size: [20, 12] });
21
+ * const top = rectangleLoop({ size: [30, 20], treatment: { kind: "round", radius: 3 } });
22
+ * loftMesh([{ profile: bottom, z: 0 }, { profile: top, z: 30 }], { correspondence: "reindex" });
23
+ * ```
17
24
  */
18
25
  export function loftMesh(stations, options) {
19
26
  return buildLoftMesh(stations, options);
@@ -40,7 +47,15 @@ export function buildLoftMesh(stations, options, allocationGuard) {
40
47
  }));
41
48
  return simpleLoftMesh(prepareLoftGeometry(profiles, options, limits, false), limits, allocationGuard);
42
49
  }
43
- /** Linearly extrudes one simple profile along +Z with robust ear-clipped caps. */
50
+ /**
51
+ * Linearly extrudes one simple profile along +Z with robust ear-clipped caps.
52
+ *
53
+ * @example Extrude point data without an engine
54
+ * ```ts
55
+ * const outline = rectangleLoop({ size: [30, 20], treatment: { kind: "round", radius: 3 } });
56
+ * extrudeMesh(outline, { height: 8 });
57
+ * ```
58
+ */
44
59
  export function extrudeMesh(profile, options) {
45
60
  const { height } = options;
46
61
  const { center, scales, twistDegrees } = resolveExtrusionControls(height, options, "extrudeMesh");
@@ -1,16 +1,46 @@
1
- /** Adds two finite XYZ vectors component-wise. @example `add3([1, 2, 3], [4, 5, 6])` */
1
+ /**
2
+ * Adds two finite XYZ vectors component-wise.
3
+ * @example
4
+ * ```ts
5
+ * add3([1, 2, 3], [4, 5, 6]); // [5, 7, 9]
6
+ * ```
7
+ */
2
8
  export const add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
3
- /** Subtracts `b` from `a`. @example `subtract3([4, 5, 6], [1, 2, 3])` */
9
+ /**
10
+ * Subtracts `b` from `a`.
11
+ * @example
12
+ * ```ts
13
+ * subtract3([4, 5, 6], [1, 2, 3]); // [3, 3, 3]
14
+ * ```
15
+ */
4
16
  export const subtract3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
5
- /** Multiplies a vector by a dimensionless scalar. @example `scale3([1, 2, 3], 2)` */
17
+ /**
18
+ * Multiplies a vector by a dimensionless scalar.
19
+ * @example
20
+ * ```ts
21
+ * scale3([1, 2, 3], 2); // [2, 4, 6]
22
+ * ```
23
+ */
6
24
  export const scale3 = (vector, scale) => [
7
25
  vector[0] * scale,
8
26
  vector[1] * scale,
9
27
  vector[2] * scale,
10
28
  ];
11
- /** Returns the scalar dot product. @example `dot3([1, 0, 0], [0, 1, 0])` */
29
+ /**
30
+ * Returns the scalar dot product.
31
+ * @example
32
+ * ```ts
33
+ * dot3([1, 0, 0], [0, 1, 0]); // 0
34
+ * ```
35
+ */
12
36
  export const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
13
- /** Returns the right-handed cross product. @example `cross3([1, 0, 0], [0, 1, 0])` */
37
+ /**
38
+ * Returns the right-handed cross product.
39
+ * @example
40
+ * ```ts
41
+ * cross3([1, 0, 0], [0, 1, 0]); // [0, 0, 1]
42
+ * ```
43
+ */
14
44
  export const cross3 = (a, b) => [
15
45
  a[1] * b[2] - a[2] * b[1],
16
46
  a[2] * b[0] - a[0] * b[2],
@@ -234,6 +234,11 @@ const standardProfile = (kind, pitch, depth, root, crest, leftRun, rightRun) =>
234
234
  * The open path spans one pitch. Radial zero is the major envelope; negative
235
235
  * values point toward the shaft. Custom paths must be finite, axially
236
236
  * non-decreasing, periodic at their endpoints, and reach radial zero.
237
+ *
238
+ * @example Printable triangular thread profile
239
+ * ```ts
240
+ * createThreadProfile({ kind: "triangular", pitch: 1.75 });
241
+ * ```
237
242
  */
238
243
  export function createThreadProfile(spec) {
239
244
  const operation = "createThreadProfile";
@@ -0,0 +1,126 @@
1
+ const MAXIMUM_UINT32 = 0xffff_ffff;
2
+ const MAXIMUM_SLOT_CAPACITY = 0x1_0000_0000;
3
+ const FORWARD = 1;
4
+ const BACKWARD = 2;
5
+ const PAIRED = 3;
6
+ const INVALID = 4;
7
+ function doubledCapacity(capacity) {
8
+ const doubled = capacity * 2;
9
+ if (doubled > MAXIMUM_SLOT_CAPACITY)
10
+ throw new RangeError("Mesh edge table exceeds Uint32 slot capacity.");
11
+ return doubled;
12
+ }
13
+ /**
14
+ * @internal Indexed edge closure and first-face records; coordinates are never welded.
15
+ * Dense record IDs remain stable when the hash slots grow. Inputs are validated Uint32
16
+ * vertex indices with distinct endpoints, and triangle IDs fit Uint32 storage.
17
+ */
18
+ export class IndexedMeshEdgeTable {
19
+ slotCapacity;
20
+ mask;
21
+ slots;
22
+ recordCapacity;
23
+ recordCount = 0;
24
+ lower;
25
+ upper;
26
+ states;
27
+ firstTriangle;
28
+ constructor(indexCount, includeTopology) {
29
+ if (!Number.isSafeInteger(indexCount) || indexCount < 0 || indexCount > MAXIMUM_UINT32)
30
+ throw new RangeError("Mesh edge table requires an index count within Uint32 capacity.");
31
+ let capacity = 16;
32
+ // Multiplication avoids the signed-int32 wrap of bitwise capacity doubling.
33
+ while (capacity < indexCount)
34
+ capacity = doubledCapacity(capacity);
35
+ this.slotCapacity = capacity;
36
+ this.mask = capacity - 1;
37
+ this.slots = new Uint32Array(capacity);
38
+ this.recordCapacity = capacity / 2;
39
+ this.lower = new Uint32Array(this.recordCapacity);
40
+ this.upper = new Uint32Array(this.recordCapacity);
41
+ this.states = new Uint8Array(this.recordCapacity);
42
+ this.firstTriangle = includeTopology ? new Uint32Array(this.recordCapacity) : null;
43
+ }
44
+ /** Returns -1 for a new edge, otherwise its first triangle (or zero without topology). */
45
+ touch(a, b, triangle) {
46
+ const lower = Math.min(a, b);
47
+ const upper = Math.max(a, b);
48
+ const state = a < b ? FORWARD : BACKWARD;
49
+ let slot = (this.hash(lower, upper) & this.mask) >>> 0;
50
+ while (this.slots[slot] !== 0) {
51
+ const record = this.slots[slot] - 1;
52
+ // Hashes wrap to int32; only the complete two Uint32 keys establish identity.
53
+ if (this.lower[record] === lower && this.upper[record] === upper) {
54
+ const previous = this.states[record];
55
+ this.states[record] =
56
+ (previous === FORWARD && state === BACKWARD) ||
57
+ (previous === BACKWARD && state === FORWARD)
58
+ ? PAIRED
59
+ : INVALID;
60
+ return this.firstTriangle?.[record] ?? 0;
61
+ }
62
+ slot = ((slot + 1) & this.mask) >>> 0;
63
+ }
64
+ // Repeat touches cannot increase occupancy; check growth only for an absent key.
65
+ if (this.recordCount + 1 > this.slotCapacity * 0.7) {
66
+ this.growSlots();
67
+ return this.touch(a, b, triangle);
68
+ }
69
+ if (this.recordCount >= MAXIMUM_UINT32)
70
+ throw new RangeError("Mesh edge table exceeds Uint32 record capacity.");
71
+ if (this.recordCount === this.recordCapacity)
72
+ this.growRecords();
73
+ const record = this.recordCount;
74
+ this.slots[slot] = record + 1;
75
+ this.lower[record] = lower;
76
+ this.upper[record] = upper;
77
+ this.states[record] = state;
78
+ if (this.firstTriangle !== null)
79
+ this.firstTriangle[record] = triangle;
80
+ this.recordCount += 1;
81
+ return -1;
82
+ }
83
+ /** Checks exactly two oppositely wound uses after every triangle has been validated. */
84
+ isClosed() {
85
+ for (let record = 0; record < this.recordCount; record += 1)
86
+ if (this.states[record] !== PAIRED)
87
+ return false;
88
+ return true;
89
+ }
90
+ hash(lower, upper) {
91
+ const mixed = Math.imul(lower, 0x9e3779b1) ^ Math.imul(upper, 0x85ebca6b);
92
+ return (mixed ^ (mixed >>> 16)) >>> 0;
93
+ }
94
+ growSlots() {
95
+ const capacity = doubledCapacity(this.slotCapacity);
96
+ const mask = capacity - 1;
97
+ const slots = new Uint32Array(capacity);
98
+ // Paired and invalid records must remain: forgetting either could admit four-face edges.
99
+ for (let record = 0; record < this.recordCount; record += 1) {
100
+ let slot = (this.hash(this.lower[record], this.upper[record]) & mask) >>> 0;
101
+ while (slots[slot] !== 0)
102
+ slot = ((slot + 1) & mask) >>> 0;
103
+ slots[slot] = record + 1;
104
+ }
105
+ this.slotCapacity = capacity;
106
+ this.mask = mask;
107
+ this.slots = slots;
108
+ }
109
+ growRecords() {
110
+ const capacity = doubledCapacity(this.recordCapacity);
111
+ const lower = new Uint32Array(capacity);
112
+ const upper = new Uint32Array(capacity);
113
+ const states = new Uint8Array(capacity);
114
+ const firstTriangle = this.firstTriangle === null ? null : new Uint32Array(capacity);
115
+ lower.set(this.lower);
116
+ upper.set(this.upper);
117
+ states.set(this.states);
118
+ if (firstTriangle !== null)
119
+ firstTriangle.set(this.firstTriangle);
120
+ this.recordCapacity = capacity;
121
+ this.lower = lower;
122
+ this.upper = upper;
123
+ this.states = states;
124
+ this.firstTriangle = firstTriangle;
125
+ }
126
+ }
@@ -1,4 +1,5 @@
1
1
  import { GeometryError } from "./errors.js";
2
+ import { IndexedMeshEdgeTable } from "./mesh-edge-table.js";
2
3
  import { writeMeshVertex } from "./numeric.js";
3
4
  import { resolveGeometryTolerance } from "./tolerance.js";
4
5
  import { transformDeterminant3, validateTransform3 } from "./transform3.js";
@@ -33,7 +34,7 @@ function inspectTriangleMesh(mesh, options = {}, mode = "validate", topology = f
33
34
  const extent = vertexCount === 0 ? 0 : Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
34
35
  const minimumDoubleArea = resolveGeometryTolerance(extent, options.tolerance).area;
35
36
  const edges = options.requireClosed || topology
36
- ? new Map()
37
+ ? new IndexedMeshEdgeTable(mesh.indices.length, topology)
37
38
  : null;
38
39
  const triangleCount = mesh.indices.length / 3;
39
40
  const parents = topology ? new Int32Array(triangleCount) : null;
@@ -64,23 +65,11 @@ function inspectTriangleMesh(mesh, options = {}, mode = "validate", topology = f
64
65
  };
65
66
  let edgeCount = 0;
66
67
  const addEdge = (a, b, triangle) => {
67
- const lower = Math.min(a, b), upper = Math.max(a, b);
68
- let neighbors = edges.get(lower);
69
- if (neighbors === undefined) {
70
- neighbors = new Map();
71
- edges.set(lower, neighbors);
72
- }
73
- let edge = neighbors.get(upper);
74
- if (edge === undefined) {
75
- edge = { count: 0, balance: 0, ...(topology ? { firstTriangle: triangle } : {}) };
76
- neighbors.set(upper, edge);
68
+ const firstTriangle = edges.touch(a, b, triangle);
69
+ if (firstTriangle === -1)
77
70
  edgeCount += 1;
78
- }
79
- else if (topology) {
80
- connect(edge.firstTriangle, triangle);
81
- }
82
- edge.count += 1;
83
- edge.balance += a < b ? 1 : -1;
71
+ else if (topology)
72
+ connect(firstTriangle, triangle);
84
73
  };
85
74
  // Keep the two hot-loop accumulators independent and local: no mutable numeric objects escape.
86
75
  let surfaceArea = 0, areaCorrection = 0, signedVolume = 0, volumeCorrection = 0;
@@ -129,14 +118,8 @@ function inspectTriangleMesh(mesh, options = {}, mode = "validate", topology = f
129
118
  addEdge(c, a, offset / 3);
130
119
  }
131
120
  }
132
- if (options.requireClosed && edges !== null) {
133
- for (const neighbors of edges.values())
134
- for (const edge of neighbors.values()) {
135
- if (edge.count !== 2 || edge.balance !== 0) {
136
- throw new GeometryError("INVALID_MESH", "Closed meshes require exactly two oppositely wound faces at every edge.");
137
- }
138
- }
139
- }
121
+ if (options.requireClosed && edges !== null && !edges.isClosed())
122
+ throw new GeometryError("INVALID_MESH", "Closed meshes require exactly two oppositely wound faces at every edge.");
140
123
  if (referenced !== null) {
141
124
  for (let vertex = 0; vertex < vertexCount; vertex += 1) {
142
125
  if (referenced[vertex] === 0) {
@@ -26,5 +26,7 @@ export const cumulativePathLengths = (path, limits = {}) => {
26
26
  }
27
27
  return cumulative;
28
28
  };
29
- /** Returns total sampled 3D path length, or zero for an empty path. */
29
+ /**
30
+ * Returns total sampled 3D path length, or zero for an empty path.
31
+ */
30
32
  export const pathLength = (path, limits = {}) => cumulativePathLengths(path, limits).at(-1) ?? 0;
@@ -99,7 +99,8 @@ function resamplePolyline(path, dimensions, options, createWriter) {
99
99
  }
100
100
  return writer.finish();
101
101
  }
102
- /** Resamples a spatial polyline at equal arc lengths with directed outgoing tangents.
102
+ /**
103
+ * Resamples a spatial polyline at equal arc lengths with directed outgoing tangents.
103
104
  * @remarks Open paths retain both endpoints; closed paths include the closing edge and omit the
104
105
  * repeated seam. Repeated adjacent inputs and explicit closing duplicates are removed. Physical
105
106
  * maxSegmentLength bounds arc-length intervals; exact counts override that policy. Output edges
@@ -68,6 +68,14 @@ const contours = (component) => [
68
68
  * Cubics share a checked uniform parameter grid, including grids selected by exact paired requests.
69
69
  * Input order and winding are preserved. Collapsed spans retain repeated logical slots, which
70
70
  * the engine maps to shared physical vertices while omitting degenerate faces.
71
+ *
72
+ * @example Match named rectangle landmarks
73
+ * ```ts
74
+ * matchProfiles([
75
+ * rectangleProfile({ size: [20, 12] }),
76
+ * rectangleProfile({ size: [30, 20], treatment: { kind: "round", radius: 3 } }),
77
+ * ]);
78
+ * ```
71
79
  */
72
80
  export function matchProfiles(profiles, options = {}) {
73
81
  if (!isArray(profiles) || profiles.length === 0 || profiles.length > 1000)
@@ -3,7 +3,14 @@ import { resolveCircularPrimitiveSegments } from "../primitive-resolution.js";
3
3
  import { assertPrimitiveOptions } from "../primitive-resolvers.js";
4
4
  import { requireFiniteProfileValues, requireProfileVertexCount } from "./validation.js";
5
5
  export { rectangleLoop } from "./rectangle.js";
6
- /** Creates an immutable centered CCW circle with +X seam and vertex or face radial sizing. */
6
+ /**
7
+ * Creates an immutable centered CCW circle with +X seam and vertex or face radial sizing.
8
+ *
9
+ * @example Explicit circular point data
10
+ * ```ts
11
+ * circleLoop({ diameter: 20, segments: 32 });
12
+ * ```
13
+ */
7
14
  export const circleLoop = (options) => {
8
15
  const operation = "circleLoop";
9
16
  assertPrimitiveOptions(options, operation);
@@ -32,7 +39,14 @@ export const circleLoop = (options) => {
32
39
  return Object.freeze([sampledRadius * Math.cos(angle), sampledRadius * Math.sin(angle)]);
33
40
  }));
34
41
  };
35
- /** Creates a centered CCW point loop with vertex or face radial sizing and an implicit seam. */
42
+ /**
43
+ * Creates a centered CCW point loop with vertex or face radial sizing and an implicit seam.
44
+ *
45
+ * @example Hexagon sized to its faces
46
+ * ```ts
47
+ * regularPolygonLoop({ radius: 10, sides: 6, sizing: "face" });
48
+ * ```
49
+ */
36
50
  export const regularPolygonLoop = (options) => {
37
51
  const operation = "regularPolygonLoop";
38
52
  const { radius, sides, rotationDegrees = 0, sizing = "vertex" } = options;
@@ -53,7 +67,14 @@ export const regularPolygonLoop = (options) => {
53
67
  return [sampledRadius * Math.cos(angle), sampledRadius * Math.sin(angle)];
54
68
  });
55
69
  };
56
- /** Creates a centered CCW alternating-radius star within the selected profile sampling ceiling. */
70
+ /**
71
+ * Creates a centered CCW alternating-radius star within the selected profile sampling ceiling.
72
+ *
73
+ * @example Five-point outline
74
+ * ```ts
75
+ * starLoop(12, 5, 6);
76
+ * ```
77
+ */
57
78
  export const starLoop = (outerRadius, points, innerRadius = outerRadius / 2, limits = {}) => {
58
79
  const operation = "starLoop";
59
80
  requireFiniteProfileValues([outerRadius, innerRadius], operation);
@@ -78,7 +78,14 @@ export function rectangleCornerSegments(corner, limits) {
78
78
  limits: selected,
79
79
  }) / 4);
80
80
  }
81
- /** Creates an immutable CCW rectangle loop. Round radius zero is square; tangent saturated endpoints occur once. */
81
+ /**
82
+ * Creates an immutable CCW rectangle loop. Round radius zero is square; tangent saturated endpoints occur once.
83
+ *
84
+ * @example Rounded rectangle outline
85
+ * ```ts
86
+ * rectangleLoop({ size: [30, 20], treatment: { kind: "round", radius: 3 } });
87
+ * ```
88
+ */
82
89
  export function rectangleLoop(options) {
83
90
  const { width, height, corners, limits } = resolveRectangle(options, "rectangleLoop");
84
91
  const counts = corners.map((corner) => {
@@ -138,7 +145,14 @@ export function rectangleLoop(options) {
138
145
  clean.pop();
139
146
  return Object.freeze(clean.map(([px, py]) => Object.freeze(options.center === false ? [px + x, py + y] : [px, py])));
140
147
  }
141
- /** Creates an identified rectangle whose edge/corner spans survive zero and saturated round radii. Outer winding is CCW. */
148
+ /**
149
+ * Creates an identified rectangle whose edge/corner spans survive zero and saturated round radii. Outer winding is CCW.
150
+ *
151
+ * @example Retain rectangle landmarks
152
+ * ```ts
153
+ * rectangleProfile({ size: [30, 20], treatment: { kind: "round", radius: 3 } });
154
+ * ```
155
+ */
142
156
  export function rectangleProfile(options) {
143
157
  const { width, height, corners, setbacks, limits } = resolveRectangle(options, "rectangleProfile");
144
158
  const id = options.id ?? "body";
@@ -2,7 +2,14 @@ import { GeometryError } from "../errors.js";
2
2
  import { resolveCircularPrimitiveSegments } from "../primitive-resolution.js";
3
3
  import { assertPrimitiveOptions } from "../primitive-resolvers.js";
4
4
  import { requireFiniteProfileValues, requireProfileVertexCount } from "./validation.js";
5
- /** Creates an immutable centered CCW ellipse with +X seam within the selected sampling ceiling. */
5
+ /**
6
+ * Creates an immutable centered CCW ellipse with +X seam within the selected sampling ceiling.
7
+ *
8
+ * @example Ellipse by full dimensions
9
+ * ```ts
10
+ * ellipseLoop({ size: [30, 20] });
11
+ * ```
12
+ */
6
13
  export function ellipseLoop(options) {
7
14
  assertPrimitiveOptions(options, "ellipseLoop");
8
15
  if ((options.radius === undefined) === (options.size === undefined)) {
@@ -20,6 +20,12 @@ const AXIS_EPSILON = 1e-10;
20
20
  * @param profile - Closed CCW `[radius, z]` profile without a duplicated seam point.
21
21
  * @param options - Optional physical or exact circumferential sampling controls. Defaults to `{}`.
22
22
  * @returns A closed indexed triangle mesh revolved counter-clockwise around +Z.
23
+ *
24
+ * @example Partial revolution with end caps
25
+ * ```ts
26
+ * // Each point is [radius, Z]; the profile seam is implicit.
27
+ * revolveMesh([[0, 0], [12, 0], [8, 20], [0, 20]], { revolveDegrees: 270 });
28
+ * ```
23
29
  */
24
30
  export const revolveMesh = (profile, options = {}) => {
25
31
  if (typeof options !== "object" || options === null || Array.isArray(options))
@@ -2,7 +2,17 @@ import { GeometryError } from "../errors.js";
2
2
  import { validateSweepOptions } from "./orientation.js";
3
3
  import { buildClosedSweepMesh } from "./sweep-closed.js";
4
4
  import { buildOpenSweepMesh } from "./sweep-open.js";
5
- /** Sweeps one explicitly ordered point loop, with caps or a geometrically matching closed seam. */
5
+ /**
6
+ * Sweeps one explicitly ordered point loop, with caps or a geometrically matching closed seam.
7
+ *
8
+ * @example Sweep point data along a bent path
9
+ * ```ts
10
+ * const profile = rectangleLoop({ size: [6, 3] });
11
+ * sweepMesh(profile, [[0, 0, 0], [0, 0, 15], [10, 0, 25]], {
12
+ * orientation: { kind: "transport", initialNormal: [0, 1, 0] },
13
+ * });
14
+ * ```
15
+ */
6
16
  export function sweepMesh(profile, path, options = {}) {
7
17
  return buildSweepMesh(profile, path, options);
8
18
  }
@@ -14,7 +14,9 @@ export const matchProfiles =
14
14
  /* @__PURE__ */ operation("matchProfiles");
15
15
  /** Copies intersections along a finite directed ray segment during the active build. */
16
16
  export const rayCast = /* @__PURE__ */ operation("rayCast");
17
- /** Returns bound local operations, optionally with derived resolution, without changing imported operations. */
17
+ /**
18
+ * Returns bound local operations, optionally with derived resolution, without changing imported operations.
19
+ */
18
20
  export function modelingFunctions(resolution) {
19
21
  const context = currentModelContext("modelingFunctions");
20
22
  return resolution === undefined ? context : context.withResolution(resolution);
@@ -19,7 +19,17 @@ function serializeVectorText({ paths, metrics }) {
19
19
  fail("Text outlines exceed 256 KiB. Use fewer characters or a simpler font.");
20
20
  return svg;
21
21
  }
22
- /** Parses bounded font bytes using optional fontkit and WOFF2 peers; no font fetching occurs here. */
22
+ /**
23
+ * Parses bounded font bytes using optional fontkit and WOFF2 peers; no font fetching occurs here.
24
+ *
25
+ * @example Prepare a label before building geometry
26
+ * ```ts
27
+ * async function prepareLabel(fontBytes: Uint8Array) {
28
+ * const font = await prepareVectorFont(fontBytes);
29
+ * return font.layout("MetaMesh", { fontSize: 8 });
30
+ * }
31
+ * ```
32
+ */
23
33
  export async function prepareVectorFont(bytes, options = {}) {
24
34
  if (bytes.byteLength === 0 || bytes.byteLength > MAX_VECTOR_FONT_BYTES)
25
35
  fail("Font files must be non-empty and at most 16 MiB.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamesh/modeling",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Scoped solid modeling and pure geometry foundations for Node and browsers.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {