Merge pull request #1184 from saturngod/fix/boolean-stroke-outlines

Fix stroke-only boolean operand outlines
This commit is contained in:
BFlat
2026-10-26 08:33:31 +07:00
committed by GitHub
2 changed files with 132 additions and 4 deletions
+32 -4
View File
@@ -209,6 +209,18 @@ pub fn stroke_area(n: &Node, path: &BezPath, closed: bool) -> Arc<BezPath> {
return Arc::new(o);
}
}
cached_stroke_outline(n, path, closed)
}
/// A non-zero stroke outline, shared by painting and boolean operands.
fn cached_stroke_outline(n: &Node, path: &BezPath, closed: bool) -> Arc<BezPath> {
let props = &n.stroke;
if let Some(sides) = props.sides
&& matches!(n.kind, NodeKind::Rectangle | NodeKind::Frame | NodeKind::Component { .. } | NodeKind::Instance { .. })
&& n.corners.uniform() == Some(0.0)
{
return Arc::new(figcraft_pathops::rect_side_strokes(n.width, n.height, sides, props.align));
}
let mut h = std::collections::hash_map::DefaultHasher::new();
hash_path(path, &mut h);
(props.weight.to_bits(), props.align as u8, props.cap_start as u8, props.cap_end as u8, props.join as u8, props.miter_limit.to_bits(), closed)
@@ -281,7 +293,7 @@ fn node_outline_at(doc: &Document, n: &Node, depth: usize) -> Option<(BezPath, F
.filter_map(|c| doc.node(*c))
.filter(|c| c.visible)
.filter_map(|c| {
let (mut p, _) = subtree_outline(doc, c, depth + 1)?;
let (mut p, _) = subtree_outline_at(doc, c, depth + 1, true)?;
p.apply_affine(c.transform);
Some((p, c.bool_op.unwrap_or(*op)))
})
@@ -305,6 +317,11 @@ fn node_outline_at(doc: &Document, n: &Node, depth: usize) -> Option<(BezPath, F
/// The union outline of a node and its descendants (for booleans of groups/frames).
pub fn subtree_outline(doc: &Document, n: &Node, depth: usize) -> Option<(BezPath, FillRule)> {
subtree_outline_at(doc, n, depth, false)
}
/// Boolean operands with only a stroke contribute the painted band, not their interior.
fn subtree_outline_at(doc: &Document, n: &Node, depth: usize, stroke_only: bool) -> Option<(BezPath, FillRule)> {
if depth > 64 {
return None;
}
@@ -313,21 +330,32 @@ pub fn subtree_outline(doc: &Document, n: &Node, depth: usize) -> Option<(BezPat
let mut out = BezPath::new();
if n.kind.is_frame_like()
&& let Some(p) = figcraft_doc::geometry::shape_path(n)
&& n.fills.iter().any(|f| f.visible)
{
out.extend(p.elements().iter().copied());
if n.fills.iter().any(|f| f.visible) {
out.extend(p.elements().iter().copied());
} else if stroke_only && n.has_visible_stroke() {
out.extend(cached_stroke_outline(n, &p, true).elements().iter().copied());
}
}
for c in n.children.iter().filter_map(|c| doc.node(*c)) {
if !c.visible {
continue;
}
if let Some((mut p, _)) = subtree_outline(doc, c, depth + 1) {
if let Some((mut p, _)) = subtree_outline_at(doc, c, depth + 1, stroke_only) {
p.apply_affine(c.transform);
out.extend(p.elements().iter().copied());
}
}
Some((out, FillRule::Winding))
}
_ if stroke_only && !n.fills.iter().any(|p| p.visible) && n.has_visible_stroke() => {
let (path, _) = node_outline_at(doc, n, depth)?;
let path = if matches!(n.kind, NodeKind::Text(_)) { path } else { figcraft_doc::geometry::stroke_geometry(n).unwrap_or(path) };
let closed = figcraft_doc::geometry::is_closed(n) || matches!(n.kind, NodeKind::BooleanOp { .. });
// Boolean folding uses non-zero winding; the canvas stroke cache also contains
// even-odd rectangle rings, so use the winding-correct stroke outline here.
Some(((*cached_stroke_outline(n, &path, closed)).clone(), FillRule::Winding))
}
_ => node_outline_at(doc, n, depth),
}
}