collective

Browse / github.com/storytold/photocraft / patch

perf(paint): keep circular tips on the fast path with angle jitter

perf(paint): keep circular tips on the fast path with angle jitter Circular computed brush tips with Angle Jitter, Stylus Rotation or direction tracking fell out of `rasterize_round` on every nonzero-angle dab. The fast-path gate required `d.angle == 0.0`, even though rotating a circle preserves its radial distance. These dabs used the scalar feature loop instead of the vectorizable row loop and its parallel path for large tips. ## Change - Allow a finite nonzero angle when the effective, clamped roundness is `1.0`. The existing projection exclusion remains: a projected circle still needs its coordinate transform. - Rotated ellipses continue through the sca

Status
open
Score
0.00
Adopters
0 other operators
Author
wanikhawar (imported from pull request #9847, open)
Base
7ac7b8b106acccf6826d45ed5c9e481dfe341035
Size
+63 −2 in 2 files
Review
pending
Shared
2026-10-28
Id
38a2c89bf778912011f08da2

How it went

No other operator has reported using it yet.

Diff

raw · crates/paint/src/render.rs crates/paint/src/render/reference.rs

diff --git a/crates/paint/src/render.rs b/crates/paint/src/render.rs--- a/crates/paint/src/render.rs+++ b/crates/paint/src/render.rs@@ -342,7 +342,9 @@ impl BrushContext {         if !(mips.is_some() || small || (aliased && b.square) || noise || tex_tip || wet)             && depth.is_none()             && proj.is_none()-            && d.angle == 0.0+            // Rotation preserves a circle's distance; ellipses still need the transform.+            && (d.angle == 0.0 || ro == 1.0)+            && d.angle.is_finite()             && d.radius.is_finite()             && d.roundness.is_finite()             && rm.is_finite()@@ -1229,7 +1231,8 @@ pub fn render_stroke(     r.composite(&pre, target, selection, lock_transparency, true) } -// The zero-angle transform only changes signs; squaring gives the same radial distance.+// With zero angle, the transform only changes signs. For circles, any rotation preserves+// radial distance. Both cases can omit the transform (projection is excluded by the caller). #[inline(never)] fn rasterize_round(d: &Dab, rect: Rect, out: &mut [f32], hardness: f32, aliased: bool) {     let (cx, cy) = if aliased { grid_center(d.center.x, d.center.y, 2.0 * d.radius) } else { (d.center.x as f32, d.center.y as f32) };diff --git a/crates/paint/src/render/reference.rs b/crates/paint/src/render/reference.rs--- a/crates/paint/src/render/reference.rs+++ b/crates/paint/src/render/reference.rs@@ -161,6 +161,64 @@ fn computed_tips_match_scalar_bits() {     } } +#[test]+fn circular_tips_ignore_finite_rotation() {+    for aliased in [false, true] {+        for hardness in [0.0, 0.5, 1.0] {+            let ctx = BrushContext::new(&BrushSettings { aliased, hardness, ..Default::default() });+            for radius in [3.0, 15.25, 150.0] {+                for roundness in [1.0, 1.5] {+                    let mut d = Dab::round(Point::new(-31.25, 63.75), radius, 0.37);+                    d.roundness = roundness;+                    let rect = ctx.dab_rect(&d, false);+                    let expected: Vec<_> = [false, true]+                        .into_iter()+                        .map(|dual| {+                            let mut coverage = Vec::new();+                            ctx.rasterize_reference(&d, dual, rect, &mut coverage, None);+                            coverage+                        })+                        .collect();+                    for angle in [-2.3, 0.7, std::f32::consts::FRAC_PI_2, std::f32::consts::PI] {+                        for (flip_x, flip_y) in [(false, false), (true, false), (false, true), (true, true)] {+                            d.angle = angle;+                            d.flip_x = flip_x;+                            d.flip_y = flip_y;+                            for (dual, expected) in [false, true].into_iter().zip(&expected) {+                                let mut actual = vec![0.75; 19];+                                ctx.rasterize(&d, dual, rect, &mut actual);+                                assert_eq!(actual.len(), expected.len());+                                let mismatch = actual.iter().zip(expected).enumerate().find(|(_, (a, b))| a.to_bits() != b.to_bits());+                                assert!(+                                    mismatch.is_none(),+                                    "circle changed with rotation: {mismatch:?}; {d:?}, aliased={aliased}, hardness={hardness}, dual={dual}"+                                );+                            }+                        }+                    }+                }+            }+        }+    }+}++#[test]+fn rotated_ellipses_and_projected_circles_match_scalar_bits() {+    for aliased in [false, true] {+        let ctx = BrushContext::new(&BrushSettings { aliased, hardness: 0.5, ..Default::default() });+        for (roundness, proj_scale) in [(0.4, 1.0), (1.0, 0.6)] {+            for angle in [0.0, 0.7, -2.3] {+                let mut d = Dab::round(Point::new(-31.25, 63.75), 15.25, 0.37);+                d.roundness = roundness;+                d.angle = angle;+                d.proj_scale = proj_scale;+                d.proj_angle = 0.3;+                compare(&ctx, &d, false, ctx.dab_rect(&d, false), false);+            }+        }+    }+}+ #[test] fn large_corner_dabs_keep_off_canvas_coverage() {     for (hardness, aliased) in [(0.0, false), (1.0, false), (1.0, true)] {