@@ -250,10 +250,22 @@ pub fn format_transform_matrix(transform: DAffine2) -> String {
250250 } ) + ")"
251251}
252252
253- fn max_scale ( transform : DAffine2 ) -> f64 {
254- let sx = transform. x_axis . length_squared ( ) ;
255- let sy = transform. y_axis . length_squared ( ) ;
256- ( sx + sy) . sqrt ( )
253+ /// `(max, min)` factors by which a unit vector is stretched under `transform`'s linear part — the
254+ /// principal and minor singular values, equal to the semi-axes of the ellipse a unit circle maps to.
255+ /// Equivalent to `(max(sx, sy), min(sx, sy))` for axis-aligned scales, but accounts for shear.
256+ fn singular_values ( transform : DAffine2 ) -> ( f64 , f64 ) {
257+ let m = transform. matrix2 ;
258+ let a = m. x_axis . x ;
259+ let b = m. x_axis . y ;
260+ let c = m. y_axis . x ;
261+ let d = m. y_axis . y ;
262+ // Eigenvalues of MᵀM via the closed form for a 2×2, both are non-negative
263+ let trace = a * a + b * b + c * c + d * d;
264+ let det = a * d - b * c;
265+ let discriminant = ( trace * trace - 4. * det * det) . max ( 0. ) . sqrt ( ) ;
266+ let largest_eigenvalue = ( trace + discriminant) * 0.5 ;
267+ let smallest_eigenvalue = ( ( trace - discriminant) * 0.5 ) . max ( 0. ) ;
268+ ( largest_eigenvalue. sqrt ( ) , smallest_eigenvalue. sqrt ( ) )
257269}
258270
259271pub fn black_or_white_for_best_contrast ( background : Option < Color > ) -> Color {
@@ -1025,7 +1037,9 @@ impl Render for Table<Vector> {
10251037 let mut svg = SvgRender :: new ( ) ;
10261038 vector_item. render_svg ( & mut svg, & render_params. for_alignment ( applied_stroke_transform) ) ;
10271039 let stroke = vector. style . stroke ( ) . unwrap ( ) ;
1028- let inflation = stroke. max_aabb_inflation ( ) * max_scale ( applied_stroke_transform) ;
1040+ // `push_id` is only `Some` when `can_draw_aligned_stroke`, which is gated on `path_is_closed`
1041+ let ( largest_scale, _) = singular_values ( applied_stroke_transform) ;
1042+ let inflation = stroke. max_aabb_inflation ( true ) * largest_scale;
10291043 let quad = Quad :: from_box ( transformed_bounds) . inflate ( inflation) ;
10301044 let ( x, y) = quad. top_left ( ) . into ( ) ;
10311045 let ( width, height) = ( quad. bottom_right ( ) - quad. top_left ( ) ) . into ( ) ;
@@ -1144,16 +1158,20 @@ impl Render for Table<Vector> {
11441158 } ;
11451159 let mut layer = false ;
11461160
1161+ // Whether the renderer will engage the stroke-alignment compositing trick (non-Center align on a fully closed path).
1162+ // Used by both the blend-layer clip rect inflation below (as `max_aabb_inflation`'s `path_is_closed` arg, equivalent here since
1163+ // the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
1164+ let stroke = element. style . stroke ( ) ;
1165+ let can_draw_aligned_stroke = stroke. as_ref ( ) . is_some_and ( |s| s. has_renderable_stroke ( ) && s. align . is_not_centered ( ) ) && element. stroke_bezier_paths ( ) . all ( |p| p. closed ( ) ) ;
1166+
11471167 let opacity = ( opacity_attr * if render_params. for_mask { 1. } else { opacity_fill_attr } ) as f32 ;
11481168 if opacity < 1. || blend_mode_attr != BlendMode :: default ( ) {
11491169 layer = true ;
1150- // `max_aabb_inflation` is in `applied_stroke_transform`-space (where the stroke is drawn).
1151- // `layer_bounds` is in path-local coords and `push_layer` re-applies `multiplied_transform`.
1152- // Divide by `max_scale(applied_stroke_transform)` so the rect, after Vello's transform, ends at the right scene extent.
1153- // Skip on a degenerate transform since nothing renders in that case.
1154- let scale = max_scale ( applied_stroke_transform) ;
1155- let stroke_inflation = element. style . stroke ( ) . as_ref ( ) . map_or ( 0. , Stroke :: max_aabb_inflation) ;
1156- let inflate_amount = if scale > 0. { stroke_inflation / scale } else { 0. } ;
1170+ // `max_aabb_inflation` is in `applied_stroke_transform`-space; `layer_bounds` is path-local and `push_layer` re-applies `multiplied_transform`.
1171+ // Divide by the smaller axial scale to cover the stroke in both axes after Vello's transform. Skip on a degenerate transform.
1172+ let ( _, smallest_scale) = singular_values ( applied_stroke_transform) ;
1173+ let stroke_inflation = stroke. as_ref ( ) . map_or ( 0. , |s| s. max_aabb_inflation ( can_draw_aligned_stroke) ) ;
1174+ let inflate_amount = if smallest_scale > 0. { stroke_inflation / smallest_scale } else { 0. } ;
11571175 let quad = Quad :: from_box ( layer_bounds) . inflate ( inflate_amount) ;
11581176 let layer_bounds = quad. bounding_box ( ) ;
11591177 scene. push_layer (
@@ -1165,11 +1183,8 @@ impl Render for Table<Vector> {
11651183 ) ;
11661184 }
11671185
1168- let can_draw_aligned_stroke =
1169- element. style . stroke ( ) . is_some_and ( |stroke| stroke. has_renderable_stroke ( ) && stroke. align . is_not_centered ( ) ) && element. stroke_bezier_paths ( ) . all ( |path| path. closed ( ) ) ;
1170-
11711186 let use_layer = can_draw_aligned_stroke;
1172- let wants_stroke_below = element . style . stroke ( ) . is_some_and ( |s| s. paint_order == vector:: style:: PaintOrder :: StrokeBelow ) ;
1187+ let wants_stroke_below = stroke. as_ref ( ) . is_some_and ( |s| s. paint_order == vector:: style:: PaintOrder :: StrokeBelow ) ;
11731188
11741189 // Closures to avoid duplicated fill/stroke drawing logic
11751190 let do_fill_path = |scene : & mut Scene , path : & kurbo:: BezPath , fill_rule : peniko:: Fill | match element. style . fill ( ) {
@@ -1312,8 +1327,10 @@ impl Render for Table<Vector> {
13121327 ) ;
13131328
13141329 let bounds = element. bounding_box_with_transform ( multiplied_transform) . unwrap_or ( layer_bounds) ;
1315- let inflation = element. style . stroke ( ) . as_ref ( ) . map_or ( 0. , Stroke :: max_aabb_inflation) ;
1316- let quad = Quad :: from_box ( bounds) . inflate ( inflation * max_scale ( applied_stroke_transform) ) ;
1330+ // This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed
1331+ let inflation = element. style . stroke ( ) . as_ref ( ) . map_or ( 0. , |stroke| stroke. max_aabb_inflation ( true ) ) ;
1332+ let ( largest_scale, _) = singular_values ( applied_stroke_transform) ;
1333+ let quad = Quad :: from_box ( bounds) . inflate ( inflation * largest_scale) ;
13171334 let bounds = quad. bounding_box ( ) ;
13181335 let rect = kurbo:: Rect :: new ( bounds[ 0 ] . x , bounds[ 0 ] . y , bounds[ 1 ] . x , bounds[ 1 ] . y ) ;
13191336
0 commit comments