@expofp/geometry 3.8.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.
@@ -0,0 +1,162 @@
1
+ import { Box } from './box.js';
2
+ import { Point } from './point.js';
3
+ /**
4
+ * A 3D triangle mesh: vertices carry an optional z coordinate, plus triangle index triplets.
5
+ * Immutable value object: readonly getters; transforms return a new instance unless a `target`
6
+ * is passed. For a planar triangulated polygon with area/containment use `Polygon` instead.
7
+ */
8
+ export class Mesh {
9
+ /** Marker so callers can narrow the type at runtime. */
10
+ isMesh = true;
11
+ /** Internal mutable vertex array (3D). */
12
+ _vertices = [];
13
+ /** Internal triangle index array. */
14
+ _indices = [];
15
+ /** Cached axis-aligned bounding box (xy only). */
16
+ _bounds = new Box();
17
+ /**
18
+ * Constructs a mesh from a vertex list and triangle index triplets.
19
+ * @param vertices - source vertices; each is cloned into a {@link Point} (default `[]`)
20
+ * @param indices - triangle index triplets referencing positions in `vertices` (default `[]`)
21
+ */
22
+ constructor(vertices = [], indices = []) {
23
+ this.set(vertices, indices);
24
+ }
25
+ /**
26
+ * Readonly array of mesh vertices as {@link Point} instances.
27
+ * @returns the vertex array
28
+ */
29
+ get vertices() {
30
+ return this._vertices;
31
+ }
32
+ /**
33
+ * Readonly array of triangle index triplets.
34
+ * @returns the index array
35
+ */
36
+ get indices() {
37
+ return this._indices;
38
+ }
39
+ /**
40
+ * Cached axis-aligned bounding {@link Box} of this mesh (xy only, z ignored).
41
+ * @returns the bounding box
42
+ */
43
+ get bounds() {
44
+ return this._bounds;
45
+ }
46
+ /**
47
+ * Mutates this mesh in place, replacing vertices/indices and refreshing the cached bounds.
48
+ * Vertices are copied into fresh {@link Point}s — do not optimize this clone away; transform
49
+ * free functions rely on it to avoid aliasing shared vertices. Use only for hot-path reuse.
50
+ * @param vertices - new vertex list
51
+ * @param indices - new triangle index list
52
+ * @returns `this`
53
+ */
54
+ set(vertices, indices) {
55
+ this._vertices = vertices.map((v) => new Point(v.x, v.y, v.z ?? 0));
56
+ this._indices = indices;
57
+ this._bounds = meshBounds(this);
58
+ return this;
59
+ }
60
+ /**
61
+ * Merges two or more meshes into a single {@link Mesh}, offsetting each mesh's triangle indices
62
+ * by the cumulative vertex count so they remain valid. Forwards to {@link meshMerge}.
63
+ * @param meshes - meshes to merge
64
+ * @returns a new merged mesh
65
+ */
66
+ static merge(...meshes) {
67
+ return meshMerge(meshes);
68
+ }
69
+ /**
70
+ * Returns a new mesh translated by `offset`. Forwards to {@link meshTranslate}.
71
+ * @param offset - translation vector
72
+ * @param target - optional mesh to write into instead of allocating a new one
73
+ * @returns translated mesh
74
+ */
75
+ translate(offset, target) {
76
+ return meshTranslate(this, offset, target ?? new Mesh());
77
+ }
78
+ /**
79
+ * Returns a new mesh scaled by `factor` about `origin`. Forwards to {@link meshScale}.
80
+ * @param factor - uniform scale or per-axis `{ x, y }` scale
81
+ * @param origin - the fixed point of the scaling; defaults to the bounding-box center
82
+ * @param target - optional mesh to write into instead of allocating a new one
83
+ * @returns scaled mesh
84
+ */
85
+ scale(factor, origin = this._bounds.center, target) {
86
+ return meshScale(this, factor, origin, target ?? new Mesh());
87
+ }
88
+ /**
89
+ * Returns a new mesh rotated by `angle` radians about `origin`. Forwards to {@link meshRotate}.
90
+ * @param angle - rotation angle in radians (positive = clockwise in y-down space)
91
+ * @param origin - the pivot of the rotation; defaults to the bounding-box center
92
+ * @param target - optional mesh to write into instead of allocating a new one
93
+ * @returns rotated mesh
94
+ */
95
+ rotate(angle, origin = this._bounds.center, target) {
96
+ return meshRotate(this, angle, origin, target ?? new Mesh());
97
+ }
98
+ }
99
+ /**
100
+ * Computes the axis-aligned bounding {@link Box} of a mesh by sweeping all vertex xy coordinates.
101
+ * The z component is ignored — bounds are always in the xy plane.
102
+ * @param m - mesh or mesh-like object
103
+ * @returns the bounding box
104
+ */
105
+ export function meshBounds(m) {
106
+ if (m.vertices.length === 0)
107
+ return new Box();
108
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
109
+ for (const v of m.vertices) {
110
+ minX = Math.min(minX, v.x);
111
+ minY = Math.min(minY, v.y);
112
+ maxX = Math.max(maxX, v.x);
113
+ maxY = Math.max(maxY, v.y);
114
+ }
115
+ return Box.fromMinMax({ x: minX, y: minY }, { x: maxX, y: maxY });
116
+ }
117
+ export function meshMerge(meshes, target) {
118
+ const vertices = [];
119
+ const indices = [];
120
+ let offset = 0;
121
+ for (const m of meshes) {
122
+ for (const v of m.vertices)
123
+ vertices.push(v);
124
+ for (const [a, b, c] of m.indices)
125
+ indices.push([a + offset, b + offset, c + offset]);
126
+ offset += m.vertices.length;
127
+ }
128
+ return (target ?? new Mesh()).set(vertices, indices);
129
+ }
130
+ export function meshTranslate(m, offset, target) {
131
+ const dz = offset.z ?? 0;
132
+ const vertices = m.vertices.map((v) => ({
133
+ x: v.x + offset.x,
134
+ y: v.y + offset.y,
135
+ z: (v.z ?? 0) + dz,
136
+ }));
137
+ return (target ?? new Mesh()).set(vertices, m.indices);
138
+ }
139
+ export function meshScale(m, factor, origin = meshBounds(m).center, target) {
140
+ const fx = typeof factor === 'number' ? factor : factor.x;
141
+ const fy = typeof factor === 'number' ? factor : factor.y;
142
+ const vertices = m.vertices.map((v) => ({
143
+ x: origin.x + (v.x - origin.x) * fx,
144
+ y: origin.y + (v.y - origin.y) * fy,
145
+ z: v.z ?? 0,
146
+ }));
147
+ return (target ?? new Mesh()).set(vertices, m.indices);
148
+ }
149
+ export function meshRotate(m, angle, origin = meshBounds(m).center, target) {
150
+ const cos = Math.cos(angle);
151
+ const sin = Math.sin(angle);
152
+ const vertices = m.vertices.map((v) => {
153
+ const dx = v.x - origin.x;
154
+ const dy = v.y - origin.y;
155
+ return {
156
+ x: origin.x + dx * cos - dy * sin,
157
+ y: origin.y + dx * sin + dy * cos,
158
+ z: v.z ?? 0,
159
+ };
160
+ });
161
+ return (target ?? new Mesh()).set(vertices, m.indices);
162
+ }
@@ -0,0 +1,206 @@
1
+ /** A 2D point/vector `{ x, y }`. */
2
+ export interface Point2Like {
3
+ /** Horizontal coordinate. */
4
+ readonly x: number;
5
+ /** Vertical coordinate. */
6
+ readonly y: number;
7
+ }
8
+ /** A 3D point/vector `{ x, y, z }`. */
9
+ export interface Point3Like {
10
+ /** Horizontal coordinate. */
11
+ readonly x: number;
12
+ /** Vertical coordinate. */
13
+ readonly y: number;
14
+ /** Depth coordinate. */
15
+ readonly z: number;
16
+ }
17
+ /** Any structural point — `z` optional. */
18
+ export interface PointLike {
19
+ /** Horizontal coordinate. */
20
+ readonly x: number;
21
+ /** Vertical coordinate. */
22
+ readonly y: number;
23
+ /** Depth coordinate (optional). */
24
+ readonly z?: number;
25
+ }
26
+ /**
27
+ * A readonly 2D vector that also exposes `width`/`height` aliases of `x`/`y` — the return type of the
28
+ * `size` getter on {@link Box} and {@link Rect}. Backed by a {@link Point} (which provides the
29
+ * aliases), so `size.x`/`size.width` and `size.y`/`size.height` both read the same value.
30
+ */
31
+ export type SizeLike = Readonly<Point2Like> & {
32
+ readonly width: number;
33
+ readonly height: number;
34
+ };
35
+ /**
36
+ * Mutable point/vector — the low-level building block other primitives and the point free functions
37
+ * write into. `width`/`height` alias `x`/`y` so a point used as a size reads naturally.
38
+ */
39
+ export declare class Point {
40
+ /** Marker so callers can narrow the type at runtime. */
41
+ readonly isPoint = true;
42
+ /** Horizontal coordinate. */
43
+ x: number;
44
+ /** Vertical coordinate. */
45
+ y: number;
46
+ /** Depth coordinate. */
47
+ z: number;
48
+ /**
49
+ * @param x - horizontal coordinate (default 0)
50
+ * @param y - vertical coordinate (default 0)
51
+ * @param z - depth coordinate (default 0)
52
+ */
53
+ constructor(x?: number, y?: number, z?: number);
54
+ /**
55
+ * Alias of {@link Point.x}.
56
+ * @returns the x coordinate
57
+ */
58
+ get width(): number;
59
+ /**
60
+ * Alias of {@link Point.y}.
61
+ * @returns the y coordinate
62
+ */
63
+ get height(): number;
64
+ /**
65
+ * Sets coordinates in place.
66
+ * @param x - horizontal coordinate
67
+ * @param y - vertical coordinate
68
+ * @param z - depth coordinate (defaults to the current z)
69
+ * @returns this
70
+ */
71
+ set(x: number, y: number, z?: number): this;
72
+ /**
73
+ * Copies from another point in place.
74
+ * @param p - source point to copy coordinates from
75
+ * @returns this
76
+ */
77
+ copy(p: PointLike): this;
78
+ /**
79
+ * Returns an independent copy, or writes into `target` if provided. See {@link pointLerp} for the
80
+ * underlying free function.
81
+ * @param target - optional point to write into; defaults to a new {@link Point}
82
+ * @returns the copy
83
+ */
84
+ clone(target?: Point): Point;
85
+ /**
86
+ * True when `p` has the same coordinates.
87
+ * @param p - point to compare against
88
+ * @returns true when all coordinates are equal
89
+ */
90
+ equals(p: PointLike): boolean;
91
+ /**
92
+ * Distance to `p`. See {@link pointDistance} for the underlying free function.
93
+ * @param p - target point
94
+ * @returns the distance
95
+ */
96
+ distanceTo(p: PointLike): number;
97
+ /**
98
+ * Interpolate toward `p` by `t`. See {@link pointLerp} for the underlying free function.
99
+ * @param p - target point
100
+ * @param t - interpolation factor (0 = this, 1 = p)
101
+ * @param target - optional point to write into; defaults to a new {@link Point}
102
+ * @returns the resulting point
103
+ */
104
+ lerp(p: PointLike, t: number, target?: Point): Point;
105
+ /**
106
+ * Rotate by `angle` radians around `origin`. See {@link pointRotate} for the underlying free
107
+ * function.
108
+ * @param angle - radians to rotate (positive = clockwise, y-down)
109
+ * @param origin - rotation pivot point
110
+ * @param target - optional point to write into; defaults to a new {@link Point}
111
+ * @returns the resulting point
112
+ */
113
+ rotate(angle: number, origin: Point2Like, target?: Point): Point;
114
+ /**
115
+ * Shift by `length` along `angle` radians. See {@link pointShift} for the underlying free function.
116
+ * @param length - distance to shift
117
+ * @param angle - direction in radians
118
+ * @param target - optional point to write into; defaults to a new {@link Point}
119
+ * @returns the resulting point
120
+ */
121
+ shift(length: number, angle: number, target?: Point): Point;
122
+ /**
123
+ * Angle (radians) at this point between rays to `a` and `b`. See {@link pointAngleBetween} for
124
+ * the underlying free function.
125
+ * @param a - first ray endpoint
126
+ * @param b - second ray endpoint
127
+ * @returns the signed angle in radians (positive = clockwise, y-down)
128
+ */
129
+ angleBetween(a: Point2Like, b: Point2Like): number;
130
+ /**
131
+ * Add a scalar (broadcast to x/y) or another point. See {@link pointAdd} for the underlying free
132
+ * function.
133
+ * @param b - scalar or point to add
134
+ * @param target - optional point to write into; defaults to a new {@link Point}
135
+ * @returns the resulting point
136
+ */
137
+ add(b: number | PointLike, target?: Point): Point;
138
+ /**
139
+ * Multiply by a scalar (x/y) or componentwise by another point. See {@link pointMultiply} for
140
+ * the underlying free function.
141
+ * @param b - scalar or point factor
142
+ * @param target - optional point to write into; defaults to a new {@link Point}
143
+ * @returns the resulting point
144
+ */
145
+ multiply(b: number | PointLike, target?: Point): Point;
146
+ }
147
+ /**
148
+ * Euclidean distance between two points (XY plane).
149
+ * @param a first point
150
+ * @param b second point
151
+ * @returns the distance
152
+ */
153
+ export declare function pointDistance(a: PointLike, b: PointLike): number;
154
+ /**
155
+ * Linear interpolation from `a` to `b` by `t`.
156
+ * @param a start point
157
+ * @param b end point
158
+ * @param t factor (0 = a, 1 = b)
159
+ * @param target optional point to write into; defaults to a new {@link Point}
160
+ * @returns the interpolated point
161
+ */
162
+ export declare function pointLerp(a: PointLike, b: PointLike, t: number, target?: Point): Point;
163
+ /**
164
+ * Adds a scalar (broadcast to x/y) or another point (componentwise) to `a`.
165
+ * @param a base point
166
+ * @param b scalar or point to add
167
+ * @param target optional point to write into; defaults to a new {@link Point}
168
+ * @returns the sum
169
+ */
170
+ export declare function pointAdd(a: PointLike, b: number | PointLike, target?: Point): Point;
171
+ /**
172
+ * Multiplies `a` by a scalar (x/y) or by another point (componentwise).
173
+ * @param a base point
174
+ * @param b scalar or point factor
175
+ * @param target optional point to write into; defaults to a new {@link Point}
176
+ * @returns the product
177
+ */
178
+ export declare function pointMultiply(a: PointLike, b: number | PointLike, target?: Point): Point;
179
+ /**
180
+ * Rotates `point` by `angle` radians around `origin` in the XY plane.
181
+ * @param point point to rotate
182
+ * @param angle radians (positive = clockwise, y-down)
183
+ * @param origin rotation pivot point
184
+ * @param target optional point to write into; defaults to a new {@link Point}
185
+ * @returns the rotated point, preserving the input depth (`z`)
186
+ */
187
+ export declare function pointRotate(point: PointLike, angle: number, origin: Point2Like, target?: Point): Point;
188
+ /**
189
+ * Shifts `point` by `length` along `angle` radians.
190
+ * @param point origin
191
+ * @param length distance
192
+ * @param angle direction in radians
193
+ * @param target optional point to write into; defaults to a new {@link Point}
194
+ * @returns the shifted point
195
+ */
196
+ export declare function pointShift(point: PointLike, length: number, angle: number, target?: Point): Point;
197
+ /**
198
+ * Angle (radians) at `center` between the rays to `a` and `b`. The result is signed — positive means
199
+ * clockwise (screen y-down convention), matching the winding used by {@link pointRotate}.
200
+ * @param center vertex
201
+ * @param a first ray endpoint
202
+ * @param b second ray endpoint
203
+ * @returns signed angle in radians (positive = clockwise, y-down)
204
+ */
205
+ export declare function pointAngleBetween(center: Point2Like, a: Point2Like, b: Point2Like): number;
206
+ //# sourceMappingURL=point.d.ts.map
@@ -0,0 +1,237 @@
1
+ import { fromPolar } from './angles.js';
2
+ /**
3
+ * Mutable point/vector — the low-level building block other primitives and the point free functions
4
+ * write into. `width`/`height` alias `x`/`y` so a point used as a size reads naturally.
5
+ */
6
+ export class Point {
7
+ /** Marker so callers can narrow the type at runtime. */
8
+ isPoint = true;
9
+ /** Horizontal coordinate. */
10
+ x;
11
+ /** Vertical coordinate. */
12
+ y;
13
+ /** Depth coordinate. */
14
+ z;
15
+ /**
16
+ * @param x - horizontal coordinate (default 0)
17
+ * @param y - vertical coordinate (default 0)
18
+ * @param z - depth coordinate (default 0)
19
+ */
20
+ constructor(x = 0, y = 0, z = 0) {
21
+ this.x = x;
22
+ this.y = y;
23
+ this.z = z;
24
+ }
25
+ /**
26
+ * Alias of {@link Point.x}.
27
+ * @returns the x coordinate
28
+ */
29
+ get width() {
30
+ return this.x;
31
+ }
32
+ /**
33
+ * Alias of {@link Point.y}.
34
+ * @returns the y coordinate
35
+ */
36
+ get height() {
37
+ return this.y;
38
+ }
39
+ /**
40
+ * Sets coordinates in place.
41
+ * @param x - horizontal coordinate
42
+ * @param y - vertical coordinate
43
+ * @param z - depth coordinate (defaults to the current z)
44
+ * @returns this
45
+ */
46
+ set(x, y, z = this.z) {
47
+ this.x = x;
48
+ this.y = y;
49
+ this.z = z;
50
+ return this;
51
+ }
52
+ /**
53
+ * Copies from another point in place.
54
+ * @param p - source point to copy coordinates from
55
+ * @returns this
56
+ */
57
+ copy(p) {
58
+ return this.set(p.x, p.y, p.z ?? 0);
59
+ }
60
+ /**
61
+ * Returns an independent copy, or writes into `target` if provided. See {@link pointLerp} for the
62
+ * underlying free function.
63
+ * @param target - optional point to write into; defaults to a new {@link Point}
64
+ * @returns the copy
65
+ */
66
+ clone(target) {
67
+ return (target ?? new Point()).set(this.x, this.y, this.z);
68
+ }
69
+ /**
70
+ * True when `p` has the same coordinates.
71
+ * @param p - point to compare against
72
+ * @returns true when all coordinates are equal
73
+ */
74
+ equals(p) {
75
+ return this.x === p.x && this.y === p.y && this.z === (p.z ?? 0);
76
+ }
77
+ /**
78
+ * Distance to `p`. See {@link pointDistance} for the underlying free function.
79
+ * @param p - target point
80
+ * @returns the distance
81
+ */
82
+ distanceTo(p) {
83
+ return pointDistance(this, p);
84
+ }
85
+ /**
86
+ * Interpolate toward `p` by `t`. See {@link pointLerp} for the underlying free function.
87
+ * @param p - target point
88
+ * @param t - interpolation factor (0 = this, 1 = p)
89
+ * @param target - optional point to write into; defaults to a new {@link Point}
90
+ * @returns the resulting point
91
+ */
92
+ lerp(p, t, target) {
93
+ return pointLerp(this, p, t, target);
94
+ }
95
+ /**
96
+ * Rotate by `angle` radians around `origin`. See {@link pointRotate} for the underlying free
97
+ * function.
98
+ * @param angle - radians to rotate (positive = clockwise, y-down)
99
+ * @param origin - rotation pivot point
100
+ * @param target - optional point to write into; defaults to a new {@link Point}
101
+ * @returns the resulting point
102
+ */
103
+ rotate(angle, origin, target) {
104
+ return pointRotate(this, angle, origin, target);
105
+ }
106
+ /**
107
+ * Shift by `length` along `angle` radians. See {@link pointShift} for the underlying free function.
108
+ * @param length - distance to shift
109
+ * @param angle - direction in radians
110
+ * @param target - optional point to write into; defaults to a new {@link Point}
111
+ * @returns the resulting point
112
+ */
113
+ shift(length, angle, target) {
114
+ return pointShift(this, length, angle, target);
115
+ }
116
+ /**
117
+ * Angle (radians) at this point between rays to `a` and `b`. See {@link pointAngleBetween} for
118
+ * the underlying free function.
119
+ * @param a - first ray endpoint
120
+ * @param b - second ray endpoint
121
+ * @returns the signed angle in radians (positive = clockwise, y-down)
122
+ */
123
+ angleBetween(a, b) {
124
+ return pointAngleBetween(this, a, b);
125
+ }
126
+ /**
127
+ * Add a scalar (broadcast to x/y) or another point. See {@link pointAdd} for the underlying free
128
+ * function.
129
+ * @param b - scalar or point to add
130
+ * @param target - optional point to write into; defaults to a new {@link Point}
131
+ * @returns the resulting point
132
+ */
133
+ add(b, target) {
134
+ return pointAdd(this, b, target);
135
+ }
136
+ /**
137
+ * Multiply by a scalar (x/y) or componentwise by another point. See {@link pointMultiply} for
138
+ * the underlying free function.
139
+ * @param b - scalar or point factor
140
+ * @param target - optional point to write into; defaults to a new {@link Point}
141
+ * @returns the resulting point
142
+ */
143
+ multiply(b, target) {
144
+ return pointMultiply(this, b, target);
145
+ }
146
+ }
147
+ /**
148
+ * Euclidean distance between two points (XY plane).
149
+ * @param a first point
150
+ * @param b second point
151
+ * @returns the distance
152
+ */
153
+ export function pointDistance(a, b) {
154
+ const dx = a.x - b.x;
155
+ const dy = a.y - b.y;
156
+ return Math.sqrt(dx * dx + dy * dy);
157
+ }
158
+ /**
159
+ * Linear interpolation from `a` to `b` by `t`.
160
+ * @param a start point
161
+ * @param b end point
162
+ * @param t factor (0 = a, 1 = b)
163
+ * @param target optional point to write into; defaults to a new {@link Point}
164
+ * @returns the interpolated point
165
+ */
166
+ export function pointLerp(a, b, t, target) {
167
+ const az = a.z ?? 0;
168
+ const bz = b.z ?? 0;
169
+ return (target ?? new Point()).set(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, az + (bz - az) * t);
170
+ }
171
+ /**
172
+ * Adds a scalar (broadcast to x/y) or another point (componentwise) to `a`.
173
+ * @param a base point
174
+ * @param b scalar or point to add
175
+ * @param target optional point to write into; defaults to a new {@link Point}
176
+ * @returns the sum
177
+ */
178
+ export function pointAdd(a, b, target) {
179
+ const isNum = typeof b === 'number';
180
+ const bx = isNum ? b : b.x;
181
+ const by = isNum ? b : b.y;
182
+ const bz = isNum ? 0 : (b.z ?? 0);
183
+ return (target ?? new Point()).set(a.x + bx, a.y + by, (a.z ?? 0) + bz);
184
+ }
185
+ /**
186
+ * Multiplies `a` by a scalar (x/y) or by another point (componentwise).
187
+ * @param a base point
188
+ * @param b scalar or point factor
189
+ * @param target optional point to write into; defaults to a new {@link Point}
190
+ * @returns the product
191
+ */
192
+ export function pointMultiply(a, b, target) {
193
+ const isNum = typeof b === 'number';
194
+ const bx = isNum ? b : b.x;
195
+ const by = isNum ? b : b.y;
196
+ const bz = isNum ? 1 : (b.z ?? 1);
197
+ return (target ?? new Point()).set(a.x * bx, a.y * by, (a.z ?? 0) * bz);
198
+ }
199
+ /**
200
+ * Rotates `point` by `angle` radians around `origin` in the XY plane.
201
+ * @param point point to rotate
202
+ * @param angle radians (positive = clockwise, y-down)
203
+ * @param origin rotation pivot point
204
+ * @param target optional point to write into; defaults to a new {@link Point}
205
+ * @returns the rotated point, preserving the input depth (`z`)
206
+ */
207
+ export function pointRotate(point, angle, origin, target) {
208
+ const cos = Math.cos(angle);
209
+ const sin = Math.sin(angle);
210
+ const dx = point.x - origin.x;
211
+ const dy = point.y - origin.y;
212
+ return (target ?? new Point()).set(origin.x + dx * cos - dy * sin, origin.y + dx * sin + dy * cos, point.z ?? 0);
213
+ }
214
+ /**
215
+ * Shifts `point` by `length` along `angle` radians.
216
+ * @param point origin
217
+ * @param length distance
218
+ * @param angle direction in radians
219
+ * @param target optional point to write into; defaults to a new {@link Point}
220
+ * @returns the shifted point
221
+ */
222
+ export function pointShift(point, length, angle, target) {
223
+ return pointAdd(point, fromPolar(length, angle), target);
224
+ }
225
+ /**
226
+ * Angle (radians) at `center` between the rays to `a` and `b`. The result is signed — positive means
227
+ * clockwise (screen y-down convention), matching the winding used by {@link pointRotate}.
228
+ * @param center vertex
229
+ * @param a first ray endpoint
230
+ * @param b second ray endpoint
231
+ * @returns signed angle in radians (positive = clockwise, y-down)
232
+ */
233
+ export function pointAngleBetween(center, a, b) {
234
+ const aa = Math.atan2(a.y - center.y, a.x - center.x);
235
+ const bb = Math.atan2(b.y - center.y, b.x - center.x);
236
+ return bb - aa;
237
+ }