diff --git a/src/add/array.js b/src/add/array.js index bf43b37c..bcb2243e 100644 --- a/src/add/array.js +++ b/src/add/array.js @@ -31,8 +31,8 @@ AP.equals = function(arr) { return true; }; -AP.peek = function() { - return this[this.length - 1]; +AP.peek = function(val = 1) { + return this[this.length - val]; }; AP.drop = function(pre = 0, post = 0) { diff --git a/src/geo/paths.js b/src/geo/paths.js index 24069381..ddc26d62 100644 --- a/src/geo/paths.js +++ b/src/geo/paths.js @@ -105,9 +105,249 @@ function poly2polyEmit(array, startPoint, emitter, opt = {}) { 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 = []; + for (let i=0, l=points.length; 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 (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 { - let exp = off_opt.outs = []; - if (poly.isOpen()) { - // shorten line by offset - const pp = poly.points, pl = pp.length; - pp[0] = pp[0].offsetPointTo(pp[1], offset); - pp[pl-1] = pp[pl-1].offsetPointTo(pp[pl-2], offset); - exp.appendAll(POLY.expand_lines(poly, offset * 0.9, z)); - // force re-nest if offsets cause ends to join - exp = POLY.flatten(exp,[],true); - this.stats.flat_line = 0; - } else if (offset) { - POLY.offset([poly], offset * 0.9, off_opt); - POLY.offset([poly], -offset * 0.9, off_opt); - this.stats.flat_poly = 0; - } - if (opts.outline) { - this.addPolys(exp.clone()); - } const faceidx = faces.length / 3; - POLY.nest(exp).forEach((poly,i) => { - poly.earcut().forEach(ep => { - ep.forEachPoint(p => { faces.push(p.x, p.y, p.z) }); + if (newPath) { + const path = poly.toPath2D(offset * 0.95); + for (let p of path.faces) { + faces.push(p.x, p.y, p.z); + } + if (opts.outline) { + this.addPolys(newPolygon().addPoints(path.left).setOpen()); + this.addPolys(newPolygon().addPoints(path.right).setOpen()); + } + } else { + let exp = off_opt.outs = []; + if (poly.isOpen()) { + // shorten line by offset + const pp = poly.points, pl = pp.length; + pp[0] = pp[0].offsetPointTo(pp[1], offset); + pp[pl-1] = pp[pl-1].offsetPointTo(pp[pl-2], offset); + exp.appendAll(POLY.expand_lines(poly, offset * 0.9, z)); + // force re-nest if offsets cause ends to join + exp = POLY.flatten(exp,[],true); + this.stats.flat_line = 0; + } else if (offset) { + POLY.offset([poly], offset * 0.9, off_opt); + POLY.offset([poly], -offset * 0.9, off_opt); + this.stats.flat_poly = 0; + } + if (opts.outline) { + this.addPolys(exp.clone()); + } + POLY.nest(exp).forEach((poly,i) => { + poly.earcut().forEach(ep => { + ep.forEachPoint(p => { faces.push(p.x, p.y, p.z) }); + }); }); - }); + } const cur = this.current; const color = opts.color ? (typeof(opts.color) === 'number' ? { line: opts.color, face: opts.color } : opts.color) : @@ -189,11 +201,53 @@ class Layers { const opts = options || {}; const height = opts.height || 1; const offset = opts.offset || 1; + polys = flat(polys); if (!polys.length) { return; } + if (newPath) { + for (let poly of polys) { + if (poly.length < (poly.open ? 2 : 3)) { + continue; + } + const z = opts.z || poly.getZ(); + const faces = poly.toPath3D(offset * 1, height, 0); + const cur = this.current; + const one = cur.paths[0]; + if (one) { + // merge all contour geometry for massive speed gain + // todo: recode to concat all geometries at once, and not incrementally + const add = one.faces.length / 3; + const feces = new Float32Array(one.faces.length + faces.length); + const indln = one.faces.length / 3; + feces.set(one.faces); + feces.set(faces, one.faces.length); + one.faces = feces; + // allow changing colors + if (opts.color) { + if (!cur.cpath) { + cur.cpath = [ Object.assign({ start: 0, count: indln - 1 }, cur.color) ]; + } + // rewrite last color count if color or opacity have changed + const pc = cur.cpath[cur.cpath.length - 1]; + if (pc.face !== opts.color.face || pc.opacity !== opts.color.opacity) { + pc.count = indln; + cur.cpath.push(Object.assign({ start: indln, count: Infinity }, opts.color)); + } + } + } else { + cur.paths.push({ index: [], faces, z }); + if (opts.color) { + cur.cpath = [ Object.assign({ start: 0, count: Infinity }, opts.color) ]; + } + } + this.stats.contour++; + } + return this; + } + const profiles = this.profiles; const prokey = `${offset}x${height}`; if (!profiles[prokey]) {