Merge branch 'dev-4.3-thinwall' into rel-4.3
This commit is contained in:
commit
33aa8a550f
11 changed files with 427 additions and 380 deletions
|
|
@ -91,11 +91,11 @@ function area2(p1, p2, p3) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCloseTo(v1, v2, dist) {
|
function isCloseTo(v1, v2, dist) {
|
||||||
return Math.abs(v1 - v2) <= (dist || base.config.precision_merge);
|
return Math.abs(v1 - v2) <= (dist ?? base.config.precision_merge);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inCloseRange(val, min, max) {
|
function inCloseRange(val, min, max, precisiom) {
|
||||||
return (isCloseTo(val, min) || val >= min) && (isCloseTo(val, max) || val <= max);
|
return (isCloseTo(val, min, precisiom) || val >= min) && (isCloseTo(val, max, precisiom) || val <= max);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -232,7 +232,7 @@ function intersect(p1, p2, p3, p4, test, parallelok) {
|
||||||
/**
|
/**
|
||||||
* used by {@link rayIntersect} and {@link Polygon.trace}
|
* used by {@link rayIntersect} and {@link Polygon.trace}
|
||||||
*/
|
*/
|
||||||
function intersectRayLine(ro, s1, p1, p2, infinite) {
|
function intersectRayLine(ro, s1, p1, p2) {
|
||||||
let keys = base.key,
|
let keys = base.key,
|
||||||
p1x = ro.x,
|
p1x = ro.x,
|
||||||
p1y = ro.y,
|
p1y = ro.y,
|
||||||
|
|
@ -259,11 +259,11 @@ function intersectRayLine(ro, s1, p1, p2, infinite) {
|
||||||
a = n1 / d;
|
a = n1 / d;
|
||||||
b = n2 / d;
|
b = n2 / d;
|
||||||
|
|
||||||
if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) {
|
if (inCloseRange(b, 0, 1, 0.00) && a >= 0) {
|
||||||
let ip = newPoint(
|
let ip = newPoint(
|
||||||
p1x + (a * s1x),
|
p1x + (a * s1x),
|
||||||
p1y + (a * s1y),
|
p1y + (a * s1y),
|
||||||
p2.z || ro.z,
|
p2.z ?? ro.z,
|
||||||
keys.NONE
|
keys.NONE
|
||||||
);
|
);
|
||||||
ip.dist = a;
|
ip.dist = a;
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,16 @@ class Point {
|
||||||
return this.key;
|
return this.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone(keys) {
|
||||||
let p = newPoint(this.x, this.y, this.z, this._key);
|
let p = newPoint(this.x, this.y, this.z, this._key);
|
||||||
if (this.a !== undefined) {
|
if (this.a !== undefined) {
|
||||||
p.a = this.a;
|
p.a = this.a;
|
||||||
}
|
}
|
||||||
|
if (keys) {
|
||||||
|
for (let key of keys) {
|
||||||
|
p[key] = this[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1104,6 +1104,14 @@ export class Polygon {
|
||||||
return this.first().isEqual(this.last());
|
return this.first().isEqual(this.last());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeIf(dist = 1) {
|
||||||
|
if (this.first().distTo2D(this.last()) <= dist) {
|
||||||
|
return this.setClosed();
|
||||||
|
} else {
|
||||||
|
return this.setOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fixClosed() {
|
fixClosed() {
|
||||||
if (this.appearsClosed()) {
|
if (this.appearsClosed()) {
|
||||||
this.points.pop();
|
this.points.pop();
|
||||||
|
|
@ -2042,13 +2050,9 @@ export class Polygon {
|
||||||
|
|
||||||
// use expand / deflate technique instead
|
// use expand / deflate technique instead
|
||||||
if (opt.pump) {
|
if (opt.pump) {
|
||||||
let p2 = POLY.offset([this], opt.pump, {
|
let p2 = POLY.offset([this], opt.pump, { z });
|
||||||
z
|
|
||||||
});
|
|
||||||
if (p2) {
|
if (p2) {
|
||||||
p2 = POLY.offset(p2, -opt.pump, {
|
p2 = POLY.offset(p2, -opt.pump, { z });
|
||||||
z
|
|
||||||
});
|
|
||||||
return p2;
|
return p2;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -2318,7 +2322,7 @@ export class Polygon {
|
||||||
|
|
||||||
// split long straight lines into segments no longer than max
|
// split long straight lines into segments no longer than max
|
||||||
// and return a new polygon
|
// and return a new polygon
|
||||||
segment(max = 1) {
|
segment(max = 1, mark) {
|
||||||
const newp = [];
|
const newp = [];
|
||||||
const points = this.points;
|
const points = this.points;
|
||||||
const length = points.length;
|
const length = points.length;
|
||||||
|
|
@ -2330,6 +2334,7 @@ export class Polygon {
|
||||||
const dy = p2.y - p1.y;
|
const dy = p2.y - p1.y;
|
||||||
const dl = Math.sqrt(dx * dx + dy * dy);
|
const dl = Math.sqrt(dx * dx + dy * dy);
|
||||||
newp.push(p1);
|
newp.push(p1);
|
||||||
|
if (mark) p1.segment = p1;
|
||||||
if (dl < max) {
|
if (dl < max) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -2344,10 +2349,12 @@ export class Polygon {
|
||||||
newp.push(newPoint(ox, oy, (p1.z + p2.z) / 2));
|
newp.push(newPoint(ox, oy, (p1.z + p2.z) / 2));
|
||||||
ox += ix;
|
ox += ix;
|
||||||
oy += iy;
|
oy += iy;
|
||||||
|
// mark new point with first point of originating segment
|
||||||
|
if (mark) newp.peek().segment = p1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newp.length > length) {
|
if (newp.length > length) {
|
||||||
return newPolygon().addPoints(newp.map(p => p.clone())).setOpenValue(this.open);
|
return newPolygon().addPoints(newp.map(p => p.clone(['segment']))).setOpenValue(this.open);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -645,7 +645,7 @@ export function offset(polys, dist, opts = {}) {
|
||||||
for (let poly of polys) {
|
for (let poly of polys) {
|
||||||
// convert to clipper format
|
// convert to clipper format
|
||||||
poly = poly.toClipper();
|
poly = poly.toClipper();
|
||||||
if (clean) poly = CleanPolygons(poly, config.clipperClean);
|
if (clean) poly = CleanPolygons(poly, opts.cleanDist ?? config.clipperClean);
|
||||||
if (simple) poly = SimplifyPolygons(poly, fill);
|
if (simple) poly = SimplifyPolygons(poly, fill);
|
||||||
coff.AddPaths(poly, join, type);
|
coff.AddPaths(poly, join, type);
|
||||||
}
|
}
|
||||||
|
|
@ -871,6 +871,7 @@ export function fillArea(polys, angle, spacing, output, minLen, maxLen) {
|
||||||
* @returns {Point[]}
|
* @returns {Point[]}
|
||||||
*/
|
*/
|
||||||
export function rayIntersect(start, slope, polygons, for_fill) {
|
export function rayIntersect(start, slope, polygons, for_fill) {
|
||||||
|
throw "deprecated";
|
||||||
let i = 0,
|
let i = 0,
|
||||||
flat = [],
|
flat = [],
|
||||||
points = [],
|
points = [],
|
||||||
|
|
|
||||||
|
|
@ -115,9 +115,9 @@ const LISTS = {
|
||||||
],
|
],
|
||||||
thin: [
|
thin: [
|
||||||
{ name: "off" },
|
{ name: "off" },
|
||||||
{ name: "type 1" },
|
{ name: "adaptive" },
|
||||||
{ name: "type 2" },
|
{ name: "legacy 1" },
|
||||||
{ name: "type 3" }
|
{ name: "legacy 2" }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,7 @@ async function path(levels, update, opts = {}) {
|
||||||
// Math.abs(op.y - lp.y),
|
// Math.abs(op.y - lp.y),
|
||||||
// Math.abs(op.z - lp.z));
|
// Math.abs(op.z - lp.z));
|
||||||
// if (moved < 0.0001) return;
|
// if (moved < 0.0001) return;
|
||||||
if( is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit
|
if (is_cam() && (out.emit == 2 || out.emit == 3 )) { // cam arc emit
|
||||||
// checks if a new poly should be started
|
// checks if a new poly should be started
|
||||||
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
||||||
current = newPolygon().setOpen();
|
current = newPolygon().setOpen();
|
||||||
|
|
@ -210,7 +210,6 @@ async function path(levels, update, opts = {}) {
|
||||||
} else if (out.emit) {
|
} else if (out.emit) {
|
||||||
// explicity G1 in CAM mode
|
// explicity G1 in CAM mode
|
||||||
// just a non-move in other modes
|
// just a non-move in other modes
|
||||||
|
|
||||||
// checks if a new poly should be started
|
// checks if a new poly should be started
|
||||||
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
if (!lastOut.emit || (ckspeed && out.speed !== lastOut.speed) || lastEnd) {
|
||||||
current = newPolygon().setOpen();
|
current = newPolygon().setOpen();
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,9 @@ class Slice {
|
||||||
} else {
|
} else {
|
||||||
// create top object from object bundle passed back by slicePost()
|
// create top object from object bundle passed back by slicePost()
|
||||||
let top = new Top(data.poly);
|
let top = new Top(data.poly);
|
||||||
top.thin_fill = data.thin_fill ? data.thin_fill.map(p => newPoint(p.x,p.y,p.z)) : undefined;
|
top.thin_fill = data.thin_fill?.map(p => newPoint(p.x,p.y,p.z));
|
||||||
|
top.thin_wall = data.thin_wall;
|
||||||
|
top.thin_sort = data.thin_sort;
|
||||||
top.fill_lines = data.fill_lines;
|
top.fill_lines = data.fill_lines;
|
||||||
top.fill_sparse = data.fill_sparse;
|
top.fill_sparse = data.fill_sparse;
|
||||||
top.fill_off = data.fill_off;
|
top.fill_off = data.fill_off;
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,12 @@ export function menu() {
|
||||||
sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"),
|
sliceDetectThin: newSelect(LANG.ad_thin_s, { title: LANG.ad_thin_l, action: thinWallSave }, "thin"),
|
||||||
outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}),
|
outputAlternating: newBoolean(LANG.ad_altr_s, onBooleanClick, {title:LANG.ad_altr_l}),
|
||||||
sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}),
|
sliceZInterleave: newBoolean(LANG.ad_zint_s, onBooleanClick, {title:LANG.ad_zint_l, show:zIntShow}),
|
||||||
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
|
sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0)}),
|
||||||
_____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }),
|
_____: newGroup(LANG.fi_menu, $('fdm-fill'), { modes:FDM, driven, hideable, separator, group:"fdm-fill" }),
|
||||||
sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"),
|
sliceFillType: newSelect(LANG.fi_type, {trigger}, "infill"),
|
||||||
sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}),
|
sliceFillSparse: newInput(LANG.fi_pcnt_s, {title:LANG.fi_pcnt_l, convert:toFloat, bound:bound(0.0,1.0), show:hasInfill}),
|
||||||
sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}),
|
sliceFillRepeat: newInput(LANG.fi_rept_s, {title:LANG.fi_rept_l, convert:toInt, bound:bound(1,10), show:fillIsLinear}),
|
||||||
sliceFillOverlap: newInput(LANG.fi_over_s, {title:LANG.fi_over_l, convert:toFloat, bound:bound(0.0,2.0), show:hasInfill}),
|
|
||||||
separator: newBlank({ class:"set-sep", driven }),
|
separator: newBlank({ class:"set-sep", driven }),
|
||||||
sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}),
|
sliceFillRate: newInput(LANG.ou_feed_s, {title:LANG.ou_feed_l, convert:toInt, bound:bound(0,500)}),
|
||||||
sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}),
|
sliceSolidRate: newInput(LANG.ou_fini_s, {title:LANG.ou_fini_l, convert:toInt, bound:bound(0,500)}),
|
||||||
|
|
@ -178,7 +179,7 @@ export function menu() {
|
||||||
outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}),
|
outputCoastDist: newInput(LANG.ad_scst_s, {title:LANG.ad_scst_l, convert:toFloat, bound:bound(0.0,10)}),
|
||||||
zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}),
|
zHopDistance: newInput(LANG.ad_zhop_s, {title:LANG.ad_zhop_l, convert:toFloat, bound:bound(0,3.0)}),
|
||||||
arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}),
|
arcTolerance: newInput(LANG.ad_arct_s, {title:LANG.ad_arct_l, convert:toFloat, bound:bound(0,1.0), show:() => { return isNotBelt() }}),
|
||||||
antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toInt, bound:bound(0,3)}),
|
antiBacklash: newInput(LANG.ad_abkl_s, {title:LANG.ad_abkl_l, convert:toFloat, bound:bound(0,3)}),
|
||||||
outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}),
|
outputLoops: newInput(LANG.ag_loop_s, {title:LANG.ag_loop_l, convert:toInt, bound:bound(-1,1000), show:isBelt}),
|
||||||
outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}),
|
outputPurgeTower: newInput(LANG.ad_purg_s, {title:LANG.ad_purg_l, convert:toInt, bound:bound(0,1000)}),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -164,31 +164,173 @@ function thin_type_2(params) {
|
||||||
|
|
||||||
function thin_type_3(params) {
|
function thin_type_3(params) {
|
||||||
// offsetN usually = extrusion width (nozzle diameter)
|
// offsetN usually = extrusion width (nozzle diameter)
|
||||||
let { z, top, count, offsetN, last, gaps } = params;
|
let { z, top, count, offsetN } = params;
|
||||||
|
|
||||||
// produce trace from outside of poly inward no more than max inset
|
// produce trace from outside of poly inward no more than max inset
|
||||||
let { noodle, remain } = top.poly.noodle(offsetN * count);
|
let noodleWidth = offsetN * count;
|
||||||
|
let { noodle, remain } = top.poly.noodle(noodleWidth);
|
||||||
|
|
||||||
// top.shells = noodle;
|
// re-expand inner offset so fill offsets align properly with expectation
|
||||||
top.gaps = last = remain;
|
remain = POLY.offset(remain, offsetN / 2);
|
||||||
|
|
||||||
let thin = top.thin_fill = [];
|
// render inset "noodle"
|
||||||
let sparse = top.fill_sparse = [];
|
// shells.appendAll(noodle);
|
||||||
|
|
||||||
|
let debugRemain = false;
|
||||||
|
let debugExtrusion = false;
|
||||||
|
|
||||||
|
let shells = top.shells; // only used for debug
|
||||||
|
let traces = top.thin_wall = [];
|
||||||
|
|
||||||
|
let midR = offsetN * 0.5; // nominal extrusion width
|
||||||
|
let minR = midR * 0.75; // smallest interior fill or single wall width
|
||||||
|
let maxR = midR * 1.5; // max double wall extrusion width
|
||||||
|
let maxF = midR * 2; // max interior extrusion width
|
||||||
|
|
||||||
|
let lines = [];
|
||||||
|
let polys = [];
|
||||||
|
|
||||||
|
for (let i=0; i<count; i++) {
|
||||||
|
let next = [];
|
||||||
|
|
||||||
|
// process each top level noodle separately
|
||||||
|
for (let n of noodle)
|
||||||
|
trace_noodle(
|
||||||
|
[ n ],
|
||||||
|
noodleWidth,
|
||||||
|
minR,
|
||||||
|
i === 0 ? midR : midR * 1.25,
|
||||||
|
i === 0 ? maxR : maxF,
|
||||||
|
{
|
||||||
|
shell: i + 1,
|
||||||
|
lines,
|
||||||
|
polys,
|
||||||
|
remain: next,
|
||||||
|
traces,
|
||||||
|
brute: false,
|
||||||
|
showChainIntersect: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// show remaining noodle after single trace
|
||||||
|
if (debugRemain) polys.push(...POLY.setZ(POLY.flatten(next), z));
|
||||||
|
|
||||||
|
noodle = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log({ lines, polys, trace });
|
||||||
|
|
||||||
|
// show extrusion
|
||||||
|
let zo = z + 0.1;
|
||||||
|
if (debugExtrusion)
|
||||||
|
for (let trace of traces)
|
||||||
|
for (let point of trace) {
|
||||||
|
polys.push(newPolygon().centerCircle(point, point.r, 12).setZ(zo = zo + 0.005));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!debugExtrusion)
|
||||||
|
POLY.setZ([...lines, ...polys], z);
|
||||||
|
|
||||||
|
if (lines.length) top.thin_fill = lines;
|
||||||
|
if (polys.length) shells.push(...polys);
|
||||||
|
|
||||||
|
// create shell depth annotation that will survive serialization
|
||||||
|
top.thin_sort = traces.map(a => a.shell);
|
||||||
|
|
||||||
|
// console.log({ this: (self.kiri_minion?.name ?? self.kiri_worker), traces });
|
||||||
|
return { traces, last: remain };
|
||||||
|
}
|
||||||
|
|
||||||
|
// trace a single extrusion line around the inside of the noodle poly
|
||||||
|
function trace_noodle(noodle, noodleWidth, minR, midR, maxR, opt = {}) {
|
||||||
|
let {
|
||||||
|
brute,
|
||||||
|
showInset,
|
||||||
|
showChainIntersect,
|
||||||
|
minArea = midR * midR,
|
||||||
|
shellStep = 1 / 100, // diameter step as % of nozzle
|
||||||
|
lines = [],
|
||||||
|
polys = [],
|
||||||
|
remain = [],
|
||||||
|
traces = [],
|
||||||
|
shell = 0
|
||||||
|
} = opt;
|
||||||
|
|
||||||
|
let sstep = 1 / shellStep;
|
||||||
|
let insets = [];
|
||||||
let scale = 1000;
|
let scale = 1000;
|
||||||
let minR = offsetN / 2;
|
let dstep = minR / 4;
|
||||||
let maxR = minR * 1.5;
|
let dtotl = 0;
|
||||||
let minSpur = offsetN * 2;
|
|
||||||
|
|
||||||
let showNoodle = false;
|
let inset = POLY.offset(noodle, -dstep, {
|
||||||
let mergeChains = true;
|
join: ClipperLib.JoinType.jtRound,
|
||||||
let interpolateShortSpur = true;
|
arc: (1/dstep) * 4
|
||||||
let showChainRawPoints = false;
|
});
|
||||||
let showChainInterpPoints = true;
|
while (inset && inset.length) {
|
||||||
let showMidline = true;
|
dtotl += dstep;
|
||||||
let showMidPoints = false;
|
for (let p of POLY.flatten(inset, [])) {
|
||||||
let showMidNormals = false;
|
p.dtotl = dtotl;
|
||||||
let showMidRadii = true;
|
}
|
||||||
let showNexuses = false;
|
insets.push(inset);
|
||||||
|
if (showInset) {
|
||||||
|
polys.appendAll(inset);
|
||||||
|
}
|
||||||
|
if (!brute) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
inset = POLY.offset(inset, -dstep, {
|
||||||
|
minArea: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// project from first inset and find greated offset intersection.
|
||||||
|
// this examples show how to brute force create a medial axis from
|
||||||
|
// micro stepping insets. the same intersect technique used with
|
||||||
|
// medial axis can be applied to this structure to produce an
|
||||||
|
// equivalent output. not as computationally efficient, but much simpler.
|
||||||
|
if (brute) {
|
||||||
|
let { intersectRayLine } = base.util;
|
||||||
|
let tests = insets.slice(1);
|
||||||
|
let source = POLY.flatten(insets[0], []);
|
||||||
|
for (let poly of source) {
|
||||||
|
// segment the smallest inset and test against all other insets
|
||||||
|
poly.segment(minR).forEachSegment((p1, p2) => {
|
||||||
|
let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 };
|
||||||
|
let len = pointDist(p1, p2);
|
||||||
|
let dx = (p1.x - p2.x) / len;
|
||||||
|
let dy = (p1.y - p2.y) / len;
|
||||||
|
let np2 = { x: np1.x + dy * 10, y: np1.y - dx * 10 };
|
||||||
|
let max = { dtotl: poly.dtotl, dist: Infinity, maxd: Infinity };
|
||||||
|
let term = false;
|
||||||
|
outer: for (let test of tests) {
|
||||||
|
for (let tpoly of POLY.flatten(test, [])) {
|
||||||
|
tpoly.forEachSegment((tp1, tp2) => {
|
||||||
|
let int = intersectRayLine(np1, { dx: dy, dy: -dx }, tp1, tp2);
|
||||||
|
if (int && int.dist > int.p1.poly.dtotl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (int && int.dist <= max.maxd && int.p1.poly.dtotl > max.dtotl) {
|
||||||
|
max.dtotl = int.p1.poly.dtotl;
|
||||||
|
max.dist = int.dist;
|
||||||
|
max.int = int;
|
||||||
|
} else if (int && int.dist <= max.maxd && int.p1.poly.dtotl === max.dtotl && int.dist < max.dist) {
|
||||||
|
max.dtotl = int.p1.poly.dtotl;
|
||||||
|
max.dist = int.dist;
|
||||||
|
max.int = int;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (term) {
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (max.int) {
|
||||||
|
lines.push(new Point(np1.x, np1.y));
|
||||||
|
lines.push(new Point(max.int.x, max.int.y));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { lines, polys };
|
||||||
|
}
|
||||||
|
|
||||||
let pointMap = new Map();
|
let pointMap = new Map();
|
||||||
let lineMap = new Map();
|
let lineMap = new Map();
|
||||||
|
|
@ -240,11 +382,8 @@ function thin_type_3(params) {
|
||||||
return rec;
|
return rec;
|
||||||
}
|
}
|
||||||
|
|
||||||
let chains = [];
|
|
||||||
|
|
||||||
// for each standalone noodle, construct medial axis
|
// for each standalone noodle, construct medial axis
|
||||||
// map medial axis points into de-duplicated segments
|
// 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 });
|
||||||
|
|
@ -253,349 +392,172 @@ function thin_type_3(params) {
|
||||||
// construct medial axis lines. this appears to produce duplicate
|
// construct medial axis lines. this appears to produce duplicate
|
||||||
// reverse segments in a subset of cases
|
// reverse segments in a subset of cases
|
||||||
let ma = JSPoly.construct_medial_axis(out, inr);
|
let ma = JSPoly.construct_medial_axis(out, inr);
|
||||||
// dedup the points so we can track them uniquely (for chain intersections)
|
// dedup the points so we can track them uniquely
|
||||||
for (let { point0, point1 } of ma) {
|
for (let { point0, point1 } of ma) {
|
||||||
pointsToLine(point0, point1);
|
pointsToLine(point0, point1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect segments into chains
|
|
||||||
for (let seg of lineMap.values()) {
|
|
||||||
let { p0, p1 } = seg;
|
|
||||||
let len = seg.len = pointDist(p0, p1);
|
|
||||||
// filter out lines where both points' radii are under the
|
|
||||||
// minR threshold. shorten and fixup lines where minR falls
|
|
||||||
// somewhere along the gradient between ends.
|
|
||||||
if (interpolateShortSpur)
|
|
||||||
if (p0.r < minR || p1.r < minR) {
|
|
||||||
if (p0.r > minR && len > minSpur) {
|
|
||||||
// move p1 toward p0 until minR met
|
|
||||||
let pct = (minR - p1.r) / (p0.r - p1.r);
|
|
||||||
let dx = p1.x - p0.x;
|
|
||||||
let dy = p1.y - p0.y;
|
|
||||||
p1.x -= dx * pct;
|
|
||||||
p1.y -= dy * pct;
|
|
||||||
p1.r = minR;
|
|
||||||
pointMap.delete(p1.key);
|
|
||||||
} else if (p1.r > minR && len > minSpur) {
|
|
||||||
// move p0 toward p1 until minR met
|
|
||||||
let pct = (minR - p0.r) / (p1.r - p0.r);
|
|
||||||
let dx = p0.x - p1.x;
|
|
||||||
let dy = p0.y - p1.y;
|
|
||||||
p0.x -= dx * pct;
|
|
||||||
p0.y -= dy * pct;
|
|
||||||
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 (let chain of chains) {
|
|
||||||
if (p0.count === 2 && p0 === chain[0]) {
|
|
||||||
// prefix chain with p1
|
|
||||||
chain.splice(0,0,p1);
|
|
||||||
add = true;
|
|
||||||
} else if (p0.count === 2 && p0 === chain.peek()) {
|
|
||||||
// append chain with p1
|
|
||||||
chain.push(p1);
|
|
||||||
add = true;
|
|
||||||
} else if (p1.count === 2 && p1 === chain[0]) {
|
|
||||||
// prefix chain with p0
|
|
||||||
chain.splice(0,0,p0);
|
|
||||||
add = true;
|
|
||||||
} else if (p1.count === 2 && p1 === chain.peek()) {
|
|
||||||
// append chain with p0
|
|
||||||
chain.push(p0);
|
|
||||||
add = true;
|
|
||||||
}
|
|
||||||
if (add) {
|
|
||||||
chain.total += len;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!add) {
|
|
||||||
chains.push([ p0, p1 ]);
|
|
||||||
chains.peek().total = len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: merge chains at next of 3 or more preferencing
|
|
||||||
// merging longer chains. then shorten remaining nexus chain
|
|
||||||
// intersections by minR
|
|
||||||
|
|
||||||
// render inset "noodle"
|
|
||||||
if (showNoodle) {
|
|
||||||
top.shells.appendAll(noodle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// divide a segment/redius into 1 or more equal subsegments
|
// 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 ];
|
||||||
} else if (cr <= maxR * 2) {
|
} else if (cr <= maxR * 2) {
|
||||||
return [ cr / 2, cr / 2 ];
|
let rem = (cr - midR * 2);
|
||||||
|
if (rem < minR) {
|
||||||
|
return [ cr / 2, cr / 2 ];
|
||||||
|
}
|
||||||
|
return [ midR, rem, midR ];
|
||||||
} else if (cr <= maxR * 3) {
|
} else if (cr <= maxR * 3) {
|
||||||
let rem = cr - minR * 2;
|
let rem = (cr - midR * 2);
|
||||||
if (rem < maxR) {
|
if (rem < minR) {
|
||||||
return [ minR, rem, minR ];
|
return [ cr / 3, cr / 3, cr / 3 ];
|
||||||
} else {
|
|
||||||
return [ minR, rem / 2, rem / 2, minR ];
|
|
||||||
}
|
}
|
||||||
|
return [ midR, rem, midR ];
|
||||||
} else {
|
} else {
|
||||||
return [ minR, ...div(cr - minR * 2), minR ];
|
return [ midR, cr - midR * 2, midR ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter chains that are too short from consideration
|
// medial axis segments for comparison
|
||||||
chains = chains.filter(c => c.total >= minR);
|
let segs = [...lineMap.values()];
|
||||||
chains.sort((c1,c2) => c2.total - c1.total);
|
|
||||||
|
|
||||||
function merge1chain() {
|
// project inset segment normals onto closest medial segment
|
||||||
let clen = chains.length;
|
// use this to find a radius which we divide into extrusion lanes
|
||||||
for (let i=0; i<clen; i++) {
|
if (!brute)
|
||||||
let ci = chains[i];
|
for (let insetp of inset) {
|
||||||
if (!ci) continue;
|
let { intersectRayLine } = base.util;
|
||||||
for (let j=i+1; j<clen; j++) {
|
let parent = new Polygon();
|
||||||
let cj = chains[j];
|
let inner = [];
|
||||||
if (!cj) continue;
|
for (let poly of insetp.flattenTo([])) {
|
||||||
if (ci[0] === cj[0]) {
|
let nupoly = poly.parent ? new Polygon() : parent;
|
||||||
ci.reverse().appendAll(cj)
|
if (poly.parent) {
|
||||||
ci.total += cj.total;
|
inner.push(nupoly);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
let ltpo;
|
||||||
return false;
|
let trace = [];
|
||||||
}
|
trace.shell = poly.parent ? shell : -shell; // shell number (- for inner/hole)
|
||||||
|
poly.segment(minR, true).forEachSegment((p1, p2) => {
|
||||||
// map chains to nexus via endpoints
|
let np1 = { x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2 };
|
||||||
let nexus = {};
|
let len = pointDist(p1, p2);
|
||||||
function addEp(point, chain) {
|
let dx = (p1.x - p2.x) / len;
|
||||||
let key = point.key;
|
let dy = (p1.y - p2.y) / len;
|
||||||
let rec = nexus[key];
|
let min = { dist: Infinity };
|
||||||
if (!rec) rec = nexus[key] = { point, chains: [] };
|
for (let seg of segs) {
|
||||||
if (rec.chains.indexOf(chain) < 0) rec.chains.push(chain);
|
let { p0, p1 } = seg;
|
||||||
}
|
let int = intersectRayLine(np1, { dx: dy, dy: -dx }, p0, p1);
|
||||||
// since chains are sorted longest to shortest,
|
if (int?.dist < min.dist && int?.dist < noodleWidth) {
|
||||||
// they will appear in the nexus record longest to shortest
|
min = int;
|
||||||
for (let chain of chains) {
|
min.seg = seg;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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 dx = (p1.x - p0.x);
|
|
||||||
let dy = (p1.y - p0.y);
|
|
||||||
let ndx = dx / len;
|
|
||||||
let ndy = dy / len;
|
|
||||||
segs.push({ p0, p1, offs, step, dr, dx, dy, ndx, ndy });
|
|
||||||
}
|
|
||||||
console.log({ segs });
|
|
||||||
// compute chain subdivisions
|
|
||||||
let slen = segs.length;
|
|
||||||
for (let si=0; si<slen; si++) {
|
|
||||||
let seg = segs[si];
|
|
||||||
let segp = segs[(si - 1 + slen) % slen];
|
|
||||||
let { p0, p1, offs, step, dr, dx, dy, ndx, ndy } = seg;
|
|
||||||
let first = si === 0;
|
|
||||||
let last = si === slen - 1;
|
|
||||||
let mid = !(first || last);
|
|
||||||
// for length 2, first and last segment are the same
|
|
||||||
if (showMidPoints) {
|
|
||||||
if (first) {
|
|
||||||
// todo: handle closed
|
|
||||||
top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
|
|
||||||
}
|
|
||||||
if (mid) {
|
|
||||||
// mid segments
|
|
||||||
top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
|
|
||||||
}
|
|
||||||
if (last) {
|
|
||||||
// todo: handle closed
|
|
||||||
top.shells.push(newPolygon().centerCircle(p0, 0.08, 10));
|
|
||||||
if (closed) {
|
|
||||||
console.log('closed');
|
|
||||||
} else {
|
|
||||||
top.shells.push(newPolygon().centerCircle(p1, 0.08, 10));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (min.p1) {
|
||||||
|
let mp1 = min.seg;
|
||||||
|
if (showChainIntersect) {
|
||||||
|
lines.push(new Point(np1.x, np1.y));
|
||||||
|
lines.push(new Point(min.x, min.y));
|
||||||
|
}
|
||||||
|
let mr = min.dist + dstep;
|
||||||
|
if (mr < minR) {
|
||||||
|
nupoly.push(new Point(min.x, min.y));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// use intersection off and divide into extrusion lanes
|
||||||
|
let pop = div(mr);
|
||||||
|
let odd = (pop.length % 2 === 1);
|
||||||
|
let len = Math.ceil(pop.length / 2);
|
||||||
|
// check if segment was claimed by a different originating segment
|
||||||
|
// for odd wall counts so that they're not emitted twice
|
||||||
|
if (odd && mp1.claimed && mp1.claimed !== p1.segment) len--;
|
||||||
|
if (len === 0) {
|
||||||
|
nupoly.push(new Point(min.x, min.y));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let npo;
|
||||||
|
if (pop.length === 1) {
|
||||||
|
// let rad_step = ((pop[0] * sstep) | 0) / sstep;
|
||||||
|
// for single wall place it exactly on medial axis
|
||||||
|
nupoly.push(new Point(min.x, min.y));
|
||||||
|
npo = { x: min.x, y: min.y, r: mr, s: p1.segment };
|
||||||
|
} else {
|
||||||
|
// radius rounded to nearest step
|
||||||
|
let rad_step = ((pop[0] * sstep) | 0) / sstep;
|
||||||
|
// otherwise use first division
|
||||||
|
// compensate for minimal inset
|
||||||
|
let off = -dstep + rad_step;
|
||||||
|
npo = {
|
||||||
|
x: np1.x + dy * off,
|
||||||
|
y: np1.y - dx * off,
|
||||||
|
r: rad_step,
|
||||||
|
s: p1.segment
|
||||||
|
};
|
||||||
|
// trace inset by full diameter
|
||||||
|
nupoly.push(new Point(np1.x + dy * off * 2, np1.y - dx * off * 2));
|
||||||
|
}
|
||||||
|
let lpo = trace.peek();
|
||||||
|
// when outputting the same segment at the same radius
|
||||||
|
// it's safe to drop the last point since path output will connect them
|
||||||
|
if (trace.length > 1 && lpo.s === npo.s && lpo.r === npo.r) {
|
||||||
|
trace.pop();
|
||||||
|
}
|
||||||
|
trace.push(npo);
|
||||||
|
if (odd && !mp1.claimed) {
|
||||||
|
mp1.claimed = p1.segment;
|
||||||
|
}
|
||||||
|
// detect trace jump and start new trace
|
||||||
|
if (ltpo && pointDist(ltpo, trace.peek()) > maxR) {
|
||||||
|
// start new trace removing jump point
|
||||||
|
// and adding that to the new trace
|
||||||
|
traces.push(trace);
|
||||||
|
let nutrace = [ trace.pop() ];
|
||||||
|
nutrace.shell = trace.shell;
|
||||||
|
// close last trace if endpoints near enough
|
||||||
|
if (pointDist(ltpo, trace[0] <= maxR)) {
|
||||||
|
trace.push(trace[0]);
|
||||||
|
}
|
||||||
|
trace = nutrace;
|
||||||
|
}
|
||||||
|
// update last trace point out
|
||||||
|
ltpo = trace.peek();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// repeat first point of trace (if close enough to be closed)
|
||||||
|
if (trace[0] && pointDist(trace[0], trace.peek()) <= maxR) {
|
||||||
|
trace.push(trace[0]);
|
||||||
}
|
}
|
||||||
{
|
if (trace.length) {
|
||||||
// todo: handle closed
|
traces.push(trace);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subtract inner extrusion offsets from parent extrusion offset
|
||||||
|
// this can result in more top level polys and must be processed
|
||||||
|
// iteratively.
|
||||||
|
let setA = [ parent ];
|
||||||
|
for (let setB of inner) {
|
||||||
|
setA = POLY.subtract(setA, [ setB ], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean and re-nest the resulting polygon soup
|
||||||
|
// subtraction cleans pre-subtract but not post and these
|
||||||
|
// outlines can have sharp unprintable interiors
|
||||||
|
let ret = POLY.nest(
|
||||||
|
POLY.flatten(setA, [], true).map(p => {
|
||||||
|
return p.clean().simplify();
|
||||||
|
}).flat()
|
||||||
|
).filter(p => p.area() >= minArea);
|
||||||
|
|
||||||
|
remain.push(...ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove point.segment markers for codec encoding between minion and worker
|
||||||
|
for (let trace of traces) {
|
||||||
|
for (let pt of trace) {
|
||||||
|
delete pt.s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
POLY.setZ([...thin, ...top.shells], z);
|
return { lines, polys, remain, traces };
|
||||||
// POLY.setZ([...top.shells], z);
|
|
||||||
|
|
||||||
return { last, gaps };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -639,21 +601,21 @@ export function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {
|
||||||
} });
|
} });
|
||||||
let ret = { last, gaps };
|
let ret = { last, gaps };
|
||||||
switch (opt.thinType) {
|
switch (opt.thinType) {
|
||||||
case "type 1":
|
case "legacy 1":
|
||||||
ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset });
|
ret = thin_type_1({ z, top, count, top_poly, offsetN, fillOffset });
|
||||||
break;
|
break;
|
||||||
case "type 2":
|
case "legacy 2":
|
||||||
ret = thin_type_2({ z, top, count, top_poly, offset1, offsetN, fillOffset, gaps, wasm });
|
ret = thin_type_2({ z, top, count, top_poly, offset1, offsetN, fillOffset, gaps, wasm });
|
||||||
break;
|
break;
|
||||||
case "type 3":
|
case "adaptive":
|
||||||
ret = thin_type_3({ z, top, count, top_poly, offsetN });
|
ret = thin_type_3({ z, top, count, offsetN });
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm });
|
ret = offset_default({ z, top, count, top_poly, offset1, offsetN, wasm });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
last = ret.last || last;
|
last = ret.last ?? last;
|
||||||
gaps = ret.gaps || gaps;
|
gaps = ret.gaps ?? gaps;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no shells, just infill, is permitted
|
// no shells, just infill, is permitted
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { base, util } from '../../../geo/base.js';
|
import { base, util } from '../../../geo/base.js';
|
||||||
import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js';
|
import { poly2polyEmit, tip2tipEmit } from '../../../geo/paths.js';
|
||||||
import { newBounds } from '../../../geo/bounds.js';
|
import { newBounds } from '../../../geo/bounds.js';
|
||||||
import { newPoint } from '../../../geo/point.js';
|
import { newPoint, Point } from '../../../geo/point.js';
|
||||||
import { newPolygon, Polygon } 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 { newPrint } from '../../core/print.js';
|
import { newPrint } from '../../core/print.js';
|
||||||
|
|
@ -834,7 +834,7 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
|
||||||
z = slice.z,
|
z = slice.z,
|
||||||
lastPoly;
|
lastPoly;
|
||||||
|
|
||||||
// support alternating shell order
|
// support alternating shell order
|
||||||
if (Math.abs(shellOrder) > 1 && slice.index % 2 === 1) {
|
if (Math.abs(shellOrder) > 1 && slice.index % 2 === 1) {
|
||||||
shellOrder = -shellOrder;
|
shellOrder = -shellOrder;
|
||||||
}
|
}
|
||||||
|
|
@ -1089,6 +1089,59 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function minThinDist(np, trace) {
|
||||||
|
return Math.min(
|
||||||
|
Math.hypot(np.x - trace[0].x, np.y - trace[0].y),
|
||||||
|
Math.hypot(np.x - trace.peek().x, np.y - trace.peek().y),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function outputAdaptiveWalls(thin, sort) {
|
||||||
|
// restore thin shell order annotation and sort on it
|
||||||
|
thin.forEach((a,i) => a.shell = sort[i] * shellOrder);
|
||||||
|
thin.forEach(t => t.shell = t.shell < 0 ? Math.abs(t.shell) + 0.5 : t.shell);
|
||||||
|
thin.sort((a,b) => a.shell - b.shell);
|
||||||
|
let np = print.lastPoint;
|
||||||
|
// perform nearest poly to poly algorithm on shells
|
||||||
|
for (;;) {
|
||||||
|
let min = { dist: Infinity };
|
||||||
|
for (let trace of thin) {
|
||||||
|
if (!min.trace) {
|
||||||
|
min.trace = trace;
|
||||||
|
} else if (np && min.trace.shell === trace.shell) {
|
||||||
|
let dist = minThinDist(np, trace);
|
||||||
|
if (dist < min.dist) min = { dist, trace };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!min.trace) {
|
||||||
|
startPoint = np;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let { trace } = min;
|
||||||
|
let fp = trace[0];
|
||||||
|
let lp = trace.peek();
|
||||||
|
// reverse trace if far end closer
|
||||||
|
if (np && Math.hypot(np.x - lp.x, np.y - lp.y) < Math.hypot(np.x - fp.x, np.y - fp.y)) {
|
||||||
|
trace.reverse();
|
||||||
|
}
|
||||||
|
// remove emitted trace
|
||||||
|
thin = thin.filter(t => t !== trace);
|
||||||
|
// retract if moving > retractDist
|
||||||
|
if (np && np.distTo2D(new Point(trace[0].x, trace[0].y)) > retractDist) {
|
||||||
|
retract();
|
||||||
|
}
|
||||||
|
let outputSpeed = trace.shell === 1 || trace.shell === 1.5 ? finishSpeed : printSpeed;
|
||||||
|
for (let pt of trace) {
|
||||||
|
np = new Point(pt.x, pt.y, z);
|
||||||
|
if (pt === trace[0]) {
|
||||||
|
print.addOutput(preout, np, 0, moveSpeed);
|
||||||
|
} else {
|
||||||
|
print.addOutput(preout, np, ((pt.r * 2) / nozzleSize) * shellMult, outputSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* output sparse infill
|
* output sparse infill
|
||||||
* @param {Polygon[]} polys
|
* @param {Polygon[]} polys
|
||||||
|
|
@ -1185,10 +1238,10 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
|
||||||
start = 0,
|
start = 0,
|
||||||
skip = false,
|
skip = false,
|
||||||
lastIndex = -1,
|
lastIndex = -1,
|
||||||
raft = opt.raft || false,
|
raft = opt.raft ?? false,
|
||||||
flow = opt.flow || 1,
|
flow = opt.flow || 1,
|
||||||
near = opt.near || (antiBacklash ? false : true),
|
near = opt.near ?? (antiBacklash ? false : true),
|
||||||
fast = opt.fast || false, // support infill only!
|
fast = opt.fast ?? false, // support infill only!
|
||||||
fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow,
|
fill = (opt.fill >= 0 ? opt.fill : fillMult) * flow,
|
||||||
thinDist = near ? thinWall : thinWall;
|
thinDist = near ? thinWall : thinWall;
|
||||||
|
|
||||||
|
|
@ -1444,6 +1497,9 @@ function slicePrintPath(print, slice, startPoint, offset, output, opt = {}) {
|
||||||
// raft
|
// raft
|
||||||
if (top.traces) outputTraces(top.traces);
|
if (top.traces) outputTraces(top.traces);
|
||||||
|
|
||||||
|
// thin wall v3
|
||||||
|
if (top.thin_wall) outputAdaptiveWalls(top.thin_wall, top.thin_sort);
|
||||||
|
|
||||||
// innermost shells
|
// innermost shells
|
||||||
let inner = next.innerShells() || [];
|
let inner = next.innerShells() || [];
|
||||||
let shells = next.shells || [];
|
let shells = next.shells || [];
|
||||||
|
|
|
||||||
|
|
@ -997,31 +997,45 @@ function doRender(slice, isSynth, params, opt = {}) {
|
||||||
.setLayer('part', Color.part)
|
.setLayer('part', Color.part)
|
||||||
.addPolys([top.poly]);
|
.addPolys([top.poly]);
|
||||||
|
|
||||||
output
|
if (top.shells?.length) output
|
||||||
.setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell)
|
.setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell)
|
||||||
.addPolys(top.shells || [], vopt({ offset, height, clean: true }));
|
.addPolys(top.shells || [], vopt({ offset, height, clean: true }));
|
||||||
|
|
||||||
output
|
if (isThin && top.thin_wall?.length) output
|
||||||
|
.setLayer("walls", Color.thin)
|
||||||
|
.addPolys(top.thin_wall.map(a => a.map(p => newPolygon().centerCircle(p, p.r, 12).setZ(slice.z))).flat(), vopt({ offset, height }));
|
||||||
|
|
||||||
|
if (!isThin && top.thin_wall?.length) output
|
||||||
|
.setLayer("walls", Color.thin)
|
||||||
|
.addPolys(top.thin_wall.map(a =>
|
||||||
|
a.length === 1 ?
|
||||||
|
newPolygon().centerCircle(a[0], a[0].r, 6) :
|
||||||
|
newPolygon(a.map(p => newPoint(p.x, p.y)))
|
||||||
|
.setZ(slice.z)
|
||||||
|
.closeIf(height)
|
||||||
|
), vopt({ offset, height }));
|
||||||
|
|
||||||
|
if (top.fill_lines?.length) output
|
||||||
.setLayer("solid fill", isSynth ? Color.support : Color.fill)
|
.setLayer("solid fill", isSynth ? Color.support : Color.fill)
|
||||||
.addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z }));
|
.addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z }));
|
||||||
|
|
||||||
if (!(slice.belt && slice.belt.anchor)) output
|
if (!(slice.belt?.anchor)) output
|
||||||
.setLayer("sparse fill", Color.infill)
|
.setLayer("sparse fill", Color.infill)
|
||||||
.addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true }))
|
.addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true }))
|
||||||
|
|
||||||
if (slice.belt && slice.belt.anchor) output
|
if (slice.belt?.anchor) output
|
||||||
.setLayer("anchor", Color.anchor)
|
.setLayer("anchor", Color.anchor)
|
||||||
.addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true }))
|
.addPolys(top.fill_sparse || [], vopt({ offset, height, outline: true, trace:true }))
|
||||||
|
|
||||||
if (top.thin_fill) output
|
if (top.thin_fill?.length) output
|
||||||
.setLayer("thin fill", Color.thin)
|
.setLayer("thin fill", Color.thin)
|
||||||
.addLines(top.thin_fill, vopt({ offset, height }));
|
.addLines(top.thin_fill, vopt({ offset, height }));
|
||||||
|
|
||||||
if (top.gaps && devel) output
|
if (top.gaps?.length && devel) output
|
||||||
.setLayer("gaps", Color.gaps)
|
.setLayer("gaps", Color.gaps)
|
||||||
.addPolys(top.gaps, vopt({ offset, height, thin: true }));
|
.addPolys(top.gaps, vopt({ offset, height, thin: true }));
|
||||||
|
|
||||||
if (isThin && devel && top.fill_off && top.fill_off.length) {
|
if (isThin && devel && top.fill_off?.length) {
|
||||||
slice.output()
|
slice.output()
|
||||||
.setLayer('fill inset', Color.inset)
|
.setLayer('fill inset', Color.inset)
|
||||||
.addPolys(top.fill_off);
|
.addPolys(top.fill_off);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue