Merge branch 'rel-4.3'

This commit is contained in:
Stewart Allen 2025-09-21 17:37:29 -04:00
commit d2659d0481
13 changed files with 377 additions and 134 deletions

View file

@ -12,7 +12,9 @@ const MESH_OUTFILE = 'src/pack/mesh-main.js';
const MESH_EXTRAS = [ ]; const MESH_EXTRAS = [ ];
const KIRI_OUTFILE = 'src/pack/kiri-main.js'; const KIRI_OUTFILE = 'src/pack/kiri-main.js';
const KIRI_EXTRAS = [ ]; const KIRI_EXTRAS = [
'src/ext/base64.js'
];
async function appendExtraModules(extras, outfile, minify = false) { async function appendExtraModules(extras, outfile, minify = false) {
const files = await glob(extras); const files = await glob(extras);

View file

@ -31,6 +31,10 @@
* `B` multi-extruder rendering of raft fails to offset the rest of the print * `B` multi-extruder rendering of raft fails to offset the rest of the print
* `B` multi-extruder purge blocks fail to generate properly for rafts * `B` multi-extruder purge blocks fail to generate properly for rafts
* `F` min layer time
* `F` separate bridging speed / fan control
* `F` elephant foot compensation
* `F` inner / outer perimeter compensation
* `F` better thin-wall (like arachne) * `F` better thin-wall (like arachne)
* `F` add scarf seams * `F` add scarf seams
* `F` add 'random' and 'seam' start point options * `F` add 'random' and 'seam' start point options

View file

@ -258,9 +258,10 @@ export class Polygon {
// of the polygon with the noodle removed (for the next pass) // of the polygon with the noodle removed (for the next pass)
noodle(width) { noodle(width) {
let clone = this.clone(true); let clone = this.clone(true);
let ins = clone.offset(width); let ins = clone.offset(width) ?? [];
let remain = ins.clone(true);
let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true)); let nood = POLY.nest(POLY.flatten([ clone, ...ins ], [], true));
return { noodle: nood, remain: ins }; return { noodle: nood, remain };
} }
// generate center crossing point cloud // generate center crossing point cloud

View file

@ -1323,7 +1323,8 @@ function init_one() {
yr = ((tl && yc) || (!tl && yt) ? ra : 1), yr = ((tl && yc) || (!tl && yt) ? ra : 1),
zr = ((tl && zc) || (!tl && zt) ? ra : 1); zr = ((tl && zc) || (!tl && zt) ? ra : 1);
// prevent null scale // prevent null scale
if (xr * yr * zr === 0) { if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) {
api.alerts.show('invalid scale value');
return; return;
} }
selection.scale(xr,yr,zr); selection.scale(xr,yr,zr);
@ -1350,7 +1351,11 @@ function init_one() {
xr = ((tl && xc) || (!tl && xt) ? ra : 1), xr = ((tl && xc) || (!tl && xt) ? ra : 1),
yr = ((tl && yc) || (!tl && yt) ? ra : 1), yr = ((tl && yc) || (!tl && yt) ? ra : 1),
zr = ((tl && zc) || (!tl && zt) ? ra : 1); zr = ((tl && zc) || (!tl && zt) ? ra : 1);
// prevent null scale
if (xv * xr < 0.1 || yv * yr < 0.1 || zv * zr < 0.1) {
api.alerts.show('invalid scale value');
return;
}
selection.scale(xr,yr,zr); selection.scale(xr,yr,zr);
ui.scaleX.was = ui.scaleX.value = xv * xr; ui.scaleX.was = ui.scaleX.value = xv * xr;
ui.scaleY.was = ui.scaleY.value = yv * yr; ui.scaleY.was = ui.scaleY.value = yv * yr;
@ -1362,6 +1367,7 @@ function init_one() {
e.target.value = val; e.target.value = val;
} }
// on enter but not on blur
space.event.onEnterKey([ space.event.onEnterKey([
ui.scaleX, selectionScale, ui.scaleX, selectionScale,
ui.scaleY, selectionScale, ui.scaleY, selectionScale,
@ -1369,6 +1375,9 @@ function init_one() {
ui.sizeX, selectionSize, ui.sizeX, selectionSize,
ui.sizeY, selectionSize, ui.sizeY, selectionSize,
ui.sizeZ, selectionSize, ui.sizeZ, selectionSize,
]);
// on enter and blur
space.event.onEnterKey([
ui.toolName, updateTool, ui.toolName, updateTool,
ui.toolNum, updateTool, ui.toolNum, updateTool,
ui.toolFluteDiam, updateTool, ui.toolFluteDiam, updateTool,

View file

@ -362,14 +362,11 @@ class Print {
// if( axis == 'I' || axis == "J") return // if( axis == 'I' || axis == "J") return
if (abs) { if (abs) {
pos[axis] = val; pos[axis] = val;
if (axis == "X") point.x = factor * pos.X + xoff.x + off.x if (axis == "X") point.x = factor * pos.X + xoff.x + off.x
else if (axis == "Y") point.y = factor * pos.Y + xoff.y + off.y else if (axis == "Y") point.y = factor * pos.Y + xoff.y + off.y
else if (axis == "Z") point.z = factor * pos.Z + xoff.z + off.z + dz else if (axis == "Z") point.z = factor * pos.Z + xoff.z + off.z + dz
} else { } else {
mov[axis] = val; // mov[axis] = val;
pos[axis] += val; pos[axis] += val;
} }
// console.log("position updated",structuredClone(pos)) // console.log("position updated",structuredClone(pos))
@ -393,7 +390,6 @@ class Print {
center = center.add(prevPoint); center = center.add(prevPoint);
center.setZ((prevPoint.z+point.z)/2+dz); center.setZ((prevPoint.z+point.z)/2+dz);
} }
return { return {
center, center,
point, point,
@ -402,10 +398,8 @@ class Print {
} }
function outputPoint(point,lastP,emit,{center,arcPoints,retract}) { function outputPoint(point,lastP,emit,{center,arcPoints,retract}) {
// non-move in a new plane means burp out // non-move in a new plane means burp out
// the old sequence and start a new one // the old sequence and start a new one
if (newlayer || (autolayer && seq.z != point.z)) { if (newlayer || (autolayer && seq.z != point.z)) {
newlayer = false; newlayer = false;
let dz = point.z - seq.z; let dz = point.z - seq.z;

View file

@ -61,7 +61,7 @@ export function createTabBox(iw, ic, n) {
const { camZBottom, camStockIndexed } = process; const { camZBottom, camStockIndexed } = process;
const sz = stock.z || bounds.max.z; const sz = stock.z || bounds.max.z;
const zto = sz - iw.track.top; const zto = sz - iw.track.top;
const zp = (env.camZBottom || camStockIndexed ? env.camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2); const zp = (camZBottom || camStockIndexed ? camZBottom : sz - track.box.d - zto) + (camTabsMidline ? 0 : camTabsHeight / 2);
ic.x += n.x * camTabsDepth / 2; // offset from part ic.x += n.x * camTabsDepth / 2; // offset from part
ic.z -= n.y * camTabsDepth / 2; // offset swap z,y ic.z -= n.y * camTabsDepth / 2; // offset swap z,y
ic.y = zp; // offset swap in world space y,z ic.y = zp; // offset swap in world space y,z

View file

@ -100,6 +100,11 @@ class OpOutline extends CamOp {
// extend cut thru (only when z bottom is 0) // extend cut thru (only when z bottom is 0)
if (workarea.bottom_z < 0) { if (workarea.bottom_z < 0) {
let last = slices[slices.length-1]; let last = slices[slices.length-1];
// when step down > full cut depth, clear slices and
// leave only the cut-thru pass(es)
if (op.down > workarea.top_stock - workarea.bottom_part) {
slices.length = 0;
}
for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) {
if (!last) continue; if (!last) continue;
let add = last.clone(true); let add = last.clone(true);

View file

@ -32,11 +32,12 @@ class OpRough extends CamOp {
let roughStock = op.all && isIndexed; let roughStock = op.all && isIndexed;
let toolDiam = new Tool(settings, op.tool).fluteDiameter(); let toolDiam = new Tool(settings, op.tool).fluteDiameter();
let trueShadow = process.camTrueShadow === true; let trueShadow = process.camTrueShadow === true;
let cutThruBypass = op.down > workarea.top_stock - workarea.bottom_part;
updateToolDiams(toolDiam); updateToolDiams(toolDiam);
// clear the stock above the area to be roughed out // clear the stock above the area to be roughed out
if (workarea.top_z > workarea.top_part) { if (workarea.top_z > workarea.top_part && !cutThruBypass) {
let shadow = state.shadow.base.clone(); let shadow = state.shadow.base.clone();
let step = toolDiam * op.step; let step = toolDiam * op.step;
let inset = roughStock ? let inset = roughStock ?
@ -258,6 +259,12 @@ class OpRough extends CamOp {
let last = slices[slices.length-1]; let last = slices[slices.length-1];
// when step down > full cut depth, clear slices and
// leave only the cut-thru pass(es)
if (cutThruBypass) {
slices.length = 0;
}
// add cut thru passes
if (workarea.bottom_z < 0) if (workarea.bottom_z < 0)
for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) { for (let zneg of base_util.lerp(0, -workarea.bottom_cut, op.down)) {
if (!last) continue; if (!last) continue;

View file

@ -38,7 +38,7 @@ class OpTrace extends CamOp {
let cutdir = ov_conv; let cutdir = ov_conv;
let polys = []; let polys = [];
let reContour = false; let reContour = false;
let canRecontour = offset !== 'none' && down === 0; let canRecontour = offset !== 'none' && down < 0;
let stockRect = stock.center && stock.x && stock.y ? let stockRect = stock.center && stock.x && stock.y ?
newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined; newPolygon().centerRectangle({x:0,y:0}, stock.x, stock.y) : undefined;
updateToolDiams(toolDiam); updateToolDiams(toolDiam);
@ -57,6 +57,7 @@ class OpTrace extends CamOp {
let max = Math.max(...zs); let max = Math.max(...zs);
if (max - min > 0.0001 && canRecontour) { if (max - min > 0.0001 && canRecontour) {
reContour = true; reContour = true;
down = 0;
} }
} }
if (false) newSliceOut(0).output() if (false) newSliceOut(0).output()

View file

@ -4,6 +4,7 @@ import { $ } from '../../../moto/webui.js';
import { api } from '../../core/api.js'; import { api } from '../../core/api.js';
import { consts } from '../../core/consts.js'; import { consts } from '../../core/consts.js';
import { settings as setconf } from '../../core/settings.js'; import { settings as setconf } from '../../core/settings.js';
import { calcTaperLength, calcTaperAngle } from './tool.js';
const DEG2RAD = Math.PI / 180; const DEG2RAD = Math.PI / 180;
@ -44,7 +45,7 @@ function selectTool(tool) {
ui.toolMetric.checked = tool.metric; ui.toolMetric.checked = tool.metric;
ui.toolType.selectedIndex = toolNames.indexOf(tool.type); ui.toolType.selectedIndex = toolNames.indexOf(tool.type);
if (tool === 'tapermill') { if (tool === 'tapermill') {
ui.toolTaperAngle.value = kiri.driver.CAM.calcTaperAngle( ui.toolTaperAngle.value = calcTaperAngle(
(tool.flute_diam - tool.taper_tip) / 2, tool.flute_len (tool.flute_diam - tool.taper_tip) / 2, tool.flute_len
).round(1); ).round(1);
} else if(tool === 'drill'){ } else if(tool === 'drill'){
@ -208,16 +209,15 @@ export function updateTool(ev) {
selectedTool.metric = ui.toolMetric.checked; selectedTool.metric = ui.toolMetric.checked;
selectedTool.type = toolNames[ui.toolType.selectedIndex]; selectedTool.type = toolNames[ui.toolType.selectedIndex];
if (selectedTool.type === 'tapermill') { if (selectedTool.type === 'tapermill') {
const CAM = kiri.driver.CAM;
const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2; const rad = (selectedTool.flute_diam - selectedTool.taper_tip) / 2;
if (ev && ev.target === ui.toolTaperAngle) { if (ev && ev.target === ui.toolTaperAngle) {
const angle = parseFloat(ev.target.value); const angle = parseFloat(ev.target.value);
const len = CAM.calcTaperLength(rad, angle * DEG2RAD); const len = calcTaperLength(rad, angle * DEG2RAD);
selectedTool.flute_len = len; selectedTool.flute_len = len;
ui.toolTaperAngle.value = angle.round(1); ui.toolTaperAngle.value = angle.round(1);
ui.toolFluteLen.value = selectedTool.flute_len.round(4); ui.toolFluteLen.value = selectedTool.flute_len.round(4);
} else { } else {
ui.toolTaperAngle.value = CAM.calcTaperAngle(rad, selectedTool.flute_len).round(1); ui.toolTaperAngle.value = calcTaperAngle(rad, selectedTool.flute_len).round(1);
} }
} else { } else {
ui.toolTaperAngle.value = 0; ui.toolTaperAngle.value = 0;

View file

@ -1,7 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { util } from '../../../geo/base.js'; import { util } from '../../../geo/base.js';
import { fdm_slice } from './slice.js'; import { fdm_slice, sliceAll } from './slice.js';
import { fdm_prepare } from './prepare.js'; import { fdm_prepare } from './prepare.js';
import { fdm_export } from './export.js'; import { fdm_export } from './export.js';
@ -39,6 +39,7 @@ export const FDM = {
extrudePerMM, extrudePerMM,
extrudeMM, extrudeMM,
slice: fdm_slice, slice: fdm_slice,
sliceAll: sliceAll,
prepare: fdm_prepare, prepare: fdm_prepare,
export: fdm_export export: fdm_export
}; };

View file

@ -2,7 +2,7 @@
import '../../../ext/jspoly.js'; import '../../../ext/jspoly.js';
import { base } from '../../../geo/base.js'; import { base } from '../../../geo/base.js';
import { newPolygon } from '../../../geo/polygon.js'; import { newPolygon, Polygon } from '../../../geo/polygon.js';
import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { polygons as POLY, fillArea } from '../../../geo/polygons.js';
import { getRangeParameters } from './driver.js'; import { getRangeParameters } from './driver.js';
import { slicer } from '../../../geo/slicer.js'; import { slicer } from '../../../geo/slicer.js';
@ -163,25 +163,47 @@ function thin_type_2(params) {
} }
function thin_type_3(params) { function thin_type_3(params) {
// offsetN usually = extrusion width (nozzle diameter)
let { z, top, count, offsetN, last, gaps } = params; let { z, top, count, offsetN, last, gaps } = params;
top.thin_fill = [];
top.fill_sparse = [];
// produce trace from outside of poly inward no more than max inset
let { noodle, remain } = top.poly.noodle(offsetN * count); let { noodle, remain } = top.poly.noodle(offsetN * count);
top.shells = noodle;
// top.shells = noodle;
top.gaps = last = remain; top.gaps = last = remain;
let thin = top.thin_fill; let thin = top.thin_fill = [];
let scale = 5000; let sparse = top.fill_sparse = [];
let scale = 1000;
let minR = offsetN / 2; let minR = offsetN / 2;
let maxR = minR * 1.5; let maxR = minR * 1.5;
let minSpur = 0; // offsetN * 3; let minSpur = offsetN * 2;
let showNoodle = false;
let mergeChains = true;
let interpolateShortSpur = true;
let showChainRawPoints = false;
let showChainInterpPoints = true;
let showMidline = true;
let showMidPoints = false;
let showMidNormals = false;
let showMidRadii = true;
let showNexuses = false;
let pointMap = new Map(); let pointMap = new Map();
let lineMap = new Map(); let lineMap = new Map();
function pointDist(p0, p1) {
return Math.hypot(p0.x-p1.x, p0.y-p1.y);
}
function point2key(point) {
return `${(point.x*100)|0}|${(point.y*100)|0}`;
}
// convert point to deduplicated / unique point record
function pointToRec(point) { function pointToRec(point) {
let key = `${(point.x*100)|0}|${(point.y*100)|0}`; let key = point2key(point);
let rec = pointMap.get(key); let rec = pointMap.get(key);
if (!rec) { if (!rec) {
rec = { rec = {
@ -196,6 +218,7 @@ function thin_type_3(params) {
return rec; return rec;
} }
// convert line to unique line record. matches reversed and duplicate lines
function pointsToLine(p0, p1) { function pointsToLine(p0, p1) {
p0 = pointToRec(p0); p0 = pointToRec(p0);
p1 = pointToRec(p1); p1 = pointToRec(p1);
@ -219,6 +242,9 @@ function thin_type_3(params) {
let chains = []; let chains = [];
// for each standalone noodle, construct medial axis
// map medial axis points into de-duplicated segments
// which will be re-connected into chains
for (let poly of noodle) { for (let poly of noodle) {
// scale the polygon (including inners) so the medial axis code works properly // scale the polygon (including inners) so the medial axis code works properly
poly = poly.clone(true).scale({ x:scale, y:scale, z:scale }); poly = poly.clone(true).scale({ x:scale, y:scale, z:scale });
@ -231,69 +257,84 @@ function thin_type_3(params) {
for (let { point0, point1 } of ma) { for (let { point0, point1 } of ma) {
pointsToLine(point0, point1); pointsToLine(point0, point1);
} }
for (let seg of lineMap.values()) { }
let { p0, p1 } = seg;
let len = seg.len = Math.hypot(p0.x-p1.x, p0.y-p1.y); // connect segments into chains
// filter out lines where both points' radii are under the for (let seg of lineMap.values()) {
// minR threshold. shorten and fixup lines where minR falls let { p0, p1 } = seg;
// somewhere along the gradient between ends. let len = seg.len = pointDist(p0, p1);
if (p0.r < minR || p1.r < minR) { // filter out lines where both points' radii are under the
if (p0.r > minR && len > minSpur) { // minR threshold. shorten and fixup lines where minR falls
// move p1 toward p0 until minR met // somewhere along the gradient between ends.
let pct = (minR - p1.r) / (p0.r - p1.r); if (interpolateShortSpur)
let dx = p1.x - p0.x; if (p0.r < minR || p1.r < minR) {
let dy = p1.y - p0.y; if (p0.r > minR && len > minSpur) {
p1.x -= dx * pct; // move p1 toward p0 until minR met
p1.y -= dy * pct; let pct = (minR - p1.r) / (p0.r - p1.r);
p1.r = minR; let dx = p1.x - p0.x;
pointMap.delete(p1.key); let dy = p1.y - p0.y;
} else if (p1.r > minR && len > minSpur) { p1.x -= dx * pct;
// move p0 toward p1 until minR met p1.y -= dy * pct;
let pct = (minR - p0.r) / (p1.r - p0.r); p1.r = minR;
let dx = p0.x - p1.x; pointMap.delete(p1.key);
let dy = p0.y - p1.y; } else if (p1.r > minR && len > minSpur) {
p0.x -= dx * pct; // move p0 toward p1 until minR met
p0.y -= dy * pct; let pct = (minR - p0.r) / (p1.r - p0.r);
p0.r = minR; let dx = p0.x - p1.x;
pointMap.delete(p0.key); let dy = p0.y - p1.y;
} else { p0.x -= dx * pct;
lineMap.delete(seg.key); p0.y -= dy * pct;
continue; p0.r = minR;
} pointMap.delete(p0.key);
} else {
// both points eliminated
pointMap.delete(p0.key);
pointMap.delete(p1.key);
lineMap.delete(seg.key);
continue;
} }
// for each line segment either add it to a chain or start a new chain }
let add; // for each line segment either add it to a chain or start a new chain
for (let chain of chains) { let add;
if (p0.count === 2 && p0.key === chain[0].key) { for (let chain of chains) {
chain.splice(0,0,{...p1, len}); if (p0.count === 2 && p0 === chain[0]) {
} else if (p0.count === 2 && p0.key === chain.peek().key) { // prefix chain with p1
chain.peek().len = len; chain.splice(0,0,p1);
chain.push({...p1}); add = true;
add = true; } else if (p0.count === 2 && p0 === chain.peek()) {
} else if (p1.count === 2 && p1.key === chain[0].key) { // append chain with p1
chain.splice(0,0,{...p0, len}); chain.push(p1);
add = true; add = true;
} else if (p1.count === 2 && p1.key === chain.peek().key) { } else if (p1.count === 2 && p1 === chain[0]) {
chain.peek().len = len; // prefix chain with p0
chain.push({...p0}); chain.splice(0,0,p0);
add = true; add = true;
} } else if (p1.count === 2 && p1 === chain.peek()) {
if (add) { // append chain with p0
chain.total += len; chain.push(p0);
break; add = true;
}
} }
if (!add) { if (add) {
chains.push([ {...p0 ,len}, {...p1} ]); chain.total += len;
chains.peek().total = len; break;
} }
} }
if (!add) {
chains.push([ p0, p1 ]);
chains.peek().total = len;
}
} }
let showMid = false; // todo: merge chains at next of 3 or more preferencing
let showCross = false; // merging longer chains. then shorten remaining nexus chain
let showRad = true; // intersections by minR
// render inset "noodle"
if (showNoodle) {
top.shells.appendAll(noodle);
}
// divide a segment/redius into 1 or more equal subsegments
function div(cr) { function div(cr) {
if (cr <= maxR) { if (cr <= maxR) {
return [ cr ]; return [ cr ];
@ -311,71 +352,249 @@ function thin_type_3(params) {
} }
} }
// add chains to thinwall output for visualization // filter chains that are too short from consideration
if (showRad) { chains = chains.filter(c => c.total >= minR);
top.shells = []; chains.sort((c1,c2) => c2.total - c1.total);
}
for (let chain of chains.filter(c => c.total >= minR)) { function merge1chain() {
// chain reduction by merging short segments let clen = chains.length;
let end = chain[0]; for (let i=0; i<clen; i++) {
let nuchain = [ end ]; let ci = chains[i];
for (let point of chain.slice(1)) { if (!ci) continue;
if (point.len === undefined || point.len + end.len >= minR) { for (let j=i+1; j<clen; j++) {
nuchain.push(point); let cj = chains[j];
end = point; if (!cj) continue;
} else { if (ci[0] === cj[0]) {
end.len += point.len; ci.reverse().appendAll(cj)
ci.total += cj.total;
cj.merged = true;
chains[j] = undefined;
return true;
} else if (ci.peek() === cj.peek()) {
ci.appendAll(cj.reverse());
ci.total += cj.total;
cj.merged = true;
chains[j] = undefined;
return true;
} else if (ci.peek() === cj[0]) {
ci.appendAll(cj);
ci.total += cj.total;
cj.merged = true;
chains[j] = undefined;
return true;
} else if (ci[0] === cj.peek()) {
cj.appendAll(ci);
cj.total += ci.total;
ci.merged = true;
chains[i] = undefined;
return true;
}
} }
} }
chain = nuchain; return false;
// emit chain with subdivision for long segments }
for (let i=0; i<chain.length-1; i++) {
// medial axis segment // map chains to nexus via endpoints
if (showMid) { let nexus = {};
thin.push(new Point(chain[i].x, chain[i].y)); function addEp(point, chain) {
thin.push(new Point(chain[i+1].x, chain[i+1].y)); let key = point.key;
let rec = nexus[key];
if (!rec) rec = nexus[key] = { point, chains: [] };
if (rec.chains.indexOf(chain) < 0) rec.chains.push(chain);
}
// since chains are sorted longest to shortest,
// they will appear in the nexus record longest to shortest
for (let chain of chains) {
addEp(chain[0], chain);
addEp(chain.peek(), chain);
}
// connect chains end to end following nexus branches that result
// in the longest final merged chain (open or closed)
// map chain endpoints to nexus points
while (mergeChains && merge1chain()) ;
// filter out null chains that were merged into other chains
chains = chains.filter(chain => chain);
// filter dup points from connecting ends
chains = chains.map(chain => chain.filter((v, i) => i === 0 || v !== chain[i - 1]));
// mark closed chains
// todo use exact point match by eliminating slice above
// then later removing repeated points in the resulting chain from merges
for (let chain of chains) {
chain.closed = (chain[0] === chain.peek());
console.log(chain.closed ? 'closed' : 'open');
if (chain.closed) chain.pop();
}
// shorten chain length by minR distance by popping or moving points
function shorten(chain) {
for (let rem=0, i=chain.length-1; i>=1 && rem < minR; i--) {
let c1 = chain[i];
let c2 = chain[i-1];
let d = pointDist(c1,c2);
if (rem + d <= minR) {
chain.pop();
rem += d;
} else {
let diff = minR - rem;
// move c1 toward c2
let pct = diff / d;
let dx = c1.x - c2.x;
let dy = c1.y - c2.y;
chain[i] = {
x: c1.x - dx * pct,
y: c1.y - dy * pct,
r: c1.r
};
rem = 0;
break;
} }
// compute medial axis segment cross section }
}
// shorten chains terminating at nexus by midR
for (let rec of Object.values(nexus)) {
let { point } = rec;
rec.shorts = 0;
if (rec.chains.length < 3) continue;
for (let chain of chains.filter(c => !c.closed)) {
if (pointDist(chain[0], point) < minR) {
chain.reverse();
shorten(chain);
chain.reverse();
rec.shorts++;
} else if (chain.peek() === point) {
shorten(chain);
rec.shorts++;
}
}
}
if (showNexuses)
for (let rec of Object.values(nexus)) {
let { point } = rec;
top.shells.push(newPolygon().centerCircle(point, 0.2, 6 - rec.shorts));
}
function renderPointNormal(pt, ndx, ndy) {
if (showMidNormals) {
let xo = ndx * pt.r;
let yo = ndy * pt.r;
thin.push(new Point(pt.x + yo, pt.y - xo));
thin.push(new Point(pt.x - yo, pt.y + xo));
}
if (showMidRadii) {
const Sx = pt.x + ndy * pt.r;
const Sy = pt.y - ndx * pt.r;
const circ = top.shells;
const pop = div(pt.r);
let acc = 0;
for (let r of pop) {
const t = acc + r;
const px = Sx + -ndy * t;
const py = Sy + ndx * t;
circ.push(newPolygon().centerCircle({x:px, y:py}, r, 10));
acc += r * 2;
}
}
}
// gather point offsets into shells
let zi = z;
for (let chain of chains) {
// zi += 0.1;
if (showChainRawPoints)
for (let pt of chain) {
top.shells.push(newPolygon().centerCircle(pt, pt.r ?? 0.1, 10));
}
// draw medial axis chain
let { closed, length } = chain;
let term = closed ? length : length - 1;
let segs = [];
for (let i=0; i<term; i++) {
let p0 = chain[i]; let p0 = chain[i];
let p1 = chain[i+1]; let p1 = chain[(i+1) % length];
let len = pointDist(p0, p1);
// medial axis segment
if (showMidline) {
thin.push(new Point(p0.x, p0.y, zi));
thin.push(new Point(p1.x, p1.y, zi));
}
// compute medial axis segment normal
let offs = len <= offsetN ? [ 0 ] : base.util.lerp(0, len, offsetN, true);
let step = 1 / offs.length;
let dr = (p1.r - p0.r); let dr = (p1.r - p0.r);
let dx = (p1.x - p0.x); let dx = (p1.x - p0.x);
let dy = (p1.y - p0.y); let dy = (p1.y - p0.y);
let ndx = dx / p0.len; let ndx = dx / len;
let ndy = dy / p0.len; let ndy = dy / len;
let offs = p0.len <= offsetN ? [ 0.5 ] : base.util.lerp(0, p0.len, offsetN, true); segs.push({ p0, p1, offs, step, dr, dx, dy, ndx, ndy });
let indx = 0; }
let step = 1 / offs.length; console.log({ segs });
// interpolate across the length of the chain segment // compute chain subdivisions
for (let off of offs) { let slen = segs.length;
let inc = (indx++ * step); for (let si=0; si<slen; si++) {
let cr = p0.r + dr * inc; let seg = segs[si];
let cx = p0.x + dx * inc; let segp = segs[(si - 1 + slen) % slen];
let cy = p0.y + dy * inc; let { p0, p1, offs, step, dr, dx, dy, ndx, ndy } = seg;
let xo = ndx * cr; let first = si === 0;
let yo = ndy * cr; let last = si === slen - 1;
if (showCross) { let mid = !(first || last);
thin.push(new Point(cx + yo, cy - xo)); // for length 2, first and last segment are the same
thin.push(new Point(cx - yo, cy + xo)); if (showMidPoints) {
if (first) {
// todo: handle closed
top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
} }
if (showRad) { if (mid) {
let circ = top.shells; // mid segments
let pop = div(cr); top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
let sx = cx + yo; }
let sy = cy - xo; if (last) {
let m = 1; // todo: handle closed
for (let r of pop) { top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
sx -= (r/cr) * yo * m; if (closed) {
sy += (r/cr) * xo * m; console.log('closed');
circ.push(newPolygon().centerCircle({ x:sx, y:sy }, r, 10)); } else {
m = 2; top.shells.push(newPolygon().centerCircle(p1, 0.08, 10));
} }
// console.log(pop);
} }
} }
{
// todo: handle closed
if (first) {
renderPointNormal(p0, ndx, ndy);
}
if (mid) {
renderPointNormal(p0, ((ndx + segp.ndx)/2), ((ndy + segp.ndy)/2));
}
if (last) {
renderPointNormal(p1, ndx, ndy);
}
}
// interpolate across the length of the chain segment
if (showChainInterpPoints)
for (let indx = 1; indx < offs.length; indx++) {
const inc = indx * step;
const cr = p0.r + dr * inc;
const cx = p0.x + dx * inc;
const cy = p0.y + dy * inc;
const xo = ndx * cr;
const yo = ndy * cr;
if (showMidPoints) {
top.shells.push(newPolygon().centerCircle({ x:cx, y:cy }, 0.05, 10));
}
renderPointNormal({ x:cx, y:cy, r:cr }, ndx, ndy);
}
} }
} }
POLY.setZ([...thin, ...top.shells], z);
// POLY.setZ([...top.shells], z);
return { last, gaps }; return { last, gaps };
} }

View file

@ -156,7 +156,7 @@ class Engine {
} }
slice() { slice() {
this.widget.setTopZ(this.settings.stock.z - (this.topOffset || 0)); this.widget.setTopZ((this.settings?.stock?.z || 0) - (this.topOffset || 0));
return new Promise((accept, reject) => { return new Promise((accept, reject) => {
client.clear(); client.clear();
client.sync([this.widget]); client.sync([this.widget]);