import { create, dist, min, max, Point, scale, scaleAndAdd, copy, } from '../vector'; import MovingNode from './MovingNode'; import SkeletonContext from './SkeletonContext'; import SkeletonEvent from './SkeletonEvent'; import SkeletonNode from './SkeletonNode'; export default class StraightSkeleton { private offsetDistance = Infinity; // Absolute value private distanceSign = -1.0; private initialNodes: SkeletonNode[] = []; private _ctx = new SkeletonContext(); constructor() {} getCtx() { return this._ctx; } /** * Sets the absolute distance in units by which the edges should be moved.
* Positive: Grow face. Negative: Shrink face.
* Defaults to Float.NEGATIVE_INFINITY. * @param distance */ setDistance(distance: number) { if (distance === Infinity) { throw 'Cannot scale outwards to infinity.'; } this.distanceSign = distance > 0 ? 1 : -1; this.offsetDistance = Math.abs(distance); } /** * Sets the epsilon value that is used for degeneracy tests. Bigger values may reduce errors due to numerical instability in certain cases. * Defaults to 0.0001f. * @param epsilon */ setEpsilon(epsilon: number) { this._ctx.setEpsilon(epsilon); } execute(contour: Point[], holes: Point[][] = [], debugStep?: boolean) { this._ctx.reset(this.offsetDistance, this.distanceSign); this.initialNodes = []; const diagonalSize = this._createNodes(contour); holes.forEach((hole) => { this._createNodes(hole); }); // When shrinking to infinity, use polygon's bounding rectangle to determine max distance (less events queued = speed up) if (this.distanceSign < 0 && this.offsetDistance == Infinity) { this._ctx.distance = diagonalSize * 0.51; } if (this._ctx.distance != 0) { this.initBisectors(); this.initEvents(); if (debugStep) { return this.loopStep(); } else { this.loop(); } } } loop() { this._ctx.time = 0; // const loop = () => { // const stopped = this._eachLoop(); // if (!stopped) { // requestAnimationFrame(loop); // } // }; // loop(); while (true) { const stopped = this._eachLoop(); if (stopped) { break; } } } loopStep() { const ctx = this._ctx; ctx.time = 0; const next = () => { ctx.printEvents(); return this._eachLoop() ? null : next; }; return next; } _eachLoop() { const ctx = this._ctx; const event = ctx.pollQueue(); if (event == null) { this.scale(ctx.distance - ctx.time); return true; } this.scale(event.time - ctx.time); ctx.time = event.time; event.handle(ctx); ctx.recheckAbortedReflexNodes(); return false; } /** * Creates MovingNodes for all the vertices. * Also calculates bounding rectangle. * @return Diagonal length of bounding rectangle. */ private _createNodes(vertices: Point[]) { const min: Point = [Infinity, Infinity]; const max: Point = [-Infinity, -Infinity]; const first = this._createNode(vertices[0], min, max); let last = first; for (let i = 1; i < vertices.length; i++) { const movingNode = this._createNode(vertices[i], min, max); // Link nodes movingNode.prev = last; last.next = movingNode; last = movingNode; } // Link last node with first first.prev = last; last.next = first; return dist(max, min); } private _createNode(vertex: Point, minPt: Point, maxPt: Point) { min(minPt, minPt, vertex); max(maxPt, maxPt, vertex); const initialNode = new SkeletonNode(); copy(initialNode.p, vertex); this.initialNodes.push(initialNode); const movingNode = this._ctx.createMovingNode(); movingNode.skelNode = initialNode; return movingNode; } initBisectors() { const degenerates: MovingNode[] = []; this._ctx.getNodes().forEach((node) => { const validBisector = node.calcBisector(this._ctx, true); if (!validBisector) { degenerates.push(node); } }); degenerates.forEach((degenerateNode) => { // Process degenerate nodes after all bisectors have been initialized if (degenerateNode.next != null) { // Check if 'degenerateNode' was already removed in previous handleInit() calls SkeletonEvent.handleInit(degenerateNode, this._ctx); } }); } initEvents() { const reflexNodes: MovingNode[] = []; this._ctx.getNodes().forEach((current) => { current.leaveSkeletonNode(); current.updateEdge(); this._ctx.tryQueueEdgeEvent(current, current.next!); if (current.isReflex()) { reflexNodes.push(current); } }); // Process the reflex nodes after all edges have been initialized with updateEdge(). reflexNodes.forEach((reflex) => { SkeletonEvent.createSplitEvents(reflex, this._ctx); }); } scale(dist: number) { if (dist == 0) return; this._ctx.getNodes().forEach((node) => { scaleAndAdd(node.skelNode.p, node.skelNode.p, node.bisector, dist); if (this.isInvalid(node.skelNode.p)) { throw new Error( 'Invalid position after scale: bisector=' + node.bisector + ', dir=' + scale(create(), node.bisector, dist) ); } }); } private isInvalid(v: Point) { return isNaN(v[0]) || !isFinite(v[0]); } // // Results // getStartNodes() { return this.initialNodes; } getEndNodes() { const movingNodes = this._ctx.getNodes(); const skelNodes: SkeletonNode[] = []; movingNodes.forEach((movingNode) => { skelNodes.push(movingNode.skelNode); }); return skelNodes; } getNodeLoops() { const nodes: Set = new Set(); this._ctx.getNodes().forEach((node) => { nodes.add(node); }); const nodeLoops: SkeletonNode[][] = []; // TODO: Make a generic class that finds such loops? (also for edge loops) while (nodes.size > 0) { const loop: SkeletonNode[] = []; nodeLoops.push(loop); const start = nodes.values().next().value; let current = start; do { // if (loop.length > 1e6) { // throw new Error('Infinity loop exists'); // } loop.push(current.skelNode); nodes.delete(current); current = current.next; } while (current != start); } return nodeLoops; } }