import { Point, create, sub, len, scale, dot, add, normalize, cross, copy, } from '../vector'; import { assert, removeFromArray } from './langUtil'; import SkeletonNode from './SkeletonNode'; import SkeletonEvent from './SkeletonEvent'; import SkeletonContext from './SkeletonContext'; export default class MovingNode { id: string; skelNode: SkeletonNode; next?: MovingNode | null; prev?: MovingNode | null; edgeDir: Point = create(); edgeCollapseTime = 0; // Bisector points in move direction which depends on whether we're growing or shrinking. Length determines speed. bisector: Point = create(); // Check possible infinity loop by spawn count. spawnCount = 0; private _reflex = false; private _events: SkeletonEvent[] = []; constructor(id: string) { this.id = id; } isReflex() { return this._reflex; } addEvent(event: SkeletonEvent) { this._events.push(event); } removeEvent(event: SkeletonEvent) { const removed = removeFromArray(this._events, event); assert(removed); } tryRemoveEvent(event: SkeletonEvent) { return removeFromArray(this._events, event); } clearEvents() { this._events = []; } events() { return this._events; } filterEvents(cb: (event: SkeletonEvent) => boolean) { this._events = this._events.filter(cb); } /** * @return True if bisector is valid and polygon is not degenerated at this corner. */ calcBisector(ctx: SkeletonContext, init = false) { if (this.next?.next == this) { return false; } // Calc direction to neighbor nodes. Make sure there's enough distance for stable calculation. const vPrev: Point = sub(create(), this.prev!.skelNode.p, this.skelNode.p); const vPrevLength = len(vPrev); if (vPrevLength < ctx.epsilon) { this.setDegenerate(); return false; } const vNext: Point = sub(create(), this.next!.skelNode.p, this.skelNode.p); const vNextLength = len(vNext); if (vNextLength < ctx.epsilon) { this.setDegenerate(); return false; } // Normalize scale(vPrev, vPrev, 1 / vPrevLength); scale(vNext, vNext, 1 / vNextLength); // Check if edges point in opposite directions with an angle of 180° between them const cos = dot(vPrev, vNext); const bisector = this.bisector; if (cos < ctx.epsilonMinusOne) { // Rotate vPrev by 90° counterclockwise bisector[0] = -vPrev[1] * ctx.distanceSign; bisector[1] = vPrev[0] * ctx.distanceSign; this._reflex = false; } else { // This fixes some cases where 90° bisectors (between adjacent edges that point in 180° different directions) // don't degenerate as they should. Presumably because these nodes advance too much (without being considered reflex) // and then lie on the wrong side of an approaching edge, and/or because of floating point inaccuracy. // Therefore we must ensure that vPrev (still) lies left of vNext. This is only a valid check if the node was not reflex // and the angle between vPrev and vNext is less than 90°. // Another way for catching more degenerates is to increase EPSILON. // TODO: THIS CREATES NEW ERRORS WHEN GROWING POLYGONS (see bug22). const reflexBefore = init || this._reflex; if (!reflexBefore && cos > 0 && cross(vPrev, vNext) > 0) { this.setDegenerate(); return false; } normalize(bisector, add(bisector, vPrev, vNext)); const sin = cross(vPrev, bisector); // Check if degenerated if (Math.abs(sin) < ctx.epsilon) { this.setDegenerate(); return false; } else { const speed = ctx.distanceSign / sin; scale(bisector, bisector, speed); this._reflex = dot(bisector, vPrev) < 0; } } return true; } updateEdge() { const edgeDir = this.edgeDir; sub(edgeDir, this.next!.skelNode.p, this.skelNode.p); const edgeLength = len(edgeDir); scale(edgeDir, edgeDir, 1 / edgeLength); // Normalize let edgeShrinkSpeed = dot(this.bisector, edgeDir); edgeShrinkSpeed -= dot(this.next!.bisector, edgeDir); // equivalent to: edgeShrinkSpeed += next.bisector.dot(edgeDir.negate()); if (edgeShrinkSpeed > 0) { this.edgeCollapseTime = edgeLength / edgeShrinkSpeed; } else { this.edgeCollapseTime = SkeletonEvent.INVALID_TIME; } } private setDegenerate() { this.bisector[0] = this.bisector[1] = 0; this._reflex = false; } leaveSkeletonNode() { // Leave a SkeletonNode at old place and create new one const oldSkelNode = this.skelNode; this.skelNode = new SkeletonNode(); copy(this.skelNode.p, oldSkelNode.p); oldSkelNode.addEdge(this.skelNode); } toString() { return `MovingNode{${this.id}}${this._reflex ? '(reflex)' : ''}`; } }