/** Copyright Stewart Allen -- All Rights Reserved */ import { broker } from '../../moto/broker.js'; import { codec } from '../../kiri/codec.js'; import { newPoint } from '../../geo/point.js'; import { newPolygon } from '../../geo/polygon.js'; import { newSlice } from '../../kiri/slice.js'; import { Slicer as topo_slicer } from './slicer2.js'; import { polygons as POLY } from '../../geo/polygons.js'; import { Tool } from './tool.js'; const RAD2DEG = 180 / Math.PI; const DEG2RAD = Math.PI / 180; class Topo { constructor() { } async generate(opt = {}) { let { state, contour, onupdate, ondone } = opt; let { widget, settings, tshadow, center, tabs, color } = opt.state; let { controller, process } = settings, animesh = parseInt(controller.animesh || 100) * 2500, axis = contour.axis.toLowerCase(), contourX = axis === "x", contourY = axis === "y", bounds = widget.getBoundingBox().clone(), tolerance = contour.tolerance, flatness = contour.flatness || (tolerance / 100), shadow = tshadow, minX = bounds.min.x, maxX = bounds.max.x, minY = bounds.min.y, maxY = bounds.max.y, zBottom = contour.bottom ? state.zBottom : 0, zMin = Math.max(bounds.min.z, zBottom) + 0.0001, boundsX = maxX - minX, boundsY = maxY - minY, inside = contour.inside, density = 1 + (contour.reduction || 0), resolution = tolerance ? tolerance : 1/Math.sqrt(animesh/(boundsX * boundsY)), tool = new Tool(settings, contour.tool), toolOffset = tool.generateProfile(resolution).profile, toolDiameter = tool.fluteDiameter(), toolStep = toolDiameter * contour.step, //tool.contourOffset(contour.step), leave = contour.leave || 0, maxangle = contour.angle, curvesOnly = contour.curves, bridge = contour.bridging || 0, // R2A = 180 / Math.PI, stepsX = Math.ceil(boundsX / resolution), stepsY = Math.ceil(boundsY / resolution), widtopo = widget.topo, topoCache = widtopo && widtopo.resolution === resolution && widtopo.diameter === toolDiameter ? widtopo : undefined, topo = widget.topo = topoCache || { data: new Float32Array(new SharedArrayBuffer(stepsX * stepsY * 4)), stepsX: stepsX, stepsY: stepsY, bounds: bounds, diameter: toolDiameter, resolution: resolution, profile: toolOffset, widget: widget, raster: true, slices: null }, data = topo.data, newslices = [], tabsMax = tabs ? Math.max(...tabs.map(tab => tab.dim.z/2 + tab.pos.z)) : 0, tabsOn = tabs, tabHeight = Math.max(process.camTabsHeight + zBottom, tabsMax), clipTab = tabsOn ? [] : null, clipTo = inside ? shadow : POLY.expand(shadow, toolDiameter/2 + resolution * 3), partOff = inside ? 0 : toolDiameter / 2 + resolution, gridDelta = Math.floor(partOff / resolution), debug = false, debug_clips = debug && true, debug_topo = debug && true, debug_topo_lines = debug && true, debug_topo_shells = debug && true, time = Date.now; if (tolerance === 0 && !topoCache) { console.log(widget.id, 'topo auto tolerance', resolution.round(4)); } // console.log({ resolution, flatness }); // used by Pocket -> Contour this.tolerance = resolution; if (tabs) { clipTab.appendAll(tabs.map(tab => { let ctab = POLY.expand([tab.poly], toolDiameter/2); ctab.forEach(ct => ct.z = tab.dim.z/2 + tab.pos.z); return ctab; }).flat()); } // debug clipTab and clipTo if (debug_clips) { const debug = newSlice(-1); const output = debug.output(); // if (clipTab) output.setLayer("clip.tab", { line: 0xff0000 }).addPolys(clipTab); if (clipTo) output.setLayer("clip.to", { line: 0x00dd00 }).addPolys(clipTo); newslices.push(debug); } const probe = this.probe = new Probe({ profile: toolOffset, data, stepsX, stepsY, boundsX, boundsY, minX, minY, zMin }); const { toolAtZ, toolAtXY, zAtXY } = probe; this.toolAtZ = toolAtZ; this.toolAtXY = toolAtXY; this.zAtXY = zAtXY; const trace = this.trace = new Trace(probe, { curvesOnly, maxangle, flatness, bridge, contourX }); if (topo.raster) { const box = topo.box = new THREE.Box2(); const params = { resolution, curvesOnly, flatness, stepsY, minY, maxY, zMin, data, box }; onupdate(0, 1, "raster"); await this.raster(widget, params, (i, l, p) => { onupdate(i / 2, l, p); }); topo.raster = false; } await this.contour({ box: topo.box, minX, maxX, minY, maxY, stepsX, stepsY, boundsX, boundsY, partOff, gridDelta, resolution, toolStep, contourX, contourY, density, clipTo, clipTab, clipTabZ: clipTab ? clipTab.map(t => t.z) : undefined, tabHeight, newslices, leave, color }, (i, l, p) => { onupdate(l / 2 + i / 2, l, p); }); ondone(newslices); return this; } async raster(widget, params, onupdate) { const { resolution } = params; const { box } = params; const { dispatch, minions } = self.kiri_worker; const vertices = widget.getGeoVertices({ unroll: true, translate: true }).toShared(); const range = { min: Infinity, max: -Infinity }; // swap XZ in shared array for (let i=0,l=vertices.length; i slice.max) { slices.push(slice); slice = { min: z, max: z + step, index }; } index++; } slices.push(slice); // console.log({ shards, range, step, slices }); let complete = 0; // define sharded ranges if (minions.running > 1) { dispatch.putCache({ key: widget.id, data: vertices }, { done: data => { // console.log({ put_cache_done: data }); }}); let promises = slices.map(slice => { return new Promise(resolve => { minions.queue({ cmd: "topo_raster", id: widget.id, params, slice }, data => { resolve(data); onupdate(++complete, slices.length, "raster"); }); }); }); // merge boxes for all rasters for contouring clipping (await Promise.all(promises)) .map(rec => rec.box) .map(box => new THREE.Box2( new THREE.Vector2(box.min.x, box.min.y), new THREE.Vector2(box.max.x, box.max.y) )) .map(box2 => { box.union(box2); return box2; }); dispatch.clearCache({}, { done: data => { // console.log({ clear_cache_done: data }); }}); } else { // iterate over shards, merge output // const output = []; for (let slice of slices) { new topo_slicer(slice.index) .setFromArray(vertices, slice) .slice(resolution) .map(rec => { const slice = newSlice(rec.z); slice.index = rec.index; slice.lines = rec.lines; for (let line of rec.lines) { const { p1, p2 } = line; if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } } raster_slice({ ...params, box, lines: rec.lines, gridx: rec.index }); return slice; }); onupdate(++complete, slices.length, "raster"); } } } async contour(params, onupdate) { const trace = this.trace; const concurrent = self.kiri_worker.minions.running; const { minX, maxX, minY, maxY, boundsX, boundsY, stepsX, stepsY } = params; const { gridDelta, resolution, density, partOff, toolStep, contourX, contourY } = params; const { clipTo, clipTab, clipTabZ, tabHeight, newslices, color, leave } = params; let stepsTaken = 0, stepsTotal = 0; if (contourX) { stepsTotal += ((maxY - minY + partOff * 2) / toolStep) | 0; } if (contourY) { stepsTotal += ((maxX - minX + partOff * 2) / toolStep) | 0; } if (stepsTotal === 0) { return; } const box = params.box.clone().expandByVector(new THREE.Vector3( partOff, partOff, 0 )); trace.init({ box, leave, clipTo, clipTab, clipTabZ, tabHeight, resolution, concurrent, density }); let resolver; let pcount = 0; let slicesY = []; let slicesX = []; let promise = new Promise(resolve => { resolver = () => { // sort output slices (required for async) slicesY.sort((a,b) => a.z - b.z); slicesX.sort((a,b) => a.z - b.z); newslices.appendAll(slicesY); newslices.appendAll(slicesX); resolve(); } }); let inc = () => { pcount++ }; let dec = () => { if (--pcount === 0 && concurrent) resolver() }; if (contourY) { onupdate(0, stepsTotal, "contour y"); // emit slice per X for (let x = minX - partOff; x <= maxX + partOff; x += toolStep) { if (x < box.min.x || x > box.max.x) continue; const gridx = Math.round(((x - minX) / boundsX) * stepsX); const gridy = -gridDelta; inc(); trace.crossY({ from: minY - partOff, to: maxY + partOff, x, gridx, gridy }, segments => { if (segments.length > 0) { let slice = newSlice(gridx); slice.camLines = segments; slice.output() .setLayer("contour y", {face: color, line: color}) .addPolys(segments); slicesY.push(slice); } onupdate(++stepsTaken, stepsTotal, "contour y"); dec(); }); } } if (contourX) { // emit slice per Y onupdate(0, stepsTotal, "contour x"); for (let y = minY - partOff; y <= maxY + partOff; y += toolStep) { if (y < box.min.y || y > box.max.y) continue; const gridy = Math.round(((y - minY) / boundsY) * stepsY); const gridx = -gridDelta; inc(); trace.crossX({ from: minX - partOff, to: maxX + partOff, y, gridx, gridy }, segments => { if (segments.length > 0) { let slice = newSlice(gridy); slice.camLines = segments; slice.output() .setLayer("contour x", {face: color, line: color}) .addPolys(segments); slicesX.push(slice); } onupdate(++stepsTaken, stepsTotal, "contour x"); dec(); }); } } if (!concurrent) resolver(); await promise; // const lines = newslices.map(s => (s.camLines || []).map(p => p.length)); // const points = lines.flat().reduce((a,b) => a+b); // console.log({ lines, points }); trace.cleanup(); } } class Probe { constructor(params) { const { data, profile } = params; const { stepsX, stepsY, boundsX, zMin, minX, minY } = params; this.params = params; // return the touching z given topo x,y and a tool profile const toolAtZ = this.toolAtZ = function(x,y) { let sx = stepsX, sy = stepsY, xl = sx - 1, yl = sy - 1; let gv, i = 0, mz = -Infinity; while (i < profile.length) { // tool profile point x, y, and z offsets const tx = profile[i++] + x; const ty = profile[i++] + y; const tz = profile[i++]; if (tx < 0 || tx > xl || ty < 0 || ty > yl) { // if outside max topo steps, use zMin gv = zMin; } else { // lookup grid value @ tx, ty gv = data[tx * sy + ty] || zMin; } // update the rest mz = Math.max(tz + gv, mz); } return Math.max(mz, zMin); } // export z probe function const rx = stepsX / boundsX; const ry = stepsX / boundsX; const toolAtXY = this.toolAtXY = function(px, py) { px = Math.round(rx * (px - minX)); py = Math.round(ry * (py - minY)); return toolAtZ(px, py); }; const zAtXY = this.zAtXY = function(px, py) { let ix = Math.round(rx * (px - minX)); let iy = Math.round(ry * (py - minY)); return data[ix * stepsY + iy] || zMin; }; } } class Trace { constructor(probe, params) { const { curvesOnly, maxangle, flatness, bridge, contourX, leave } = params; this.params = params; this.probe = probe; let trace, slice, latent, lastPP, lastSlope; const newslice = this.newslice = () => { this.slice = slice = []; } const newtrace = this.newtrace = function() { trace = newPolygon().setOpen(); } const end_poly = this.end_poly = function(point) { if (latent) { trace.push(latent); } if (trace.length > 0) { // add additional constraint on min perimeter() if (trace.length > 1) { slice.push(trace); } newtrace(); } lastPP = undefined; latent = undefined; lastSlope = undefined; if (point) { trace.push(point); lastPP = point; } } const log = function(map) { for (let key in map) { const val = map[key]; if (typeof val === 'number') { map[key] = val.round(4); } } console.log(...arguments); } const push_point = this.push_point = function(x, y, z) { const newP = newPoint(x, y, z); const lastP = lastPP;//trace.last(); if (lastP) { const dl = (x - lastP.x) || (y - lastP.y); const dz = z - lastP.z; const slope = Math.atan2(dz, dl); if (curvesOnly && Math.abs(dz) < flatness) { end_poly(newP); } else if (lastSlope !== undefined && Math.abs(lastSlope - slope) < 0.001) { latent = newP; } else { if (latent) { trace.push(latent); latent = undefined; } if (curvesOnly) { const dv = contourX ? Math.abs(lastP.x - x) : Math.abs(lastP.y - y); // const dz = lastPP.z - z; const angle = Math.atan2( Math.abs(dz), dv) * RAD2DEG; if (angle > maxangle) { end_poly(); } } trace.push(newP); } lastSlope = slope; } else { trace.push(newP); } lastPP = newP; } const object = this.object = this; this.inClip = function (clips, checkZ, point) { for (let i=0; i { then(codec.decode(data.slice)); }); } else { this.crossY_sync(params, then); } } crossX(params, then) { const { minions } = self.kiri_worker; if (minions && this.cross.concurrent) { minions.queue({ cmd: "trace_x", params }, data => { then(codec.decode(data.slice)); }); } else { this.crossX_sync(params, then); } } crossY_sync(params, then) { const { push_point, end_poly, newtrace, newslice, inClip } = this.object; const { clipTab, tabHeight, clipTo, box, resolution, density, leave } = this.cross; const { toolAtZ } = this.probe; let { from, to, x, gridx, gridy } = params; const step = resolution * density; const checkr = newPoint(0,0); newslice(); newtrace(); for (let y = from; y < to; y += step) { if (y < box.min.y || y > box.max.y) { gridy += density; continue; } // find tool z at grid point let tv = toolAtZ(gridx, gridy); checkr.x = x; checkr.y = y; // when tabs are on and this point is inside the // tab polygon, ensure z is at least tabHeight if (clipTab && tv < tabHeight && inClip(clipTab, tv, checkr)) { tv = this.tabZ; } // if the value is on the floor and inside the clip // poly (usually shadow), end the segment if (clipTo && !inClip(clipTo, undefined, checkr)) { end_poly(); gridy += density; continue; } push_point(x, y, tv + leave); gridy += density; } end_poly(); then(this.slice); } crossX_sync(params, then) { const { push_point, end_poly, newtrace, newslice, inClip } = this.object; const { clipTab, tabHeight, clipTo, box, resolution, density, leave } = this.cross; const { toolAtZ } = this.probe; let { from, to, y, gridx, gridy } = params; const step = resolution * density; const checkr = newPoint(0,0); newslice(); newtrace(); for (let x = from; x < to; x += step) { if (x < box.min.x || x > box.max.x) { gridx += density; continue; } // find tool z at grid point let tv = toolAtZ(gridx, gridy); checkr.x = x; checkr.y = y; // when tabs are on and this point is inside the // tab polygon, ensure z is at least tabHeight if (clipTab && tv < tabHeight && inClip(clipTab, tv, checkr)) { tv = this.tabZ; } // if the value is on the floor and inside the clip // poly (usually shadow), end the segment if (clipTo && !inClip(clipTo, undefined, checkr)) { end_poly(); gridx += density; continue; } push_point(x, y, tv + leave); gridx += density; } end_poly(); then(this.slice); } } function raster_slice(inputs) { const { lines, data, box, resolution, curvesOnly } = inputs; const { flatness, zMin, minY, maxY, stepsY, gridx } = inputs; const { slice } = inputs; let gridy, gridi, // index gridv, // value i, il, j, x, y, tv; // filter lines pairs to only surface "up-facing", "uncovered" lines let points = []; // emit an array of valid line-pairs const len = lines.length; outer: for (let i=0; i p2.y) { const tp = p1; p1 = p2; p2 = tp }; // eliminate if points "under" other lines for (let j=0; j= i-1 && j <= i+1) continue; let l2 = lines[j], p3 = l2.p1, p4 = l2.p2; // sort p3,p4 by y for comparison if (p3.y > p4.y) { const tp = p3; p3 = p4; p4 = tp }; // it's under the other line if (Math.max(p1.z,p2.z) < Math.min(p3.z,p4.z)) { // it's inside the other line, too, so skip if (p1.y >= p3.y && p2.y <= p4.y) continue outer; } } points.push(p1, p2); box.expandByPoint(p1); box.expandByPoint(p2); } gridy = 0; // rasterize one x slice for (y = minY; y < maxY && gridy < stepsY; y += resolution) { gridi = gridx * stepsY + gridy; gridv = data[gridi] || zMin; // strategy using raw lines (faster slice, but more lines) for (i=0, il=points.length; i gridv || p2.z > gridv); // segment crosses grid y const spansy = (p1.y <= y && p2.y >= y); if (crossz && spansy) { // compute intersection of z ray up // and segment at this grid point const dy = p1.y - p2.y, dz = p1.z - p2.z, pct = (p1.y - y) / dy, nz = p1.z - (dz * pct); // save if point is greater than existing grid point if (nz > gridv) { gridv = data[gridi] = Math.max(nz, zMin); if (slice) slice.output() .setLayer("heights", {face: 0, line: 0}) .addLine( newPoint(p1.x, y, 0), newPoint(p1.x, y, gridv) ); } } } gridy++; } // remove flat lines when curvesOnly if (curvesOnly) { let nup = []; for (let i=0, p=points, l=p.length; i= flatness) { nup.push(p1, p2); } } points = nup; } return points; }; broker.subscribe("minion.started", msg => { const { funcs, cache, reply, log } = msg; funcs.topo_raster = (data, seq) => { const { id, slice, params } = data; const { resolution } = params; const vertices = cache[id]; const box = new THREE.Box2(); new topo_slicer(slice.index) .setFromArray(vertices, slice) .slice(resolution) .forEach(rec => { const { z, index, lines } = rec; for (let line of lines) { const { p1, p2 } = line; if (!p1.swapped) { p1.swapXZ(); p1.swapped = true } if (!p2.swapped) { p2.swapXZ(); p2.swapped = true } } raster_slice({ ...params, box, lines, gridx: index }); }); // only pass back bounds of rasters to be merged reply({ seq, box }); }; funcs.trace_init = data => { const { cache } = self; data.cross.clipTo = codec.decode(data.cross.clipTo); data.cross.clipTab = codec.decode(data.cross.clipTab); const probe = new Probe(data.probe); const trace = new Trace(probe, data.trace); cache.trace = { probe, trace, cross: data.cross }; trace.init(data.cross); }; funcs.trace_y = (data, seq) => { const { cache } = self; const { trace } = cache.trace; trace.crossY_sync(data.params, slice => { slice = codec.encode(slice); reply({ seq, slice }); }); }; funcs.trace_x = (data, seq) => { const { cache } = self; const { trace } = cache.trace; trace.crossX_sync(data.params, slice => { slice = codec.encode(slice); reply({ seq, slice }); }); }; funcs.trace_cleanup = () => { delete cache.trace; }; }); export async function generate(opt) { return new Topo().generate(opt); }