/** Copyright Stewart Allen -- All Rights Reserved */ import { base } from './base.js'; const { util, config } = base; const { sqr, numOrDefault } = util; const DEG2RAD = Math.PI / 180; /** * emit each element in an array based on * the next closest endpoint. arrays contain * elements with { first, last } points and * may be open polys, unlike poly2polyEmit */ export 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 */ export 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; } export 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 }); } export 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 }; } export 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 }; } export function v2pl(rec) { let p = rec.vp.clone(); p.x += rec.dx; p.y += rec.dy; p.vp = rec.vp; return p; } export function v2pr(rec) { let p = rec.vp.clone(); p.x -= rec.dx; p.y -= rec.dy; p.vp = rec.vp; return p; } export 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); let ln = l1.vp.normal; let rn = r1.vp.normal; if (split_left && split_right) { faces.push(l1, l2, r1); faces.push(r2, r1, l2); fl = fl || l2; fr = fr || r2; normals.push(ln.dx, ln.dy, zn); normals.push(ln.dx, ln.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(ln.dx, ln.dy, zn); } else if (split_left) { faces.push(l1, l2, r1); fl = fl || l2; fr = fr || r1; normals.push(ln.dx, ln.dy, zn); normals.push(ln.dx, ln.dy, zn); normals.push(-rn.dx, -rn.dy, zn); } else { // split right faces.push(r2, r1, l1); fl = fl || l1; fr = fr || r2; normals.push(-rn.dx, -rn.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(ln.dx, ln.dy, zn); } } } 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); let ln = l0.vp.normal; let rn = r0.vp.normal; normals.push(ln.dx, ln.dy, zn); normals.push(ln.dx, ln.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(ln.dx, ln.dy, zn); } } if (open) { // move open ends by offset length const p0 = points[0].normal; const l0 = left[0]; const r0 = right[0]; // improve visuals of open but 90 degree overlapping ends const move = offset * 0.99; l0.x += p0.dy * move; l0.y -= p0.dx * move; r0.x += p0.dy * move; r0.y -= p0.dx * move; const pn = points.peek(2).normal; const ln = left.peek(); const rn = right.peek(); ln.x -= pn.dy * move; ln.y += pn.dx * move; rn.x -= pn.dy * move; rn.y += pn.dx * move; } if (!open && faces) { let l1 = left.peek(1); let r1 = right.peek(1); let ln = l1.vp.normal; let rn = r1.vp.normal; faces.push(fl, l1, fr); faces.push(r1, fr, l1); normals.push(ln.dx, ln.dy, zn); normals.push(ln.dx, ln.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(-rn.dx, -rn.dy, zn); normals.push(ln.dx, ln.dy, zn); } return { left, right, faces, normals, open }; } export function pathTo3D(path, height, z) { const { faces, normals, left, right, open } = path; const out = []; const nrm = []; 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); } nrm.appendAll(normals); // reverse normals to match faces, but underside so reverse Z as well for (let i=normals.length-1; i>0; i-=3) { nrm.push(normals[i-2]); nrm.push(normals[i-1]); nrm.push(-normals[i-0]); } for (let i=0, l=left.length, tl = open ? l-1 : l; i} an array of points representing the arc. */ export function arcToPath (start, end, arcdivs=24, opts) { let { clockwise, center, radius } = opts; // @type {Point} if (end.x === undefined && end.x === undefined && center === undefined) { // bambu generates loop z or wipe loop arcs in place // console.log({ skip_empty_arc: rec }); return; } if (center) { // center = center.add(start); center.r = center.distTo2D(start); } else if (radius !== undefined) { let pd = { x: end.x - start.x, y: end.y - start.y }; //position delta let dst = Math.sqrt(pd.x * pd.x + pd.y * pd.y) / 2; // distance let pr2; if (Math.abs(dst - radius) < 0.001) { // center point radius pr2 = { x: (end.x + start.x) / 2, y: (end.y + start.y) / 2}; } else { // triangulate pr2 = base.util.center2pr(start, end, radius, clockwise); } center.x = pr2.x; center.y = pr2.y; center.r = radius; } else { console.log({malfomed_arc: {radius,center, clockwise, start, end}}); } //deltas let dx = start.x - end.x; let dy = start.y - end.y; let dz = start.z - end.z; // line angles let a1 = Math.atan2(center.y - start.y, center.x - start.x) + Math.PI; let a2 = Math.atan2(center.y - end.y, center.x - end.x) + Math.PI; let ad = base.util.thetaDiff(a1, a2, clockwise); // angle difference in radians let samePoint = Math.abs(ad) < 0.001 let ofFull = Math.abs(ad)/(2*Math.PI); let steps = samePoint? arcdivs : Math.max(Math.floor( arcdivs * ofFull),4); let step = (samePoint? (Math.PI*2) : ad) / steps; let numPoints = steps/ step; let zStart = start.z; let zStep = dz / numPoints; let rot = a1 + step; //unused deltas let da = Math.abs(a1 - a2); let dd = Math.sqrt(dx * dx + dy * dy); // LOG({index, da, dd, first: pos, last: rec, center, a1, a2, ad, step, steps, rot, line}); // G0G1(false, [`X${center.x}`, `Y${center.y}`, `E1`]); // under 1 degree arc and 5mm, convert to straight line // if (da < 0.005 && dd < 5) { // G0G1(false, [`X${end.x}`, `Y${end.y}`, `E1`]); // return ; // } let arr = [] // point accumulator for (let i=0; i<=steps-2; i++) { if (isNaN(center.r) || isNaN(center.x) || isNaN(center.y)) { console.log({malfomed_arc: {radius, clockwise, start, end}}); } arr.push(newPoint( center.x + Math.cos(rot) * center.r, center.y + Math.sin(rot) * center.r, zStart, )); zStart += zStep; rot += step; } // console.log(arr,start,end); return arr } export class FloatPacker { constructor(size, factor) { this.size = size; this.factor = Math.min(factor || 1.2, 1.1); this.array = new Float32Array(size); this.pos = 0; } push() { const array = this.array; const size = this.size; const args = arguments.length; if (this.pos + args >= size) { let nusize = ((size * this.factor) | 0) + args; let nuarray = new Float32Array(nusize); nuarray.set(array); this.array = nuarray; this.size = nusize; } for (let i=0; i= 0.9) { return this.array.subarray(0, this.pos); } else { return this.array.slice(0, this.pos); } } }