@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.
- package/CHANGELOG.md +6 -0
- package/README.md +82 -20
- package/dist/api/engine.d.ts +21 -4
- package/dist/api/geometry.d.ts +288 -64
- package/dist/api/index.d.ts +533 -94
- package/dist/api/text.d.ts +20 -4
- package/dist/engine/index.js +12 -1
- package/dist/geometry/curves/sampling.js +2 -1
- package/dist/geometry/heightfield.js +8 -0
- package/dist/geometry/loft.js +16 -1
- package/dist/geometry/math/vector3.js +35 -5
- package/dist/geometry/mechanical/thread-profiles.js +5 -0
- package/dist/geometry/mesh-edge-table.js +126 -0
- package/dist/geometry/mesh.js +8 -25
- package/dist/geometry/paths/path.js +3 -1
- package/dist/geometry/paths/resampling.js +2 -1
- package/dist/geometry/profiles/matched.js +8 -0
- package/dist/geometry/profiles/profiles.js +24 -3
- package/dist/geometry/profiles/rectangle.js +16 -2
- package/dist/geometry/profiles/shapes.js +8 -1
- package/dist/geometry/revolve/revolve-mesh.js +6 -0
- package/dist/geometry/sweeps/sweep-mesh.js +11 -1
- package/dist/model/operations.js +3 -1
- package/dist/text/index.js +11 -1
- package/package.json +1 -1
package/dist/api/text.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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";
|
package/dist/engine/index.js
CHANGED
|
@@ -61,7 +61,18 @@ function engineFromModule(module, engineLimits) {
|
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
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
|
-
/**
|
|
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);
|
package/dist/geometry/loft.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
}
|
package/dist/geometry/mesh.js
CHANGED
|
@@ -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
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
}
|
package/dist/model/operations.js
CHANGED
|
@@ -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
|
-
/**
|
|
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);
|
package/dist/text/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
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.");
|