import { distSquare } from '../vector'; import { assert } from './langUtil'; import MovingNode from './MovingNode'; import SkeletonContext from './SkeletonContext'; import SplitEvent from './SplitEvent'; export default class SkeletonEvent { public static INVALID_TIME = NaN; public readonly time: number; // Always positive protected eventPriority = 0; constructor(time: number) { this.time = time; } compareTo(other: SkeletonEvent) { if (this.time === other.time) { return this.eventPriority - other.eventPriority; } return this.time - other.time; } onEventQueued() {} onEventAborted(adjacentNode: MovingNode, ctx: SkeletonContext) {} onEventAborted2( edgeNode0: MovingNode, edgeNode1: MovingNode, ctx?: SkeletonContext ) {} handle(ctx: SkeletonContext) {} /** * Always aborts events of 'node'. */ protected static handle(node: MovingNode, ctx: SkeletonContext) { //System.out.println("handle " + node); while (SkeletonEvent.ensureValidPolygon(node, ctx)) { const validBisector = node.calcBisector(ctx); if (validBisector) { node.leaveSkeletonNode(); node.updateEdge(); node.prev!.updateEdge(); SkeletonEvent.createEvents(node, ctx); return; } node = SkeletonEvent.handleDegenerateAngle(node, ctx); } } static handleInit(node: MovingNode, ctx: SkeletonContext) { while (SkeletonEvent.ensureValidPolygon(node, ctx)) { const validBisector = node.calcBisector(ctx); if (validBisector) { return; } node = SkeletonEvent.handleDegenerateAngle(node, ctx); } } /** * Check for valid polygon and handle case in which a polygon degenerates to a line. * @return True if polygon consists of >2 edges. */ private static ensureValidPolygon(node: MovingNode, ctx: SkeletonContext) { const next = node.next!; assert(next != node); if (next != node.prev) { return true; } // Degenerated polygon node.skelNode.addDegenerationEdge(next.skelNode); ctx.removeMovingNode(node); ctx.removeMovingNode(next); return false; } static createEvents(node: MovingNode, ctx: SkeletonContext) { ctx.abortEvents(node); // Create edge events if edge is shrinking ctx.tryQueueEdgeEvent(node, node.next!); ctx.tryQueueEdgeEvent(node.prev!, node); SkeletonEvent.createAllSplitEvents(node, ctx); } // TODO: Test reflex nodes against all edges in other loops too? That would allow multiple disconnected initial loops. /** * Tests adjacent edges of 'node' against other eligible reflex vertices in MovingNodes-loop. * If 'node' is reflex, tests it against all eligible edges. * Eligible tests: Minimum distance between reflex node and candidate edge = 2 edges in between * * A triangle cannot be concave. A concave quadrilateral (arrowhead) doesn't need split events. * Minimum vertices for split events = 5. */ private static createAllSplitEvents(node: MovingNode, ctx: SkeletonContext) { let current = node.next!.next!; // processed in first step before loop const end = node.prev!.prev!; // excluded from loop, but processed in last step after loop // Ignore triangles and quads if (current == end.next || current == end) { return; } const nodeIsReflex = node.isReflex(); let nearestSplit: SplitEvent | null | undefined; // First step: Test 'current' vertex only against first adjacent edge (node.prev->node). // Test 'node' against current edge. if (current.isReflex()) { ctx.tryQueueSplitEvent(current, node.prev!, node); } if (nodeIsReflex) { nearestSplit = ctx.tryReplaceNearestSplitEvent( node, current, current.next!, nearestSplit ); } const nodes = ctx.getNodes(); nodes.forEach((current) => { if (current == node || current.next == node || current.prev == node) { return; } if (current.isReflex()) { ctx.tryQueueSplitEvent(current, node, node.next!); ctx.tryQueueSplitEvent(current, node.prev!, node); } if (nodeIsReflex) { // Condition is constant. Manual optimization (loop unswitching) not worth it. nearestSplit = ctx.tryReplaceNearestSplitEvent( node, current, current.next!, nearestSplit ); } }); // Last step: Test 'current' only against second adjacent edge (node->node.next) // Don't test "nodeIsReflex" against this last edge. if (current.isReflex()) { ctx.tryQueueSplitEvent(current, node, node.next!); } if (nearestSplit != null) { ctx.enqueue(nearestSplit); } } static createSplitEvents(reflexNode: MovingNode, ctx: SkeletonContext) { let nearestSplit: SplitEvent | null | undefined = null; const nodes = ctx.getNodes(); nodes.forEach((current) => { if ( current == reflexNode || current.next == reflexNode || current.prev == reflexNode ) { return; } nearestSplit = ctx.tryReplaceNearestSplitEvent( reflexNode, current, current.next!, nearestSplit ); }); if (nearestSplit != null) { ctx.enqueue(nearestSplit); } } private static handleDegenerateAngle(node: MovingNode, ctx: SkeletonContext) { // Remove node, connect node.prev <-> node.next const o1 = node.prev!; const o2 = node.next!; assert(o1.next == node); assert(o2.prev == node); o1.next = o2; o2.prev = o1; let connectionTarget: MovingNode; if ( distSquare(node.skelNode.p, o1.skelNode.p) < distSquare(node.skelNode.p, o2.skelNode.p) ) { connectionTarget = o1; } else { connectionTarget = o2; } node.skelNode.addDegenerationEdge(connectionTarget.skelNode); ctx.removeMovingNode(node); return connectionTarget; } }