Skip to content

Commit

Permalink
Add Line2D.containsPoint, Line2D.covers, Improve Line2D.equals, Line2…
Browse files Browse the repository at this point in the history
…D.overlaps, add unit tests
  • Loading branch information
Immugio committed Apr 15, 2024
1 parent f5e88f4 commit 7b6daf7
Show file tree
Hide file tree
Showing 7 changed files with 489 additions and 9 deletions.
77 changes: 69 additions & 8 deletions src/Line2D.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const _startEnd = /*@__PURE__*/ new Vec2();

export class Line2D {

readonly #target: Vec2 = new Vec2();

constructor(public start: Vec2, public end: Vec2, public index: number = 0) {
}

Expand Down Expand Up @@ -161,6 +163,26 @@ export class Line2D {
return [this.start, this.end];
}

/**
* Check that this line segment contains provided point.
* @param p
* @param tolerance
*/
public containsPoint(p: Vector2, tolerance: number = 0): boolean {
const closestPointToPoint = this.closestPointToPoint(p, true, this.#target);
return closestPointToPoint.distanceTo(p) <= tolerance;
}

/**
* Distance from this line to the provided point.
* @param param
* @param clampToLine
*/
public distanceToPoint(p: Vector2, clampToLine: boolean = true): number {
const closestPointToPoint = this.closestPointToPoint(p, clampToLine, this.#target);
return closestPointToPoint.distanceTo(p);
}

/**
* Returns the direction of this line.
*/
Expand Down Expand Up @@ -284,16 +306,38 @@ export class Line2D {
/**
* Returns true if there is any overlap between this line and the @other line section.
*/
public overlaps(other: Line2D): boolean {
if (!this.isCollinearWithTouchOrOverlap(other)) {
return false;
public overlaps(other: Line2D, distanceTolerance: number = 0, parallelTolerance: number = 0): boolean {
// Special case
if (this.equals(other, distanceTolerance)) {
return true;
}

if (this.start.equals(other.start) && this.end.equals(other.end)) {
return true;
// Always have to be parallel
if (this.isParallelTo(other, parallelTolerance)) {
// To pass as overlapping, at least one point has to be within the other line, but they mush not be equal - "touching" is not considered overlapping

const otherStartEqualsToAnyOfThisPoint = other.start.distanceTo(this.start) <= distanceTolerance || other.start.distanceTo(this.end) <= distanceTolerance;
if (this.containsPoint(other.start, distanceTolerance) && !otherStartEqualsToAnyOfThisPoint) {
return true;
}

const otherEndEqualsToAnyOfThisPoint = other.end.distanceTo(this.start) <= distanceTolerance || other.end.distanceTo(this.end) <= distanceTolerance;
if (this.containsPoint(other.end, distanceTolerance) && !otherEndEqualsToAnyOfThisPoint) {
return true;
}

const thisStartEqualsToAnyOfOtherPoint = this.start.distanceTo(other.start) <= distanceTolerance || this.start.distanceTo(other.end) <= distanceTolerance;
if (other.containsPoint(this.start, distanceTolerance) && !thisStartEqualsToAnyOfOtherPoint) {
return true;
}

const thisEndEqualsToAnyOfOtherPoint = this.end.distanceTo(other.start) <= distanceTolerance || this.end.distanceTo(other.end) <= distanceTolerance;
if (other.containsPoint(this.end, distanceTolerance) && !thisEndEqualsToAnyOfOtherPoint) {
return true;
}
}

return !this.start.equals(other.end) && !this.end.equals(other.start);
return false;
}

/**
Expand Down Expand Up @@ -382,6 +426,19 @@ export class Line2D {
return result;
}

/**
* Checks if the current line covers another line.
* 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.
* Both distance and angle tolerance can be provided.
*/
public covers(other: Line2D, tolerance: number = 0, parallelTolerance: number = 0): boolean {
if (!this.isParallelTo(other, parallelTolerance)) {
return false;
}

return this.containsPoint(other.start, tolerance) && this.containsPoint(other.end, tolerance);
}

/**
* Returns a new line that is the projection of this line onto @other. Uses `closestPointToPoint` to find the projection.
* @param other
Expand Down Expand Up @@ -759,8 +816,12 @@ export class Line2D {
return new Line3D(this.start.in3DSpace(y), this.end.in3DSpace(y));
}

public equals(other: Line2D): boolean {
return !!other && this.start.equals(other.start) && this.end.equals(other.end);
public equals(other: Line2D, tolerance: number = 0): boolean {
if (!tolerance) {
return !!other && this.start.equals(other.start) && this.end.equals(other.end);
}

return !!other && this.start.distanceTo(other.start) <= tolerance && this.end.distanceTo(other.end) <= tolerance;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Line3D.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export class Line3D extends Line3 {
}

/**
* Distance from this line to provided point.
* Distance from this line to the provided point.
* @param p
* @param clampToLine
*/
Expand Down
110 changes: 110 additions & 0 deletions src/__tests__/Line2D.containsPoint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Vector2 } from "three";
import { Line2D } from "../Line2D";
import { Vec2 } from "../Vec2";

describe("containsPoint", () => {

it("should return true if the point is on the line section", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const point = new Vector2(5, 5);
const tolerance = 0;

// Act
const contains = line.containsPoint(point, tolerance);

// Assert
expect(contains).toBe(true);
});

it("should return true if the point is very close to the line section within the tolerance", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const point = new Vector2(5.5, 5.5);
const tolerance = 1;

// Act
const contains = line.containsPoint(point, tolerance);

// Assert
expect(contains).toBe(true);
});

it("should return true if the point is exactly on the start point of the line section", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const point = new Vector2(0, 0);
const tolerance = 0;

// Act
const contains = line.containsPoint(point, tolerance);

// Assert
expect(contains).toBe(true);
});

it("should return true if the point is exactly on the end point of the line section", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const point = new Vector2(10, 10);
const tolerance = 0;

// Act
const contains = line.containsPoint(point, tolerance);

// Assert
expect(contains).toBe(true);
});

it("should return false when the point is not on the line section and not within the tolerance", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(10, 0));
const point = new Vector2(5, 1.1);
const tolerance = 1;

// Act
const result = line.containsPoint(point, tolerance);

// Assert
expect(result).toBe(false);
});

it("should return true when the point is on the line section", () => {
// Arrange
const line = Line2D.fromCoordinates(0, 0, 10, 0);
const point = new Vector2(5, 0);

// Act
const result = line.containsPoint(point);

// Assert
expect(result).toBe(true);
});

it("should return true when the point is on the line section", () => {
// Arrange
const line = Line2D.fromCoordinates(0, 0, 0, 10);
const point = new Vector2(0, 5);
const tolerance = 0;

// Act
const result = line.containsPoint(point, tolerance);

// Assert
expect(result).toBe(true);
});

it("should return true when the point is on the line section", () => {
// Arrange
const line = Line2D.fromCoordinates(0, 0, 2, 2);
const point = new Vector2(1, 1);
const tolerance = 0;

// Act
const result = line.containsPoint(point, tolerance);

// Assert
expect(result).toBe(true);
});

});
95 changes: 95 additions & 0 deletions src/__tests__/Line2D.covers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Line2D } from "../Line2D";
import { Vec2 } from "../Vec2";

