Skip to content

Commit d4d210e

Browse files
committed
feat: add plane sweep algorithm
1 parent 9010481 commit d4d210e

File tree

2 files changed

+246
-0
lines changed

2 files changed

+246
-0
lines changed

Geometry/PlaneSweep.js

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* This class implements a Line Segment Intersection algorithm using the Plane Sweep technique.
3+
* It detects intersections between a set of line segments in a 2D plane.
4+
* @see {@link https://en.wikipedia.org/wiki/Sweep_line_algorithm}
5+
* @class
6+
*/
7+
export default class PlaneSweep {
8+
/** @private */
9+
#segments
10+
11+
/** @private */
12+
#events
13+
14+
/** @private */
15+
#activeSet
16+
17+
/**
18+
* Creates a Line Segment Intersection instance.
19+
* @constructor
20+
* @param {Array<{start: {x: number, y: number}, end: {x: number, y: number}}> } segments - An array of line segments defined by start and end points.
21+
* @throws {Error} Will throw an error if the segments array is empty or invalid.
22+
*/
23+
constructor(segments) {
24+
this.#validateSegments(segments)
25+
26+
this.#segments = segments
27+
this.#events = []
28+
this.#activeSet = new Set()
29+
this.#initializeEvents()
30+
}
31+
32+
/**
33+
* Validates that the input is a non-empty array of segments.
34+
* @private
35+
* @param {Array} segments - The array of line segments to validate.
36+
* @throws {Error} Will throw an error if the input is not a valid array of segments.
37+
*/
38+
#validateSegments(segments) {
39+
if (
40+
!Array.isArray(segments) ||
41+
segments.length === 0 ||
42+
!segments.every((seg) => seg.start && seg.end)
43+
) {
44+
throw new Error(
45+
'segments must be a non-empty array of objects with both start and end properties.'
46+
)
47+
}
48+
}
49+
50+
/**
51+
* Initializes the event points for the sweep line algorithm.
52+
* @private
53+
*/
54+
#initializeEvents() {
55+
for (const segment of this.#segments) {
56+
const startEvent = { point: segment.start, type: 'start', segment }
57+
const endEvent = { point: segment.end, type: 'end', segment }
58+
59+
// Ensure start is always before end in terms of x-coordinates
60+
if (
61+
startEvent.point.x > endEvent.point.x ||
62+
(startEvent.point.x === endEvent.point.x &&
63+
startEvent.point.y > endEvent.point.y)
64+
) {
65+
this.#events.push(endEvent)
66+
this.#events.push(startEvent)
67+
} else {
68+
this.#events.push(startEvent)
69+
this.#events.push(endEvent)
70+
}
71+
}
72+
73+
// Sort events by x-coordinate, then by type (start before end)
74+
this.#events.sort((a, b) => {
75+
if (a.point.x === b.point.x) {
76+
return a.type === 'start' ? -1 : 1
77+
}
78+
return a.point.x - b.point.x
79+
})
80+
}
81+
82+
/**
83+
* Checks if two line segments intersect.
84+
* @private
85+
* @param {{start: {x: number, y: number}, end: {x: number, y: number}}} seg1 - The first segment.
86+
* @param {{start: {x: number, y: number}, end: {x: number, y: number}}} seg2 - The second segment.
87+
* @returns {boolean} True if the segments intersect, otherwise false.
88+
*/
89+
#doSegmentsIntersect(seg1, seg2) {
90+
const ccw = (A, B, C) =>
91+
(C.y - A.y) * (B.x - A.x) > (B.y - A.y) * (C.x - A.x)
92+
return (
93+
ccw(seg1.start, seg2.start, seg2.end) !==
94+
ccw(seg1.end, seg2.start, seg2.end) &&
95+
ccw(seg1.start, seg1.end, seg2.start) !==
96+
ccw(seg1.start, seg1.end, seg2.end)
97+
)
98+
}
99+
100+
/**
101+
* Executes the Plane Sweep algorithm to find all intersections.
102+
* @public
103+
* @returns {Array<{segment1: *, segment2: *}>} An array of intersecting segment pairs.
104+
*/
105+
findIntersections() {
106+
const intersections = []
107+
108+
for (const event of this.#events) {
109+
const { type, segment } = event
110+
111+
if (type === 'start') {
112+
this.#activeSet.add(segment)
113+
114+
// Check for intersections with neighboring active segments
115+
const neighbors = Array.from(this.#activeSet).filter(
116+
(seg) => seg !== segment
117+
)
118+
for (const neighbor of neighbors) {
119+
if (this.#doSegmentsIntersect(neighbor, segment)) {
120+
intersections.push({ segment1: neighbor, segment2: segment })
121+
}
122+
}
123+
} else if (type === 'end') {
124+
// Remove the segment from the active set
125+
this.#activeSet.delete(segment)
126+
}
127+
}
128+
129+
return intersections
130+
}
131+
}

