diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 5b585827..47f4d295 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -854,8 +854,9 @@ impl Render for Table { (id, mask_type, vector_row) }); - if vector.is_branching() { - for mut face_path in vector.construct_faces().filter(|face| !(face.area() < 0.0)) { + let use_face_fill = vector.use_face_fill(); + if use_face_fill { + for mut face_path in vector.construct_faces().filter(|face| face.area() >= 0.) { face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); let face_d = face_path.to_svg(); @@ -917,7 +918,7 @@ impl Render for Table { render_params.override_paint_order = can_draw_aligned_stroke && can_use_paint_order; let mut style = row.element.style.clone(); - if needs_separate_alignment_fill || vector.is_branching() { + if needs_separate_alignment_fill || use_face_fill { style.clear_fill(); } @@ -929,6 +930,10 @@ impl Render for Table { } attributes.push_val(fill_and_stroke); + if vector.is_branching() && !use_face_fill { + attributes.push("fill-rule", "evenodd"); + } + let opacity = row.alpha_blending.opacity(render_params.for_mask); if opacity < 1. { attributes.push("opacity", opacity.to_string()); @@ -1024,10 +1029,10 @@ impl Render for Table { let wants_stroke_below = row.element.style.stroke().is_some_and(|s| s.paint_order == vector::style::PaintOrder::StrokeBelow); // Closures to avoid duplicated fill/stroke drawing logic - let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath| match row.element.style.fill() { + let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match row.element.style.fill() { Fill::Solid(color) => { let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); + scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path); } Fill::Gradient(gradient) => { let mut stops = peniko::ColorStops::new(); @@ -1079,25 +1084,27 @@ impl Render for Table { Default::default() }; let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array()); - scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), path); + scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), path); } Fill::None => {} }; + // Branching vectors without regions (e.g. mesh grids) need face-by-face fill rendering. + let use_face_fill = row.element.use_face_fill(); let do_fill = |scene: &mut Scene| { - if row.element.is_branching() { - // For branching paths, fill each face separately - for mut face_path in row.element.construct_faces().filter(|face| !(face.area() < 0.0)) { + if use_face_fill { + for mut face_path in row.element.construct_faces().filter(|face| face.area() >= 0.) { face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); let mut kurbo_path = kurbo::BezPath::new(); for element in face_path { kurbo_path.push(element); } - do_fill_path(scene, &kurbo_path); + do_fill_path(scene, &kurbo_path, peniko::Fill::NonZero); } + } else if row.element.is_branching() { + do_fill_path(scene, &path, peniko::Fill::EvenOdd); } else { - // Simple fill of the entire path - do_fill_path(scene, &path); + do_fill_path(scene, &path, peniko::Fill::NonZero); } }; diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index b8cfff23..fdec38cc 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -710,6 +710,10 @@ impl FaceSideSet { self.set.insert(self.index(side)); } + fn remove(&mut self, side: FaceSide) { + self.set.set(self.index(side), false); + } + fn contains(&self, side: FaceSide) -> bool { self.set.contains(self.index(side)) } @@ -1102,6 +1106,19 @@ impl Vector { (0..self.point_domain.len()).any(|point_index| self.segment_domain.connected_count(point_index) > 2) } + pub fn has_regions(&self) -> bool { + !self.region_domain.id.is_empty() + } + + /// Determines if face-by-face fill rendering should be used. + /// Branching vectors without regions (e.g. mesh grids) need face-by-face fill rendering. + /// Branching vectors with regions (e.g. boolean operation results) use even-odd fill + /// on the main stroke path instead, since face decomposition can't determine which + /// bounded faces should vs. shouldn't be filled in boolean results. + pub fn use_face_fill(&self) -> bool { + self.is_branching() && !self.has_regions() + } + pub fn construct_faces(&self) -> FaceIterator<'_, Upstream> { let mut adjacency: Vec> = vec![Vec::new(); self.point_domain.len()]; for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() { @@ -1134,7 +1151,14 @@ impl Vector { if seen.contains(side) { continue; } - if (self.construct_face(&adjacency, side, &mut faces, &mut seen)).is_none() { + if self.construct_face(&adjacency, side, &mut faces, &mut seen).is_none() { + // Undo `seen` markings for sides added during this failed face construction, + // so they remain available for future face constructions starting from different sides. + if let Some(&last_start) = faces.face_start.last() { + for &failed_side in &faces.sides[last_start..] { + seen.remove(failed_side); + } + } faces.backtrack(); } }