@immugio/three-math-extensions 0.3.4 → 0.3.6

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/cjs/Line2D.js +90 -13
  3. package/docs/classes/BoundingBox.md +121 -121
  4. package/docs/classes/Line2D.md +1366 -1366
  5. package/docs/classes/Line3D.md +831 -831
  6. package/docs/classes/Polygon.md +297 -297
  7. package/docs/classes/Rectangle.md +291 -291
  8. package/docs/classes/Size2.md +55 -55
  9. package/docs/classes/Vec2.md +282 -282
  10. package/docs/classes/Vec3.md +338 -338
  11. package/docs/interfaces/Point2.md +30 -30
  12. package/docs/interfaces/Point3.md +41 -41
  13. package/docs/modules.md +209 -209
  14. package/eslint.config.mjs +111 -111
  15. package/esm/Line2D.js +90 -13
  16. package/package.json +62 -62
  17. package/src/BoundingBox.ts +13 -13
  18. package/src/Line2D.ts +951 -857
  19. package/src/Line3D.ts +586 -586
  20. package/src/MathConstants.ts +1 -1
  21. package/src/Point2.ts +3 -3
  22. package/src/Point3.ts +4 -4
  23. package/src/Polygon.ts +286 -286
  24. package/src/Rectangle.ts +92 -92
  25. package/src/Size2.ts +3 -3
  26. package/src/Vec2.ts +124 -124
  27. package/src/Vec3.ts +167 -167
  28. package/src/containsPoint.ts +65 -65
  29. package/src/directions.ts +9 -9
  30. package/src/directions2d.ts +7 -7
  31. package/src/ensurePolygonClockwise.ts +9 -9
  32. package/src/extendOrTrimPolylinesAtIntersections.ts +10 -10
  33. package/src/getPolygonArea.ts +21 -21
  34. package/src/index.ts +24 -24
  35. package/src/isContinuousClosedShape.ts +24 -24
  36. package/src/isPointInPolygon.ts +23 -23
  37. package/src/isPolygonClockwise.ts +15 -15
  38. package/src/normalizeAngleDegrees.ts +6 -6
  39. package/src/normalizeAngleRadians.ts +14 -14
  40. package/src/offsetPolyline.ts +26 -26
  41. package/src/polygonPerimeter.ts +13 -13
  42. package/src/sortLinesByConnections.ts +45 -45
  43. package/types/Line2D.d.ts +12 -5
