diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 4edb9043e4..c5bf3666d2 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1363,6 +1363,41 @@ impl Shape { } fn transform_selrect(&mut self, transform: &Matrix) { + // Translation-only fast path. The general code rebuilds bounds + // by transforming all four corners (`bounds.transform`), then + // re-derives self.transform from those bounds. For pure + // translation, selrect just shifts and self.transform doesn't + // change shape (only its translation component). Doing this + // cheaply per-shape during a layout drag matters because + // `Shape::apply_transform` is called once per modified shape + // per pointer-move (N+1 times for layouts with N children). + let eps = 1e-4_f32; + let is_translation_only = (transform.scale_x() - 1.0).abs() < eps + && (transform.scale_y() - 1.0).abs() < eps + && transform.skew_x().abs() < eps + && transform.skew_y().abs() < eps + && transform.persp_x().abs() < eps + && transform.persp_y().abs() < eps; + + if is_translation_only { + let tx = transform.translate_x(); + let ty = transform.translate_y(); + // For pure translation, only `selrect` shifts — `self.transform` + // represents the shape's rotation/scale *around its center*, + // which translation leaves untouched. The general path + // confirms this: it derives `self.transform` from + // `bounds.transform_matrix()` after a translate, which still + // yields the original rotation matrix (translation lives in + // bounds position, not in `transform_matrix`). + self.selrect = math::Rect::from_xywh( + self.selrect.left + tx, + self.selrect.top + ty, + self.selrect.width(), + self.selrect.height(), + ); + return; + } + let mut center = self.selrect.center(); center = transform.map_point(center); diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index ab7dc5acbf..d9b72e189c 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -236,6 +236,46 @@ impl Path { } pub fn transform(&mut self, mtx: &Matrix) { + // Translation-only fast path. During interactive translation + // drags the layout's children all receive the same pure- + // translation modifier, and `Path::transform` is called once + // per child per pointer-move. The general path walks every + // segment doing `mtx.map_point(...)` (a 2x3 matrix multiply + // per control point) plus a Skia `make_transform` (full path + // walk in C++). For pure translation that work reduces to + // simple x+=tx, y+=ty additions plus Skia's much cheaper + // `with_offset` op. Saves a meaningful slice of per-rAF cost + // on path-heavy layouts (icons, vector illustrations, etc). + let eps = 1e-4_f32; + let is_translation_only = (mtx.scale_x() - 1.0).abs() < eps + && (mtx.scale_y() - 1.0).abs() < eps + && mtx.skew_x().abs() < eps + && mtx.skew_y().abs() < eps + && mtx.persp_x().abs() < eps + && mtx.persp_y().abs() < eps; + + if is_translation_only { + let tx = mtx.translate_x(); + let ty = mtx.translate_y(); + self.segments.iter_mut().for_each(|s| match s { + Segment::MoveTo(p) | Segment::LineTo(p) => { + p.0 += tx; + p.1 += ty; + } + Segment::CurveTo((c1, c2, p)) => { + c1.0 += tx; + c1.1 += ty; + c2.0 += tx; + c2.1 += ty; + p.0 += tx; + p.1 += ty; + } + _ => {} + }); + self.skia_path = self.skia_path.with_offset((tx, ty)); + return; + } + self.segments.iter_mut().for_each(|s| match s { Segment::MoveTo(p) => { let np = mtx.map_point(skia::Point::new(p.0, p.1));