Skip to content

Commit ce4aa46

Browse files
committed
✨ feat: enhance XYZ class
1 parent a520df8 commit ce4aa46

File tree

2 files changed

+278
-60
lines changed

2 files changed

+278
-60
lines changed

packages/chili-core/src/math/xyz.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ export type XYZLike = { x: number; y: number; z: number };
99

1010
@Serializer.register(["x", "y", "z"])
1111
export class XYZ {
12-
static readonly zero = new XYZ(0, 0, 0);
13-
static readonly unitX = new XYZ(1, 0, 0);
14-
static readonly unitY = new XYZ(0, 1, 0);
15-
static readonly unitZ = new XYZ(0, 0, 1);
16-
static readonly one = new XYZ(1, 1, 1);
12+
static readonly zero = Object.freeze(new XYZ(0, 0, 0));
13+
static readonly unitX = Object.freeze(new XYZ(1, 0, 0));
14+
static readonly unitY = Object.freeze(new XYZ(0, 1, 0));
15+
static readonly unitZ = Object.freeze(new XYZ(0, 0, 1));
16+
static readonly unitNX = Object.freeze(new XYZ(-1, 0, 0));
17+
static readonly unitNY = Object.freeze(new XYZ(0, -1, 0));
18+
static readonly unitNZ = Object.freeze(new XYZ(0, 0, -1));
19+
static readonly one = Object.freeze(new XYZ(1, 1, 1));
1720

1821
@Serializer.serialze()
1922
readonly x: number;
@@ -36,6 +39,15 @@ export class XYZ {
3639
return [this.x, this.y, this.z];
3740
}
3841

42+
static fromArray(arr: number[]) {
43+
if (!arr) return XYZ.zero;
44+
45+
const x = arr.at(0) ?? 0;
46+
const y = arr.at(1) ?? 0;
47+
const z = arr.at(2) ?? 0;
48+
return new XYZ(x, y, z);
49+
}
50+
3951
cross(right: XYZLike): XYZ {
4052
return new XYZ(
4153
this.y * right.z - this.z * right.y,
@@ -54,7 +66,11 @@ export class XYZ {
5466
}
5567

5668
reverse(): XYZ {
57-
return new XYZ(-this.x, -this.y, -this.z);
69+
const x = MathUtils.almostEqual(this.x, 0) ? 0 : -this.x;
70+
const y = MathUtils.almostEqual(this.y, 0) ? 0 : -this.y;
71+
const z = MathUtils.almostEqual(this.z, 0) ? 0 : -this.z;
72+
73+
return new XYZ(x, y, z);
5874
}
5975

6076
multiply(scalar: number): XYZ {
@@ -142,6 +158,13 @@ export class XYZ {
142158
);
143159
}
144160

161+
isPerpendicularTo(right: XYZLike, tolerance: number = 1e-6): boolean {
162+
const angle = this.angleTo(right);
163+
if (angle === undefined) return false;
164+
165+
return Math.abs(angle - Math.PI * 0.5) < tolerance;
166+
}
167+
145168
isParallelTo(right: XYZLike, tolerance: number = 1e-6): boolean {
146169
const angle = this.angleTo(right);
147170
if (angle === undefined) return false;
Lines changed: 249 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,260 @@
11
// Part of the Chili3d Project, under the AGPL-3.0 License.
22
// See LICENSE file in the project root for full license information.
33

4-
import { XY, XYZ } from "../src";
5-
6-
describe("test xyz", () => {
7-
test("test xyz", () => {
8-
const a = new XYZ(10, 0, 0);
9-
const b = new XYZ(0, 10, 0);
10-
expect(a.add(b)).toStrictEqual(new XYZ(10, 10, 0));
11-
12-
expect(XYZ.center(a, b)).toStrictEqual(new XYZ(5, 5, 0));
13-
expect(a.cross(b)).toStrictEqual(new XYZ(0, 0, 100));
14-
expect(a.distanceTo(b)).toBe(Math.sqrt(200));
15-
expect(a.divided(0.5)).toStrictEqual(new XYZ(20, 0, 0));
16-
expect(a.dot(b)).toBe(0);
17-
expect(a.multiply(2)).toStrictEqual(new XYZ(20, 0, 0));
18-
expect(a.normalize()).toStrictEqual(new XYZ(1, 0, 0));
19-
expect(a.sub(b)).toStrictEqual(new XYZ(10, -10, 0));
4+
import { Precision, XY, XYZ } from "../src";
5+
6+
describe("XYZ class tests", () => {
7+
describe("Basic properties and constructor", () => {
8+
test("should create XYZ instance with given values", () => {
9+
const point = new XYZ(1, 2, 3);
10+
expect(point.x).toBe(1);
11+
expect(point.y).toBe(2);
12+
expect(point.z).toBe(3);
13+
14+
expect(XYZ.fromArray(undefined as any)).toEqual(XYZ.zero);
15+
expect(XYZ.fromArray([1, 2, 3])).toEqual(point);
16+
expect(XYZ.fromArray([1, 2])).toEqual(new XYZ(1, 2, 0));
17+
expect(XYZ.fromArray([1, 2, 3, 4])).toEqual(new XYZ(1, 2, 3));
18+
});
19+
20+
test("should have correct static properties", () => {
21+
expect(XYZ.zero).toEqual(new XYZ(0, 0, 0));
22+
expect(XYZ.unitX).toEqual(new XYZ(1, 0, 0));
23+
expect(XYZ.unitY).toEqual(new XYZ(0, 1, 0));
24+
expect(XYZ.unitZ).toEqual(new XYZ(0, 0, 1));
25+
expect(XYZ.unitNX).toEqual(new XYZ(-1, 0, 0));
26+
expect(XYZ.unitNY).toEqual(new XYZ(0, -1, 0));
27+
expect(XYZ.unitNZ).toEqual(new XYZ(0, 0, -1));
28+
expect(XYZ.one).toEqual(new XYZ(1, 1, 1));
29+
});
30+
31+
test("should convert to string and array correctly", () => {
32+
const point = new XYZ(1, 2, 3);
33+
expect(point.toString()).toBe("1, 2, 3");
34+
expect(point.toArray()).toEqual([1, 2, 3]);
35+
});
2036
});
2137

22-
test("test angle", () => {
23-
const a = new XYZ(10, 0, 0);
24-
const b = new XYZ(0, 10, 0);
25-
expect(XYZ.zero.angleTo(new XYZ(0, 0, 0))).toBe(undefined);
26-
expect(a.angleTo(b)).toBe(Math.PI / 2);
27-
expect(a.angleTo(new XYZ(10, 0, 0))).toBe(0);
28-
expect(a.angleTo(new XYZ(10, 10, 0))).toBe(Math.PI / 4);
29-
expect(a.angleTo(new XYZ(0, 10, 0))).toBe(Math.PI / 2);
30-
expect(a.angleTo(new XYZ(-10, 10, 0))).toBe((Math.PI * 3) / 4);
31-
expect(a.angleTo(new XYZ(-10, 0, 0))).toBe(Math.PI);
32-
expect(a.angleTo(new XYZ(-10, -10, 0))).toBe((Math.PI * 3) / 4);
33-
expect(a.angleTo(new XYZ(10, -10, 0))).toBe(Math.PI / 4);
34-
35-
expect(XYZ.zero.angleOnPlaneTo(XYZ.zero, XYZ.unitZ)).toBe(undefined);
36-
expect(a.angleOnPlaneTo(b, XYZ.unitZ)).toBe(Math.PI / 2);
37-
expect(a.angleOnPlaneTo(new XYZ(10, 0, 0), XYZ.unitZ)).toBe(0);
38-
expect(a.angleOnPlaneTo(new XYZ(10, 10, 0), XYZ.unitZ)).toBe(Math.PI / 4);
39-
expect(a.angleOnPlaneTo(new XYZ(0, 10, 0), XYZ.unitZ)).toBe(Math.PI / 2);
40-
expect(a.angleOnPlaneTo(new XYZ(-10, 10, 0), XYZ.unitZ)).toBe((Math.PI * 3) / 4);
41-
expect(a.angleOnPlaneTo(new XYZ(-10, 0, 0), XYZ.unitZ)).toBe(Math.PI);
42-
expect(a.angleOnPlaneTo(new XYZ(-10, -10, 0), XYZ.unitZ)).toBe((Math.PI * 5) / 4);
43-
expect(a.angleOnPlaneTo(new XYZ(10, -10, 0), XYZ.unitZ)).toBe((Math.PI * 7) / 4);
44-
45-
expect(new XYZ(10, 10, 0).angleOnPlaneTo(a, XYZ.unitZ)).toBe((Math.PI * 7) / 4);
46-
expect(a.angleOnPlaneTo(new XYZ(10, 10, 0), new XYZ(0, 0, -1))).toBe((Math.PI * 7) / 4);
38+
describe("Arithmetic operations", () => {
39+
test("should add vectors correctly", () => {
40+
const a = new XYZ(1, 2, 3);
41+
const b = new XYZ(4, 5, 6);
42+
const result = a.add(b);
43+
expect(result).toEqual(new XYZ(5, 7, 9));
44+
});
45+
46+
test("should subtract vectors correctly", () => {
47+
const a = new XYZ(5, 7, 9);
48+
const b = new XYZ(1, 2, 3);
49+
const result = a.sub(b);
50+
expect(result).toEqual(new XYZ(4, 5, 6));
51+
});
52+
53+
test("should multiply by scalar correctly", () => {
54+
const point = new XYZ(1, 2, 3);
55+
const result = point.multiply(3);
56+
expect(result).toEqual(new XYZ(3, 6, 9));
57+
});
58+
59+
test("should divide by scalar correctly", () => {
60+
const point = new XYZ(10, 20, 30);
61+
const result = point.divided(2);
62+
expect(result).toEqual(new XYZ(5, 10, 15));
63+
});
64+
65+
test("should return undefined when dividing by zero", () => {
66+
const point = new XYZ(10, 20, 30);
67+
const result = point.divided(0);
68+
expect(result).toBeUndefined();
69+
70+
const result2 = point.divided(Precision.Float / 2); // very small number
71+
expect(result2).toBeUndefined();
72+
});
73+
74+
test("should reverse the vector correctly", () => {
75+
const point = new XYZ(1, 2, 3);
76+
const reversed = point.reverse();
77+
expect(reversed).toEqual(new XYZ(-1, -2, -3));
78+
79+
const zero = XYZ.zero.reverse();
80+
expect(zero).toEqual(new XYZ(0, 0, 0));
81+
});
4782
});
4883

49-
test("test xy", () => {
50-
const v1 = XY.unitX;
51-
const v2 = XY.unitY;
52-
expect(v1.angleTo(v2)).toBe(Math.PI / 2);
84+
describe("Mathematical operations", () => {
85+
test("should calculate cross product correctly", () => {
86+
const a = new XYZ(1, 0, 0);
87+
const b = new XYZ(0, 1, 0);
88+
const result = a.cross(b);
89+
expect(result).toEqual(new XYZ(0, 0, 1));
90+
91+
const c = new XYZ(2, 3, 4);
92+
const d = new XYZ(5, 6, 7);
93+
const result2 = c.cross(d);
94+
expect(result2).toEqual(new XYZ(-3, 6, -3)); // (3*7-4*6, 4*5-2*7, 2*6-3*5)
95+
});
96+
97+
test("should calculate dot product correctly", () => {
98+
const a = new XYZ(1, 2, 3);
99+
const b = new XYZ(4, 5, 6);
100+
expect(a.dot(b)).toBe(32); // 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32
101+
});
102+
103+
test("should calculate length and length squared correctly", () => {
104+
const point = new XYZ(3, 4, 5);
105+
expect(point.lengthSq()).toBe(50); // 9 + 16 + 25
106+
expect(point.length()).toBe(Math.sqrt(50));
107+
});
108+
109+
test("should normalize vector correctly", () => {
110+
const point = new XYZ(3, 0, 0);
111+
const normalized = point.normalize();
112+
expect(normalized).toEqual(new XYZ(1, 0, 0));
113+
114+
const unit = new XYZ(1, 1, 1);
115+
const normalized2 = unit.normalize();
116+
const length = Math.sqrt(3);
117+
expect(normalized2).toEqual(new XYZ(1 / length, 1 / length, 1 / length));
118+
});
119+
120+
test("should return undefined for zero vector normalization", () => {
121+
const normalized = XYZ.zero.normalize();
122+
expect(normalized).toBeUndefined();
123+
});
124+
});
125+
126+
describe("Distance and geometric operations", () => {
127+
test("should calculate distance correctly", () => {
128+
const a = new XYZ(0, 0, 0);
129+
const b = new XYZ(3, 4, 0);
130+
expect(a.distanceTo(b)).toBe(5); // 3-4-5 triangle
131+
132+
const c = new XYZ(1, 1, 1);
133+
const d = new XYZ(4, 5, 1);
134+
expect(c.distanceTo(d)).toBe(5); // 3-4-5 triangle in xy plane
135+
});
136+
137+
test("should calculate center correctly", () => {
138+
const p1 = new XYZ(0, 0, 0);
139+
const p2 = new XYZ(4, 6, 8);
140+
const center = XYZ.center(p1, p2);
141+
expect(center).toEqual(new XYZ(2, 3, 4));
142+
});
143+
});
144+
145+
describe("Angle operations", () => {
146+
test("should calculate angle correctly", () => {
147+
const a = new XYZ(10, 0, 0);
148+
const b = new XYZ(0, 10, 0);
149+
expect(a.angleTo(b)).toBe(Math.PI / 2);
150+
expect(a.angleTo(new XYZ(10, 0, 0))).toBe(0);
151+
expect(a.angleTo(new XYZ(10, 10, 0))).toBe(Math.PI / 4);
152+
expect(a.angleTo(new XYZ(0, 10, 0))).toBe(Math.PI / 2);
153+
expect(a.angleTo(new XYZ(-10, 10, 0))).toBe((Math.PI * 3) / 4);
154+
expect(a.angleTo(new XYZ(-10, 0, 0))).toBe(Math.PI);
155+
expect(a.angleTo(new XYZ(-10, -10, 0))).toBe((Math.PI * 3) / 4);
156+
expect(a.angleTo(new XYZ(10, -10, 0))).toBe(Math.PI / 4);
157+
});
158+
159+
test("should return undefined for zero vector angle", () => {
160+
expect(XYZ.zero.angleTo(new XYZ(0, 0, 0))).toBe(undefined);
161+
expect(XYZ.zero.angleTo(new XYZ(1, 0, 0))).toBeUndefined();
162+
expect(new XYZ(1, 0, 0).angleTo(XYZ.zero)).toBeUndefined();
163+
});
164+
165+
test("should calculate angle on plane correctly", () => {
166+
const a = new XYZ(10, 0, 0);
167+
const b = new XYZ(0, 10, 0);
168+
expect(a.angleOnPlaneTo(b, XYZ.unitZ)).toBe(Math.PI / 2);
169+
expect(a.angleOnPlaneTo(new XYZ(10, 0, 0), XYZ.unitZ)).toBe(0);
170+
expect(a.angleOnPlaneTo(new XYZ(10, 10, 0), XYZ.unitZ)).toBe(Math.PI / 4);
171+
expect(a.angleOnPlaneTo(new XYZ(0, 10, 0), XYZ.unitZ)).toBe(Math.PI / 2);
172+
expect(a.angleOnPlaneTo(new XYZ(-10, 10, 0), XYZ.unitZ)).toBe((Math.PI * 3) / 4);
173+
expect(a.angleOnPlaneTo(new XYZ(-10, 0, 0), XYZ.unitZ)).toBe(Math.PI);
174+
expect(a.angleOnPlaneTo(new XYZ(-10, -10, 0), XYZ.unitZ)).toBe((Math.PI * 5) / 4);
175+
expect(a.angleOnPlaneTo(new XYZ(10, -10, 0), XYZ.unitZ)).toBe((Math.PI * 7) / 4);
176+
177+
expect(new XYZ(10, 10, 0).angleOnPlaneTo(a, XYZ.unitZ)).toBe((Math.PI * 7) / 4);
178+
expect(a.angleOnPlaneTo(new XYZ(10, 10, 0), new XYZ(0, 0, -1))).toBe((Math.PI * 7) / 4);
179+
});
180+
181+
test("should return undefined for zero normal in angleOnPlaneTo", () => {
182+
const a = new XYZ(1, 0, 0);
183+
const b = new XYZ(0, 1, 0);
184+
expect(a.angleOnPlaneTo(b, XYZ.zero)).toBeUndefined();
185+
expect(XYZ.zero.angleOnPlaneTo(XYZ.zero, XYZ.unitZ)).toBe(undefined);
186+
});
187+
});
188+
189+
describe("Rotation operations", () => {
190+
test("should rotate vector correctly", () => {
191+
const v = XYZ.unitX.add(XYZ.unitZ);
192+
expect(v.rotate(v, 90)?.isEqualTo(v)).toBeTruthy();
193+
expect(
194+
XYZ.unitX.rotate(XYZ.unitZ, Math.PI / 4)?.isEqualTo(new XYZ(1, 1, 0).normalize()!),
195+
).toBeTruthy();
196+
expect(XYZ.unitX.rotate(XYZ.unitZ, Math.PI / 2)?.isEqualTo(new XYZ(0, 1, 0))).toBeTruthy();
197+
expect(XYZ.unitX.rotate(XYZ.unitZ, Math.PI / 1)?.isEqualTo(new XYZ(-1, 0, 0))).toBeTruthy();
198+
expect(XYZ.unitX.rotate(XYZ.unitZ, Math.PI * 1.5)?.isEqualTo(new XYZ(0, -1, 0))).toBeTruthy();
199+
200+
const result = XYZ.unitX.rotate(XYZ.unitZ, Math.PI / 2);
201+
expect(result?.isEqualTo(XYZ.unitY, 1e-10)).toBeTruthy();
202+
203+
const result2 = XYZ.unitY.rotate(XYZ.unitZ, Math.PI / 2);
204+
expect(result2?.isEqualTo(XYZ.unitX.reverse(), 1e-10)).toBeTruthy();
205+
});
206+
207+
test("should handle edge cases for rotation", () => {
208+
const result = XYZ.unitX.rotate(XYZ.zero, Math.PI / 2);
209+
expect(result).toBeUndefined();
210+
});
211+
});
212+
213+
describe("Comparison methods", () => {
214+
test("isEqualTo should work correctly", () => {
215+
const a = new XYZ(1, 2, 3);
216+
const b = new XYZ(1, 2, 3);
217+
expect(a.isEqualTo(b)).toBeTruthy();
218+
219+
const c = new XYZ(1.1, 2, 3);
220+
expect(a.isEqualTo(c, 0.2)).toBeTruthy(); // within tolerance
221+
expect(a.isEqualTo(c, 0.05)).toBeFalsy(); // outside tolerance
222+
});
223+
224+
test("isPerpendicularTo should work correctly", () => {
225+
const a = new XYZ(1, 0, 0);
226+
const b = new XYZ(0, 1, 0);
227+
expect(a.isPerpendicularTo(b)).toBeTruthy();
228+
expect(a.isPerpendicularTo(b.reverse())).toBeTruthy();
229+
230+
const c = new XYZ(1, 1, 0);
231+
expect(a.isPerpendicularTo(c, 0.1)).toBeFalsy();
232+
});
233+
234+
test("isParallelTo should work correctly", () => {
235+
const a = new XYZ(1, 0, 0);
236+
const b = new XYZ(2, 0, 0);
237+
expect(a.isParallelTo(b)).toBeTruthy();
238+
239+
const c = new XYZ(-1, 0, 0);
240+
expect(a.isParallelTo(c)).toBeTruthy(); // opposite direction is still parallel
241+
});
242+
243+
test("isOppositeTo should work correctly", () => {
244+
const a = new XYZ(1, 0, 0);
245+
const b = new XYZ(-1, 0, 0);
246+
expect(a.isOppositeTo(b)).toBeTruthy();
247+
248+
const c = new XYZ(0, 1, 0);
249+
expect(a.isOppositeTo(c)).toBeFalsy();
250+
});
53251
});
54252

55-
test("test rotate", () => {
56-
const v = XYZ.unitX.add(XYZ.unitZ);
57-
expect(v.rotate(v, 90)?.isEqualTo(v)).toBeTruthy();
58-
expect(
59-
XYZ.unitX.rotate(XYZ.unitZ, Math.PI / 4)?.isEqualTo(new XYZ(1, 1, 0).normalize()!),
60-
).toBeTruthy();
61-
expect(XYZ.unitX.rotate(XYZ.unitZ, Math.PI / 2)?.isEqualTo(new XYZ(0, 1, 0))).toBeTruthy();
62-
expect(XYZ.unitX.rotate(XYZ.unitZ, Math.PI / 1)?.isEqualTo(new XYZ(-1, 0, 0))).toBeTruthy();
63-
expect(XYZ.unitX.rotate(XYZ.unitZ, Math.PI * 1.5)?.isEqualTo(new XYZ(0, -1, 0))).toBeTruthy();
253+
describe("XY class tests", () => {
254+
test("should calculate angle for XY vectors", () => {
255+
const v1 = XY.unitX;
256+
const v2 = XY.unitY;
257+
expect(v1.angleTo(v2)).toBe(Math.PI / 2);
258+
});
64259
});
65260
});

0 commit comments

Comments
 (0)