@@ -1,25 +1,25 @@
1
- // The point type P must implement an isNear method that accepts the same point type and an optional tolerance.
2
- type Point<P extends { isNear(other: P, tolerance?: number): boolean }> = { isNear(other: P, tolerance?: number): boolean };
3
-
4
- // This matches the signatures on Vec2 and Vec3 (for example, isNear(v: Vector2, maxDistance?: number)).
5
- export function isContinuousClosedShape<P extends Point<P>>(lines: { start: P; end: P }[], tolerance: number = 0): boolean {
6
- if (lines.length < 3) {
7
- return false; // A shape needs at least 3 lines to be closed
8
- }
9
-
10
- // Starting from the first line, check if the end of the current line is the start of the next line
11
- for (let i = 0; i < lines.length - 1; i++) {
12
- const endCurrent = lines[i].end;
13
- const startNext = lines[i + 1].start;
14
-
15
- if (!endCurrent.isNear(startNext, tolerance)) {
16
- return false; // If the end of the current line and start of the next line are not close, return false
17
- }
18
- }
19
-
20
- // Lastly, we should check the end of the last line with the start of the first line
21
- const endLast = lines[lines.length - 1].end;
22
- const startFirst = lines[0].start;
23
-
24
- return endLast.isNear(startFirst, tolerance);
1
+ // The point type P must implement an isNear method that accepts the same point type and an optional tolerance.
2
+ type Point<P extends { isNear(other: P, tolerance?: number): boolean }> = { isNear(other: P, tolerance?: number): boolean };
3
+
4
+ // This matches the signatures on Vec2 and Vec3 (for example, isNear(v: Vector2, maxDistance?: number)).
5
+ export function isContinuousClosedShape<P extends Point<P>>(lines: { start: P; end: P }[], tolerance: number = 0): boolean {
6
+ if (lines.length < 3) {
7
+ return false; // A shape needs at least 3 lines to be closed
8
+ }
9
+
10
+ // Starting from the first line, check if the end of the current line is the start of the next line
11
+ for (let i = 0; i < lines.length - 1; i++) {
12
+ const endCurrent = lines[i].end;
13
+ const startNext = lines[i + 1].start;
14
+
15
+ if (!endCurrent.isNear(startNext, tolerance)) {
16
+ return false; // If the end of the current line and start of the next line are not close, return false
17
+ }
18
+ }
19
+
20
+ // Lastly, we should check the end of the last line with the start of the first line
21
+ const endLast = lines[lines.length - 1].end;
22
+ const startFirst = lines[0].start;
23
+
24
+ return endLast.isNear(startFirst, tolerance);
25
25
  }
@@ -1,24 +1,24 @@
1
- import { Point2 } from "./Point2";
2
-
3
- /**
4
- * Check if a point is inside a polygon
5
- * Warning: The function returns unreliable results for points on the polygon boundary.
6
- * Obsolete, use containsPoint instead.
7
- * @param p
8
- * @param point
9
- */
10
- export function isPointInPolygon(p: Point2[], point: Point2): boolean {
11
- const x = point.x, y = point.y;
12
-
13
- let i: number, j: number, c = false;
14
-
15
- for (i = 0, j = p.length - 1; i < p.length; j = i++) {
16
- if ((((p[i].y <= y) && (y < p[j].y)) ||
17
- ((p[j].y <= y) && (y < p[i].y))) &&
18
- (x < (p[j].x - p[i].x) * (y - p[i].y) / (p[j].y - p[i].y) + p[i].x)) {
19
- c = !c;
20
- }
21
- }
22
-
23
- return c;
1
+ import { Point2 } from "./Point2";
2
+
3
+ /**
4
+ * Check if a point is inside a polygon
5
+ * Warning: The function returns unreliable results for points on the polygon boundary.
6
+ * Obsolete, use containsPoint instead.
7
+ * @param p
8
+ * @param point
9
+ */
10
+ export function isPointInPolygon(p: Point2[], point: Point2): boolean {
11
+ const x = point.x, y = point.y;
12
+
13
+ let i: number, j: number, c = false;
14
+
15
+ for (i = 0, j = p.length - 1; i < p.length; j = i++) {
16
+ if ((((p[i].y <= y) && (y < p[j].y)) ||
17
+ ((p[j].y <= y) && (y < p[i].y))) &&
18
+ (x < (p[j].x - p[i].x) * (y - p[i].y) / (p[j].y - p[i].y) + p[i].x)) {
19
+ c = !c;
20
+ }
21
+ }
22
+
23
+ return c;
24
24
  }
@@ -1,16 +1,16 @@
1
- import { Point2 } from "./Point2";
2
-
3
- /*
4
- * Determines if a polygon is clockwise or counter-clockwise.
5
- * X increases to the right, Y increases downwards.
6
- * Based on this answer https://stackoverflow.com/a/18472899/1837173 - the result is inverted, they assume the inverse y-axis
7
- */
8
- export function isPolygonClockwise(vertices: Point2[]): boolean {
9
- let sum = 0.0;
10
- for (let i = 0; i < vertices.length; i++) {
11
- const v1 = vertices[i];
12
- const v2 = vertices[(i + 1) % vertices.length];
13
- sum += (v2.x - v1.x) * (v2.y + v1.y);
14
- }
15
- return sum < 0.0;
1
+ import { Point2 } from "./Point2";
2
+
3
+ /*
4
+ * Determines if a polygon is clockwise or counter-clockwise.
5
+ * X increases to the right, Y increases downwards.
6
+ * Based on this answer https://stackoverflow.com/a/18472899/1837173 - the result is inverted, they assume the inverse y-axis
7
+ */
8
+ export function isPolygonClockwise(vertices: Point2[]): boolean {
9
+ let sum = 0.0;
10
+ for (let i = 0; i < vertices.length; i++) {
11
+ const v1 = vertices[i];
12
+ const v2 = vertices[(i + 1) % vertices.length];
13
+ sum += (v2.x - v1.x) * (v2.y + v1.y);
14
+ }
15
+ return sum < 0.0;
16
16
  }
@@ -1,7 +1,7 @@
1
- /**
2
- * Normalizes an angle in degrees to the range [0, 360].
3
- * @param angle in degrees
4
- */
5
- export function normalizeAngleDegrees(angle: number): number {
6
- return ((angle % 360) + 360) % 360;
1
+ /**
2
+ * Normalizes an angle in degrees to the range [0, 360].
3
+ * @param angle in degrees
4
+ */
5
+ export function normalizeAngleDegrees(angle: number): number {
6
+ return ((angle % 360) + 360) % 360;
7
7
  }
@@ -1,15 +1,15 @@
1
- import { TwoPI } from "./MathConstants";
2
-
3
- /**
4
- * Normalize an angle in radians to the range of 0 to 2π.
5
- * @param angle in radians
6
- */
7
- export function normalizeAngleRadians(angle: number): number {
8
- angle = angle % TwoPI; // Use modulus to get the angle within the range of 0 to 2π
9
-
10
- if (angle < 0) { // Add 2π if the angle is negative
11
- angle = angle + TwoPI;
12
- }
13
-
14
- return angle;
1
+ import { TwoPI } from "./MathConstants";
2
+
3
+ /**
4
+ * Normalize an angle in radians to the range of 0 to 2π.
5
+ * @param angle in radians
6
+ */
7
+ export function normalizeAngleRadians(angle: number): number {
8
+ angle = angle % TwoPI; // Use modulus to get the angle within the range of 0 to 2π
9
+
10
+ if (angle < 0) { // Add 2π if the angle is negative
11
+ angle = angle + TwoPI;
12
+ }
13
+
14
+ return angle;
15
15
  }
@@ -1,27 +1,27 @@
1
- import { Line2D } from "./Line2D";
2
-
3
- export function offsetPolyline(lines: Line2D[], offset: number, tolerance: number = 0): Line2D[] {
4
- const isClosed = lines[0].start.isNear(lines.at(-1).end, tolerance);
5
-
6
- for (let i = 0; i < lines.length; i++) {
7
- const isFirst = i === 0;
8
- const isLast = i === lines.length - 1;
9
-
10
- const line = lines[i];
11
- line.translateLeft(offset);
12
-
13
- const next = lines[(i + 1) % lines.length];
14
- if (!isLast || isClosed) {
15
- line.extendToOrTrimAtIntersection(next);
16
- next.extendToOrTrimAtIntersection(line);
17
- }
18
-
19
- const previous = lines[(i + lines.length - 1) % lines.length];
20
- if (!isFirst || isClosed) {
21
- line.extendToOrTrimAtIntersection(previous);
22
- previous.extendToOrTrimAtIntersection(line);
23
- }
24
- }
25
-
26
- return lines;
1
+ import { Line2D } from "./Line2D";
2
+
3
+ export function offsetPolyline(lines: Line2D[], offset: number, tolerance: number = 0): Line2D[] {
4
+ const isClosed = lines[0].start.isNear(lines.at(-1).end, tolerance);
5
+
6
+ for (let i = 0; i < lines.length; i++) {
7
+ const isFirst = i === 0;
8
+ const isLast = i === lines.length - 1;
9
+
10
+ const line = lines[i];
11
+ line.translateLeft(offset);
12
+
13
+ const next = lines[(i + 1) % lines.length];
14
+ if (!isLast || isClosed) {
15
+ line.extendToOrTrimAtIntersection(next);
16
+ next.extendToOrTrimAtIntersection(line);
17
+ }
18
+
19
+ const previous = lines[(i + lines.length - 1) % lines.length];
20
+ if (!isFirst || isClosed) {
21
+ line.extendToOrTrimAtIntersection(previous);
22
+ previous.extendToOrTrimAtIntersection(line);
23
+ }
24
+ }
25
+
26
+ return lines;
27
27
  }
@@ -1,14 +1,14 @@
1
- import { Vec2 } from "./Vec2";
2
-
3
- export function polygonPerimeter(polygon: Vec2[], forceClosedPolygon: boolean = false): number {
4
- if (forceClosedPolygon && !polygon[0].equals(polygon.at(-1))) {
5
- polygon = [...polygon];
6
- polygon.push(polygon[0].clone());
7
- }
8
-
9
- let length = 0;
10
- for (let i = 0; i < polygon.length - 1; i++) {
11
- length += polygon[i].distanceTo(polygon[i + 1]);
12
- }
13
- return length;
1
+ import { Vec2 } from "./Vec2";
2
+
3
+ export function polygonPerimeter(polygon: Vec2[], forceClosedPolygon: boolean = false): number {
4
+ if (forceClosedPolygon && !polygon[0].equals(polygon.at(-1))) {
5
+ polygon = [...polygon];
6
+ polygon.push(polygon[0].clone());
7
+ }
8
+
9
+ let length = 0;
10
+ for (let i = 0; i < polygon.length - 1; i++) {
11
+ length += polygon[i].distanceTo(polygon[i + 1]);
12
+ }
13
+ return length;
14
14
  }
@@ -1,46 +1,46 @@
1
- import { Line2D } from "./Line2D";
2
-
3
- /**
4
- * Sort connected lines by their connections.
5
- * When the polygon is open, the first line must be the line that has no connection at the start.
6
- * When the polygon is open, the last line must be the line that has no connection at the end.
7
- * If the lines form a closed polygon, any line can be the first line.
8
- */
9
- export function sortLinesByConnections(lines: Line2D[], tolerance: number = 0): Line2D[] {
10
- const remainingLines = [...lines];
11
- const startLineIndex = findStartLineIndex(remainingLines, tolerance);
12
-
13
- const sortedLines: Line2D[] = [remainingLines.splice(startLineIndex, 1)[0]];
14
-
15
- while (remainingLines.length > 0) {
16
- const lastLine = sortedLines[sortedLines.length - 1];
17
- const nextLineIndex = remainingLines.findIndex(line => lastLine.end.isNear(line.start, tolerance));
18
-
19
- if (nextLineIndex === -1) {
20
- console.log("Lines do not form a connected path");
21
- return [...sortedLines, ...remainingLines];
22
- }
23
-
24
- sortedLines.push(remainingLines.splice(nextLineIndex, 1)[0]);
25
- }
26
-
27
- return sortedLines;
28
- }
29
-
30
- /**
31
- * Find the index of the starting line.
32
- * A starting line is defined as a line that has no other line connected to its start.
33
- * If such a line does not exist, it means that the lines form a closed polygon, return 0.
34
- * @param lines
35
- * @param tolerance
36
- */
37
- function findStartLineIndex(lines: Line2D[], tolerance: number): number {
38
- for (let i = 0; i < lines.length; i++) {
39
- const startLine = lines[i];
40
- const isStartLine = !lines.some(line => line.end.isNear(startLine.start, tolerance));
41
- if (isStartLine) {
42
- return i;
43
- }
44
- }
45
- return 0;
1
+ import { Line2D } from "./Line2D";
2
+
3
+ /**
4
+ * Sort connected lines by their connections.
5
+ * When the polygon is open, the first line must be the line that has no connection at the start.
6
+ * When the polygon is open, the last line must be the line that has no connection at the end.
7
+ * If the lines form a closed polygon, any line can be the first line.
8
+ */
9
+ export function sortLinesByConnections(lines: Line2D[], tolerance: number = 0): Line2D[] {
10
+ const remainingLines = [...lines];
11
+ const startLineIndex = findStartLineIndex(remainingLines, tolerance);
12
+
13
+ const sortedLines: Line2D[] = [remainingLines.splice(startLineIndex, 1)[0]];
14
+
15
+ while (remainingLines.length > 0) {
16
+ const lastLine = sortedLines[sortedLines.length - 1];
17
+ const nextLineIndex = remainingLines.findIndex(line => lastLine.end.isNear(line.start, tolerance));
18
+
19
+ if (nextLineIndex === -1) {
20
+ console.log("Lines do not form a connected path");
21
+ return [...sortedLines, ...remainingLines];
22
+ }
23
+
24
+ sortedLines.push(remainingLines.splice(nextLineIndex, 1)[0]);
25
+ }
26
+
27
+ return sortedLines;
28
+ }
29
+
30
+ /**
31
+ * Find the index of the starting line.
32
+ * A starting line is defined as a line that has no other line connected to its start.
33
+ * If such a line does not exist, it means that the lines form a closed polygon, return 0.
34
+ * @param lines
35
+ * @param tolerance
36
+ */
37
+ function findStartLineIndex(lines: Line2D[], tolerance: number): number {
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const startLine = lines[i];
40
+ const isStartLine = !lines.some(line => line.end.isNear(startLine.start, tolerance));
41
+ if (isStartLine) {
42
+ return i;
43
+ }
44
+ }
45
+ return 0;
46
46
  }
package/types/Line2D.d.ts CHANGED
@@ -130,9 +130,12 @@ export declare class Line2D {
130
130
  isPointOnInfiniteLine(point: Point2): boolean;
131
131
  /**
132
132
  * Returns true if other line is collinear and overlaps or at least touching this line.
133
+ * Uses tolerances to allow for small gaps and angle differences.
133
134
  * @param other
135
+ * @param distanceTolerance Maximum distance between lines or points to be considered touching/overlapping
136
+ * @param parallelTolerance Maximum angle difference in radians for lines to be considered parallel
134
137
  */
135
- isCollinearWithTouchOrOverlap(other: Line2D): boolean;
138
+ isCollinearWithTouchOrOverlap(other: Line2D, distanceTolerance?: number, parallelTolerance?: number): boolean;
136
139
  /**
137
140
  * Returns true if there is any overlap between this line and the @other line section.
138
141
  */
@@ -144,20 +147,24 @@ export declare class Line2D {
144
147
  getOverlap(other: Line2D): Line2D;
145
148
  /**
146
149
  * Joins a copy of @line with the @other line.
147
- * Other must be parallel to this line.
150
+ * Other must be parallel to this line (within tolerance).
148
151
  * Returns null if there is no overlap
149
152
  * Clones the line, does not modify.
150
153
  * @param line
151
154
  * @param other
155
+ * @param distanceTolerance Maximum distance between lines or points to be considered touching/overlapping
156
+ * @param parallelTolerance Maximum angle difference in radians for lines to be considered parallel
152
157
  */
153
- static joinLine(line: Line2D, other: Line2D): Line2D;
158
+ static joinLine(line: Line2D, other: Line2D, distanceTolerance?: number, parallelTolerance?: number): Line2D;
154
159
  /**
155
160
  * Joins provided lines into several joined lines.
156
- * Lines must be parallel for joining.
161
+ * Lines must be parallel for joining (within tolerance).
157
162
  * Clone the lines, does not modify.
158
163
  * @param lines
164
+ * @param distanceTolerance Maximum distance between lines or points to be considered touching/overlapping
165
+ * @param parallelTolerance Maximum angle difference in radians for lines to be considered parallel
159
166
  */
160
- static joinLines(lines: Line2D[]): Line2D[];
167
+ static joinLines(lines: Line2D[], distanceTolerance?: number, parallelTolerance?: number): Line2D[];
161
168
  /**
162
169
  * Checks if the current line covers another line.
163
170
  * A line is considered to cover another line if they are parallel and both the start and end points of the other line are contained within the current line.