diff --git a/src/pslg.ts b/src/pslg.ts index 8ea8234..27e12ff 100644 --- a/src/pslg.ts +++ b/src/pslg.ts @@ -28,7 +28,12 @@ export class Point { constructor( public readonly coordinate: Coordinate, public readonly name?: string, - ) {} + ) { + this.coordinate = { + x: parseFloat(coordinate.x.toFixed(12)), + y: parseFloat(coordinate.y.toFixed(12)), + } + } get x() { return this.coordinate.x @@ -49,6 +54,14 @@ export class Point { ) } + isLeftOf(x: Point) { + return this.x < x.x + } + + isRightOf(x: Point) { + return this.x > x.x + } + public static midpoint(a: Point, b: Point): Point { const x = (a.x + b.x) / 2 const y = (a.y + b.y) / 2 @@ -68,6 +81,17 @@ export class Point { } export class Segment { + static fromCombinedHorizontals(a: Segment, b: Segment) { + const leftmostA = a.getLeftmostPoint() + const leftmostB = b.getLeftmostPoint() + const rightmostA = a.getRightmostPoint() + const rightmostB = b.getRightmostPoint() + + const leftmost = leftmostA.x < leftmostB.x ? leftmostA : leftmostB + const rightmost = rightmostA.x > rightmostB.x ? rightmostA : rightmostB + return new Segment(leftmost, rightmost) + } + constructor( public readonly from: Point, public readonly to: Point, @@ -112,6 +136,19 @@ export class Segment { return new Segment(this.from.clone(), this.to.clone()) } + getOtherPoint(x: Point) { + if ( this.from.is(x) ) return this.to + return this.from + } + + getLeftmostPoint() { + return this.from.x < this.to.x ? this.from : this.to + } + + getRightmostPoint() { + return this.getOtherPoint(this.getLeftmostPoint()) + } + is(x: Segment) { return ( ( @@ -148,64 +185,69 @@ export class Segment { ) } + yValueIsWithinRange(y: number, inclusive = true) { + if ( inclusive ) { + return y >= this.ymin && y <= this.ymax + } + + return y > this.ymin && y < this.ymax + } + + xValueIsWithinRange(x: number, inclusive = true) { + if ( inclusive ) { + return x >= this.xmin && x <= this.xmax + } + + return x > this.xmin && x < this.xmax + } + hasPoint(x: Point) { - return !( + return ( this.from.is(x) || this.to.is(x) ) } - getIntersectionWith(x: Segment): Point | undefined { - const slope = this.slope - const xSlope = x.slope - - if ( slope === xSlope ) return - - const b1 = this.yIntercept - const b2 = x.yIntercept - const m1 = this.slope - const m2 = x.slope - - if ( Math.abs(m1) === 0 && Math.abs(m2) === Infinity ) { - if ( x.xmin <= this.xmax && x.xmin >= this.xmin ) { - if ( this.ymin <= x.ymax && this.ymin >= x.ymin ) { - return Point.from(x.xmin, this.ymin) - } - } - } + getIntersectionWithin(x: Segment): Point | undefined { + const intersection = this.getIntersectionWith(x) - if ( Math.abs(m1) === Infinity && Math.abs(m2) === 0 ) { - if ( this.xmin <= x.xmax && this.xmin >= x.xmin ) { - if ( x.ymin <= this.ymax && x.ymin >= this.ymin ) { - return Point.from(this.xmin, x.ymin) - } + if ( intersection ) { + if ( !this.hasPoint(intersection) && !x.hasPoint(intersection) ) { + return intersection } } - - const intersectX = (b1 - b2) / (m2 - m1) - const intersectY = this.getYAtX(intersectX) - if ( intersectX <= Math.max(this.from.x, this.to.x) && intersectX >= Math.min(this.from.x, this.to.x) ) { - return Point.from(intersectX, intersectY) - } } - getIntersectionWithin(x: Segment): Point | undefined { - const slope = this.slope - const xSlope = x.slope + getIntersectionWith(seg: Segment): Point | undefined { + const [x1, y1] = [this.from.x, this.from.y] + const [x2, y2] = [this.to.x, this.to.y] + const [x3, y3] = [seg.from.x, seg.from.y] + const [x4, y4] = [seg.to.x, seg.to.y] - // FIXME account for overlapping parallel lines - if ( slope === xSlope ) return + const x12 = x1 - x2 + const x34 = x3 - x4 + const y12 = y1 - y2 + const y34 = y3 - y4 - const b1 = this.yIntercept - const b2 = x.yIntercept - const m1 = this.slope - const m2 = x.slope + const c = x12 * y34 - y12 * x34 - const intersectX = (b1 - b2) / (m2 - m1) - const intersectY = this.getYAtX(intersectX) - if ( intersectX < Math.max(this.from.x, this.to.x) && intersectX > Math.min(this.from.x, this.to.x) ) { - return Point.from(intersectX, intersectY) + if ( !Math.abs(c) ) { + return } + + const a = x1 * y2 - y1 * x2 + const b = x3 * y4 - y3 * x4 + + const x = (a * x34 - b * x12) / c + const y = (a * y34 - b * y12) / c + + const point = Point.from(x, y) + if ( + this.xValueIsWithinRange(point.x) + && seg.xValueIsWithinRange(point.x) + && this.yValueIsWithinRange(point.y) + && seg.yValueIsWithinRange(point.y) + ) return point } getLength(): number { @@ -268,6 +310,10 @@ export class Segment { get ymax() { return Math.max(this.from.y, this.to.y) } + + toQuickDisplay() { + return [this.from.coordinate, this.to.coordinate] + } } export class Circle { @@ -706,6 +752,10 @@ export class Graph { }) } + getSegmentsEndingAt(point: Point): Segment[] { + return this.segments.filter(segment => segment.hasPoint(point)) + } + removeSegment(x: Segment) { this.segments = this.segments.filter(segment => !segment.is(x)) } diff --git a/src/trapezoidTriangulation.ts b/src/trapezoidTriangulation.ts index 5dd1538..010b013 100644 --- a/src/trapezoidTriangulation.ts +++ b/src/trapezoidTriangulation.ts @@ -55,72 +55,72 @@ export function triangulate(originalGraph: Graph): Graph { // For each vertex in the original graph, create a horizontal line that // extends in both directions until it intersects with either (1) the boundary // or (2) a segment in the graph. - for ( const point of graph.points ) { + const originalPoints = [...graph.points] + for ( const point of originalPoints ) { if ( boundary.isBoundaryPoint(point) ) continue // skip boundary points - // Create the segment extending out to the left boundary - const leftPoint = Point.from(leftBound.from.x, point.y) - let leftSegment = new Segment(point, leftPoint) - - // Get segments that intersect with this - const leftIntersectingSegments = (graph.segments - .map(segment => { - if ( boundary.isBoundarySegment(segment) ) { - // Exclude boundary segments - return undefined - } - - return { - segment, - intersect: segment.getIntersectionWithin(leftSegment), - } - }) - .filter(group => group && group.intersect) as Array<{segment: Segment, intersect: Point}>) - .sort((a, b) => { - return b.intersect.x - a.intersect.x // Sort by right-most x-value - }) - - // Check if there was a nearer intersecting segment - const firstLeftIntersectingSegment = leftIntersectingSegments?.[0]?.segment - if ( firstLeftIntersectingSegment ) { - // Modify the leftSegment to end at the intersection point - const leftIntersect = graph.findExistingPointOrAdd(leftIntersectingSegments[0].intersect) - leftSegment = new Segment(point, leftIntersect) + // First, check if there is a horizontal segment ending at this point + // extending toward the left + const hasLeftHorizon = graph.getSegmentsEndingAt(point) + .some(segment => segment.isHorizontal() && segment.getOtherPoint(point).isLeftOf(point)) + + let leftmostPoint: Point = point + let rightmostPoint: Point = point + let leftIntersectionRay: Segment + let rightIntersectionRay: Segment + + if ( !hasLeftHorizon ) { + const leftRaySegment = new Segment(point, Point.from(boundary.getLeftBoundary().xmin, point.y)) + const [leftIntersectingSegment, leftIntersectingPoint] = getFirstIntersectingSegmentInDirection( + leftRaySegment, + boundary, + graph, + GraphDirection.LEFT + ) + + leftmostPoint = leftIntersectingPoint + leftIntersectionRay = leftIntersectingSegment } - // Create the segment extending out to the right boundary - const rightPoint = Point.from(rightBound.from.x, point.y) - let rightSegment = new Segment(point, rightPoint) - - // Get segments that intersect with this - const rightIntersectingSegments = (graph.segments - .map(segment => { - if ( boundary.isBoundarySegment(segment) ) { - // Exclude boundary segments - return undefined - } - - return { - segment, - intersect: segment.getIntersectionWithin(rightSegment), - } - }) - .filter(group => group && group.intersect) as Array<{segment: Segment, intersect: Point}>) - .sort((a, b) => { - return a.intersect.x - b.intersect.x // Sort by left-most x-value - }) - - // Check if there was a nearer intersecting segment - const firstRightIntersectingSegment = rightIntersectingSegments?.[0]?.segment - if ( firstRightIntersectingSegment ) { - // Modify the leftSegment to end at the intersection point - const rightIntersect = graph.findExistingPointOrAdd(rightIntersectingSegments[0].intersect) - rightSegment = new Segment(point, rightIntersect) + // First, check if there is a horizontal segment ending at this point + // extending toward the left + const hasRightHorizon = graph.getSegmentsEndingAt(point) + .some(segment => segment.isHorizontal() && segment.getOtherPoint(point).isRightOf(point)) + + if ( !hasRightHorizon ) { + const rightRaySegment = new Segment(point, Point.from(boundary.getRightBoundary().xmin, point.y)) + const [rightIntersectingSegment, rightIntersectingPoint] = getFirstIntersectingSegmentInDirection( + rightRaySegment, + boundary, + graph, + GraphDirection.RIGHT + ) + + rightmostPoint = rightIntersectingPoint + rightIntersectionRay = rightIntersectingSegment } - const graphLeftSegment = graph.findExistingSegmentOrAdd(leftSegment) - const graphRightSegment = graph.findExistingSegmentOrAdd(rightSegment) - trapezoidSegments.push(graphLeftSegment, graphRightSegment) + if ( !leftmostPoint.is(rightmostPoint) ) { + // Check if this point has a line segment extending from both sides. + // If so, then the line segment will bisect a captive area to create 2 trapezoids, + // so we need to make 2 line segments. + + let hasSegmentBelow = false + let hasSegmentAbove = false + graph.getSegmentsEndingAt(point) + .forEach(segment => { + const otherPoint = segment.getOtherPoint(point) + if ( otherPoint.y > point.y ) hasSegmentAbove = true + if ( otherPoint.y < point.y ) hasSegmentBelow = true + }) + + if ( hasSegmentAbove && hasSegmentBelow ) { + trapezoidSegments.push(graph.findExistingSegmentOrAdd(new Segment(leftmostPoint, point))) + trapezoidSegments.push(graph.findExistingSegmentOrAdd(new Segment(rightmostPoint, point))) + } else { + trapezoidSegments.push(graph.findExistingSegmentOrAdd(new Segment(leftmostPoint, rightmostPoint))) + } + } } // Now, go through and identify trapezoids for all the horizontal segments we just added @@ -154,6 +154,7 @@ export function triangulate(originalGraph: Graph): Graph { GraphDirection.LEFT ) + console.log('got leftIntersectSegment', leftBoundaryHorizontalSegment.toQuickDisplay(), leftIntersectSegment.toQuickDisplay(), leftIntersectPoint.coordinate) leftBoundaryHorizontalSegment = new Segment(verticalMidpoint, leftIntersectPoint) // Repeat to get the right boundary @@ -196,21 +197,42 @@ export function triangulate(originalGraph: Graph): Graph { // Let's find the segments that make up the trapezoid // We will do this by re-creating segments for the four sides of the trapezoid // Split the left-side on the intersection point - let [leftSegment1, leftSegment2] = leftIntersectSegment.splitAt(leftIntersectPoint) // This is not right. Needs to be `segment`'s intersect point, not the midpoint intersect point - graph.removeSegment(leftIntersectSegment) - leftSegment1 = graph.findExistingSegmentOrAdd(leftSegment1) - leftSegment2 = graph.findExistingSegmentOrAdd(leftSegment2) + const leftSegmentIntersectPoint = leftIntersectSegment.getIntersectionWith(segment) + if ( !leftSegmentIntersectPoint ) { + console.log('!leftSegmentIntersectPoint', segment.toQuickDisplay(), leftIntersectSegment.toQuickDisplay()) + throw new Error('Unable to find trapezoid segment intersection') + } - // We care about the upper-segment from the split, as that is the bound of our trapezoid - const trapezoidLeftBoundSegment = leftSegment1.ymin === leftIntersectPoint.y ? leftSegment1 : leftSegment2 + // TODO Account for the case where we don't need to split the segment. + let trapezoidLeftBoundSegment = leftIntersectSegment + let leftSegment1: Segment | undefined + let leftSegment2: Segment | undefined + if ( !leftIntersectSegment.hasPoint(leftSegmentIntersectPoint) ) { + let [localLeftSegment1, localLeftSegment2] = leftIntersectSegment.splitAt(leftSegmentIntersectPoint) + graph.removeSegment(leftIntersectSegment) + leftSegment1 = graph.findExistingSegmentOrAdd(localLeftSegment1) + leftSegment2 = graph.findExistingSegmentOrAdd(localLeftSegment2) + + // We care about the upper-segment from the split, as that is the bound of our trapezoid + trapezoidLeftBoundSegment = leftSegment1.ymin === leftSegmentIntersectPoint.y ? leftSegment1 : leftSegment2 + } // Repeat this process for the right-side segment - let [rightSegment1, rightSegment2] = rightIntersectSegment.splitAt(rightIntersectPoint) - graph.removeSegment(rightIntersectSegment) - rightSegment1 = graph.findExistingSegmentOrAdd(rightSegment1) - rightSegment2 = graph.findExistingSegmentOrAdd(rightSegment2) - - const trapezoidRightBoundSegment = rightSegment1.ymin === rightBoundaryPoint.y ? leftSegment1 : leftSegment2 + const rightSegmentIntersectPoint = rightIntersectSegment.getIntersectionWith(segment) + if ( !rightSegmentIntersectPoint ) throw new Error('Unable to find trapezoid segment intersection') + + let trapezoidRightBoundSegment = rightIntersectSegment + let rightSegment1: Segment | undefined + let rightSegment2: Segment | undefined + if ( !rightIntersectSegment.hasPoint(rightSegmentIntersectPoint) ) { + let [localRightSegment1, localRightSegment2] = rightIntersectSegment.splitAt(rightSegmentIntersectPoint) + graph.removeSegment(rightIntersectSegment) + rightSegment1 = graph.findExistingSegmentOrAdd(localRightSegment1) + rightSegment2 = graph.findExistingSegmentOrAdd(localRightSegment2) + + // We care about the upper-segment from the split, as that is the bound of our trapezoid + trapezoidRightBoundSegment = rightSegment1.ymin === rightSegmentIntersectPoint.y ? rightSegment1 : rightSegment2 + } // Now we have all 4 bounding segments. We find the bisector that creates // triangles with the largest minimum angle. @@ -220,25 +242,25 @@ export function triangulate(originalGraph: Graph): Graph { const upperRightPoint = graph.findExistingPointOrAdd(Point.from(upperIntersectSegment.xmax, upperIntersectSegment.ymax)) const bottomLeftBisectorSegment = new Segment(lowerLeftPoint, upperRightPoint) - // const bottomLeftBisectorUpperTriangle = new Triangle([bottomLeftBisectorSegment, upperIntersectSegment, trapezoidLeftBoundSegment]) + const bottomLeftBisectorUpperTriangle = new Triangle([bottomLeftBisectorSegment, upperIntersectSegment, trapezoidLeftBoundSegment]) // const bottomLeftBisectorLowerTriangle = new Triangle([bottomLeftBisectorSegment, segment, trapezoidRightBoundSegment]) // const bottomLeftBisectorMinAngle = Math.min(bottomLeftBisectorUpperTriangle.getMinimumAngle(), bottomLeftBisectorLowerTriangle.getMinimumAngle()) - const upperLeftPoint = graph.findExistingPointOrAdd(Point.from(upperIntersectSegment.xmin, upperIntersectSegment.ymax)) - const lowerRightPoint = graph.findExistingPointOrAdd(Point.from(segment.xmax, segment.ymin)) - + // const upperLeftPoint = graph.findExistingPointOrAdd(Point.from(upperIntersectSegment.xmin, upperIntersectSegment.ymax)) + // const lowerRightPoint = graph.findExistingPointOrAdd(Point.from(segment.xmax, segment.ymin)) + // // const topRightBisectorSegment = new Segment(upperLeftPoint, lowerRightPoint) // const upperRightBisectorUpperTriangle = new Triangle([topRightBisectorSegment, upperIntersectSegment, trapezoidRightBoundSegment]) // const upperRightBisectorLowerTriangle = new Triangle([topRightBisectorSegment, trapezoidLeftBoundSegment, segment]) // const upperRightBisectorMinAngle = Math.min(upperRightBisectorUpperTriangle.getMinimumAngle(), upperRightBisectorLowerTriangle.getMinimumAngle()) - + // // const optimalBisectorUpperTriangle = upperRightBisectorMinAngle > bottomLeftBisectorMinAngle ? upperRightBisectorUpperTriangle : bottomLeftBisectorUpperTriangle // const optimalBisectorLowerTriangle = upperRightBisectorMinAngle > bottomLeftBisectorMinAngle ? upperRightBisectorLowerTriangle : bottomLeftBisectorLowerTriangle - - // Add the triangles to the graph + // + // // Add the triangles to the graph // const upperTriangleSegments = optimalBisectorUpperTriangle.sides.map(side => graph.findExistingSegmentOrAdd(side)) // graph.findExistingTriangleOrAdd(new Triangle(upperTriangleSegments as [Segment, Segment, Segment])) - + // // const lowerTriangleSegments = optimalBisectorLowerTriangle.sides.map(side => graph.findExistingSegmentOrAdd(side)) // graph.findExistingTriangleOrAdd(new Triangle(lowerTriangleSegments as [Segment, Segment, Segment])) }