improve sla supports

This commit is contained in:
Stewart Allen 2026-05-19 14:32:51 -04:00
commit 41629b2c76
4 changed files with 560 additions and 110 deletions

View file

@ -400,11 +400,12 @@ export const conf = {
slaFillDensity: 0, slaFillDensity: 0,
slaFillLine: 0.5, slaFillLine: 0.5,
slaFirstOffset: 0, slaFirstOffset: 0,
slaSupportLayers: 10, slaSupportLayers: 0,
slaSupportDensity: 0.5, slaSupportDensity: 0.1,
slaSupportSize: 0.6, slaSupportSize: 0.4,
slaSupportPoints: 4, slaSupportPoints: 4,
slaSupportGap: 10, slaSupportAngle: 35,
slaSupportGap: 0,
slaSupportEnable: false slaSupportEnable: false
} }
}, },

View file

@ -43,11 +43,12 @@ export function menu() {
slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}), slaFillDensity: newInput(LANG.sa_ifdn_s, {title:LANG.sa_ifdn_l, convert:toFloat, bound:bound(0,1)}),
slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}), slaFillLine: newInput(LANG.sa_iflw_s, {title:LANG.sa_iflw_l, convert:toFloat, bound:bound(0,5)}),
slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }), slaSupport: newGroup(LANG.sa_supp_m, $('sla-support'), { modes:SLA, group:"sla-support", driven, separator }),
slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(5,100)}), slaSupportLayers: newInput(LANG.sa_slyr_s, {title:LANG.sa_slyr_l, convert:toInt, bound:bound(0,100)}),
slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(3,30)}), slaSupportGap: newInput(LANG.sa_slgp_s, {title:LANG.sa_slgp_l, convert:toInt, bound:bound(0,30)}),
slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}), slaSupportDensity: newInput(LANG.sa_sldn_s, {title:LANG.sa_sldn_l, convert:toFloat, bound:bound(0.01,0.9)}),
slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}), slaSupportSize: newInput(LANG.sa_slsz_s, {title:LANG.sa_slsz_l, convert:toFloat, bound:bound(0.1,1)}),
slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}), slaSupportPoints: newInput(LANG.sa_slpt_s, {title:LANG.sa_slpt_l, convert:toInt, bound:bound(3,10)}),
slaSupportAngle: newInput(LANG.sa_slan_s, {title:LANG.sa_slan_l, convert:toFloat, bound:bound(5,45)}),
slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}), slaSupportEnable: newBoolean(LANG.enable, onBooleanClick, {title:LANG.sl_slen_l}),
slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }), slaOutput: newGroup(LANG.sa_outp_m, $('sla-output'), { modes:SLA, driven, separator, group:"sla-output" }),
slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}), slaFirstOffset: newInput(LANG.sa_opzo_s, {title:LANG.sa_opzo_l, convert:toFloat, bound:bound(0,1)}),

View file

