From e99e45c2c647cebf2092684041a7ab948e2d06bd Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sun, 14 Jun 2026 23:49:39 +0800 Subject: [PATCH] perf(canvas): avoid cloning simple path points --- .../src/widgets/canvas_viewport_paint.rs | 47 +++++++++++++++---- .../widgets/canvas_viewport_paint_tests.rs | 14 +++++- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs index 6cd92013a..2d84f331c 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs @@ -164,23 +164,38 @@ fn flatten_segment( } } -/// Flatten a Path scene node into a doc-space polyline — cubic -/// segments whose endpoints carry handles are tessellated; a -/// handle-free path falls back to the straight `points` polyline. -/// A closed path appends the last-anchor → first-anchor segment. -pub(crate) fn flatten_path(node: &SceneNode) -> Vec { +pub(crate) enum PathPoints<'a> { + Borrowed(&'a [Point2D]), + Owned(Vec), +} + +impl<'a> PathPoints<'a> { + pub(crate) fn as_slice(&self) -> &[Point2D] { + match self { + Self::Borrowed(points) => points, + Self::Owned(points) => points.as_slice(), + } + } +} + +/// Flatten a Path scene node into doc-space points, borrowing the +/// original point slice for the common handle-free open-path case. +pub(crate) fn flatten_path_points(node: &SceneNode) -> PathPoints<'_> { let anchors = &node.path_anchors; let has_handle = anchors .iter() .any(|a| a.handle_in.is_some() || a.handle_out.is_some()); if anchors.len() < 2 || !has_handle { + if !node.path_closed { + return PathPoints::Borrowed(&node.points); + } let mut out = node.points.clone(); // Closed handle-free path — link the polyline back to its // start so the closing edge is drawn. if node.path_closed && out.len() > 2 { out.push(out[0]); } - return out; + return PathPoints::Owned(out); } let mut out = Vec::with_capacity(anchors.len() * 16 + 16); out.push(anchors[0].pos); @@ -190,7 +205,18 @@ pub(crate) fn flatten_path(node: &SceneNode) -> Vec { if node.path_closed { flatten_segment(&anchors[anchors.len() - 1], &anchors[0], &mut out); } - out + PathPoints::Owned(out) +} + +/// Flatten a Path scene node into a doc-space polyline — cubic +/// segments whose endpoints carry handles are tessellated; a +/// handle-free path falls back to the straight `points` polyline. +/// A closed path appends the last-anchor → first-anchor segment. +pub(crate) fn flatten_path(node: &SceneNode) -> Vec { + match flatten_path_points(node) { + PathPoints::Borrowed(points) => points.to_vec(), + PathPoints::Owned(points) => points, + } } /// Push a children-clip for a `clipContent` container (root frames @@ -516,11 +542,12 @@ fn paint_node_inner( // Bezier-aware: when the path carries anchors with control // handles, flatten each cubic segment; otherwise fall back // to the straight `points` polyline. - let polyline = flatten_path(node); + let polyline = flatten_path_points(node); + let points = polyline.as_slice(); // A closed path with a fill paints its enclosed area. let filled = node.path_closed && node.fill.is_some(); if filled { - let world: Vec = polyline.iter().map(|p| to_world(*p)).collect(); + let world: Vec = points.iter().map(|p| to_world(*p)).collect(); cx.backend.fill_polygon(&world, node.fill.unwrap()); } // Stroke: an explicit stroke always paints; with no @@ -536,7 +563,7 @@ fn paint_node_inner( None => None, }; if let Some((color, width)) = stroke { - for pair in polyline.windows(2) { + for pair in points.windows(2) { cx.backend .stroke_line(to_world(pair[0]), to_world(pair[1]), color, width); } diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_paint_tests.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_paint_tests.rs index 9e688ec1b..90f6f7cf8 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_paint_tests.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_paint_tests.rs @@ -491,7 +491,7 @@ mod text_tests { mod path_tests { use crate::layout_scene::{NodeKind, SceneAnchor, SceneNode, ScenePointType}; - use crate::widgets::canvas_viewport_paint::flatten_path; + use crate::widgets::canvas_viewport_paint::{flatten_path, flatten_path_points, PathPoints}; use crate::{Point2D, Rect}; fn anchor(x: f32, y: f32, hout: Option) -> SceneAnchor { @@ -511,6 +511,18 @@ mod path_tests { assert_eq!(flatten_path(&n), n.points); } + #[test] + fn handle_free_open_path_borrows_points_without_allocating() { + let mut n = SceneNode::leaf("p", NodeKind::Path); + n.points = vec![Point2D::new(0.0, 0.0), Point2D::new(10.0, 0.0)]; + n.path_anchors = vec![anchor(0.0, 0.0, None), anchor(10.0, 0.0, None)]; + + let points = flatten_path_points(&n); + + assert!(matches!(points, PathPoints::Borrowed(_))); + assert_eq!(points.as_slice(), n.points.as_slice()); + } + #[test] fn curved_segment_tessellates_into_many_points() { let mut n = SceneNode::leaf("p", NodeKind::Path);