Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,8 +1025,8 @@ impl Render for Table<Vector> {
let mut svg = SvgRender::new();
vector_item.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
let stroke = vector.style.stroke().unwrap();
let weight = stroke.effective_width() * max_scale(applied_stroke_transform);
let quad = Quad::from_box(transformed_bounds).inflate(weight);
let inflation = stroke.max_aabb_inflation() * max_scale(applied_stroke_transform);
let quad = Quad::from_box(transformed_bounds).inflate(inflation);
let (x, y) = quad.top_left().into();
let (width, height) = (quad.bottom_right() - quad.top_left()).into();

Expand Down Expand Up @@ -1147,8 +1147,14 @@ impl Render for Table<Vector> {
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. || blend_mode_attr != BlendMode::default() {
layer = true;
let weight = element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform));
// `max_aabb_inflation` is in `applied_stroke_transform`-space (where the stroke is drawn).
// `layer_bounds` is in path-local coords and `push_layer` re-applies `multiplied_transform`.
// Divide by `max_scale(applied_stroke_transform)` so the rect, after Vello's transform, ends at the right scene extent.
// Skip on a degenerate transform since nothing renders in that case.
let scale = max_scale(applied_stroke_transform);
let stroke_inflation = element.style.stroke().as_ref().map_or(0., Stroke::max_aabb_inflation);
let inflate_amount = if scale > 0. { stroke_inflation / scale } else { 0. };
let quad = Quad::from_box(layer_bounds).inflate(inflate_amount);
let layer_bounds = quad.bounding_box();
scene.push_layer(
peniko::Fill::NonZero,
Expand Down Expand Up @@ -1306,8 +1312,8 @@ impl Render for Table<Vector> {
);

let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
let weight = element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let quad = Quad::from_box(bounds).inflate(weight * max_scale(applied_stroke_transform));
let inflation = element.style.stroke().as_ref().map_or(0., Stroke::max_aabb_inflation);
let quad = Quad::from_box(bounds).inflate(inflation * max_scale(applied_stroke_transform));
let bounds = quad.bounding_box();
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);

Expand Down
15 changes: 15 additions & 0 deletions node-graph/libraries/vector-types/src/vector/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,21 @@ impl Stroke {
}
}

/// Worst-case upper bound on the perpendicular extent (per side) of the visible stroke from the path
/// centerline, accounting for stroke alignment, miter join overshoot, and square cap diagonal extent.
/// Used as a cheap, safe inflation amount for renderer clip rects so alignment compositing layers
/// don't crop the actual stroke geometry. Constant-time — no path traversal.
///
/// Tight for round/bevel joins with butt/round caps. Otherwise overestimates: miter joins are assumed
/// to reach the miter limit at every join (most don't), and square caps are assumed to sit at 45° to
/// the axes (rarely the case). For an exact bound, use `Vector::stroke_inclusive_bounding_box_with_transform`
/// at the cost of running kurbo to compute the stroke's outline path.
pub fn max_aabb_inflation(&self) -> f64 {
let join_factor = if self.join == StrokeJoin::Miter { self.join_miter_limit.max(1.) } else { 1. };
let cap_factor = if self.cap == StrokeCap::Square { core::f64::consts::SQRT_2 } else { 1. };
self.effective_width() * 0.5 * join_factor.max(cap_factor)
}

pub fn dash_lengths(&self) -> String {
if self.dash_lengths.is_empty() {
"none".to_string()
Expand Down
Loading