/** Copyright Stewart Allen -- All Rights Reserved */ "use strict"; // path & routing output utilities // dep: geo.base // dep: geo.point gapp.register("geo.paths", [], (root, exports) => { const { base } = root; const { util, config, newPoint } = base; const { sqr, numOrDefault } = util; const DEG2RAD = Math.PI / 180; /** * emit each element in an array based on * the next closest endpoint. */ function tip2tipEmit(array, startPoint, emitter) { let mindist, dist, found, count = 0; for (;;) { found = null; mindist = Infinity; array.forEach(function(el) { if (el.delete) return; dist = startPoint.distTo2D(el.first); if (dist < mindist) { found = {el:el, first:el.first, last:el.last}; mindist = dist; } dist = startPoint.distTo2D(el.last); if (dist < mindist) { found = {el:el, first:el.last, last:el.first}; mindist = dist; } }); if (found) { found.el.delete = true; startPoint = found.last; emitter(found.el, found.first, ++count); } else { break; } } return startPoint; } /** * like tip2tipEmit but accepts an array of polygons and the next closest * point can be anywhere in the adjacent polygon. should be re-written * to be more like outputOrderClosest() and have the option to account for * depth in determining distance */ function poly2polyEmit(array, startPoint, emitter, opt = {}) { let marker = opt.mark || 'delete'; let mindist, dist, found, count = 0; for (;;) { found = null; mindist = Infinity; for (let poly of array) { if (poly[marker]) { continue; } if (poly.isOpen()) { const d2f = startPoint.distTo2D(poly.first()); const d2l = startPoint.distTo2D(poly.last()); if (d2f > mindist && d2l > mindist) { continue; } if (d2l < mindist && d2l < d2f && opt.swapdir !== false) { poly.reverse(); found = {poly:poly, index:0, point:poly.first()}; mindist = d2l; } else if (d2f < mindist) { found = {poly:poly, index:0, point:poly.first()}; mindist = d2f; } continue; } let area = poly.open ? 1 : poly.area(); poly.forEachPoint(function(point, index) { dist = opt.weight ? startPoint.distTo3D(point) * area * area : startPoint.distTo2D(point); if (dist < mindist) { found = {poly:poly, index:index, point:point}; mindist = dist; } }); } if (!found || opt.term) { break; } found.poly[marker] = true; startPoint = emitter(found.poly, found.index, ++count, startPoint) || found.point; } // undo delete marks if (opt.perm !== true) { array.forEach(function(poly) { poly[marker] = false }); } return startPoint; } function calc_normal(p1, p2) { let dx = p2.x - p1.x; let dy = p2.y - p1.y; let len = Math.sqrt(dx * dx + dy * dy); let mn = (1 / len); dx *= mn; dy *= mn; return({ dx: dy, dy: -dx, p1, p2, len }); } function end_vertex(n1, n2, off, start) { let dx, dy; if (start) { dx = n2.dx * off; dy = n2.dy * off; } else { dx = n1.dx * off; dy = n1.dy * off; } return { dx, dy, vp: n1.p2 }; } function calc_vertex(n1, n2, off, vp) { let dx, dy, io, vl, q, r; r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy); q = off / r; // handle spurs that switch back 180 degrees if (q === Infinity) { q = 0; } dx = (n1.dx + n2.dx) * q; dy = (n1.dy + n2.dy) * q; // io tells us whether we're turning left or right io = (n1.dx * n2.dy - n2.dx * n1.dy); // vertex length can be compared to the previons and next // segment lengths to see if we're highly acute vl = Math.sqrt(dx * dx + dy * dy); return { dx, dy, vp: vp || n1.p2, io, vl }; } function v2pl(rec) { let p = rec.vp.clone(); p.x += rec.dx; p.y += rec.dy; p.vp = rec.vp; return p; } function v2pr(rec) { let p = rec.vp.clone(); p.x -= rec.dx; p.y -= rec.dy; p.vp = rec.vp; return p; } function pointsToPath(points, offset, open, miter = 1.5) { const absoff = Math.abs(offset); // calculate segment normals which are used to calculate vertex normals // next segment info is associated with the current point const nupoints = []; const length = points.length; if (length === 2 && points[0].isEqual(points[1])) { return { }; } const dedup = (open && length > 2) || (!open && length > 3); for (let i=0; i 0) { // right split_left = vl > absoff * miter; split_right = vl > Math.min(n1.len, n2.len) + absoff; } else { // left split_right = vl > absoff * miter; split_left = vl > Math.min(n1.len, n2.len) + absoff; } let l0 = left.peek(1); let r0 = right.peek(1); if (split_left || split_right) { // shorten each leg and insert new point let delta = 0.1; let np1 = p1.clone().move({ x: n1.dy * delta, y: -n1.dx * delta, z: 0 }); let np2 = p1.clone().move({ x:-n2.dy * delta, y: n2.dx * delta, z: 0 }); let sn1 = np1.normal = calc_normal(np1, np2); let sn2 = np2.normal = p1.normal; let nv1 = calc_vertex(n1.p1.normal, sn1, offset, np1); let nv2 = calc_vertex(sn1, sn2, offset, np2); if (split_right) { right.push(v2pr(nv1), v2pr(nv2)); } else { right.push(v2pr(vn)); } if (split_left) { left.push(v2pl(nv1), v2pl(nv2)); } else { left.push(v2pl(vn)); } if (faces) { let l1 = left.peek(1); let r1 = right.peek(1); let l2 = left.peek(2); let r2 = right.peek(2); if (split_left && split_right) { faces.push(l1, l2, r1); faces.push(r2, r1, l2); fl = fl || l1; fr = fr || r2; } else if (split_left) { faces.push(l1, l2, r1); fl = fl || l2; fr = fr || r1; } else { // split right faces.push(r2, r1, l1); fl = fl || l1; fr = fr || r2; } } } else { left.push(v2pl(vn)); right.push(v2pr(vn)); fl = fl || left.peek(1); fr = fr || right.peek(1); } if (faces && l0 && r0) { let l1 = left.peek(split_left ? 2 : 1); let r1 = right.peek(split_right ? 2 : 1); faces.push(l1, l0, r1); faces.push(r0, r1, l0); } } if (open) { // move open ends by offset length const p0 = points[0].normal; const l0 = left[0]; const r0 = right[0]; l0.x += p0.dy * offset; l0.y -= p0.dx * offset; r0.x += p0.dy * offset; r0.y -= p0.dx * offset; const pn = points.peek(2).normal; const ln = left.peek(); const rn = right.peek(); ln.x -= pn.dy * offset; ln.y += pn.dx * offset; rn.x -= pn.dy * offset; rn.y += pn.dx * offset; } if (!open && faces) { let l1 = left.peek(1); let r1 = right.peek(1); faces.push(fl, l1, fr); faces.push(r1, fr, l1); } return { left, right, faces, open }; } function pathTo3D(path, height, z) { const { faces, left, right, open } = path; const out = []; if (!(faces && left && right)) { return []; } if (z !== undefined) { for (let p of faces) { p.z = z; } } for (let p of faces) { out.push(p.x, p.y, p.z - height); } for (let p of faces.slice().reverse()) { out.push(p.x, p.y, p.z + height); } for (let i=0, l=left.length, tl = open ? l-1 : l; i