Geometry/Test/PlaneSweep.test.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import PlaneSweep from '../PlaneSweep'
2+
3+
describe('PlaneSweep', () => {
4+
describe('Constructor', () => {
5+
test('creates an instance with valid segments', () => {
6+
const segments = [
7+
{ start: { x: 1, y: 1 }, end: { x: 4, y: 4 } },
8+
{ start: { x: 1, y: 4 }, end: { x: 4, y: 1 } }
9+
]
10+
const intersection = new PlaneSweep(segments)
11+
expect(intersection).toBeInstanceOf(PlaneSweep)
12+
})
13+
14+
test('throws an error if segments array is invalid', () => {
15+
expect(() => new PlaneSweep([])).toThrow(
16+
'segments must be a non-empty array of objects with both start and end properties.'
17+
)
18+
expect(() => new PlaneSweep([{ start: { x: 0, y: 0 } }])).toThrow(
19+
'segments must be a non-empty array of objects with both start and end properties.'
20+
)
21+
})
22+
})
23+
24+
describe('Intersection Detection', () => {
25+
test('detects intersections correctly', () => {
26+
const segments = [
27+
{ start: { x: 1, y: 1 }, end: { x: 4, y: 4 } },
28+
{ start: { x: 1, y: 4 }, end: { x: 4, y: 1 } },
29+
{ start: { x: 5, y: 5 }, end: { x: 6, y: 6 } }
30+
]
31+
const intersection = new PlaneSweep(segments)
32+
const result = intersection.findIntersections()
33+
34+
// Check if there is one intersection found
35+
expect(result).toHaveLength(1) // Ensure there's one intersection
36+
37+
const intersectingPair = result[0]
38+
39+
// Check that both segments in the intersection are part of the original segments
40+
const segment1 = intersectingPair.segment1
41+
const segment2 = intersectingPair.segment2
42+
43+
const isSegment1Valid =
44+
(segment1.start.x === segments[0].start.x &&
45+
segment1.start.y === segments[0].start.y &&
46+
segment1.end.x === segments[0].end.x &&
47+
segment1.end.y === segments[0].end.y) ||
48+
(segment1.start.x === segments[1].start.x &&
49+
segment1.start.y === segments[1].start.y &&
50+
segment1.end.x === segments[1].end.x &&
51+
segment1.end.y === segments[1].end.y)
52+
53+
const isSegment2Valid =
54+
(segment2.start.x === segments[0].start.x &&
55+
segment2.start.y === segments[0].start.y &&
56+
segment2.end.x === segments[0].end.x &&
57+
segment2.end.y === segments[0].end.y) ||
58+
(segment2.start.x === segments[1].start.x &&
59+
segment2.start.y === segments[1].start.y &&
60+
segment2.end.x === segments[1].end.x &&
61+
segment2.end.y === segments[1].end.y)
62+
63+
expect(isSegment1Valid).toBe(true)
64+
expect(isSegment2Valid).toBe(true)
65+
})
66+
67+
test('returns an empty array if there are no intersections', () => {
68+
const segments = [
69+
{ start: { x: 1, y: 1 }, end: { x: 2, y: 2 } },
70+
{ start: { x: 3, y: 3 }, end: { x: 4, y: 4 } }
71+
]
72+
const intersection = new PlaneSweep(segments)
73+
const result = intersection.findIntersections()
74+
expect(result).toEqual([])
75+
})
76+
77+
test('handles vertical and horizontal lines', () => {
78+
const segments = [
79+
{ start: { x: 2, y: 0 }, end: { x: 2, y: 3 } }, // Vertical line
80+
{ start: { x: 0, y: 2 }, end: { x: 3, y: 2 } } // Horizontal line
81+
]
82+
const intersection = new PlaneSweep(segments)
83+
const result = intersection.findIntersections()
84+
85+
// Check if intersection contains the correct segments regardless of order
86+
expect(result).toHaveLength(1) // Ensure there's one intersection
87+
88+
const intersectingPair = result[0]
89+
90+
// Check that both segments in the intersection are part of the original segments
91+
const isSegment1Valid =
92+
(intersectingPair.segment1.start.x === segments[0].start.x &&
93+
intersectingPair.segment1.start.y === segments[0].start.y &&
94+
intersectingPair.segment1.end.x === segments[0].end.x &&
95+
intersectingPair.segment1.end.y === segments[0].end.y) ||
96+
(intersectingPair.segment1.start.x === segments[1].start.x &&
97+
intersectingPair.segment1.start.y === segments[1].start.y &&
98+
intersectingPair.segment1.end.x === segments[1].end.x &&
99+
intersectingPair.segment1.end.y === segments[1].end.y)
100+
101+
const isSegment2Valid =
102+
(intersectingPair.segment2.start.x === segments[0].start.x &&
103+
intersectingPair.segment2.start.y === segments[0].start.y &&
104+
intersectingPair.segment2.end.x === segments[0].end.x &&
105+
intersectingPair.segment2.end.y === segments[0].end.y) ||
106+
(intersectingPair.segment2.start.x === segments[1].start.x &&
107+
intersectingPair.segment2.start.y === segments[1].start.y &&
108+
intersectingPair.segment2.end.x === segments[1].end.x &&
109+
intersectingPair.segment2.end.y === segments[1].end.y)
110+
111+
expect(isSegment1Valid).toBe(true)
112+
expect(isSegment2Valid).toBe(true)
113+
})
114+
})
115+
})

0 commit comments

Comments
 (0)