// Scan direction is parallel to the sweepline which moves in the perpendicular direction; // i.e. scan direction is "sideways" along the sweepline. We also have lookahead scans // that enqueue events along the scan-primary coordinate (in the direction of the scan, i.e. // X for Hscan, Y for Vscan) to handle reflections from non-orthogonal obstacle sides, // and lookback scans that have not had their reflections calculated because they reflect import {Direction} from '../../math/geometry/direction' import {Point} from '../../math/geometry/point' import {Polyline} from '../../math/geometry/polyline' import {PolylinePoint} from '../../math/geometry/polylinePoint' import {Rectangle} from '../../math/geometry/rectangle' import {RBNode} from '../../math/RBTree/rbNode' import {SweepEvent} from '../spline/coneSpanner/SweepEvent' import {VisibilityGraph} from '../visibility/VisibilityGraph' import {VisibilityVertex} from '../visibility/VisibilityVertex' import {BasicObstacleSide, HighObstacleSide, LowObstacleSide} from './BasicObstacleSide' import {BasicReflectionEvent} from './basicReflectionEvent' import {BasicVertexEvent} from './BasicVertexEvent' import {EventQueue} from './EventQueue' import {GroupBoundaryCrossingMap} from './GroupBoundaryCrossingMap' import {HighReflectionEvent} from './HighReflectionEvent' import {LookaheadScan} from './LookaheadScan' import {CloseVertexEvent, HighBendVertexEvent, LowBendVertexEvent} from './MiscVertexEvents' import {LowReflectionEvent} from './LowReflectionEvent' import {NeighborSides} from './NeighborSides' import {Obstacle} from './obstacle' import {ObstacleTree} from './ObstacleTree' import {OpenVertexEvent} from './OpenVertexEvent' import {PointComparer} from './PointComparer' import {RectilinearScanLine} from './RectilinearScanLine' import {ScanDirection} from './ScanDirection' import {ScanSegmentTree} from './ScanSegmentTree' import {SpliceUtility} from './SpliceUtility' import {StaticGraphUtility} from './StaticGraphUtility' import {VisibilityVertexRectilinear} from './VisibilityVertexRectiline' import {CompassVector} from '../../math/geometry/compassVector' // backward from the scanline and thus must be picked up on a subsequent perpendicular sweep. export abstract class VisibilityGraphGenerator { // The direction of the current scan - perpendicular to the sweep direction // (i.e. a vertical sweep will scan along a horizontal line). protected ScanDirection: ScanDirection // The visibility graph generated by GenerateVisibilityGraph. VisibilityGraph: VisibilityGraph constructor(wantReflections: boolean) { this.ScanDirection = ScanDirection.HorizontalInstance this.eventQueue = new EventQueue() this.HorizontalScanSegments = new ScanSegmentTree(ScanDirection.HorizontalInstance) this.VerticalScanSegments = new ScanSegmentTree(ScanDirection.VerticalInstance) this.wantReflections = wantReflections } // The events we generate and process during the scan. protected eventQueue: EventQueue // The line segments produced during the vertex event processing. HorizontalScanSegments: ScanSegmentTree VerticalScanSegments: ScanSegmentTree protected get ParallelScanSegments(): ScanSegmentTree { return this.ScanDirection.IsHorizontal ? this.HorizontalScanSegments : this.VerticalScanSegments } protected get PerpendicularScanSegments(): ScanSegmentTree { return this.ScanDirection.IsHorizontal ? this.VerticalScanSegments : this.HorizontalScanSegments } // For lookahead points, we record the point of the intersection on the reflecting side, then // whenever we load a side, we check for active lookahead lines within this range. Since we // are just intersecting rays, we only care about the X (H scan) or Y (V scan) coordinate. lookaheadScan: LookaheadScan private wantReflections: boolean // This is the tree of rectangle nodes of the input obstacles' padded encompassing polylines. ObstacleTree: ObstacleTree = new ObstacleTree() // The scanline of obstacle sides, for processing events. protected scanLine: RectilinearScanLine // This ensures that sentinels lie outside the graph boundaries, to ensure the scanline orders // orders the sentinels properly. Its borders are not drawn; instead the borders of objects // already contain padding and this is assumed to be sufficient to provide space for routing. /* const */ static SentinelOffset = 1 // This is a map of all group boundary crossings for the current scanline event being processed, // including the direction (along the scanline axis, horizontal or vertical) that a path crossing // that boundary takes toward the group's interior. A point may be in here twice in the event of // groups sharing a boundary. protected CurrentGroupBoundaryCrossingMap: GroupBoundaryCrossingMap = new GroupBoundaryCrossingMap() // For scanline traversal. protected LowNeighborSides: NeighborSides = new NeighborSides() protected HighNeighborSides: NeighborSides = new NeighborSides() static NewVisibilityGraph(): VisibilityGraph { const ret = new VisibilityGraph() ret.VertexFactory = (point) => new VisibilityVertexRectilinear(point) return ret } // Generate the visibility graph along which edges will be routed. GenerateVisibilityGraph() { // Generate the Polyline tree from the padded shapes. ObstacleTree allows us to do // obstacle hit-testing for dynamic obstacles, as well as providing input for the Nudger. // Each PolylinePoint contains a reference to the Polyline of which it is a member. if (this.ObstacleTree.Root == null) { return } // Now enqueue initial events for the vertical sweep (horizontal scan); start with the lowest // vertices and then we'll load subsequent vertices (and intersections) as we encounter them. // We'll defer the generation of vertex events in the perpendicular direction until we're done // with this sweep, to save memory; but we may enqueue intersection events in the perpendicular // direction during this sweep. this.InitializeEventQueue(ScanDirection.HorizontalInstance) // Create the sentinels and add them to the scanline. Do NOT add them to the event queue // because we're not going to close them. We're also effectively adding only the inner side // perpendicular to the scan, but we need the polyline for segment-subsumption tracking. // Creating with two points has the lines "clockwise" (and counterclockwise) as above. // // We don't need to store the sentinels as we retrieve them from [SweepEvent].Vertex.Polyline. // And we only need the low sentine's HighSide and the high sentinel's LowSide, but we use both // to save Obstacle from having to worry about the special case. Note: reflection will // automatically not happen when hitting sentinels because their borders are IsPerpendicular. let scanlineSentinelOrdinal: number = Obstacle.FirstSentinelOrdinal // Low sentinel... let lowerCorner = new Point( this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset, ) let upperCorner = new Point( this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset, ) let sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal++) this.scanLine.Insert(sentinel.ActiveHighSide, this.ObstacleTree.GraphBox.leftBottom) // High sentinel... lowerCorner = new Point( this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset, ) upperCorner = new Point( this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset, ) sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal++) this.scanLine.Insert(sentinel.ActiveLowSide, this.ObstacleTree.GraphBox.leftBottom) // Process the Hscan events.` //DevTraceInfoVgGen(1, "Processing Horizontal Scan events"); this.ProcessEvents() // Now do the horizontal sweep (vertical scan). Note: Because we use Cartesian coordinates // rather than Page coordinates, the High and Low sides reverse the direction they traverse // when doing vertical scan; this information is passed as a ctor param. This allows for // consistent comparisons. this.InitializeEventQueue(ScanDirection.VerticalInstance) // Lower sentinel... lowerCorner = new Point( this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset, ) upperCorner = new Point( this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset, ) sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal++) this.scanLine.Insert(sentinel.ActiveHighSide, this.ObstacleTree.GraphBox.leftBottom) // Upper sentinel lowerCorner = new Point( this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset, ) upperCorner = new Point( this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset, this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset, ) sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal) this.scanLine.Insert(sentinel.ActiveLowSide, this.ObstacleTree.GraphBox.leftBottom) // Process the Vscan events. // DevTraceInfoVgGen(1, "Processing Vertical Scan events"); this.ProcessEvents() } // // ReSharper disable InconsistentNaming // protected static Debug_AssertGraphIsRectilinear(graph: VisibilityGraph, this.ObstacleTree: this.ObstacleTree) { // this.#if TEST_MSAGL // if (graph.Edges.Any(edge => !PointComparer.IsPureDirection(PointComparer.GetDirections(edge.SourcePoint, edge.TargetPoint)))) // { // StaticGraphUtility.Assert(false, "Generated VisibilityGraph contains non-rectilinear lines", this.ObstacleTree, graph); // return; // } // this.#endif // } static ScanLineIntersectSidePBS(site: Point, side: BasicObstacleSide, scanDir: ScanDirection): Point { // Note: we don't assert that site and side are not PointComparer.Equal, because ScanLine calls // this on sides that share vertices. /*Assert.assert( !scanDir.IsFlatS(side), 'flat sides should not be in the scanline or encountered on lookahead scan', )*/ // We know that we will have an intersection if the side is adjacent in the scanline, so // we can optimize the calculation to project along the slope of the BasicObstacleSide. // Also, due to rounding, we need to make sure that when intersecting the side, we're not // falling short due to rounding error; that can be a problem if we're right at a vertex // of that obstacle, because then there is no intersection with the perpendicular line // from that vertex. So make sure we are at least to the nearest coordinate of that side. // Note: Calculate slope here using 'dir' rather than side.SlopeInverse because Reflection // lookaheads calculate the perpendicular intersection and side.Slope(Inverse) is always // relative to the scanline parallel. const dir: Point = side.Direction let ix = side.Start.x let iy = side.Start.y if (scanDir.IsHorizontal) { ix += (dir.x / dir.y) * (site.y - side.Start.y) ix = SpliceUtility.MungeIntersect(site.x, ix, side.Start.x, side.End.x) iy = site.y } else { ix = site.x iy += (dir.y / dir.x) * (site.x - side.Start.x) iy = SpliceUtility.MungeIntersect(site.y, iy, side.Start.y, side.End.y) } return new Point(ix, iy) } GetOpenVertex(poly: Polyline): PolylinePoint { let lowest: PolylinePoint = poly.startPoint let next: PolylinePoint = this.TraversePolylineForEvents(lowest) // We want the bottom vertex to be the lowest in scanline-parallel coordinate if there is a // flat bottom side, and we've guaranteed that the lines are oriented clockwise. This means // that we want a <= comparison of the current node vs. the candidate, to store the last // lowest vertex of the clockwise rotation. Stop when we've turned upward from descending/flat. let iPrevCmp: number = this.PointCompare(next.point, lowest.point) for (; ; next = this.TraversePolylineForEvents(next)) { const iCurCmp: number = this.PointCompare(next.point, lowest.point) if (iCurCmp <= 0) { lowest = next } else if (iCurCmp > 0 && iPrevCmp <= 0) { break } iPrevCmp = iCurCmp } return lowest } TraversePolylineForEvents(polyPoint: PolylinePoint): PolylinePoint { // When loading scanline events, we'll go clockwise for horizontal scan, where the // scanline-parallel coordinate increases to the right, or counterclockwise for vertical // scan, where the scanline-parallel coordinate increases to the left. return this.ScanDirection.IsHorizontal ? polyPoint.nextOnPolyline : polyPoint.prevOnPolyline } InitializeEventQueue(scanDir: ScanDirection) { this.ScanDirection = scanDir this.eventQueue.Reset(this.ScanDirection) this.EnqueueBottomVertexEvents() this.scanLine = new RectilinearScanLine(this.ScanDirection, this.ObstacleTree.GraphBox.leftBottom) this.lookaheadScan = new LookaheadScan(this.ScanDirection) } EnqueueBottomVertexEvents() { for (const obstacle of this.ObstacleTree.GetAllPrimaryObstacles()) { const bottomVertex: PolylinePoint = this.GetOpenVertex(obstacle.VisibilityPolyline) this.eventQueue.Enqueue(new OpenVertexEvent(obstacle, bottomVertex)) } } // end EnqueueBottomVertexEvents IsFlat(side: BasicObstacleSide): boolean { return this.ScanDirection.IsFlatS(side) } IsPerpendicular(side: BasicObstacleSide): boolean { // If it's perpendicular we won't generate reflections. return this.ScanDirection.IsPerpendicularS(side) } // Params are event site (vertex point) and the obstacle side adjacent to that site. protected ScanLineIntersectSide(site: Point, side: BasicObstacleSide): Point { return VisibilityGraphGenerator.ScanLineIntersectSidePBS(site, side, this.ScanDirection) } protected SideReflectsUpward(side: BasicObstacleSide): boolean { // Returns false if vertical. if (side instanceof LowObstacleSide) { // Low side slopes upward if slope is positive (to the high direction). return this.ScanDirection.Coord(side.End) > this.ScanDirection.Coord(side.Start) } // High side slopes upward if slope is negative (to the low direction). return this.ScanDirection.Coord(side.End) < this.ScanDirection.Coord(side.Start) } SideReflectsDownward(side: BasicObstacleSide): boolean { // Returns false if vertical. if (side instanceof LowObstacleSide) { // Low side slopes downward if slope is negative (to the low direction). return this.ScanDirection.Coord(side.End) < this.ScanDirection.Coord(side.Start) } // High side slopes downward if slope is positive (to the high direction). return this.ScanDirection.Coord(side.End) > this.ScanDirection.Coord(side.Start) } // Calculate reflections from the lines, depending on line side (Low vs. High) and slope. // Because the low neighbor intersection is on a high side of its obstacle // and vice-versa, then the "side" of a lowNbor is a highSide, and vice versa. protected StoreLookaheadSite(initialObstacle: Obstacle, reflectingSide: BasicObstacleSide, reflectionSite: Point, wantExtreme: boolean) { if (!this.wantReflections) { return } // If the line is perpendicular, we won't generate reflections (they'd be redundant). if (!this.IsPerpendicular(reflectingSide)) { // If this is hitting an extreme vertex in the forward direction, we will (or already did) create a normal // ScanSegment in the perpendicular direction. if (!wantExtreme && !StaticGraphUtility.PointIsInRectangleInterior(reflectionSite, reflectingSide.Obstacle.VisibilityBoundingBox)) { return } // We can only do upward reflections, which fortunately is all we need. if (this.SideReflectsUpward(reflectingSide)) { // We defer actually creating the perpendicular line until we've processed // the reflection event we're about to enqueue, so we may legitimately encounter // two (or more) reflections at the same point along the parallel-to-scanline // coordinate, if we have a side that is nearly but not quite perpendicular to // the scanline. For example: // \ // \----------------------------- line2 // \---------------------------- line1 // Assume the vertical side is very close to perpendicular; then as we process // the horizontal lines in the upward direction, we may generate two lookahead // reflections at coordinates that are sufficiently far apart in the vertical // coordinate (in this example, Y) that the lines are not subsumed, but are // sufficiently close in the horizontal coordinate (in this example, X) that // the perpendicular lines would be subsumed. In that case, we look here to see // if there is an active lookahead scan for the reflecting site's parallel coordinate; // if so, then because we know that the side reflects upward, we also know that // the perpendicular line from the lowest segment's reflection site will intersect // the higher segments here, providing the VisibilityVertices we need, so we can // safely ignore the duplicate lookahead. // Don't worry about enqueueing a reflection at the extreme scanline-parallel // vertex because we'll MergeSegments to handle that. if (this.lookaheadScan.Find(reflectionSite) == null) { this.lookaheadScan.Add(new BasicReflectionEvent(initialObstacle, reflectingSide.Obstacle, reflectionSite)) //DevTraceInfoVgGen(1, "Storing reflection lookahead site {0}", reflectionSite); } else { // DevTraceInfoVgGen(1, "Reflection lookahead site {0} already exists", reflectionSite); } } } } // Load any lookahead scan ray intersections with a side we've just added. LoadReflectionEvents(sideToQueue: BasicObstacleSide) { this.LoadReflectionEventsBB(sideToQueue, sideToQueue) } // sideWithRange is either the same as sideToQueue, if that side is being loaded by an // OpenVertexEvent, or is a different side that is just closing. protected LoadReflectionEventsBB(sideToQueue: BasicObstacleSide, sideWithRange: BasicObstacleSide) { // If this line reflects upward then it cannot receive rays from below (they would pass // through its obstacle), and of course a perpendicular lookahead line will never // intersect a perpendicular side. if (sideToQueue == null || this.SideReflectsUpward(sideToQueue) || this.IsPerpendicular(sideToQueue)) { return } // If there is no overlap in the rectangles along the current axis, there is nothing // to do. This reduces (but doesn't prevent) duplicate events being loaded. const bbox1 = Rectangle.mkPP(sideToQueue.Start, sideToQueue.End) const bbox2 = Rectangle.mkPP(sideWithRange.Start, sideWithRange.End) if (this.ScanDirection.IsHorizontal ? !bbox1.intersectsOnX(bbox2) : !bbox1.intersectsOnY(bbox2)) { return } // Make sure we order the endpoints from low to high along the scanline parallel, and get only // the intersection. RectilinearFileTests.Nudger_Overlap* exercise reflection lookahead subranges. const bboxIntersect: Rectangle = Rectangle.intersect(bbox1, bbox2) const low: Point = bboxIntersect.leftBottom const high: Point = bboxIntersect.rightTop // This is inclusive of the endpoints of sideWithRange, to be sure that we remove the item // from LookaheadScan; if it's on an extreme vertex in the perpendicular sweep then it will // stop the chain; see TestRectilinear.Reflection_Staircase_Stops_At_BoundingBox_Side*. let lookaheadSiteNode: RBNode = this.lookaheadScan.FindFirstInRange(low, high) while (lookaheadSiteNode != null) { // Calculate the lookahead intersection with this side in the perpendicular direction to // the scanline. Note: due to rounding error, this may be different from the calculation // in the parallel direction when the scanline gets up to the ScanDirection.PerpCoord(intersect); // this will be adjusted in ScanSegmentTree.MergeSegments. const intersect: Point = VisibilityGraphGenerator.ScanLineIntersectSidePBS( lookaheadSiteNode.item.Site, sideToQueue, this.ScanDirection.PerpendicularInstance, ) //DevTraceInfoVgGen(1, "Loading reflection from lookahead site {0} to intersect at {1}", lookaheadSiteNode.item.Site, intersect); // DevTraceInfoVgGen(2, " side {0})", sideToQueue); // same indent as AddSegment // In some cases where the ActiveLowSide and ActiveHighSide of an obstacle lean in the same // direction such that LowSide is above HighSide, e.g. on the horizontal pass when they both // lean to the right, the delayed-lookahead in CloseVertex (obstacle close) event may find the // high side spanning the scanline-parallel coordinate range where its low side has enqueued // lookahead events. In that case the intersection will be less than the enqueueing site so // ignore it. See RectilinearTests.ReflectionsSitedByLowSideAreNotLoadedByHighSide.) // Similarly, if this is at the same perpendicular coordinate as the current scanline // position, ignore it; otherwise we could back up in the scanline's parallel coordinate. // Since we retrieved events for the endpoint of any previous side, this won't be // encountered on a bend vertex event; therefore we're on a near-flat bottom side // so we're parallel to the extreme-vertex line and it's fine to just absorb the photon. // This also handles the case of reflections into intersecting sides - at some point // they converge such that the intersection is not ahead of the lookahead site. if (this.ScanDirection.ComparePerpCoord(intersect, lookaheadSiteNode.item.Site) > 0) { // Add an event to continue the chain, "shifting" the site's reflecting // obstacle back to the initialObstacle position. We must load this here // and process it in ConfirmLookaheadEvent so it will be removed from // the lookahead list; we can't remove it here if it doesn't satisfy the // staircase requirements, because this may be called from loading a "higher" // side (during the sweep) which could steal events from the lower side. this.AddReflectionEvent(lookaheadSiteNode.item, sideToQueue, intersect) } else if (lookaheadSiteNode.item.ReflectingObstacle !== sideToQueue.Obstacle) { //DevTraceInfoVgGen(1, " (discarding reflection at intersect {0} as it is not ahead of the previous site)", intersect); // We need to remove the site. We're in the middle of Node enumeration so just // mark the site and on function exit we'll remove any so marked. this.lookaheadScan.MarkStaleSite(lookaheadSiteNode.item) } else { // DevTraceInfoVgGen(1, " (skipping reflection at intersect {0} as it is the same obstacle)", intersect); } // Get the next item, leaving the current one in the lookahead scan until // we actually process the event; this lets us know whether an intervening // obstacle may be opened and intercepted the reflection. ConfirmLookaheadEvents // will actually do the removal when the lowest side containing the lookahead // site is loaded. See RectilinearTests.ReflectionsRemoveInterceptedSite. lookaheadSiteNode = this.lookaheadScan.FindNextInRange(lookaheadSiteNode, high) } // endwhile previousSiteNode this.lookaheadScan.RemoveStaleSites() } // Determine whether the event is valid and do some common processing. AddPerpendicularReflectionSegment( currentEvent: BasicReflectionEvent, eventSide: BasicObstacleSide, nborSide: BasicObstacleSide, ): boolean { // If eventSide is null it means we had the wrong side type as a scanline neighbor. // If another obstacle opened up, then that obstacle (or another intervening one) should have // drained this reflection event. /*Assert.assert(null != eventSide, 'eventSide should not be null')*/ // Between the time currentEvent was queued and the time we're now processing it, another // obstacle may have opened between the previousSite and the eventSite, in which case it // removed currentEvent from the queue already. So currentEvent may be stale. The new // obstacle may have stored *another* lookahead site with the same scanline-parallel // coordinate (but higher up perpendicularly). So remove the exact site of currentEvent; // otherwise the currentEvent could be a stale event with the lower scanline-parallel // coordinate, and would remove the site from the lookahead list before the "live" event // looks for it. See RectilinearTests.ReflectionsRemoveInterceptedSite. if (this.lookaheadScan.RemoveExact(currentEvent.PreviousSite)) { /*Assert.assert( currentEvent.InitialObstacle == currentEvent.PreviousSite.ReflectingObstacle, 'Inconsistency: currentEvent.InitialObstacle !== currentEvent.PreviousSite.ReflectingObstacle', )*/ // ReSharper disable HeuristicUnreachableCode // ReSharper disable ConditionIsAlwaysTrueOrFalse if (eventSide == null) { // We've removed the event so there's nothing else to do. return false } // ReSharper restore ConditionIsAlwaysTrueOrFalse // ReSharper restore HeuristicUnreachableCode // If the two sides intersect ahead of the scanline, we don't want the reflection. // If the reflecting side is flat, no reflection is done - that's handled by OpenVertexEvent. /*Assert.assert( !this.IsFlat(eventSide), 'Flat sides should not be encountered in reflections', )*/ if (currentEvent.PreviousSite.IsStaircaseStep(currentEvent.ReflectingObstacle)) { // We need to draw the perpendicular lines here because we may be on the second // sweep so there won't be a subsequent sweep to draw them. And if we're on the // second sweep, we may have already loaded this segment as part of a continuation // of an overlapped segment. Either way, we only want this if we are not on an extreme // edge of the target obstacle (reflectingObstacle for the perpendicular segment, // nborSide.Obstacle for the parallel segment). Extreme vertices will generate segments. // See TestRectilinear.Reflection_Staircase_Stops_At_BoundingBox_Side*. if (!StaticGraphUtility.PointIsInRectangleInterior(currentEvent.Site, currentEvent.ReflectingObstacle.VisibilityBoundingBox)) { return false } //DevTraceInfoVgGen(1, "Perpendicular Reflection - Adding Segment [{0} -> {1}]", currentEvent.PreviousSite.Site, currentEvent.Site); //DevTraceInfoVgGen(2, " -> side {0}", eventSide); // same indent as AddSegment; eventSide is highNbor if (!this.InsertPerpendicularReflectionSegment(currentEvent.PreviousSite.Site, currentEvent.Site)) { return false } // If the neighbor continues the staircase and the parallel segment would hit a non-extreme point // on the neighbor, return true and the Low/HighReflectionEvent handler will add the parallel segment. if (nborSide != null && currentEvent.IsStaircaseStep(nborSide.Obstacle)) { return this.ScanLineCrossesObstacle(currentEvent.Site, nborSide.Obstacle) } //DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is not an outgoing staircase step; discontinuing", currentEvent.PreviousSite); } else { //DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is not an incoming staircase step; discontinuing", currentEvent.PreviousSite); } } else { //DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is no longer in the lookahead table; skipping", currentEvent.PreviousSite); } return false } protected abstract InsertPerpendicularReflectionSegment(start: Point, end: Point): boolean private AddParallelReflectionSegment( eventObstacle: Obstacle, lowNborSide: BasicObstacleSide, highNborSide: BasicObstacleSide, action: BasicReflectionEvent, ): boolean { { // If this is reflecting to a low neighbor, then that intersect is 'start' in the low-to-high // sequence, and the event site is the end; otherwise we start at the event site and end at // the high neighbor. const intersect = this.ScanLineIntersectSide(action.Site, lowNborSide ?? highNborSide) const start = lowNborSide != null ? intersect : action.Site const end = lowNborSide != null ? action.Site : intersect // Now get the opposite neighbors so AddSegment can continue the reflection chain. if (lowNborSide == null) { lowNborSide = this.scanLine.NextLowB(highNborSide).item } else { highNborSide = this.scanLine.NextHighB(lowNborSide).item } return this.InsertParallelReflectionSegment(start, end, eventObstacle, lowNborSide, highNborSide, action) } } abstract InsertParallelReflectionSegment( start: Point, end: Point, eventObstacle: Obstacle, lowNborSide: BasicObstacleSide, highNborSide: BasicObstacleSide, action: BasicReflectionEvent, ): boolean AddReflectionEvent(previousSite: BasicReflectionEvent, side: BasicObstacleSide, site: Point): void { /*Assert.assert( null != this.scanLine.Find(side), "AddReflectionEvent could not find 'side' in the scanline", )*/ // Add an event that will be drained when a side spanning the scanline-parallel is loaded // as the sweep moves "up". const lowSide = side if (lowSide != null) { this.eventQueue.Enqueue(new LowReflectionEvent(previousSite, lowSide, site)) } else { this.eventQueue.Enqueue(new HighReflectionEvent(previousSite, side, site)) } } AddSideToScanLine(side: BasicObstacleSide, scanPos: Point): RBNode { const node: RBNode = this.scanLine.Insert(side, scanPos) // Now get any pending LookaheadScan intersections along this side. this.LoadReflectionEvents(side) return node } RemoveSideFromScanLine(sideNode: RBNode, scanPos: Point) { this.scanLine.Remove(sideNode.item, scanPos) } PointCompare(lhs: Point, rhs: Point): number { return this.ScanDirection.Compare(lhs, rhs) } Clear() { this.ObstacleTree.Clear() this.eventQueue = new EventQueue() this.HorizontalScanSegments = new ScanSegmentTree(ScanDirection.HorizontalInstance) this.VerticalScanSegments = new ScanSegmentTree(ScanDirection.VerticalInstance) this.VisibilityGraph = null } private ProcessEvents() { // Note: Sentinel vertices are not in EventQueue so eventcount will go to 0. while (this.eventQueue.Count > 0) { const evt: SweepEvent = this.eventQueue.Dequeue() if (evt instanceof OpenVertexEvent) { this.ProcessEventO(evt) } else if (evt instanceof LowBendVertexEvent) { this.ProcessEventLB(evt) } else if (evt instanceof HighBendVertexEvent) { this.ProcessEventHB(evt) } else if (evt instanceof CloseVertexEvent) { this.ProcessEventCV(evt) } else if (evt instanceof LowReflectionEvent) { this.ProcessEventLR(evt) } else if (evt instanceof HighReflectionEvent) { this.ProcessEventHR(evt) } else { this.ProcessCustomEvent(evt) } this.LowNeighborSides.Clear() this.HighNeighborSides.Clear() } // endwhile there are events // Ensure we have no leftovers in the scanline - we should have the two sentinels and nothing else. /*Assert.assert( 2 === this.scanLine.Count, 'There are leftovers in the scanline', )*/ } ProcessCustomEvent(evt: SweepEvent) { // These are events specific to the derived class; by default there are none. /*Assert.assert(false, 'Unknown event type ' + evt)*/ } protected ScanLineCrossesObstacle(eventSite: Point, obstacle: Obstacle): boolean { // An inner or outer neighbor's side is only an overlap start/stop candidate if its obstacle // brackets the open/close event's Perpendicular Scan coord. return ( this.ScanDirection.ComparePerpCoord(eventSite, obstacle.VisibilityBoundingBox.leftBottom) > 0 && this.ScanDirection.ComparePerpCoord(eventSite, obstacle.VisibilityBoundingBox.rightTop) < 0 ) } FindInitialNeighborSides( sideNode: RBNode, t: { lowNborSideNode: RBNode highNborSideNode: RBNode }, ) { t.lowNborSideNode = this.scanLine.NextLowR(sideNode) t.highNborSideNode = this.scanLine.NextHighR(sideNode) } // As described in the doc, we stop at the first neighbor of the appropriate side type that we touch // the border of, even if that's just skimming along the extreme vertex of it, because those will // continue the chain of open/close+addSegment, and we don't want to follow the full length of the // segment each time if there are a lot of collinear obstacle open/close events. protected FindNeighborsBRR( vertexEvent: BasicVertexEvent, lowSideNode: RBNode, highSideNode: RBNode, ) { this.LowNeighborSides.Clear() this.HighNeighborSides.Clear() // Find the first HighObstacleSide in the low (scanline-decreasing) direction (this may be the low // sentinel) and the lowest LowObstacleSide toward that that we cross *through*, if any. Then do // the same thing in the high direction. If we are not overlapped, then we'll jump out immediately // from SkipToNeighbor, so there won't be a lot of redundant effort in that case. this.FindNeighbors(vertexEvent, lowSideNode, this.LowNeighborSides) this.FindNeighbors(vertexEvent, highSideNode, this.HighNeighborSides) } protected FindNeighbors(vertexEvent: BasicVertexEvent, sideNode: RBNode, neighborSides: NeighborSides) { // vertexEvent.Site is on one of vertexEvent.Obstacle.Active(Low|High)Side, so we must get the // appropriate vertex on whichever one of those Active*Sides is sideNode. const sideReferencePoint = vertexEvent instanceof OpenVertexEvent ? sideNode.item.Start : sideNode.item.End const t: { lowNborSideNode: RBNode highNborSideNode: RBNode } = {lowNborSideNode: null, highNborSideNode: null} this.FindInitialNeighborSides(sideNode, t) this.SkipToNeighbor(this.ScanDirection.OppositeDirection, sideNode.item, sideReferencePoint, t.lowNborSideNode, neighborSides) this.SkipToNeighbor(this.ScanDirection.Dir, sideNode.item, sideReferencePoint, t.highNborSideNode, neighborSides) } SkipToNeighbor( nborSearchDir: Direction, side: BasicObstacleSide, sideReferencePoint: Point, nborNode: RBNode, neighborSides: NeighborSides, ) { // Find the first neighbor side (LowObstacleSide if going high, HighObstacleSide if going low) and // the side of opposite type (which would potentially end overlap), that that we cross *through*, if any. let overlapSideNode: RBNode = null let interveningGroupSide: BasicObstacleSide = null for (; ; nborNode = this.scanLine.Next(nborSearchDir, nborNode)) { // Ignore the opposite side of the current obstacle. if (nborNode.item.Obstacle === side.Obstacle) { continue } if (nborNode.item.Obstacle.IsGroup) { if (this.ProcessGroupSideEncounteredOnTraversalToNeighbor(nborNode, sideReferencePoint, nborSearchDir)) { // Keep the first one (outermost) encountered. if (interveningGroupSide == null) { interveningGroupSide = nborNode.item } } continue } // Check for overlap-ending obstacle. if (nborNode.item instanceof HighObstacleSide === StaticGraphUtility.IsAscending(nborSearchDir)) { if (this.ScanLineCrossesObstacle(sideReferencePoint, nborNode.item.Obstacle)) { overlapSideNode = nborNode interveningGroupSide = null } continue } // If we're here, we found the neighbor we were looking for. break } neighborSides.SetSides(nborSearchDir, nborNode, overlapSideNode, interveningGroupSide) } // end this.ProcessEvent(CloseVertexEvent) private ProcessGroupSideEncounteredOnTraversalToNeighbor( nborNode: RBNode, sideReferencePoint: Point, nborSearchDir: Direction, ): boolean { if (!this.ScanLineCrossesObstacle(sideReferencePoint, nborNode.item.Obstacle)) { return false } // We don't stop overlap or neighbor-traversal for groups, because we must go through the boundary; // neither do we create overlapped edges (unless we're inside a non-group obstacle). Instead we turn // the boundary crossing on or off based on group membership at ShortestPath-time. const dirToInsideOfGroup: Direction = nborNode.item instanceof LowObstacleSide === StaticGraphUtility.IsAscending(nborSearchDir) ? nborSearchDir : CompassVector.OppositeDir(nborSearchDir) const intersect = this.ScanLineIntersectSide(sideReferencePoint, nborNode.item) this.CurrentGroupBoundaryCrossingMap.AddIntersection(intersect, nborNode.item.Obstacle, dirToInsideOfGroup) return true } FindNeighborsAndProcessVertexEvent( lowSideNode: RBNode, highSideNode: RBNode, vertexEvent: BasicVertexEvent, ) { this.CurrentGroupBoundaryCrossingMap.Clear() this.FindNeighborsBRR(vertexEvent, lowSideNode, highSideNode) this.ProcessVertexEvent(lowSideNode, highSideNode, vertexEvent) // Clear this again because we don't want Reflections to access stale values. this.CurrentGroupBoundaryCrossingMap.Clear() } protected abstract ProcessVertexEvent( lowSideNode: RBNode, highSideNode: RBNode, vertexEvent: BasicVertexEvent, ): void ProcessEventO(openVertEvent: OpenVertexEvent) { // First insert the two new lines into the scanline. Note: Although the lines are clockwise oriented, // LowObstacleSide and HighObstacleSide take a parameter to know when to go counterclockwise. const obstacle = openVertEvent.Obstacle obstacle.CreateInitialSides(openVertEvent.Vertex, this.ScanDirection) /*Assert.assert( !this.IsFlat(obstacle.ActiveLowSide), 'OpenVertexEvent ActiveLowSide should not be flat', )*/ /*Assert.assert( !this.IsFlat(obstacle.ActiveHighSide), 'RemoveCollinearSides should have been called', )*/ //DevTraceIfFlatSide(true, obstacle.ActiveLowSide.Start, obstacle.ActiveHighSide.Start); // Adding can rotate the RBTree which modifies RBNodes so get the lowSideNode after adding highSideNode. // AddSideToScanLine loads any reflection events for the side. this.AddSideToScanLine(obstacle.ActiveLowSide, openVertEvent.Site) const highSideNode: RBNode = this.AddSideToScanLine(obstacle.ActiveHighSide, openVertEvent.Site) const lowSideNode: RBNode = this.scanLine.Find(obstacle.ActiveLowSide) // Get the neighbors. In the simple, non-overlapped case, we'll generate a segment between them which // includes the vertex point (and any flat border of the current obstacle). These neighbors will // never be null; one or both may be the fake sentinel borders at the graphBox limits. this.FindNeighborsAndProcessVertexEvent(lowSideNode, highSideNode, openVertEvent) // Look for Reflections. If the ScanSegments we just added generated any // ReflectionEvents, then the Active*Side to that side may cover them, or if we're overlapped, // the Active*Side of the overlapping obstacle may if there is a non-overlapped segment extension. // Check the neighbors in both directions. const lowReflector = this.LowNeighborSides.GroupSideInterveningBeforeLowNeighbor ?? this.LowNeighborSides.LowNeighborSide if (this.SideReflectsUpward(lowReflector)) { this.LoadReflectionEvents(obstacle.ActiveLowSide) } const highReflector = this.HighNeighborSides.GroupSideInterveningBeforeHighNeighbor ?? this.HighNeighborSides.HighNeighborSide if (this.SideReflectsUpward(highReflector)) { this.LoadReflectionEvents(obstacle.ActiveHighSide) } // If this is a flat side it must absorb any outstanding reflection sites. if (obstacle.ActiveHighSide.Start !== obstacle.ActiveLowSide.Start) { // Create a temp HighObstacleSide so the "next vertex" moves in the correct direction. const tempSide = new HighObstacleSide(obstacle, openVertEvent.Vertex, this.ScanDirection) this.lookaheadScan.RemoveSitesForFlatBottom(tempSide.Start, tempSide.End) } // Add events for the low and high sides. this.EnqueueLowBendVertexEvent(obstacle.ActiveLowSide) this.EnqueueHighBendOrCloseVertexEvent(obstacle.ActiveHighSide) } // end this.ProcessEvent(OpenVertexEvent) ProcessEventLB(lowVertEvent: LowBendVertexEvent) { // Note: we only draw lines only from "interesting" vertices, which would be those // that open or close an obstacle, as well as staircase Reflections (see doc). This means // Low/HighVertexEvents routines will just track the change in ActiveLowSide/ActiveHighSide. // This is a vertex on the low side of the obstacle. Update the ActiveLowSide in the obstacle // and scanline (this also checks for Reflection events). const obstacle = lowVertEvent.Obstacle const lowSide = new LowObstacleSide(obstacle, lowVertEvent.Vertex, this.ScanDirection) // If the new lowSide is flat we don't remove it due to potential overlaps; that lets any collinear // OpenVertexEvents know they are overlapped (touching === overlap). When we get to CloseVertexEvent, // keeping the current ActiveLowSide in the scanline tells us how when the interior sides stop // so we know when we've found a neighbor. Similarly, if we're turning down toward the // scanline, we let CloseVertexEvent remove the side (in case there are coincident vertices). // That leaves the case of still ascending, where we replace the side in the scanline. if (this.ScanDirection.ComparePerpCoord(lowSide.End, lowSide.Start) > 0) { this.RemoveSideFromScanLine(this.scanLine.Find(obstacle.ActiveLowSide), lowVertEvent.Site) this.AddSideToScanLine(lowSide, lowVertEvent.Site) obstacle.ActiveLowSide = lowSide this.EnqueueLowBendVertexEvent(lowSide) } } // end this.ProcessEvent(LowBendVertexEvent) EnqueueLowBendVertexEvent(lowSide: BasicObstacleSide) { // We've already ensured the extension is valid so just queue the next event. this.eventQueue.Enqueue(new LowBendVertexEvent(lowSide.Obstacle, lowSide.EndVertex)) } ProcessEventHB(highVertEvent: HighBendVertexEvent) { // See comments in LowBendVertexEvent; this is mostly the same thing to the other side. const obstacle = highVertEvent.Obstacle const highSide = new HighObstacleSide(obstacle, highVertEvent.Vertex, this.ScanDirection) this.RemoveSideFromScanLine(this.scanLine.Find(obstacle.ActiveHighSide), highVertEvent.Site) const highSideNode: RBNode = this.AddSideToScanLine(highSide, highVertEvent.Site) obstacle.ActiveHighSide = highSide this.EnqueueHighBendOrCloseVertexEvent(obstacle.ActiveHighSide) // If this is an extreme high-side lateral vertex on the horizontal pass turning to an upward-reflecting // side - i.e. if it is a vertex on the right-most border of the bounding box, and the new HighObstacleSide // has negative slope - then we may have a situation where a neighbor LowObstacleSide reflects downward and // spans this entire HighObstacleSide and a little more: // # // # // . # // \ # // . # // # // The ".\." side is completely spanned by the '#' side and because of the tilt directions, lookahead // will not happen, therefore reflections will not happen and there will be no scansegments between the // two sides. This could lead to spurious overlaps. So in this case we drop in an extra lookahead. // (This is not an issue for other tilt directions - there is always a reflection chain generated). // Test is RectilinearFileTests.Overlap_ExtremeSide_Lookahead. if ( this.wantReflections && this.ScanDirection.IsHorizontal && highSide.Start.x === obstacle.VisibilityBoundingBox.right && this.SideReflectsUpward(highSide) ) { const nborSideNode = this.scanLine.NextHighR(highSideNode) if (nborSideNode.item instanceof LowObstacleSide && this.SideReflectsDownward(nborSideNode.item)) { if (!obstacle.isOverlapped || !this.ObstacleTree.PointIsInsideAnObstacle(highSide.Start, this.ScanDirection)) { this.StoreLookaheadSite(nborSideNode.item.Obstacle, highSide, highSide.Start, true) this.LoadReflectionEvents(nborSideNode.item) } } } } EnqueueHighBendOrCloseVertexEvent(highSide: BasicObstacleSide) { // If the next side segment after highSide is ascending from the scanline we want to queue another // HighBendVertexEvent; otherwise it is flat or turns down toward the scanline so queue a CloseVertexEvent. const obstacle: Obstacle = highSide.Obstacle const nextHighSideEnd: PolylinePoint = this.ScanDirection.IsHorizontal ? highSide.EndVertex.prevOnPolyline : highSide.EndVertex.nextOnPolyline if (this.ScanDirection.ComparePerpCoord(nextHighSideEnd.point, highSide.End) > 0) { this.eventQueue.Enqueue(new HighBendVertexEvent(obstacle, highSide.EndVertex)) } else { this.eventQueue.Enqueue(new CloseVertexEvent(obstacle, highSide.EndVertex)) } } // end this.ProcessEvent(HighBendVertexEvent) CreateCloseEventSegmentsAndFindNeighbors(closeVertEvent: CloseVertexEvent) { const obstacle = closeVertEvent.Obstacle // DevTraceIfFlatSide(false, obstacle.ActiveLowSide.End, obstacle.ActiveHighSide.End); let lowSideNode: RBNode = this.scanLine.Find(obstacle.ActiveLowSide) let highSideNode: RBNode = this.scanLine.Find(obstacle.ActiveHighSide) // Two sides coming together at a top point will be reverse-ordered in the scanline, // because their projections ahead of the intersection are slope-based. This must // be the case in order to maintain scanline consistency. Therefore we need to // check the comparison and reverse the local variables if necessary. Fortunately // we only concern ourselves with the actual Low-vs-High side type for neighbors, // not the sides of the obstacle. if (1 === this.scanLine.Compare(obstacle.ActiveLowSide, obstacle.ActiveHighSide)) { const temp = lowSideNode lowSideNode = highSideNode highSideNode = temp } // As with OpenVertexEvent, the idea here is to find the neighbors and draw the line between them // that includes the event vertex (and any flat top obstacle side), with consideration for overlaps. this.FindNeighborsAndProcessVertexEvent(lowSideNode, highSideNode, closeVertEvent) // Inner overlaps: any overlapped sides coming out of the obstacle must have reflection events // drained for any that were generated from sides of the closing obstacle if they extend // outside the closing obstacle. if (this.wantReflections && obstacle.isOverlapped) { for ( let nextNode: RBNode = this.scanLine.NextHighR(lowSideNode); nextNode.item !== highSideNode.item; nextNode = this.scanLine.NextHighR(nextNode) ) { this.LoadReflectionEvents(nextNode.item) } } // Remove the obstacle from the Scanline. This comes after the foregoing which is why it's a // separate function; the RBTree modifies RBNodes, so doing the .Remove at the end of a separate // function ensures we won't access RBNodes that may no longer contain what we expect. this.scanLine.Remove(obstacle.ActiveLowSide, closeVertEvent.Site) this.scanLine.Remove(obstacle.ActiveHighSide, closeVertEvent.Site) } ProcessEventCV(closeVertEvent: CloseVertexEvent) { // This event closes the obstacle. It removes the ActiveLowSide and ActiveHighSide from the scanline and does // not add new sides. As above, see comments in OpenVertexEvent and its callees for more detailed explanations. this.CreateCloseEventSegmentsAndFindNeighbors(closeVertEvent) // For reflection, in addition to the delayed lookahead we do in the other *VertexEvents, we need to // detect pending reflections up to an already loaded obstacle side. This may be transitive; using // H directions as an example: // (A). Obstacles A and B lean rightward such that a vertical line can be drawn from the lower side // of ObstacleA downward to the right of ObstacleB (missing it) and hitting Obstacle C. // (B). ObstacleC's start point is above the start points of Obstacles A and B. // (C). ObjectC reflects a Lookahead scan up along the line cited in (A). // To handle this, whenever we close an object, see if either nbor side spans lookahead scan points. // If so, then add the events. Otherwise, we know that a new side for those nbor objects, or for // subsequent higher objects, will be created at some point (unless we run out of obstacles). // Since we do reflections only in staircase situations, these lookahead events will be discarded // (because the existing edge would have to have already been processed as an immediate neighbor, // in order to satisfy the definition of a staircase). See RectilinearTests.ReflectionsDetectedByAlreadyLoadedSide. // Check the neighbors in both directions for reflection events. // When querying for lookahead sites for a nborSide, always test the full nborSide range if // the opposite nborSide reflects upward, because we will have generated a lookahead site for the // current eventObstacle on that upward-reflecting nborSide. For example, restricting the // lowNborSide lookahead-site query to the currentEventObstacle.ActiveLowSide would pick up // any lookahead sites stored on eventObstacle.ActiveLowSide, but would not pick up a lookahead // site that the current CloseVertexEvent just stored on the highNborSide). // Fix: Updated this to remove restricted-range as it skips the range that includes the far side of the // current obstacle when called for neighbors that extend across the top vertex. const lowNborSide = this.LowNeighborSides.LowNeighbor.item const highNborSide = this.HighNeighborSides.HighNeighbor.item const obstacle = closeVertEvent.Obstacle this.LoadReflectionEvents(lowNborSide) this.LoadReflectionEvents(highNborSide) // This prepares the object for the second (perpendicular) sweep. obstacle.Close() } ProcessEventLR(lowIntEvent: LowReflectionEvent) { // Unlike LowBendVertexEvent we don't update the ActiveLowSide in the obstacle and scanline. const obstacle = lowIntEvent.Side.Obstacle // Add a perpendicular segment from the previous site to the lowNbor intersection, then from // the lowNbor intersection to the event site. const lowNborSide = this.scanLine.NextLowB(lowIntEvent.Side).item if (this.AddPerpendicularReflectionSegment(lowIntEvent, lowIntEvent.Side, lowNborSide)) { if (this.AddParallelReflectionSegment(obstacle, lowNborSide, null, lowIntEvent)) { // We may have just added a reflection that reflects back onto obstacle.ActiveLowSide. this.LoadReflectionEvents(obstacle.ActiveLowSide) } } } // end this.ProcessEvent(LowReflectionEvent) ProcessEventHR(highIntEvent: HighReflectionEvent) { // Unlike HighBendVertexEvent we don't update the ActiveHighSide in the obstacle and scanline. const obstacle = highIntEvent.Side.Obstacle // Add a perpendicular segment from the previous site to the highNbor intersection, then from // the highNbor intersection to the event site. const highNborSide = this.scanLine.NextHighB(highIntEvent.Side).item if (this.AddPerpendicularReflectionSegment(highIntEvent, highIntEvent.Side, highNborSide)) { if (this.AddParallelReflectionSegment(obstacle, null, highNborSide, highIntEvent)) { // We may have just added a reflection that reflects back onto obstacle.ActiveHighSide. this.LoadReflectionEvents(obstacle.ActiveHighSide) } } } // end this.ProcessEvent(HighReflectionEvent) MakeInBoundsLocation(location: Point): Point { const xPos: number = Math.max(location.x, this.ObstacleTree.GraphBox.left) const yPos: number = Math.max(location.y, this.ObstacleTree.GraphBox.bottom) return new Point(Math.min(xPos, this.ObstacleTree.GraphBox.right), Math.min(yPos, this.ObstacleTree.GraphBox.top)) } IsInBoundsV(vertex: VisibilityVertex): boolean { return this.IsInBoundsP(vertex.point) } IsInBoundsP(p: Point): boolean { return PointComparer.EqualPP(p, this.MakeInBoundsLocation(p)) } }