@davepagurek/flo-mat
Version:
Medial / Scale Axis Transform (MAT/SAT) Library.
187 lines (153 loc) • 6.15 kB
text/typescript
/** @internal */
declare const _debug_: Debug;
import { Debug } from '../debug/debug.js';
import { LlRbTree } from 'flo-ll-rb-tree';
import { distanceBetween, fromTo, interpolate, rotate, translate } from 'flo-vector2d';
import { CpNode } from '../cp-node/cp-node.js';
import { Loop } from 'flo-boolean';
import { Circle } from '../geometry/circle.js';
import { PointOnShape } from '../point-on-shape/point-on-shape.js';
import { getOsculatingCircle } from '../point-on-shape/get-osculating-circle.js';
import { addDebugInfo } from './add-debug-info.js';
import { TXForDebugging } from './x-for-debugging.js';
import { findEquidistantPointOnLineDd } from './find-equidistant-point-on-line-dd.js';
import { getInitialBezierPieces } from './get-initial-bezier-pieces.js';
import { getCloseBoundaryPointsCertified } from '../closest-boundary-point/get-close-boundary-points-certified.js';
import { reduceRadius } from './reduce-radius.js';
import { squaredDistanceBetweenDd } from './squared-distance-between-dd.js';
import { cullBezierPieces2 } from './cull-bezier-pieces.js';
import { add1Prong } from './add-1-prong.js';
import { createPos } from '../point-on-shape/create-pos.js';
const { sqrt, abs, sin, cos } = Math;
/**
* @internal
* Adds a 2-prong to the MAT. The first point on the shape boundary is given and
* the second one is found by the algorithm.
*
* A 2-prong is defined as a MAT circle that touches the shape at exactly 2
* points.
*
* Before any 2-prongs are added the entire shape is our δΩ.
*
* As per the paper by Choi, Choi, Moon and Wee:
* "The starting point of this algorithm is a choice of a circle Br(x)
* centered at an interior point x which contains two boundary portions c and
* d of dΩ as in Fig. 19."
* In fact, we (and they) start by fixing one point on the boundary beforehand.
* @param loops A shape represented by path loops
* @param extreme The extreme coordinate value of the shape
* @param squaredDiagonalLength The squared diagonal length of the shape
* bounding box.
* @param y The source point of the 2-prong to be found
* @param isHoleClosing True if this is a hole-closing two-prong, false otherwise
* @param k The loop array index
*/
function find2Prong(
angle: number,
loops: Loop[],
extreme: number,
squaredDiagonalLength: number,
cpTrees: Map<Loop,LlRbTree<CpNode>>,
y: PointOnShape,
isHoleClosing: boolean,
k: number,
for1Prong: boolean): { circle: Circle, zs: PointOnShape[] } | undefined {
const MAX_ITERATIONS = 25;
const squaredSeperationTolerance = ((2**-21)*extreme)**2;
const errorTolerance = (2**-46)*extreme;
const maxOsculatingCircleRadius = sqrt(squaredDiagonalLength);
const minCurvature = 1/maxOsculatingCircleRadius;
let xO: number[]; // the original x to mitigate drift
const p = y.p;
let rO: number;
if (isHoleClosing) {
xO = [p[0], p[1] - maxOsculatingCircleRadius];
rO = maxOsculatingCircleRadius;
} else {
if (angle === 0) {
({ center: xO, radius: rO } = getOsculatingCircle(minCurvature, y));
} else {
({ center: xO, radius: rO } = getOsculatingCircle(minCurvature, y, true));
const v = fromTo(y.p, xO);
const v_ = rotate(sin(angle), cos(angle))(v);
xO = translate(y.p)(v_);
}
}
// The boundary piece that should contain the other point of
// the 2-prong circle. (Defined by start and end points).
const { bezierPieces, δ } = getInitialBezierPieces(
angle, isHoleClosing, k, loops, cpTrees, y, { center: xO, radius: rO }
);
/** The center of the two-prong (successively refined) */
let x = xO;
// The lines below is an optimization.
const r_ = sqrt(reduceRadius(extreme, bezierPieces, p, xO));
if (rO > r_) {
x = interpolate(p, xO, r_/rO);
}
/** Trace the convergence (for debugging). */
const xs: TXForDebugging[] = [];
/** The antipode of the two-prong (successively refined) */
let zs: PointOnShape[] = undefined!;
let z: PointOnShape = undefined!;
let bezierPieces_ = bezierPieces;
let i = 0;
while (i < MAX_ITERATIONS) {
const xy = squaredDistanceBetweenDd(x, y.p);
if (i < 5) { bezierPieces_ = cullBezierPieces2(bezierPieces_, x, xy); }
zs = getCloseBoundaryPointsCertified(
bezierPieces_, x, y.curve, y.t,
for1Prong && i == 0 && rO !== 1/minCurvature,
angle
).map(info => createPos(info.curve, info.t, false));
z = zs[0];
if (z === undefined) {
addDebugInfo2(isHoleClosing);
return undefined;
}
const xz = squaredDistanceBetweenDd(x, z.p);
const yz = squaredDistanceBetweenDd(y.p, z.p);
// if on first try
if (i === 0) {
if (rO < (1 - 2**-6)*sqrt(xz)) {
add1Prong(rO, xO, cpTrees, y);
return undefined;
}
// return undefined;
}
if (typeof _debug_ !== 'undefined') { xs.push({ x, y, z: createPos(z.curve, z.t, false), t: y.t }); }
if (!isHoleClosing) {
if (yz <= squaredSeperationTolerance) {
// if (typeof _debug_ !== 'undefined') { console.log(`failed: seperation too small - ${sqrt(yz)}`); }
return undefined;
}
}
// Find the point on the line connecting y with x that is
// equidistant from y and z. This will be our next x.
const nextX = findEquidistantPointOnLineDd(x, y.p, z.p);
const error = abs(sqrt(xy) - sqrt(xz));
// if (xy < xz) { return undefined; }
x = nextX;
if (error < errorTolerance) {
break;
}
i++;
if (i === MAX_ITERATIONS) {
// Convergence was too slow.
if (typeof _debug_ !== 'undefined') { console.log('failed (slow): max iterations reached'); }
return undefined;
}
}
const circle = { center: x, radius: distanceBetween(x, z.p) };
if (typeof _debug_ !== 'undefined') { addDebugInfo(bezierPieces, false, x, y, z, circle!, δ!, xs, isHoleClosing); }
return { circle, zs: [z] };
}
function addDebugInfo2(isHoleClosing: boolean) {
if (typeof _debug_ !== 'undefined') {
const elems = _debug_.generated.elems;
const elem = isHoleClosing ? elems.twoProng_holeClosing : elems.twoProng_regular
const elemStr = isHoleClosing ? 'hole-closing: ' + elem.length : 'regular: ' + elem.length;
console.log('failed: no closest point - ' + elemStr);
}
}
export { find2Prong }