import { copy, create, cross, equal, scaleAndAdd, sub } from '../vector'; import { assert } from './langUtil'; import MovingNode from './MovingNode'; import SkeletonEvent from './SkeletonEvent'; import SkeletonContext from './SkeletonContext'; export default class SplitEvent extends SkeletonEvent { readonly reflexNode: MovingNode; readonly op0: MovingNode; // Opposite edge start readonly op1: MovingNode; // Opposite edge end eventPriority = 2; constructor( reflexNode: MovingNode, opposite0: MovingNode, opposite1: MovingNode, time: number ) { super(time); this.reflexNode = reflexNode; this.op0 = opposite0; this.op1 = opposite1; assert(reflexNode != opposite0); assert(reflexNode != opposite1); assert(opposite0 != opposite1); assert(opposite0.next == opposite1); } public static calcTime( reflexNode: MovingNode, edgeStart: MovingNode, distanceSign: number ) { // Calc component of bisector orthogonal to edge (perpendicular dot product) const bisectorSpeed = cross(reflexNode.bisector, edgeStart.edgeDir); const edgeSpeed = -distanceSign; const approachSpeed = bisectorSpeed + edgeSpeed; // Check on which side the reflex node lies, relative to directed edge. // The determinant's sign indicates the side. Its magnitude is the orthogonal distance of the reflex node to the edge. // (Component of 'reflexRelative' orthogonal to edgeDir) const reflexRelative = sub( create(), reflexNode.skelNode.p, edgeStart.skelNode.p ); const sideDistance = cross(reflexRelative, edgeStart.edgeDir); if (sideDistance == 0) { return SplitEvent.canHit(reflexNode, edgeStart, distanceSign, 0); } // Negative speed means distance between reflex vertex and opposite edge increases with time if (SplitEvent.correctSpeed(approachSpeed, sideDistance) <= 0) { return this.INVALID_TIME; } // One of these values will be negative. The resulting time is always positive. const time = -sideDistance / approachSpeed; return SplitEvent.canHit(reflexNode, edgeStart, distanceSign, time); } private static correctSpeed(approachSpeed: number, sideDistance: number) { // Adjust speed to side. return sideDistance > 0 ? -approachSpeed : approachSpeed; } /** * Check if reflex actually collides with opposite edge in the future. * Do this before creating the event and not inside of handle() to avoid creating unnecessary events * which would slow down the queue and introduce superfluous scaling steps and hence rounding errors. */ private static canHit( reflexNode: MovingNode, edgeStart: MovingNode, distanceSign: number, time: number ) { // Check if edge collapses before split occurs. If edge grows (invalid edgeCollapseTime = NaN), this will evaluate to false. if (time >= edgeStart.edgeCollapseTime) { return this.INVALID_TIME; } // This check is not reliable because there could be another event that prevents the collapse of neighboring edges. /* // Check if reflexNode's neighbor edges collapse before split occurs. This would abort the SplitEvent anyway, making it superfluous. if(time >= reflexNode.edgeCollapseTime || time >= reflexNode.prev.edgeCollapseTime) { return INVALID_TIME; } */ // Check on which side 'reflexFuture' lies relative to the bisectors at start and end of this edge const reflexFuture = scaleAndAdd( create(), reflexNode.skelNode.p, reflexNode.bisector, time ); const reflexRelative = sub(create(), reflexFuture, edgeStart.skelNode.p); const side0 = cross(edgeStart.bisector, reflexRelative); if (side0 * distanceSign < 0) return this.INVALID_TIME; const edgeEnd = edgeStart.next!; sub(reflexRelative, reflexFuture, edgeEnd.skelNode.p); const side1 = cross(edgeEnd.bisector, reflexRelative); if (side1 * distanceSign > 0) return this.INVALID_TIME; return time; } public onEventQueued() { this.reflexNode.addEvent(this); this.op0.addEvent(this); this.op1.addEvent(this); } public onEventAborted(adjacentNode: MovingNode, ctx: SkeletonContext) { const reflexNode = this.reflexNode; const op0 = this.op0; const op1 = this.op1; ctx.addAbortedReflex(reflexNode); if (adjacentNode == reflexNode) { op0.removeEvent(this); op1.removeEvent(this); } else if (adjacentNode == op0) { reflexNode.removeEvent(this); op1.removeEvent(this); } else { assert(adjacentNode == op1); reflexNode.removeEvent(this); op0.removeEvent(this); } } public onEventAborted2( edgeNode0: MovingNode, edgeNode1: MovingNode, ctx: SkeletonContext ) { ctx.addAbortedReflex(this.reflexNode); this.reflexNode.removeEvent(this); } public handle(ctx: SkeletonContext) { assert(this.op0.next == this.op1); ctx.abortEvents2(this.op0, this.op1); const node0 = this.reflexNode; const reflexNext = this.reflexNode.next!; const reflexPrev = this.reflexNode.prev!; const op0 = this.op0; const op1 = this.op1; this.reflexNode.skelNode.setReflex(); const node1 = ctx.createMovingNode(this.reflexNode.id + '+'); node1.spawnCount = this.reflexNode.spawnCount + 1; if (node1.spawnCount > 1e4) { throw `Possible infinite loop when handling node ${this.reflexNode.skelNode.p}`; } // Both MovingNodes use same SkeletonNode which will stay at this place. // If they receive a valid bisector, a new SkeletonNode is made for them later in handle(). node1.skelNode = node0.skelNode; // Update node0 links assert(node0.next == reflexNext); assert(reflexNext.prev == node0); // if (!equal(node0.skelNode.p, op0.skelNode.p)) { // node0.prev = op0; // op0.next = node0; // SkeletonEvent.handle(node0, ctx); // Aborts events of reflexNode // } else { // // Remove node0 // op0.next = reflexNext; // reflexNext.prev = op0; // ctx.removeMovingNode(node0); // } // // Update node1 links // if (!equal(node1.skelNode.p, op1.skelNode.p)) { // node1.next = op1; // op1.prev = node1; // node1.prev = reflexPrev; // reflexPrev.next = node1; // SkeletonEvent.handle(node1, ctx); // } node0.prev = op0; op0.next = node0; // Update node1 links node1.next = op1; op1.prev = node1; node1.prev = reflexPrev; reflexPrev.next = node1; SkeletonEvent.handle(node0, ctx); // Aborts events of reflexNode SkeletonEvent.handle(node1, ctx); } public toString() { return ( 'SplitEvent{' + this.reflexNode.id + ' => ' + this.op0.id + '-' + this.op1.id + '} in ' + this.time ); } }