@ -83,7 +83,7 @@ export function sla_slice(settings, widget, onupdate, ondone) {
slices.push(cap); slices.push(cap);
} }
// prepend raft layers to slices array // prepend raft layers to slices array
if (process.slaSupportEnable && process.slaSupportLayers) { if (process.slaSupportEnable && process.slaSupportLayers > 0) {
let layers = process.slaSupportLayers, let layers = process.slaSupportLayers,
zoff = height / 2, zoff = height / 2,
snew = [], snew = [],
@ -150,7 +150,7 @@ export function sla_slice(settings, widget, onupdate, ondone) {
solidLayers ? 10 : 0, // shell fill solidLayers ? 10 : 0, // shell fill
!solidLayers ? 10 : 0, // solid !solidLayers ? 10 : 0, // solid
process.slaFillDensity && process.slaShell ? 60 : 0, // infill process.slaFillDensity && process.slaShell ? 60 : 0, // infill
process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity ? 100 : 0 process.slaSupportEnable && process.slaSupportDensity ? 100 : 0
].reduce((t,v) => { return t+v }); ].reduce((t,v) => { return t+v });
work_remain = work_total; work_remain = work_total;
forSlices(slices, 5, (slice,index) => { forSlices(slices, 5, (slice,index) => {
@ -204,7 +204,7 @@ export function sla_slice(settings, widget, onupdate, ondone) {
fillPolys(slice, settings); fillPolys(slice, settings);
}, "infill"); }, "infill");
} }
if (process.slaSupportEnable && process.slaSupportLayers && process.slaSupportDensity) { if (process.slaSupportEnable && process.slaSupportDensity) {
computeSupports(widget, process, progress => { computeSupports(widget, process, progress => {
doupdate(100 * progress, "support"); doupdate(100 * progress, "support");
}); });
@ -280,12 +280,13 @@ function computeSupports(widget, process, progress) {
baseIndex = widget.lastraft ? widget.lastraft.index : 0, baseIndex = widget.lastraft ? widget.lastraft.index : 0,
spacing = supportSpacing(process), spacing = supportSpacing(process),
points = Math.bound(process.slaSupportPoints || 8, 5, 16), points = Math.bound(process.slaSupportPoints || 8, 5, 16),
tipRadius = Math.bound(process.slaSupportSize * 0.35, 0.12, 0.45), tipRadius = Math.bound(process.slaSupportSize * 0.08, 0.025, 0.08),
branchRadius = Math.bound(process.slaSupportSize * 0.55, tipRadius, 0.75), branchRadius = Math.bound(process.slaSupportSize * 0.24, tipRadius, 0.32),
trunkRadius = Math.bound(process.slaSupportSize * 0.85, branchRadius, 1.25), trunkRadius = Math.bound(process.slaSupportSize * 0.55, branchRadius, 1.20),
segmentLayers = Math.max(4, Math.round(2 / process.slaSlice)), segmentLayers = Math.max(6, Math.round(4 / process.slaSlice)),
contacts = collectSupportContacts(slices, process, spacing, tipRadius), contacts = collectSupportContacts(slices, process, spacing, tipRadius),
levels = new Map(); levels = new Map(),
segments = [];
slices.forEach(slice => { slices.forEach(slice => {
delete slice.supports; delete slice.supports;
@ -302,6 +303,8 @@ function computeSupports(widget, process, progress) {
return; return;
} }
clusterSupportContacts(contacts, spacing);
contacts.forEach(contact => { contacts.forEach(contact => {
routeSupportTree({ routeSupportTree({
slices, slices,
@ -314,10 +317,139 @@ function computeSupports(widget, process, progress) {
branchRadius, branchRadius,
trunkRadius, trunkRadius,
segmentLayers, segmentLayers,
spacing spacing,
segments
}); });
progress(1 / contacts.length); progress(0.65 / contacts.length);
}); });
emitSupportSegments({
slices,
baseIndex,
points,
segments,
tipRadius,
branchRadius,
trunkRadius,
progress
});
unionSupportLayers(slices, progress);
}
function clusterSupportContacts(contacts, spacing) {
let clusters = [],
radius = supportClusterRadius(spacing),
radius2 = radius * radius;
contacts.slice().sort((a, b) => b.area - a.area).forEach(contact => {
let best, bestDist = Infinity;
for (let cluster of clusters) {
let dx = contact.point.x - cluster.point.x,
dy = contact.point.y - cluster.point.y,
dist2 = dx * dx + dy * dy;
if (dist2 <= radius2 && dist2 < bestDist) {
best = cluster;
bestDist = dist2;
}
}
if (!best) {
best = {
point: newPoint(contact.point.x, contact.point.y, contact.point.z),
contacts: [],
weight: 0
};
clusters.push(best);
}
best.contacts.push(contact);
best.weight += Math.max(contact.area, 1);
best.point.x = lerp(best.point.x, contact.point.x, Math.max(contact.area, 1) / best.weight);
best.point.y = lerp(best.point.y, contact.point.y, Math.max(contact.area, 1) / best.weight);
});
clusters.forEach(cluster => {
splitClusterRoots(cluster, spacing);
cluster.contacts.forEach(contact => {
contact.cluster = cluster;
});
});
}
function splitClusterRoots(cluster, spacing) {
let contacts = cluster.contacts,
maxLoad = maxTrunkLoad(spacing),
count = Math.min(8, Math.ceil(contacts.length / maxLoad));
if (count <= 1) {
let root = {
point: newPoint(cluster.point.x, cluster.point.y, cluster.point.z),
center: cluster.point,
contacts,
nodes: new Map()
};
cluster.roots = [ root ];
contacts.forEach(contact => {
contact.root = root;
contact.trunk = root.point;
});
return;
}
let seeds = [ contacts[0] ];
while (seeds.length < count) {
let best, bestDist = -1;
contacts.forEach(contact => {
let dist = Math.min(...seeds.map(seed => contact.point.distTo2D(seed.point)));
if (dist > bestDist) {
best = contact;
bestDist = dist;
}
});
seeds.push(best);
}
cluster.roots = seeds.map(seed => ({
point: newPoint(seed.point.x, seed.point.y, seed.point.z),
center: newPoint(seed.point.x, seed.point.y, seed.point.z),
contacts: [],
nodes: new Map(),
weight: 0
}));
contacts.forEach(contact => {
let root = nearestRoot(cluster.roots, contact.point),
weight = Math.max(contact.area, 1);
root.contacts.push(contact);
root.weight += weight;
root.center.x = lerp(root.center.x, contact.point.x, weight / root.weight);
root.center.y = lerp(root.center.y, contact.point.y, weight / root.weight);
contact.root = root;
});
cluster.roots.forEach(root => {
root.point = newPoint(root.center.x, root.center.y, root.center.z);
root.contacts.forEach(contact => {
contact.trunk = root.point;
});
});
}
function nearestRoot(roots, point) {
let best = roots[0],
bestDist = Infinity;
roots.forEach(root => {
let dist = point.distTo2D(root.point);
if (dist < bestDist) {
best = root;
bestDist = dist;
}
});
return best;
} }
function collectSupportContacts(slices, process, spacing, tipRadius) { function collectSupportContacts(slices, process, spacing, tipRadius) {
@ -409,76 +541,323 @@ function sampleBridgePolygon(poly, z, spacing, points) {
function routeSupportTree(args) { function routeSupportTree(args) {
let { let {
slices, baseIndex, levels, contact, process, points, slices, baseIndex, levels, contact, process,
tipRadius, branchRadius, trunkRadius, segmentLayers, spacing tipRadius, branchRadius, trunkRadius, segmentLayers, spacing, segments
} = args; } = args;
let current = { let route = planSupportRoute({
sliceIndex: contact.slice.index,
point: contact.point,
radius: tipRadius
};
while (current.sliceIndex > baseIndex) {
let nextIndex = nextSupportLevel(current.sliceIndex, baseIndex, segmentLayers),
depth = (contact.slice.index - nextIndex) / Math.max(contact.slice.index - baseIndex, 1),
vertical = (current.sliceIndex - nextIndex) * process.slaSlice,
maxMove = maxTreeMove(vertical),
radius = lerp(branchRadius, trunkRadius, depth),
target = {
sliceIndex: nextIndex,
point: treeTargetPoint(current.point, contact.point, depth, spacing, maxMove),
radius
},
fit = fitSegmentTarget(slices, current, target, radius, contact.slice.index, maxMove),
mergeSearch = mergeSearchRadius(spacing, radius, depth, maxMove),
merge = findMergeNode(levels, nextIndex, fit.point, mergeSearch);
target.point = fit.point;
if (merge && current.point.distTo2D(merge.point) <= maxMove) {
let mergeCollisions = segmentCollisions(slices, current, merge,
Math.max(radius, merge.radius), contact.slice.index);
if (mergeCollisions === 0) {
target = merge;
target.merged = true;
target.radius = Math.max(target.radius, radius);
} else {
addLevelNode(levels, target);
}
} else {
addLevelNode(levels, target);
}
emitSupportSegment({
slices, slices,
points, baseIndex,
from: current, contact,
to: target process,
tipRadius,
branchRadius,
trunkRadius,
segmentLayers,
spacing
}); });
if (target.merged) { if (route.length < 2) return;
let current = route[0],
routeNodes = [ current ];
for (let index=1; index<route.length; index++) {
let planned = route[index],
depth = (contact.slice.index - planned.sliceIndex) / Math.max(contact.slice.index - baseIndex, 1),
vertical = (current.sliceIndex - planned.sliceIndex) * process.slaSlice,
maxMove = maxTreeMove(vertical, process),
mergeSearch = mergeSearchRadius(spacing, planned.radius, depth, maxMove),
merge = findMergeNode(levels, planned.sliceIndex, planned.point, mergeSearch),
target = planned;
if (merge && current.point.distTo2D(merge.point) <= maxMove * 1.75 &&
!segmentCollides(slices, current, merge, routeSegmentRadius(current, merge,
tipRadius, branchRadius, trunkRadius), contact.slice.index)) {
target = merge;
target.merged = true;
target.radius = Math.max(target.radius, planned.radius);
} else {
addLevelNode(levels, target);
}
let hit = firstSegmentCollision(slices, current, target,
routeSegmentRadius(current, target, tipRadius, branchRadius, trunkRadius), contact.slice.index);
if (hit) {
let terminator = contactTerminator(slices, current, target, hit, tipRadius);
if (terminator) {
segments.push({
from: current,
to: terminator
});
}
break; break;
} }
current.down = target;
addNodeLoad(target, current.load);
segments.push({ from: current, to: target });
routeNodes.push(target);
if (target.sliceIndex === baseIndex || target.grounded) {
routeNodes.forEach(node => {
node.grounded = true;
});
}
if (target.merged) break;
current = target; current = target;
} }
} }
function planSupportRoute(args) {
let {
slices, baseIndex, contact, process,
tipRadius, branchRadius, trunkRadius, segmentLayers, spacing
} = args;
let start = {
sliceIndex: contact.slice.index,
point: contact.point,
radius: tipRadius,
load: 1,
tip: true,
cost: 0
}, currentIndex = start.sliceIndex, candidates = [ start ];
while (currentIndex > baseIndex && candidates.length) {
let nextIndex = nextSupportLevel(currentIndex, baseIndex, segmentLayers),
depth = (contact.slice.index - nextIndex) / Math.max(contact.slice.index - baseIndex, 1),
radius = lerp(branchRadius, trunkRadius, depth),
next = new Map();
candidates.forEach(candidate => {
let vertical = (candidate.sliceIndex - nextIndex) * process.slaSlice,
maxMove = maxTreeMove(vertical, process),
root = clusterNodeForContact(contact, slices, nextIndex, depth),
desired = root ? root.point :
treeTargetPoint(candidate.point, contact.trunk, depth, spacing, maxMove);
routeCandidatePoints(candidate.point, desired, maxMove).forEach(point => {
point.z = slices[nextIndex].z;
let node = {
sliceIndex: nextIndex,
point,
radius: root ? Math.max(root.radius, radius) : radius,
load: 0,
root: root?.root,
prev: candidate
},
checkRadius = Math.max(radius, candidate.radius),
collision = firstSegmentCollision(slices, candidate, node, checkRadius, contact.slice.index),
supportHit = supportCollides(slices[nextIndex], point, checkRadius),
miss = collision ? collision.count : 0,
score = candidate.cost +
miss * 100000 +
(supportHit ? 50000 : 0) +
point.distTo2D(desired) * 8 +
candidate.point.distTo2D(point);
node.cost = score;
keepBestRouteCandidate(next, node, spacing);
});
});
candidates = Array.from(next.values())
.sort((a, b) => a.cost - b.cost)
.slice(0, 12);
currentIndex = nextIndex;
}
let end = candidates.sort((a, b) => a.cost - b.cost)[0];
if (!end) return [ start ];
let route = [];
while (end) {
route.unshift(end);
end = end.prev;
}
route[0] = start;
return route;
}
function emitSupportSegments(args) {
let { slices, baseIndex, points, segments, tipRadius, branchRadius, trunkRadius, progress } = args,
total = Math.max(segments.length, 1);
if (segments.length === 0) {
progress(0.25);
return;
}
segments.forEach(segment => {
emitSupportSegment({
slices,
points,
from: segment.from,
to: segment.to,
baseIndex,
tipRadius,
branchRadius,
trunkRadius
});
progress(0.25 / total);
});
}
function emitSupportSegment(args) { function emitSupportSegment(args) {
let { slices, points, from, to } = args, let { slices, points, from, to, baseIndex, tipRadius, branchRadius, trunkRadius } = args;
span = Math.max(1, from.sliceIndex - to.sliceIndex);
if (!to.terminator && to.sliceIndex === baseIndex) {
return emitBaseFlareSegment({
slices,
points,
from,
to,
tipRadius,
branchRadius,
trunkRadius
});
}
emitLinearSupportSegment({
slices,
points,
from,
to,
tipRadius,
branchRadius,
trunkRadius
});
}
function emitLinearSupportSegment(args) {
let { slices, points, from, to, tipRadius, branchRadius, trunkRadius } = args,
span = Math.max(1, from.sliceIndex - to.sliceIndex),
fromRadius = supportNodeRadius(from, tipRadius, branchRadius, trunkRadius),
toRadius = to.terminator ? tipRadius : supportNodeRadius(to, tipRadius, branchRadius, trunkRadius);
for (let index=from.sliceIndex; index>=to.sliceIndex; index--) { for (let index=from.sliceIndex; index>=to.sliceIndex; index--) {
if (from.tip && index === from.sliceIndex) continue;
let t = (from.sliceIndex - index) / span, let t = (from.sliceIndex - index) / span,
slice = slices[index], slice = slices[index],
radius = lerp(from.radius, to.radius, t), radius = lerp(fromRadius, toRadius, t),
point = interpolatePoint(from.point, to.point, t, slice.z); point = interpolatePoint(from.point, to.point, t, slice.z);
addSupportCircle(slice, point, radius, points); addSupportCircle(slice, point, radius, points);
} }
} }
function emitBaseFlareSegment(args) {
let { slices, points, from, to, tipRadius, branchRadius, trunkRadius } = args,
span = from.sliceIndex - to.sliceIndex,
layerHeight = supportLayerHeight(slices, to.sliceIndex),
flareLayers = Math.min(span - 1, Math.max(4, Math.round(3 / layerHeight)));
if (span <= 5 || flareLayers < 3) {
return emitLinearSupportSegment(args);
}
let hubIndex = to.sliceIndex + flareLayers,
hubT = (from.sliceIndex - hubIndex) / span,
hub = {
sliceIndex: hubIndex,
point: interpolatePoint(from.point, to.point, hubT, slices[hubIndex].z),
radius: supportNodeRadius(to, tipRadius, branchRadius, trunkRadius),
load: to.load || 1
},
height = Math.max((slices[hubIndex].z - slices[to.sliceIndex].z), layerHeight),
toRadius = supportNodeRadius(to, tipRadius, branchRadius, trunkRadius),
feet = baseFlareFeet(to, height, toRadius, slices[to.sliceIndex].z);
emitLinearSupportSegment({
slices,
points,
from,
to: hub,
tipRadius,
branchRadius,
trunkRadius
});
feet.forEach(foot => {
emitLinearSupportSegment({
slices,
points,
from: hub,
to: foot,
tipRadius,
branchRadius,
trunkRadius
});
addSupportCircle(slices[foot.sliceIndex], foot.point, foot.padRadius, points);
});
}
function baseFlareFeet(node, height, radius, z) {
let load = Math.max(node.load || 1, 1),
count = load > 16 ? 4 : 3,
maxSpread = height * Math.tan(32 * Math.PI / 180),
spread = Math.min(maxSpread, Math.max(radius * 4, 1.2 + Math.sqrt(load) * 0.25)),
seed = Math.abs(Math.sin(node.point.x * 12.9898 + node.point.y * 78.233)) * Math.PI * 2,
feet = [];
for (let i=0; i<count; i++) {
let angle = seed + (Math.PI * 2 * i / count),
point = newPoint(
node.point.x + Math.cos(angle) * spread,
node.point.y + Math.sin(angle) * spread,
z
),
footRadius = Math.max(radius * 0.85, radius - 0.05);
feet.push({
sliceIndex: node.sliceIndex,
point,
radius: footRadius,
load: 1,
padRadius: Math.max(radius * 1.35, footRadius + 0.35)
});
}
return feet;
}
function supportLayerHeight(slices, index) {
let slice = slices[index],
next = slices[Math.min(index + 1, slices.length - 1)];
return Math.max(0.01, Math.abs((next?.z || slice.z) - slice.z) || 0.05);
}
function supportNodeRadius(node, tipRadius, branchRadius, trunkRadius) {
if (node.tip) return tipRadius;
let load = Math.max(node.load || 1, 1),
areaRadius = branchRadius * Math.sqrt(load),
maxRadius = maxTrunkRadius(trunkRadius),
target = Math.min(maxRadius, Math.max(branchRadius, areaRadius));
return Math.max(node.radius || branchRadius, target);
}
function routeSegmentRadius(from, to, tipRadius, branchRadius, trunkRadius) {
let toLoad = (to.load || 0) + (from.load || 1),
toCheck = Object.assign({}, to, { load: toLoad });
return Math.max(
supportNodeRadius(from, tipRadius, branchRadius, trunkRadius),
to.terminator ? tipRadius : supportNodeRadius(toCheck, tipRadius, branchRadius, trunkRadius)
);
}
function addNodeLoad(node, load) {
while (node) {
node.load = (node.load || 0) + load;
node = node.down;
}
}
function addSupportCircle(slice, point, radius, points) { function addSupportCircle(slice, point, radius, points) {
let support = newPolygon() let support = newPolygon()
.centerCircle(point, radius, points, true) .centerCircle(point, radius, points, true)
@ -493,6 +872,7 @@ function findMergeNode(levels, sliceIndex, point, radius) {
let best, bestDist = Infinity; let best, bestDist = Infinity;
for (let node of nodes) { for (let node of nodes) {
if (!node.grounded) continue;
let dist = point.distTo2D(node.point); let dist = point.distTo2D(node.point);
if (dist < bestDist && dist <= radius) { if (dist < bestDist && dist <= radius) {
best = node; best = node;
@ -508,6 +888,24 @@ function addLevelNode(levels, node) {
nodes.push(node); nodes.push(node);
} }
function clusterNodeForContact(contact, slices, sliceIndex, depth) {
let root = contact.root;
if (!root || depth < 0.4) return;
let node = root.nodes.get(sliceIndex);
if (!node) {
node = {
sliceIndex,
point: newPoint(root.point.x, root.point.y, slices[sliceIndex].z),
radius: 0,
load: 0,
root
};
root.nodes.set(sliceIndex, node);
}
return node;
}
function nextSupportLevel(currentIndex, baseIndex, segmentLayers) { function nextSupportLevel(currentIndex, baseIndex, segmentLayers) {
let aboveBase = currentIndex - baseIndex; let aboveBase = currentIndex - baseIndex;
if (aboveBase <= segmentLayers) return baseIndex; if (aboveBase <= segmentLayers) return baseIndex;
@ -523,49 +921,31 @@ function supportCollides(slice, point, radius) {
}); });
} }
function fitSegmentTarget(slices, from, to, radius, contactIndex, maxMove) { function routeCandidatePoints(from, point, maxMove) {
let candidates = segmentTargetCandidates(from.point, to.point, maxMove), let out = [],
best = to.point,
bestCollisions = Infinity,
bestScore = Infinity;
for (let candidate of candidates) {
let fit = {
sliceIndex: to.sliceIndex,
point: candidate,
radius: to.radius
};
let collisions = segmentCollisions(slices, from, fit, radius, contactIndex);
let score = collisions * 10000 + to.point.distTo2D(candidate) * 10 + from.point.distTo2D(candidate);
if (collisions < bestCollisions || (collisions === bestCollisions && score < bestScore)) {
best = candidate;
bestCollisions = collisions;
bestScore = score;
if (collisions === 0) break;
}
}
return {
point: best,
collisions: bestCollisions
};
}
function segmentTargetCandidates(from, point, maxMove) {
let out = [
point,
newPoint(from.x, from.y, point.z)
],
dirs = [ dirs = [
[1, 0], [-1, 0], [0, 1], [0, -1], [1, 0], [0.707, 0.707], [0, 1], [-0.707, 0.707],
[0.707, 0.707], [-0.707, 0.707], [-1, 0], [-0.707, -0.707], [0, -1], [0.707, -0.707]
[0.707, -0.707], [-0.707, -0.707] ],
]; push = point => {
let limited = limitTreeMove(from, point, maxMove);
if (!out.some(existing => existing.distTo2D(limited) < 0.01)) {
out.push(limited);
}
};
for (let scale of [0.5, 1]) { push(point);
push(newPoint(from.x, from.y, point.z));
for (let scale of [0.4, 0.7, 1]) {
let dist = maxMove * scale; let dist = maxMove * scale;
for (let dir of dirs) { for (let dir of dirs) {
out.push(newPoint( push(newPoint(
from.x + dir[0] * dist,
from.y + dir[1] * dist,
point.z
));
push(newPoint(
point.x + dir[0] * dist, point.x + dir[0] * dist,
point.y + dir[1] * dist, point.y + dir[1] * dist,
point.z point.z
@ -576,15 +956,64 @@ function segmentTargetCandidates(from, point, maxMove) {
return out; return out;
} }
function keepBestRouteCandidate(map, node, spacing) {
let bucket = Math.max(0.5, spacing * 0.35),
current;
let key = [
node.sliceIndex,
Math.round(node.point.x / bucket),
Math.round(node.point.y / bucket)
].join(':');
current = map.get(key);
if (!current || node.cost < current.cost) {
map.set(key, node);
}
}
function mergeSearchRadius(spacing, radius, depth, maxMove) { function mergeSearchRadius(spacing, radius, depth, maxMove) {
return Math.max( return Math.max(
radius * 2, radius * 3,
Math.min(spacing * (0.35 + depth * 0.65), maxMove) Math.min(spacing * (0.65 + depth), maxMove + radius * 2)
); );
} }
function segmentCollisions(slices, from, to, radius, contactIndex) { function supportClusterRadius(spacing) {
return Math.max(5, spacing * 1.75);
}
function maxTrunkLoad(spacing) {
return Math.max(6, Math.round(spacing * 1.25));
}
function maxTrunkRadius(trunkRadius) {
return Math.max(trunkRadius, Math.min(3, trunkRadius * 5));
}
function unionSupportLayers(slices, progress) {
let supportSlices = slices.filter(slice => slice.supports?.length > 1),
total = Math.max(supportSlices.length, 1);
if (supportSlices.length === 0) {
progress(0.10);
return;
}
supportSlices.forEach(slice => {
slice.supports = POLY.union(slice.supports, 0, true);
progress(0.10 / total);
});
}
function segmentCollides(slices, from, to, radius, contactIndex) {
return !!firstSegmentCollision(slices, from, to, radius, contactIndex);
}
function firstSegmentCollision(slices, from, to, radius, contactIndex) {
let span = Math.max(1, from.sliceIndex - to.sliceIndex), let span = Math.max(1, from.sliceIndex - to.sliceIndex),
first,
clean,
collisions = 0; collisions = 0;
for (let index=from.sliceIndex; index>=to.sliceIndex; index--) { for (let index=from.sliceIndex; index>=to.sliceIndex; index--) {
@ -593,11 +1022,27 @@ function segmentCollisions(slices, from, to, radius, contactIndex) {
t = (from.sliceIndex - index) / span, t = (from.sliceIndex - index) / span,
point = interpolatePoint(from.point, to.point, t, slice.z); point = interpolatePoint(from.point, to.point, t, slice.z);
if (supportCollides(slice, point, radius)) { if (supportCollides(slice, point, radius)) {
if (!first) first = { index, t, point, clean };
collisions++; collisions++;
} else if (!first) {
clean = { index, t, point };
} }
} }
return collisions; if (first) first.count = collisions;
return first;
}
function contactTerminator(slices, from, to, hit, tipRadius) {
if (!hit.point) return;
return {
sliceIndex: hit.index,
point: hit.point,
radius: tipRadius,
load: 1,
terminator: true
};
} }
function supportSpacing(process) { function supportSpacing(process) {
@ -607,9 +1052,10 @@ function supportSpacing(process) {
); );
} }
function maxTreeMove(vertical) { function maxTreeMove(vertical, process) {
// Keep branches printable by capping horizontal drift to about 18 degrees. let angle = Math.bound(process.slaSupportAngle || 18, 5, 45),
return Math.max(0.05, vertical * 0.3249); radians = angle * Math.PI / 180;
return Math.max(0.05, vertical * Math.tan(radians));
} }
function dedupePoints(points, minDist) { function dedupePoints(points, minDist) {
@ -623,13 +1069,13 @@ function dedupePoints(points, minDist) {
return out; return out;
} }
function treeTargetPoint(point, _origin, depth, spacing, maxMove) { function treeTargetPoint(point, origin, depth, spacing, maxMove) {
let grid = spacing * (2 + depth * 3), let grid = spacing * (2 + depth * 3),
target = { target = origin || {
x: Math.round(point.x / grid) * grid, x: Math.round(point.x / grid) * grid,
y: Math.round(point.y / grid) * grid y: Math.round(point.y / grid) * grid
}, },
blend = Math.min(0.5, 0.12 + depth * 0.35); blend = origin ? Math.min(0.85, 0.35 + depth * 0.45) : Math.min(0.5, 0.12 + depth * 0.35);
return limitTreeMove(point, newPoint( return limitTreeMove(point, newPoint(
lerp(point.x, target.x, blend), lerp(point.x, target.x, blend),

View file

@ -1007,8 +1007,8 @@ self.lang['en-us'] = {
sa_iflw_l: ["hatch line width","in millimeters"], sa_iflw_l: ["hatch line width","in millimeters"],
sa_supp_m: "support", sa_supp_m: "support",
sa_slyr_s: "base layers", sa_slyr_s: "raft layers",
sa_slyr_l: ["base support layers","value range 0-10"], sa_slyr_l: ["support raft layers","0 disables raft","value range 0-100"],
sa_slgp_s: "gap layers", sa_slgp_s: "gap layers",
sa_slgp_l: ["number of layers between","raft and bottom of object"], sa_slgp_l: ["number of layers between","raft and bottom of object"],
sa_sldn_s: "density", sa_sldn_s: "density",
@ -1017,6 +1017,8 @@ self.lang['en-us'] = {
sa_slsz_l: ["max size of a","support pillar","in millimeters"], sa_slsz_l: ["max size of a","support pillar","in millimeters"],
sa_slpt_s: "points", sa_slpt_s: "points",
sa_slpt_l: ["number of points in","each support pillar","in millimeters"], sa_slpt_l: ["number of points in","each support pillar","in millimeters"],
sa_slan_s: "angle",
sa_slan_l: ["max branch angle","from vertical","5-45 degrees"],
sl_slen_l: "enable supports", sl_slen_l: "enable supports",
sa_outp_m: "output", sa_outp_m: "output",