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;
}
}