UNPKG

@davepagurek/flo-mat

Version:

Medial / Scale Axis Transform (MAT/SAT) Library.

187 lines (153 loc) 6.15 kB
/** @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 }