You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mesh/src/trapezoidTriangulation.ts

272 lines
13 KiB

import {
addBoundingSquareTo,
getDirectionSorter,
Graph,
GraphBoundary,
GraphDirection,
Point,
Segment,
SegmentWithIntersection, Triangle
} from "./pslg";
export function getFirstIntersectingSegmentInDirection(raySegment: Segment, boundary: GraphBoundary, graph: Graph, direction: GraphDirection): [Segment, Point] {
const intersectingSegment = boundary.getBoundary(direction)
const intersectingPoint = raySegment.getIntersectionWith(intersectingSegment)
if ( !intersectingPoint ) {
console.log(
'getFirstIntersectingSegmentInDirection',
[raySegment.from.coordinate, raySegment.to.coordinate],
[intersectingSegment.from.coordinate, intersectingSegment.to.coordinate],
intersectingPoint
)
throw new RangeError('Ray segment does not extend to boundary in the given direction!')
}
// Now, collect all non-boundary segments that intersect with the ray segment
const intersections = (graph.segments
.map(segment => {
if ( boundary.isBoundarySegment(segment) ) return undefined
return {
segment,
intersect: segment.getIntersectionWithin(raySegment)
}
})
.filter(x => x && x.intersect) as SegmentWithIntersection[])
.sort(getDirectionSorter(direction))
const intersection = intersections[0]
if ( intersection ) {
return [intersection.segment, intersection.intersect]
}
return [intersectingSegment, intersectingPoint]
}
export function triangulate(originalGraph: Graph): Graph {
const graph = originalGraph.clone()
const boundary = addBoundingSquareTo(graph)
const leftBound = boundary.getLeftBoundary()
const rightBound = boundary.getRightBoundary()
const trapezoidSegments: Segment[] = []
// 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.
const originalPoints = [...graph.points]
for ( const point of originalPoints ) {
if ( boundary.isBoundaryPoint(point) ) continue // skip boundary points
// 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
}
// 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
}
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
for ( const segment of trapezoidSegments ) {
// First, find the trapezoid formed with the segment as the bottom
// Create a vertical segment from the midpoint of the segment to the top boundary
const horizontalMidpoint = segment.getMidpoint()
let upperBoundaryPoint = Point.from(horizontalMidpoint.x, boundary.ymax)
let upperBoundaryVerticalSegment = new Segment(horizontalMidpoint, upperBoundaryPoint)
const [upperIntersectSegment, upperIntersectPoint] = getFirstIntersectingSegmentInDirection(
upperBoundaryVerticalSegment,
boundary,
graph,
GraphDirection.UP
)
upperBoundaryVerticalSegment = new Segment(horizontalMidpoint, upperIntersectPoint)
// Now we have the upper and lower boundaries of the trapezoid.
// So, we need to figure out the left and right boundaries next.
// Get the midpoint of the vertical segment
const verticalMidpoint = upperBoundaryVerticalSegment.getMidpoint()
let leftBoundaryPoint = Point.from(boundary.xmin, verticalMidpoint.y)
let leftBoundaryHorizontalSegment = new Segment(verticalMidpoint, leftBoundaryPoint)
const [leftIntersectSegment, leftIntersectPoint] = getFirstIntersectingSegmentInDirection(
leftBoundaryHorizontalSegment,
boundary,
graph,
GraphDirection.LEFT
)
console.log('got leftIntersectSegment', leftBoundaryHorizontalSegment.toQuickDisplay(), leftIntersectSegment.toQuickDisplay(), leftIntersectPoint.coordinate)
leftBoundaryHorizontalSegment = new Segment(verticalMidpoint, leftIntersectPoint)
// Repeat to get the right boundary
let rightBoundaryPoint = Point.from(boundary.xmax, verticalMidpoint.y)
let rightBoundaryHorizontalSegment = new Segment(verticalMidpoint, rightBoundaryPoint)
const [rightIntersectSegment, rightIntersectPoint] = getFirstIntersectingSegmentInDirection(
rightBoundaryHorizontalSegment,
boundary,
graph,
GraphDirection.RIGHT,
)
rightBoundaryHorizontalSegment = new Segment(verticalMidpoint, rightIntersectPoint)
// Now, check if we actually have a 4-bound trapezoid, or if we have a triangle
const points = Point.distinct([
segment.from,
segment.to,
upperIntersectSegment.from,
upperIntersectSegment.to,
])
if ( points.length === 3 ) {
// We found a triangle! Less work.
// Create the triangle and push it onto the graph
const [p1, p2, p3] = points.map(x => graph.findExistingPointOrAdd(x))
const s12 = graph.findExistingSegmentOrAdd(new Segment(p1, p2))
const s23 = graph.findExistingSegmentOrAdd(new Segment(p2, p3))
const s31 = graph.findExistingSegmentOrAdd(new Segment(p3, p1))
graph.findExistingTriangleOrAdd(new Triangle([s12, s23, s31]))
continue // FIXME - remove to handle below-segment case
}
if ( points.length !== 4 ) {
throw new RangeError('Found shape with invalid number of distinct points!')
}
// Now, we have the 4 bounding segments of the trapezoid.
// 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
const leftSegmentIntersectPoint = leftIntersectSegment.getIntersectionWith(segment)
if ( !leftSegmentIntersectPoint ) {
console.log('!leftSegmentIntersectPoint', segment.toQuickDisplay(), leftIntersectSegment.toQuickDisplay())
throw new Error('Unable to find trapezoid segment intersection')
}
// 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
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.
// First, try making triangles from bottom-left to top-right
const lowerLeftPoint = graph.findExistingPointOrAdd(Point.from(segment.xmin, segment.ymin))
const upperRightPoint = graph.findExistingPointOrAdd(Point.from(upperIntersectSegment.xmax, upperIntersectSegment.ymax))
const bottomLeftBisectorSegment = new Segment(lowerLeftPoint, upperRightPoint)
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 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
// 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]))
}
// FIXME handle the lower-trapezoid case
return graph
}