-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
feat: add find line segment Intersection algorithm #1705
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
/** | ||
* @typedef {Object} Point | ||
* @property {number} x - The x-coordinate of the point. | ||
* @property {number} y - The y-coordinate of the point. | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} Segment | ||
* @property {Point} start - The start point of the segment. | ||
* @property {Point} end - The end point of the segment. | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} Event | ||
* @property {Point} point - The point where the event occurs. | ||
* @property {Array<string>} types - Types of the event ('left', 'right', or 'intersection'). | ||
* @property {Array<Segment>} segments - Segments associated with this event. | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} Intersection | ||
* @property {Segment} segment1 - First segment of the intersection. | ||
* @property {Segment} segment2 - Second segment of the intersection. | ||
* @property {Point} point - The point of intersection. | ||
*/ | ||
|
||
/** | ||
* Creates a new point. | ||
* @param {number} x - The x-coordinate. | ||
* @param {number} y - The y-coordinate. | ||
* @returns {Point} The created point. | ||
*/ | ||
export const createPoint = (x, y) => ({ x, y }) | ||
|
||
/** | ||
* Creates a new event. | ||
* @param {Point} point - The point where the event occurs. | ||
* @param {Array<string>} types - Types of the event. | ||
* @param {Array<Segment>} segments - Segments associated with this event. | ||
* @returns {Event} The created event. | ||
*/ | ||
export const createEvent = (point, types, segments) => ({ | ||
point, | ||
types, | ||
segments | ||
}) | ||
|
||
/** | ||
* Compares two points lexicographically. | ||
* @param {Point} a - The first point. | ||
* @param {Point} b - The second point. | ||
* @returns {number} Negative if a < b, positive if a > b, zero if equal. | ||
*/ | ||
export const comparePoints = (a, b) => { | ||
if (a.x !== b.x) return a.x - b.x | ||
return a.y - b.y | ||
} | ||
|
||
/** | ||
* Compares two segments based on their y-coordinate at the current x-coordinate of the sweep line. | ||
* @param {Segment} a - The first segment. | ||
* @param {Segment} b - The second segment. | ||
* @returns {number} Negative if a < b, positive if a > b, zero if equal. | ||
*/ | ||
export const compareSegmentsY = (a, b) => a.start.y - b.start.y | ||
|
||
/** | ||
* Calculates the intersection point of two line segments. | ||
* @param {Segment} seg1 - First segment. | ||
* @param {Segment} seg2 - Second segment. | ||
* @returns {Point|null} The intersection point, or null if segments are parallel. | ||
*/ | ||
export const calculateIntersectionPoint = (seg1, seg2) => { | ||
const x1 = seg1.start.x, | ||
y1 = seg1.start.y | ||
const x2 = seg1.end.x, | ||
y2 = seg1.end.y | ||
const x3 = seg2.start.x, | ||
y3 = seg2.start.y | ||
const x4 = seg2.end.x, | ||
y4 = seg2.end.y | ||
|
||
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) | ||
if (Math.abs(denom) < Number.EPSILON) return null // parallel lines | ||
|
||
const x = | ||
((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom | ||
const y = | ||
((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom | ||
|
||
return createPoint(x, y) | ||
} | ||
|
||
/** | ||
* Handles potential intersection between two segments. | ||
* @param {Segment} seg1 - First segment. | ||
* @param {Segment} seg2 - Second segment. | ||
* @param {Array<Event>} events - Array of events to update. | ||
* @param {Array<Intersection>} intersections - Array of intersections to update. | ||
*/ | ||
export const handlePotentialIntersection = ( | ||
seg1, | ||
seg2, | ||
events, | ||
intersections | ||
) => { | ||
const intersectionPoint = calculateIntersectionPoint(seg1, seg2) | ||
if (intersectionPoint) { | ||
events.push(createEvent(intersectionPoint, ['intersection'], [seg1, seg2])) | ||
intersections.push({ | ||
segment1: seg1, | ||
segment2: seg2, | ||
point: intersectionPoint | ||
}) | ||
} | ||
} | ||
|
||
/** | ||
* Finds all intersections among a set of line segments using the Bentley-Ottmann algorithm. | ||
* @param {Array<Segment>} segments - Array of line segments to check for intersections. | ||
* @returns {Array<Intersection>} Array of all intersections found. | ||
*/ | ||
const findIntersections = (segments) => { | ||
const events = [] | ||
const intersections = [] | ||
let sweepLineStatus = [] | ||
|
||
// Create initial events | ||
segments.forEach((segment) => { | ||
events.push(createEvent(segment.start, ['left'], [segment])) | ||
events.push(createEvent(segment.end, ['right'], [segment])) | ||
}) | ||
|
||
// Sort events | ||
events.sort((a, b) => { | ||
const pointCompare = comparePoints(a.point, b.point) | ||
if (pointCompare !== 0) return pointCompare | ||
|
||
// If points are the same, prioritize: intersection > left > right | ||
const typePriority = { intersection: 0, left: 1, right: 2 } | ||
return ( | ||
Math.min(...a.types.map((t) => typePriority[t])) - | ||
Math.min(...b.types.map((t) => typePriority[t])) | ||
) | ||
}) | ||
|
||
// Process events | ||
events.forEach((event) => { | ||
if (event.types.includes('left')) { | ||
event.segments.forEach((segment) => { | ||
sweepLineStatus.push(segment) | ||
sweepLineStatus.sort(compareSegmentsY) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is O(n log n), resulting in a total time complexity of O(n² log n), which is worse than the naive brute force algorithm. As said, the sweep line status structure needs to use a sorted set / map data structure. There should be efficient implementations of some in this repo; you need to use one. |
||
const index = sweepLineStatus.indexOf(segment) | ||
const lower = index > 0 ? sweepLineStatus[index - 1] : null | ||
const upper = | ||
index < sweepLineStatus.length - 1 ? sweepLineStatus[index + 1] : null | ||
if (lower) | ||
handlePotentialIntersection(segment, lower, events, intersections) | ||
if (upper) | ||
handlePotentialIntersection(segment, upper, events, intersections) | ||
}) | ||
} | ||
|
||
if (event.types.includes('right')) { | ||
event.segments.forEach((segment) => { | ||
const index = sweepLineStatus.indexOf(segment) | ||
const lower = index > 0 ? sweepLineStatus[index - 1] : null | ||
const upper = | ||
index < sweepLineStatus.length - 1 ? sweepLineStatus[index + 1] : null | ||
sweepLineStatus = sweepLineStatus.filter((s) => s !== segment) | ||
if (lower && upper) | ||
handlePotentialIntersection(lower, upper, events, intersections) | ||
}) | ||
} | ||
|
||
if (event.types.includes('intersection')) { | ||
// Re-check all pairs of segments at this x-coordinate for intersections | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also suffers from poor time complexity: First we do a linear filtering, then we do a quadratic brute force finding of intersections. |
||
const segmentsAtX = sweepLineStatus.filter( | ||
(s) => | ||
Math.min(s.start.x, s.end.x) <= event.point.x && | ||
Math.max(s.start.x, s.end.x) >= event.point.x | ||
) | ||
for (let i = 0; i < segmentsAtX.length; i++) { | ||
for (let j = i + 1; j < segmentsAtX.length; j++) { | ||
handlePotentialIntersection( | ||
segmentsAtX[i], | ||
segmentsAtX[j], | ||
events, | ||
intersections | ||
) | ||
} | ||
} | ||
} | ||
}) | ||
|
||
return intersections | ||
} | ||
|
||
export default findIntersections |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import findIntersections, { | ||
createPoint, | ||
createEvent, | ||
comparePoints, | ||
compareSegmentsY, | ||
calculateIntersectionPoint, | ||
handlePotentialIntersection | ||
} from '../FindIntersections' | ||
|
||
describe('Geometry Functions', () => { | ||
describe('createPoint', () => { | ||
it('should create a point with given coordinates', () => { | ||
const point = createPoint(1, 2) | ||
expect(point).toEqual({ x: 1, y: 2 }) | ||
}) | ||
}) | ||
|
||
describe('createEvent', () => { | ||
it('should create an event with the given parameters', () => { | ||
const point = createPoint(1, 2) | ||
const event = createEvent(point, ['left'], []) | ||
expect(event).toEqual({ | ||
point, | ||
types: ['left'], | ||
segments: [] | ||
}) | ||
}) | ||
}) | ||
|
||
describe('comparePoints', () => { | ||
it('should return negative if first point is less than second', () => { | ||
const a = createPoint(1, 2) | ||
const b = createPoint(2, 3) | ||
expect(comparePoints(a, b)).toBeLessThan(0) | ||
}) | ||
|
||
it('should return positive if first point is greater than second', () => { | ||
const a = createPoint(2, 3) | ||
const b = createPoint(1, 2) | ||
expect(comparePoints(a, b)).toBeGreaterThan(0) | ||
}) | ||
|
||
it('should return zero if points are equal', () => { | ||
const a = createPoint(1, 2) | ||
const b = createPoint(1, 2) | ||
expect(comparePoints(a, b)).toBe(0) | ||
}) | ||
}) | ||
|
||
describe('compareSegmentsY', () => { | ||
it('should compare segments based on their start y-coordinates', () => { | ||
const seg1 = { start: { x: 0, y: 1 }, end: { x: 1, y: 1 } } | ||
const seg2 = { start: { x: 0, y: 2 }, end: { x: 1, y: 2 } } | ||
expect(compareSegmentsY(seg1, seg2)).toBe(-1) | ||
}) | ||
|
||
it('should return zero if segments have the same start y-coordinate', () => { | ||
const seg1 = { start: { x: 0, y: 1 }, end: { x: 1, y: 1 } } | ||
const seg2 = { start: { x: -1, y: 1 }, end: { x: -2, y: 1 } } | ||
expect(compareSegmentsY(seg1, seg2)).toBe(0) | ||
}) | ||
|
||
it('should return positive if first segment is greater than second', () => { | ||
const seg1 = { start: { x: 0, y: 3 }, end: { x: 1, y: 3 } } | ||
const seg2 = { start: { x: -1, y: 2 }, end: { x: -2, y: 2 } } | ||
expect(compareSegmentsY(seg1, seg2)).toBe(1) | ||
}) | ||
}) | ||
|
||
describe('calculateIntersectionPoint', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only |
||
it('should return null for parallel segments', () => { | ||
const seg1 = { start: createPoint(0, 0), end: createPoint(5, 5) } | ||
const seg2 = { start: createPoint(0, 1), end: createPoint(5, 6) } | ||
expect(calculateIntersectionPoint(seg1, seg2)).toBeNull() | ||
}) | ||
|
||
it('should return intersection point for intersecting segments', () => { | ||
const seg1 = { start: createPoint(0, 0), end: createPoint(5, 5) } | ||
const seg2 = { start: createPoint(0, 5), end: createPoint(5, 0) } | ||
const intersection = calculateIntersectionPoint(seg1, seg2) | ||
expect(intersection).toEqual(createPoint(2.5, 2.5)) | ||
}) | ||
|
||
it('should handle vertical and horizontal lines correctly', () => { | ||
const seg1 = { start: createPoint(0, -1), end: createPoint(0, 1) } // vertical line | ||
const seg2 = { start: createPoint(-1, 0), end: createPoint(1, 0) } // horizontal line | ||
const intersection = calculateIntersectionPoint(seg1, seg2) | ||
|
||
expect(intersection.x).toBeCloseTo(0) // Check for close proximity to zero | ||
expect(intersection.y).toBeCloseTo(0) // Check for close proximity to zero | ||
}) | ||
|
||
it('should handle coincident segments correctly', () => { | ||
const seg1 = { start: createPoint(0, -1), end: createPoint(0, -3) } | ||
const seg2 = { start: createPoint(0, -3), end: createPoint(0, -4) } | ||
expect(calculateIntersectionPoint(seg1, seg2)).toBeNull() // should not intersect as they are collinear but not overlapping | ||
}) | ||
|
||
it('should handle edge cases for intersection calculations', () => { | ||
const seg1 = { start: createPoint(-10, -10), end: createPoint(-10, -10) } | ||
const seg2 = { start: createPoint(-10, -10), end: createPoint(-10, -10) } | ||
|
||
const intersection = calculateIntersectionPoint(seg1, seg2) | ||
|
||
expect(intersection).toBeNull() // Expect null since they are coincident lines. | ||
}) | ||
}) | ||
|
||
describe('handlePotentialIntersection', () => { | ||
it('should push intersection event and intersection data when segments intersect', () => { | ||
const events = [] | ||
const intersections = [] | ||
const seg1 = { start: createPoint(0, 0), end: createPoint(5, 5) } | ||
const seg2 = { start: createPoint(0, 5), end: createPoint(5, -5) } | ||
|
||
handlePotentialIntersection(seg1, seg2, events, intersections) | ||
|
||
expect(events.length).toBeGreaterThan(0) | ||
expect(intersections.length).toBeGreaterThan(0) | ||
}) | ||
|
||
it('should not push anything when segments do not intersect', () => { | ||
const events = [] | ||
const intersections = [] | ||
const seg1 = { start: createPoint(0, -10), end: createPoint(-10, -20) } | ||
const seg2 = { start: createPoint(-20, -30), end: createPoint(-30, -40) } | ||
|
||
handlePotentialIntersection(seg1, seg2, events, intersections) | ||
|
||
expect(events.length).toBe(0) | ||
expect(intersections.length).toBe(0) | ||
}) | ||
}) | ||
|
||
describe('findIntersections', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This really needs better, interesting tests with a couple segments and a couple events happening. Ideally even large randomized tests against the brute force algorithm. |
||
it('should find intersections among segments correctly', () => { | ||
const segments = [ | ||
{ start: createPoint(0, 0), end: createPoint(5, 5) }, | ||
{ start: createPoint(0, 5), end: createPoint(5, 0) }, | ||
{ start: createPoint(6, 6), end: createPoint(7, 7) } | ||
] | ||
|
||
const result = findIntersections(segments) | ||
|
||
expect(result.length).toBeGreaterThanOrEqual(1) // There should be at least one intersection | ||
}) | ||
|
||
it('should return an empty array when no intersections exist', () => { | ||
const segments = [ | ||
{ start: createPoint(10, 10), end: createPoint(20, 20) }, | ||
{ start: createPoint(30, 30), end: createPoint(40, 40) } | ||
] | ||
|
||
const result = findIntersections(segments) | ||
|
||
expect(result).toEqual([]) // No intersections should be found | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This, and many of the other typedefs (Segment, Event, maybe Intersection) should probably be classes; the functions on them make sense as methods (for example a
findIntersection
method for Segment).findIntersections
should stay a function however.If you decide to keep (some of) these as "implicit" "structs" for whatever reason, the
create...
functions are still useless boilerplate which should be removed (and definitely doesn't need to be tested).