describe("covers", () => {

it("should return true when the other line is parallel and both endpoints are contained within the current line", () => {
// Arrange
const line1 = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const line2 = new Line2D(new Vec2(2, 2), new Vec2(8, 8));

// Act
const result = line1.covers(line2);

// Assert
expect(result).toBe(true);
});

it("should return true when the other line is parallel and both endpoints are contained within the current line within tolerance", () => {
// Arrange
const line1 = new Line2D(new Vec2(0, 0), new Vec2(10, 0));
const line2 = new Line2D(new Vec2(2, 1), new Vec2(8, 1));
const tolerance = 1;

// Act
const result = line1.covers(line2, tolerance);

// Assert
expect(result).toBe(true);
});

it("should return false when the other line is not within the tolerance and both endpoints are contained within the current line within tolerance", () => {
// Arrange
const line1 = new Line2D(new Vec2(0, 0), new Vec2(10, 0));
const line2 = new Line2D(new Vec2(2, 2), new Vec2(8, 1));
const tolerance = 10;

// Act
const result = line1.covers(line2, tolerance);

// Assert
expect(result).toBe(false);
});

it("should return true when the other line direction is within tolerance and both endpoints are contained within the current line within tolerance", () => {
// Arrange
const line1 = new Line2D(new Vec2(0, 0), new Vec2(10, 0));
const line2 = new Line2D(new Vec2(0, 0), new Vec2(5, 5));
const tolerance = 10;

// Act
const result = line1.covers(line2, tolerance, Math.PI / 4);

// Assert
expect(result).toBe(true);
});

it("should return true when the other line is identical to the current line", () => {
// Arrange
const line1 = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const line2 = new Line2D(new Vec2(0, 0), new Vec2(10, 10));

// Act
const result = line1.covers(line2);

// Assert
expect(result).toBe(true);
});

it("should return false when the other line is parallel but one endpoint is not contained within the current line", () => {
// Arrange
const line1 = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const line2 = new Line2D(new Vec2(2, 2), new Vec2(12, 12));

// Act
const result = line1.covers(line2);

// Assert
expect(result).toBe(false);
});

it("should return false when the other line is not parallel to the current line", () => {
// Arrange
const line1 = new Line2D(new Vec2(0, 0), new Vec2(10, 10));
const line2 = new Line2D(new Vec2(0, 0), new Vec2(5, 15));
const tolerance1 = 20;
const parallelTolerance = 0;

// Act
const result = line1.covers(line2, tolerance1, parallelTolerance);

// Assert
expect(result).toBe(false);
});

});
67 changes: 67 additions & 0 deletions src/__tests__/Line2D.distanceToPoint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Vector2 } from "three";
import { Line2D } from "../Line2D";
import { Vec2 } from "../Vec2";

describe("distanceToPoint", () => {

it("should return the correct distance when the point is on the line segment", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(0, 5));
const point = new Vector2(0, 3);

// Act
const distance = line.distanceToPoint(point);

// Assert
expect(distance).toBe(0);
});

it("should return the correct distance when the point is on the line but not on the line segment when measuring to line segment", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(0, 5));
const point = new Vector2(0, 6);

// Act
const distance = line.distanceToPoint(point);

// Assert
expect(distance).toBeCloseTo(1);
});

it("should return the correct distance when the point is on the line but not on the line segment when measuring to line", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(0, 5));
const point = new Vector2(0, 6);

// Act
const distance = line.distanceToPoint(point, false);

// Assert
expect(distance).toBeCloseTo(0);
});

it("should return 0 when the point is on the line", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(2, 2));
const point = new Vector2(1, 1);

// Act
const distance = line.distanceToPoint(point);

// Assert
expect(distance).toBe(0);
});

it("should return the correct distance when the point is not on the line", () => {
// Arrange
const line = new Line2D(new Vec2(0, 0), new Vec2(2, 2));
const point = new Vector2(3, 1);

// Act
const distance = line.distanceToPoint(point);

// Assert
expect(distance).toBeCloseTo(1.414, 3);
});

});
Loading

0 comments on commit 7b6daf7

Please sign in to comment.