okgeometry-api 0.5.0 → 0.5.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.
package/src/Point.ts ADDED
@@ -0,0 +1,145 @@
1
+ import { Vec3 } from "./Vec3.js";
2
+ import type { Plane } from "./Plane.js";
3
+ import type { RotationAxis } from "./types.js";
4
+
5
+ /**
6
+ * Immutable 3D point for representing positions in space.
7
+ * All operations return new Point instances.
8
+ */
9
+ export class Point {
10
+ constructor(
11
+ public readonly x: number,
12
+ public readonly y: number,
13
+ public readonly z: number
14
+ ) {}
15
+
16
+ /**
17
+ * Compute the distance to another point.
18
+ * @param other - Target point
19
+ * @returns Euclidean distance between points
20
+ */
21
+ distanceTo(other: Point): number {
22
+ const dx = this.x - other.x;
23
+ const dy = this.y - other.y;
24
+ const dz = this.z - other.z;
25
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
26
+ }
27
+
28
+ /**
29
+ * Convert this point to a vector (from origin).
30
+ * @returns Vec3 with same coordinates
31
+ */
32
+ toVec3(): Vec3 {
33
+ return new Vec3(this.x, this.y, this.z);
34
+ }
35
+
36
+ /**
37
+ * Add a vector to this point.
38
+ * @param v - Vector to add
39
+ * @returns New point offset by vector
40
+ */
41
+ add(v: Vec3): Point {
42
+ return new Point(this.x + v.x, this.y + v.y, this.z + v.z);
43
+ }
44
+
45
+ /**
46
+ * Translate this point by an offset vector.
47
+ * Alias for add().
48
+ * @param offset - Translation vector
49
+ * @returns New translated point
50
+ */
51
+ translate(offset: Vec3): Point {
52
+ return this.add(offset);
53
+ }
54
+
55
+ /**
56
+ * Rotate around an axis by angle in radians using Rodrigues' formula.
57
+ * @param axis - Vec3 (rotation around origin) or Line (rotation around arbitrary axis)
58
+ * @param angle - Rotation angle in radians
59
+ * @returns Rotated point
60
+ */
61
+ rotate(axis: RotationAxis, angle: number): Point {
62
+ if ('start' in axis && 'end' in axis) {
63
+ const dir = new Vec3(axis.end.x - axis.start.x, axis.end.y - axis.start.y, axis.end.z - axis.start.z);
64
+ const shifted = new Point(this.x - axis.start.x, this.y - axis.start.y, this.z - axis.start.z);
65
+ const rotated = shifted.rotate(dir, angle);
66
+ return new Point(rotated.x + axis.start.x, rotated.y + axis.start.y, rotated.z + axis.start.z);
67
+ }
68
+ const len = Math.sqrt(axis.x * axis.x + axis.y * axis.y + axis.z * axis.z);
69
+ if (len < 1e-15) return this.clone();
70
+ const k = { x: axis.x / len, y: axis.y / len, z: axis.z / len };
71
+ const cos = Math.cos(angle);
72
+ const sin = Math.sin(angle);
73
+ const dot = k.x * this.x + k.y * this.y + k.z * this.z;
74
+ const cx = k.y * this.z - k.z * this.y;
75
+ const cy = k.z * this.x - k.x * this.z;
76
+ const cz = k.x * this.y - k.y * this.x;
77
+ return new Point(
78
+ this.x * cos + cx * sin + k.x * dot * (1 - cos),
79
+ this.y * cos + cy * sin + k.y * dot * (1 - cos),
80
+ this.z * cos + cz * sin + k.z * dot * (1 - cos),
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Project this point onto a plane.
86
+ * @param plane - Target plane
87
+ * @param direction - Optional projection direction (default: perpendicular to plane)
88
+ * @returns Projected point on plane
89
+ */
90
+ projectOntoPlane(plane: Plane, direction?: Vec3): Point {
91
+ return direction ? plane.projectPointAlongDirection(this, direction) : plane.projectPoint(this);
92
+ }
93
+
94
+ /**
95
+ * Get the vector from another point to this point.
96
+ * @param other - Origin point
97
+ * @returns Vector from other to this
98
+ */
99
+ sub(other: Point): Vec3 {
100
+ return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z);
101
+ }
102
+
103
+ /**
104
+ * Create a copy of this point.
105
+ * @returns New point with same coordinates
106
+ */
107
+ clone(): Point {
108
+ return new Point(this.x, this.y, this.z);
109
+ }
110
+
111
+ /**
112
+ * Check equality with another point within tolerance.
113
+ * @param other - Point to compare
114
+ * @param eps - Tolerance (default 1e-10)
115
+ * @returns True if points are approximately equal
116
+ */
117
+ equals(other: Point, eps = 1e-10): boolean {
118
+ return (
119
+ Math.abs(this.x - other.x) < eps &&
120
+ Math.abs(this.y - other.y) < eps &&
121
+ Math.abs(this.z - other.z) < eps
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Convert to array format.
127
+ * @returns Tuple [x, y, z]
128
+ */
129
+ toArray(): [number, number, number] {
130
+ return [this.x, this.y, this.z];
131
+ }
132
+
133
+ /**
134
+ * Create a Point from a plain object or JSON.
135
+ * @param json - Object with x, y, z properties
136
+ * @returns New Point instance
137
+ */
138
+ static fromJSON(json: any): Point {
139
+ if (json instanceof Point) return json;
140
+ return new Point(json.x ?? 0, json.y ?? 0, json.z ?? 0);
141
+ }
142
+
143
+ /** Origin point (0, 0, 0) */
144
+ static readonly ORIGIN = new Point(0, 0, 0);
145
+ }
@@ -0,0 +1,306 @@
1
+ import { ensureInit } from "./engine.js";
2
+ import { Point } from "./Point.js";
3
+ import { Line } from "./Line.js";
4
+ import { Arc } from "./Arc.js";
5
+ import { Vec3 } from "./Vec3.js";
6
+ import { NurbsCurve } from "./NurbsCurve.js";
7
+ import type { Plane } from "./Plane.js";
8
+ import type { CurveSegment, RotationAxis } from "./types.js";
9
+ import { SegmentTypeCode } from "./types.js";
10
+ import { pointsToCoords, coordsToPoints } from "./BufferCodec.js";
11
+ import * as wasm from "../wasm/okgeometrycore_bg.js";
12
+
13
+ /**
14
+ * Composite curve made of sequential Line and Arc segments.
15
+ */
16
+ export class PolyCurve {
17
+ public readonly segments: CurveSegment[];
18
+
19
+ constructor(segments: CurveSegment[]) {
20
+ this.segments = segments;
21
+ }
22
+
23
+ /** Create a PolyCurve by connecting points with line segments */
24
+ static byPoints(points: Point[]): PolyCurve {
25
+ if (points.length < 2) return new PolyCurve([]);
26
+ const segs: CurveSegment[] = [];
27
+ for (let i = 0; i < points.length - 1; i++) {
28
+ segs.push(new Line(points[i], points[i + 1]));
29
+ }
30
+ return new PolyCurve(segs);
31
+ }
32
+
33
+ get startPoint(): Point {
34
+ if (this.segments.length === 0) return Point.ORIGIN;
35
+ const s = this.segments[0];
36
+ return s instanceof Line ? s.start : s.pointAt(0);
37
+ }
38
+
39
+ get endPoint(): Point {
40
+ if (this.segments.length === 0) return Point.ORIGIN;
41
+ const s = this.segments[this.segments.length - 1];
42
+ return s instanceof Line ? s.end : s.pointAt(1);
43
+ }
44
+
45
+ /** Returns all constituent curves (Line and Arc segments) in order. */
46
+ get curves(): CurveSegment[] {
47
+ return this.segments;
48
+ }
49
+
50
+ /**
51
+ * Sample all segments into a flat array of points for rendering.
52
+ * Line segments produce 2 points; Arc segments are sampled with `arcSamples` points.
53
+ * Consecutive duplicate junction points are removed.
54
+ */
55
+ sample(arcSamples = 32): Point[] {
56
+ const pts: Point[] = [];
57
+ for (const seg of this.segments) {
58
+ let segPts: Point[];
59
+ if (seg instanceof Line) {
60
+ segPts = [seg.start, seg.end];
61
+ } else {
62
+ // Arc: sample into multiple points for smooth rendering
63
+ segPts = seg.sample(arcSamples);
64
+ }
65
+ for (const p of segPts) {
66
+ // Skip duplicate junction points
67
+ if (pts.length > 0 && pts[pts.length - 1].equals(p)) continue;
68
+ pts.push(p);
69
+ }
70
+ }
71
+ return pts;
72
+ }
73
+
74
+ /** Evaluate a point on the curve at normalized parameter t (0 = start, 1 = end). */
75
+ pointAt(t: number): Point {
76
+ if (this.segments.length === 0) return Point.ORIGIN;
77
+ if (t <= 0) return this.startPoint;
78
+ if (t >= 1) return this.endPoint;
79
+
80
+ const lengths = this.segments.map(s => s.length());
81
+ const total = lengths.reduce((a, b) => a + b, 0);
82
+ if (total === 0) return this.startPoint;
83
+
84
+ let target = t * total;
85
+ for (let i = 0; i < this.segments.length; i++) {
86
+ if (target <= lengths[i] || i === this.segments.length - 1) {
87
+ const localT = lengths[i] > 0 ? target / lengths[i] : 0;
88
+ return this.segments[i].pointAt(Math.min(localT, 1));
89
+ }
90
+ target -= lengths[i];
91
+ }
92
+ return this.endPoint;
93
+ }
94
+
95
+ isClosed(eps = 1e-10): boolean {
96
+ return this.startPoint.equals(this.endPoint, eps);
97
+ }
98
+
99
+ length(): number {
100
+ return this.segments.reduce((sum, s) => sum + s.length(), 0);
101
+ }
102
+
103
+ rotate(axis: RotationAxis, angle: number): PolyCurve {
104
+ return new PolyCurve(this.segments.map(s => s.rotate(axis, angle)));
105
+ }
106
+
107
+ translate(offset: Vec3): PolyCurve {
108
+ return new PolyCurve(this.segments.map(s => s.translate(offset)));
109
+ }
110
+
111
+ projectOntoPlane(plane: Plane, direction?: Vec3, arcSamples = 32): PolyCurve {
112
+ const proj = (p: Point) => direction ? plane.projectPointAlongDirection(p, direction) : plane.projectPoint(p);
113
+ const segs: CurveSegment[] = [];
114
+ for (const s of this.segments) {
115
+ if (s instanceof Line) {
116
+ segs.push(new Line(proj(s.start), proj(s.end)));
117
+ } else {
118
+ // Arc projects to elliptical arc — approximate with line segments
119
+ const pts = s.sample(arcSamples);
120
+ for (let i = 0; i < pts.length - 1; i++) {
121
+ segs.push(new Line(proj(pts[i]), proj(pts[i + 1])));
122
+ }
123
+ }
124
+ }
125
+ return new PolyCurve(segs);
126
+ }
127
+
128
+ /**
129
+ * Fillet corners of this PolyCurve with arcs of the given radius.
130
+ * Extracts the vertex points, calls WASM fillet_corners, returns a new PolyCurve.
131
+ */
132
+ fillet(radius: number, normal?: Vec3): PolyCurve {
133
+ ensureInit();
134
+ if (this.segments.length < 2) {
135
+ throw new Error("PolyCurve.fillet() requires at least 2 segments");
136
+ }
137
+
138
+ // Extract all vertex points from segments
139
+ const pts: Point[] = [];
140
+ for (const seg of this.segments) {
141
+ if (seg instanceof Line) {
142
+ if (pts.length === 0) pts.push(seg.start);
143
+ pts.push(seg.end);
144
+ } else {
145
+ // Arc: use start and end points
146
+ if (pts.length === 0) pts.push(seg.pointAt(0));
147
+ pts.push(seg.pointAt(1));
148
+ }
149
+ }
150
+ if (pts.length < 3) {
151
+ throw new Error("PolyCurve.fillet() requires at least 3 vertices");
152
+ }
153
+
154
+ const coords = new Float64Array(pts.length * 3);
155
+ for (let i = 0; i < pts.length; i++) {
156
+ coords[i * 3] = pts[i].x;
157
+ coords[i * 3 + 1] = pts[i].y;
158
+ coords[i * 3 + 2] = pts[i].z;
159
+ }
160
+
161
+ const nx = normal?.x ?? 0, ny = normal?.y ?? 0, nz = normal?.z ?? 0;
162
+ const buf = wasm.fillet_polycurve(coords, radius, nx, ny, nz);
163
+ if (buf.length < 1) {
164
+ throw new Error("PolyCurve.fillet() failed - WASM returned empty result");
165
+ }
166
+ return PolyCurve.fromSegmentData(buf);
167
+ }
168
+
169
+ /** Decode a PolyCurve from WASM segment buffer [count, type, ...data, ...] */
170
+ static fromSegmentData(buf: Float64Array | number[]): PolyCurve {
171
+ const count = buf[0];
172
+ const segs: CurveSegment[] = [];
173
+ let idx = 1;
174
+ for (let i = 0; i < count; i++) {
175
+ const type = buf[idx++];
176
+ if (type === SegmentTypeCode.Line) {
177
+ // Line: 6 floats
178
+ const s = new Point(buf[idx], buf[idx + 1], buf[idx + 2]);
179
+ const e = new Point(buf[idx + 3], buf[idx + 4], buf[idx + 5]);
180
+ segs.push(new Line(s, e));
181
+ idx += 6;
182
+ } else if (type === SegmentTypeCode.Arc) {
183
+ // Arc: 9 floats
184
+ const center = new Point(buf[idx], buf[idx + 1], buf[idx + 2]);
185
+ const normal = new Vec3(buf[idx + 3], buf[idx + 4], buf[idx + 5]);
186
+ const radius = buf[idx + 6];
187
+ const startAngle = buf[idx + 7];
188
+ const endAngle = buf[idx + 8];
189
+ segs.push(new Arc(center, radius, startAngle, endAngle, normal));
190
+ idx += 9;
191
+ }
192
+ }
193
+ return new PolyCurve(segs);
194
+ }
195
+
196
+ /**
197
+ * Chamfer corners of this PolyCurve by cutting each corner with a straight segment.
198
+ * Returns a new PolyCurve of line segments only.
199
+ */
200
+ chamfer(distance: number): PolyCurve {
201
+ ensureInit();
202
+ if (this.segments.length < 2) {
203
+ throw new Error("PolyCurve.chamfer() requires at least 2 segments");
204
+ }
205
+
206
+ const pts: Point[] = [];
207
+ for (const seg of this.segments) {
208
+ if (seg instanceof Line) {
209
+ if (pts.length === 0) pts.push(seg.start);
210
+ pts.push(seg.end);
211
+ } else {
212
+ if (pts.length === 0) pts.push(seg.pointAt(0));
213
+ pts.push(seg.pointAt(1));
214
+ }
215
+ }
216
+ if (pts.length < 3) {
217
+ throw new Error("PolyCurve.chamfer() requires at least 3 vertices");
218
+ }
219
+
220
+ const coords = new Float64Array(pts.length * 3);
221
+ for (let i = 0; i < pts.length; i++) {
222
+ coords[i * 3] = pts[i].x;
223
+ coords[i * 3 + 1] = pts[i].y;
224
+ coords[i * 3 + 2] = pts[i].z;
225
+ }
226
+
227
+ const buf = wasm.chamfer_polycurve(coords, distance);
228
+ if (buf.length < 1) {
229
+ throw new Error("PolyCurve.chamfer() failed - WASM returned empty result");
230
+ }
231
+ return PolyCurve.fromSegmentData(buf);
232
+ }
233
+
234
+ /**
235
+ * Offset this PolyCurve by a distance in the given plane.
236
+ * Extracts vertex points, offsets as a polyline, returns a new PolyCurve of line segments.
237
+ */
238
+ offset(distance: number, normal?: Vec3): PolyCurve {
239
+ ensureInit();
240
+ if (this.segments.length < 1) {
241
+ throw new Error("PolyCurve.offset() requires at least 1 segment");
242
+ }
243
+
244
+ const pts: Point[] = [];
245
+ for (const seg of this.segments) {
246
+ if (seg instanceof Line) {
247
+ if (pts.length === 0) pts.push(seg.start);
248
+ pts.push(seg.end);
249
+ } else {
250
+ if (pts.length === 0) pts.push(seg.pointAt(0));
251
+ pts.push(seg.pointAt(1));
252
+ }
253
+ }
254
+ if (pts.length < 2) {
255
+ throw new Error("PolyCurve.offset() requires at least 2 vertices");
256
+ }
257
+
258
+ const coords = new Float64Array(pts.length * 3);
259
+ for (let i = 0; i < pts.length; i++) {
260
+ coords[i * 3] = pts[i].x;
261
+ coords[i * 3 + 1] = pts[i].y;
262
+ coords[i * 3 + 2] = pts[i].z;
263
+ }
264
+
265
+ const nx = normal?.x ?? 0, ny = normal?.y ?? 0, nz = normal?.z ?? 0;
266
+ const result = wasm.offset_polyline_curve(coords, distance, nx, ny, nz);
267
+ if (result.length < 6) {
268
+ throw new Error("PolyCurve.offset() failed - WASM returned insufficient data");
269
+ }
270
+
271
+ // Convert flat coords back to line segments
272
+ const segs: CurveSegment[] = [];
273
+ for (let i = 0; i < result.length - 3; i += 3) {
274
+ const s = new Point(result[i], result[i + 1], result[i + 2]);
275
+ const e = new Point(result[i + 3], result[i + 4], result[i + 5]);
276
+ segs.push(new Line(s, e));
277
+ }
278
+ return new PolyCurve(segs);
279
+ }
280
+
281
+ /**
282
+ * Convert to an exact NURBS curve representation via WASM.
283
+ * Lines become degree-1 NURBS, Arcs become degree-2 rational NURBS.
284
+ * All segments are degree-elevated to a common degree and joined.
285
+ */
286
+ toNurbs(): NurbsCurve {
287
+ ensureInit();
288
+ const segData: number[] = [this.segments.length];
289
+ for (const seg of this.segments) {
290
+ if (seg instanceof Line) {
291
+ segData.push(SegmentTypeCode.Line);
292
+ segData.push(seg.start.x, seg.start.y, seg.start.z);
293
+ segData.push(seg.end.x, seg.end.y, seg.end.z);
294
+ } else {
295
+ // Arc
296
+ segData.push(SegmentTypeCode.Arc);
297
+ segData.push(seg.center.x, seg.center.y, seg.center.z);
298
+ segData.push(seg.normal.x, seg.normal.y, seg.normal.z);
299
+ segData.push(seg.radius, seg.startAngle, seg.endAngle);
300
+ }
301
+ }
302
+ const buf = wasm.polycurve_to_nurbs(new Float64Array(segData));
303
+ if (buf.length < 2) throw new Error("Failed to convert PolyCurve to NURBS");
304
+ return NurbsCurve.fromData(buf);
305
+ }
306
+ }
package/src/Polygon.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { Point } from "./Point.js";
2
+ import { Vec3 } from "./Vec3.js";
3
+ import { Polyline } from "./Polyline.js";
4
+ import type { Plane } from "./Plane.js";
5
+ import type { RotationAxis } from "./types.js";
6
+
7
+ /**
8
+ * A closed polygon defined by vertices.
9
+ * Extends Polyline semantics with automatic closure.
10
+ * If the last point doesn't equal the first, closure is added automatically.
11
+ */
12
+ export class Polygon extends Polyline {
13
+ /**
14
+ * Create a new polygon.
15
+ * @param points - Vertices of the polygon (will be closed automatically if needed)
16
+ */
17
+ constructor(points: Point[]) {
18
+ // Ensure closure: if last point != first, append first
19
+ if (points.length >= 3 && !points[0].equals(points[points.length - 1])) {
20
+ super([...points, points[0]]);
21
+ } else {
22
+ super(points);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Number of edges (vertices minus the closing duplicate).
28
+ */
29
+ get edgeCount(): number {
30
+ return Math.max(0, this.points.length - 1);
31
+ }
32
+
33
+ /**
34
+ * Translate this polygon by an offset vector.
35
+ * @param offset - Translation vector
36
+ * @returns New translated polygon
37
+ */
38
+ override translate(offset: Vec3): Polygon {
39
+ const pl = super.translate(offset);
40
+ return new Polygon(pl.points);
41
+ }
42
+
43
+ /**
44
+ * Rotate this polygon around an axis.
45
+ * @param axis - Rotation axis (Vec3 through origin, or Line for arbitrary axis)
46
+ * @param angle - Rotation angle in radians
47
+ * @returns New rotated polygon
48
+ */
49
+ override rotate(axis: RotationAxis, angle: number): Polygon {
50
+ const pl = super.rotate(axis, angle);
51
+ return new Polygon(pl.points);
52
+ }
53
+
54
+ /**
55
+ * Offset this polygon perpendicular to its edges.
56
+ * @param distance - Offset distance
57
+ * @param normal - Reference normal for determining offset direction
58
+ * @returns New offset polygon
59
+ */
60
+ override offset(distance: number, normal?: Vec3): Polygon {
61
+ const pl = super.offset(distance, normal);
62
+ return new Polygon(pl.points);
63
+ }
64
+
65
+ /**
66
+ * Project this polygon onto a plane.
67
+ * @param plane - Target plane
68
+ * @param direction - Optional projection direction (default: perpendicular to plane)
69
+ * @returns New projected polygon
70
+ */
71
+ override projectOntoPlane(plane: Plane, direction?: Vec3): Polygon {
72
+ const pl = super.projectOntoPlane(plane, direction);
73
+ return new Polygon(pl.points);
74
+ }
75
+ }
@@ -0,0 +1,153 @@
1
+ import { ensureInit } from "./engine.js";
2
+ import { Point } from "./Point.js";
3
+ import { Vec3 } from "./Vec3.js";
4
+ import { Mesh } from "./Mesh.js";
5
+ import { Line } from "./Line.js";
6
+ import { PolyCurve } from "./PolyCurve.js";
7
+ import type { Plane } from "./Plane.js";
8
+ import type { RotationAxis } from "./types.js";
9
+ import { pointsToCoords, coordsToPoints } from "./BufferCodec.js";
10
+ import * as wasm from "../wasm/okgeometrycore_bg.js";
11
+
12
+ /**
13
+ * Open or closed polyline defined by a sequence of points.
14
+ * Supports length, evaluation, transformation, and extrusion operations.
15
+ */
16
+ export class Polyline {
17
+ public readonly points: Point[];
18
+
19
+ constructor(points: Point[]) {
20
+ this.points = points;
21
+ }
22
+
23
+ /**
24
+ * Get the total length of this polyline.
25
+ * @returns Sum of segment lengths
26
+ */
27
+ length(): number {
28
+ ensureInit();
29
+ return wasm.polyline_length(this.toCoords());
30
+ }
31
+
32
+ /**
33
+ * Evaluate a point on the polyline at normalized parameter t.
34
+ * @param t - Parameter in [0, 1] (0 = first point, 1 = last point)
35
+ * @returns Point on polyline at parameter t
36
+ */
37
+ pointAt(t: number): Point {
38
+ ensureInit();
39
+ const r = wasm.polyline_point_at(this.toCoords(), t);
40
+ return new Point(r[0], r[1], r[2]);
41
+ }
42
+
43
+ /**
44
+ * Check if this polyline is closed.
45
+ * @param eps - Tolerance for comparing first and last points
46
+ * @returns True if first and last points coincide
47
+ */
48
+ isClosed(eps = 1e-10): boolean {
49
+ if (this.points.length < 2) return false;
50
+ return this.points[0].equals(this.points[this.points.length - 1], eps);
51
+ }
52
+
53
+ /**
54
+ * Extrude this polyline along a direction vector.
55
+ * @param direction - Extrusion direction and magnitude
56
+ * @param segments - Number of segments along extrusion (default 1)
57
+ * @param caps - Whether to cap the ends (default false)
58
+ * @returns Mesh representing the extruded surface
59
+ */
60
+ extrude(direction: Vec3, segments = 1, caps = false): Mesh {
61
+ ensureInit();
62
+ const buf = wasm.extrude_polyline(
63
+ this.toCoords(),
64
+ direction.x, direction.y, direction.z,
65
+ segments, caps
66
+ );
67
+ return Mesh.fromBuffer(buf);
68
+ }
69
+
70
+ /**
71
+ * Translate this polyline by an offset vector.
72
+ * @param offset - Translation vector
73
+ * @returns New translated polyline
74
+ */
75
+ translate(offset: Vec3): Polyline {
76
+ return new Polyline(this.points.map(p => p.add(offset)));
77
+ }
78
+
79
+ /**
80
+ * Rotate this polyline around an axis.
81
+ * @param axis - Rotation axis (Vec3 through origin, or Line for arbitrary axis)
82
+ * @param angle - Rotation angle in radians
83
+ * @returns New rotated polyline
84
+ */
85
+ rotate(axis: RotationAxis, angle: number): Polyline {
86
+ return new Polyline(this.points.map(p => p.rotate(axis, angle)));
87
+ }
88
+
89
+ /**
90
+ * Offset this polyline perpendicular to its segments.
91
+ * @param distance - Offset distance
92
+ * @param normal - Reference normal for determining offset direction
93
+ * @returns New offset polyline
94
+ */
95
+ offset(distance: number, normal?: Vec3): Polyline {
96
+ ensureInit();
97
+ const nx = normal?.x ?? 0, ny = normal?.y ?? 0, nz = normal?.z ?? 0;
98
+ const result = wasm.offset_polyline_curve(this.toCoords(), distance, nx, ny, nz);
99
+ return new Polyline(coordsToPoints(result));
100
+ }
101
+
102
+ /**
103
+ * Project this polyline onto a plane.
104
+ * @param plane - Target plane
105
+ * @param direction - Optional projection direction (default: perpendicular to plane)
106
+ * @returns New projected polyline
107
+ */
108
+ projectOntoPlane(plane: Plane, direction?: Vec3): Polyline {
109
+ const proj = (p: Point) => direction ? plane.projectPointAlongDirection(p, direction) : plane.projectPoint(p);
110
+ return new Polyline(this.points.map(proj));
111
+ }
112
+
113
+ /**
114
+ * Fillet corners of this polyline with arcs of the given radius.
115
+ * @param radius - Fillet radius
116
+ * @param normal - Reference normal for arc orientation (auto-detected if not provided)
117
+ * @returns PolyCurve with Line and Arc segments
118
+ */
119
+ fillet(radius: number, normal?: Vec3): PolyCurve {
120
+ ensureInit();
121
+ if (this.points.length < 3) {
122
+ throw new Error("Polyline.fillet() requires at least 3 points");
123
+ }
124
+
125
+ const nx = normal?.x ?? 0, ny = normal?.y ?? 0, nz = normal?.z ?? 0;
126
+ const buf = wasm.fillet_polycurve(this.toCoords(), radius, nx, ny, nz);
127
+ if (buf.length < 1) {
128
+ throw new Error("Polyline.fillet() failed - WASM returned empty result");
129
+ }
130
+
131
+ return PolyCurve.fromSegmentData(buf);
132
+ }
133
+
134
+ /**
135
+ * Convert this polyline to a PolyCurve (line segments only).
136
+ * @returns PolyCurve with Line segments connecting consecutive points
137
+ */
138
+ toPolyCurve(): PolyCurve {
139
+ const segs: Line[] = [];
140
+ for (let i = 0; i < this.points.length - 1; i++) {
141
+ segs.push(new Line(this.points[i], this.points[i + 1]));
142
+ }
143
+ return new PolyCurve(segs);
144
+ }
145
+
146
+ /**
147
+ * Convert points to flat coordinate array for WASM.
148
+ * @returns Float64Array [x1,y1,z1, x2,y2,z2, ...]
149
+ */
150
+ private toCoords(): Float64Array {
151
+ return pointsToCoords(this.points);
152
+ }
153
+ }