From 9b5b26386c45c9c85005ff5d0c870746ce20a2c7 Mon Sep 17 00:00:00 2001 From: sagudev <16504129+sagudev@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:37:47 +0200 Subject: [PATCH] canvas: Use wrapped `kurbo::BezPath` for path everywhere (#37967) This PR removes existing path(segment) abstractions in favor of `kurbo::BezPath`, well actually wrapped `kurbo::BezPath`, to ensure building of valid paths. This allows us better Path2D building in script and doing all validation and segmentation there and also allows us remove blocking is_point_in_path on Path2D as we can now do this in script. Current path is still done on canvas thread side as it will be harder to move to script (will be done as a follow up), but it now uses this new path abstraction. Using kurbo also allows us to ditch our manual svgpath parser with the one provided by kurbo. Same code is stolen from: https://github.com/servo/servo/pull/36821. Testing: Existing WPT tests Fixes: #37904 wpt run: https://github.com/sagudev/servo/actions/runs/16172191716 --------- Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com> --- Cargo.lock | 4 + Cargo.toml | 1 + components/canvas/Cargo.toml | 1 + components/canvas/backend.rs | 207 +------- components/canvas/canvas_data.rs | 171 +++---- components/canvas/canvas_paint_thread.rs | 30 +- components/canvas/raqote_backend.rs | 163 ++----- components/script/Cargo.toml | 1 + components/script/canvas_state.rs | 21 +- components/script/dom/path2d.rs | 242 ++-------- components/script/lib.rs | 2 - components/script/svgpath/mod.rs | 13 - components/script/svgpath/number.rs | 198 -------- components/script/svgpath/path.rs | 393 ---------------- components/script/svgpath/stream.rs | 162 ------- components/shared/canvas/Cargo.toml | 1 + components/shared/canvas/canvas.rs | 442 +++++++++++++++--- .../path-objects/2d.path.arc.shape.1.html.ini | 3 - .../2d.path.isPointInpath.multi.path.html.ini | 4 - .../path-objects/2d.path.arc.shape.1.html.ini | 4 - .../2d.path.arc.shape.1.worker.js.ini | 4 - .../2d.path.isPointInpath.multi.path.html.ini | 3 - ...ath.isPointInpath.multi.path.worker.js.ini | 3 - 23 files changed, 571 insertions(+), 1502 deletions(-) delete mode 100644 components/script/svgpath/mod.rs delete mode 100644 components/script/svgpath/number.rs delete mode 100644 components/script/svgpath/path.rs delete mode 100644 components/script/svgpath/stream.rs delete mode 100644 tests/wpt/meta/html/canvas/element/path-objects/2d.path.arc.shape.1.html.ini delete mode 100644 tests/wpt/meta/html/canvas/element/path-objects/2d.path.isPointInpath.multi.path.html.ini delete mode 100644 tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.html.ini delete mode 100644 tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.worker.js.ini delete mode 100644 tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.html.ini delete mode 100644 tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.worker.js.ini diff --git a/Cargo.lock b/Cargo.lock index 16a21955977..46079445c71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1087,6 +1087,7 @@ dependencies = [ "font-kit", "fonts", "ipc-channel", + "kurbo", "log", "lyon_geom", "net_traits", @@ -1108,6 +1109,7 @@ dependencies = [ "euclid", "glow", "ipc-channel", + "kurbo", "malloc_size_of_derive", "pixels", "serde", @@ -4681,6 +4683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c" dependencies = [ "arrayvec", + "serde", "smallvec", ] @@ -7089,6 +7092,7 @@ dependencies = [ "itertools 0.14.0", "jstraceable_derive", "keyboard-types", + "kurbo", "layout_api", "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index d09c35cf96d..4cf809c447d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ ipc-channel = "0.20" itertools = "0.14" js = { package = "mozjs", git = "https://github.com/servo/mozjs" } keyboard-types = "0.7" +kurbo = "0.11" libc = "0.2" log = "0.4" mach2 = "0.4" diff --git a/components/canvas/Cargo.toml b/components/canvas/Cargo.toml index 9284c2f89bc..5d0a9f144c3 100644 --- a/components/canvas/Cargo.toml +++ b/components/canvas/Cargo.toml @@ -21,6 +21,7 @@ euclid = { workspace = true } font-kit = "0.14" fonts = { path = "../fonts" } ipc-channel = { workspace = true } +kurbo = { workspace = true } log = { workspace = true } lyon_geom = "1.0.4" net_traits = { workspace = true } diff --git a/components/canvas/backend.rs b/components/canvas/backend.rs index 78c48efc8c7..195d7d99799 100644 --- a/components/canvas/backend.rs +++ b/components/canvas/backend.rs @@ -3,17 +3,15 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use canvas_traits::canvas::{ - CompositionOrBlending, FillOrStrokeStyle, LineCapStyle, LineJoinStyle, PathSegment, + CompositionOrBlending, FillOrStrokeStyle, LineCapStyle, LineJoinStyle, Path, }; use compositing_traits::SerializableImageData; -use euclid::Angle; use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; -use lyon_geom::Arc; use pixels::Snapshot; use style::color::AbsoluteColor; use webrender_api::ImageDescriptor; -use crate::canvas_data::{CanvasPaintState, Filter, PathBuilderRef, TextRun}; +use crate::canvas_data::{CanvasPaintState, Filter, TextRun}; pub(crate) trait Backend: Clone + Sized { type Pattern<'a>: PatternHelpers + Clone; @@ -22,7 +20,6 @@ pub(crate) trait Backend: Clone + Sized { type DrawOptions: DrawOptionsHelpers + Clone; type CompositionOp; type DrawTarget: GenericDrawTarget; - type Path: GenericPath + Clone; type SourceSurface; type GradientStop; type GradientStops; @@ -80,7 +77,7 @@ pub(crate) trait GenericDrawTarget { sigma: f32, operator: B::CompositionOp, ); - fn fill(&mut self, path: &B::Path, pattern: &B::Pattern<'_>, draw_options: &B::DrawOptions); + fn fill(&mut self, path: &Path, pattern: &B::Pattern<'_>, draw_options: &B::DrawOptions); fn fill_text( &mut self, text_runs: Vec, @@ -97,12 +94,12 @@ pub(crate) trait GenericDrawTarget { fn get_size(&self) -> Size2D; fn get_transform(&self) -> Transform2D; fn pop_clip(&mut self); - fn push_clip(&mut self, path: &B::Path); + fn push_clip(&mut self, path: &Path); fn push_clip_rect(&mut self, rect: &Rect); fn set_transform(&mut self, matrix: &Transform2D); fn stroke( &mut self, - path: &B::Path, + path: &Path, pattern: &B::Pattern<'_>, stroke_options: &B::StrokeOptions, draw_options: &B::DrawOptions, @@ -119,200 +116,6 @@ pub(crate) trait GenericDrawTarget { fn snapshot(&self) -> Snapshot; } -/// A generic Path that abstracts the interface for raqote's PathBuilder/Path. -pub(crate) trait GenericPath> { - fn new() -> Self; - fn transform(&mut self, transform: &Transform2D); - fn arc( - &mut self, - origin: Point2D, - radius: f32, - start_angle: f32, - end_angle: f32, - anticlockwise: bool, - ) { - Self::ellipse( - self, - origin, - radius, - radius, - 0., - start_angle, - end_angle, - anticlockwise, - ); - } - fn bezier_curve_to( - &mut self, - control_point1: &Point2D, - control_point2: &Point2D, - control_point3: &Point2D, - ); - fn close(&mut self); - #[allow(clippy::too_many_arguments)] - fn ellipse( - &mut self, - origin: Point2D, - radius_x: f32, - radius_y: f32, - rotation_angle: f32, - start_angle: f32, - end_angle: f32, - anticlockwise: bool, - ) { - let mut start = Angle::radians(start_angle); - let mut end = Angle::radians(end_angle); - - // Wrap angles mod 2 * PI if necessary - if !anticlockwise && start > end + Angle::two_pi() || - anticlockwise && end > start + Angle::two_pi() - { - start = start.positive(); - end = end.positive(); - } - - // Calculate the total arc we're going to sweep. - let sweep = match anticlockwise { - true => { - if end - start == Angle::two_pi() { - -Angle::two_pi() - } else if end > start { - -(Angle::two_pi() - (end - start)) - } else { - -(start - end) - } - }, - false => { - if start - end == Angle::two_pi() { - Angle::two_pi() - } else if start > end { - Angle::two_pi() - (start - end) - } else { - end - start - } - }, - }; - - let arc: Arc = Arc { - center: origin, - radii: Vector2D::new(radius_x, radius_y), - start_angle: start, - sweep_angle: sweep, - x_rotation: Angle::radians(rotation_angle), - }; - - self.line_to(arc.from()); - - if sweep.radians.abs() < 1e-3 { - return; - } - - arc.for_each_quadratic_bezier(&mut |q| { - self.quadratic_curve_to(&q.ctrl, &q.to); - }); - } - fn get_current_point(&mut self) -> Option>; - fn line_to(&mut self, point: Point2D); - fn move_to(&mut self, point: Point2D); - fn quadratic_curve_to(&mut self, control_point: &Point2D, end_point: &Point2D); - fn svg_arc( - &mut self, - radius_x: f32, - radius_y: f32, - rotation_angle: f32, - large_arc: bool, - sweep: bool, - end_point: Point2D, - ) { - let Some(start) = self.get_current_point() else { - return; - }; - - let arc = lyon_geom::SvgArc { - from: start, - to: end_point, - radii: lyon_geom::vector(radius_x, radius_y), - x_rotation: lyon_geom::Angle::degrees(rotation_angle), - flags: lyon_geom::ArcFlags { large_arc, sweep }, - }; - - arc.for_each_quadratic_bezier(&mut |q| { - self.quadratic_curve_to(&q.ctrl, &q.to); - }); - } - fn contains_point(&self, x: f64, y: f64, path_transform: &Transform2D) -> bool; - fn add_segments(&mut self, path: &[PathSegment]) { - let mut build_ref = PathBuilderRef:: { - builder: self, - transform: Transform2D::identity(), - }; - for &seg in path { - match seg { - PathSegment::ClosePath => build_ref.close(), - PathSegment::MoveTo { x, y } => build_ref.move_to(&Point2D::new(x, y)), - PathSegment::LineTo { x, y } => build_ref.line_to(&Point2D::new(x, y)), - PathSegment::Quadratic { cpx, cpy, x, y } => { - build_ref.quadratic_curve_to(&Point2D::new(cpx, cpy), &Point2D::new(x, y)) - }, - PathSegment::Bezier { - cp1x, - cp1y, - cp2x, - cp2y, - x, - y, - } => build_ref.bezier_curve_to( - &Point2D::new(cp1x, cp1y), - &Point2D::new(cp2x, cp2y), - &Point2D::new(x, y), - ), - PathSegment::ArcTo { - cp1x, - cp1y, - cp2x, - cp2y, - radius, - } => build_ref.arc_to(&Point2D::new(cp1x, cp1y), &Point2D::new(cp2x, cp2y), radius), - PathSegment::Ellipse { - x, - y, - radius_x, - radius_y, - rotation, - start_angle, - end_angle, - anticlockwise, - } => build_ref.ellipse( - &Point2D::new(x, y), - radius_x, - radius_y, - rotation, - start_angle, - end_angle, - anticlockwise, - ), - PathSegment::SvgArc { - radius_x, - radius_y, - rotation, - large_arc, - sweep, - x, - y, - } => build_ref.svg_arc( - radius_x, - radius_y, - rotation, - large_arc, - sweep, - &Point2D::new(x, y), - ), - } - } - } - fn bounding_box(&self) -> Rect; -} - pub(crate) trait PatternHelpers { fn is_zero_size_gradient(&self) -> bool; fn x_bound(&self) -> Option; diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index 77755265c2c..b61e4d1e73a 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -26,7 +26,7 @@ use unicode_script::Script; use webrender_api::ImageKey; use crate::backend::{ - Backend, DrawOptionsHelpers as _, GenericDrawTarget as _, GenericPath, PatternHelpers, + Backend, DrawOptionsHelpers as _, GenericDrawTarget as _, PatternHelpers, StrokeOptionsHelpers as _, }; @@ -43,42 +43,32 @@ const MIN_WR_IMAGE_SIZE: Size2D = Size2D::new(1, 1); /// draw the path, we convert it back to userspace and draw it /// with the correct transform applied. /// TODO: De-abstract now that Azure is removed? -enum PathState { +enum PathState { /// Path in user-space. If a transform has been applied but /// but no further path operations have occurred, it is stored /// in the optional field. - UserSpacePath(B::Path, Option>), + UserSpacePath(Path, Option>), /// Path in device-space. - DeviceSpacePath(B::Path), + DeviceSpacePath(Path), } /// A wrapper around a stored PathBuilder and an optional transformation that should be /// applied to any points to ensure they are in the matching device space. -pub(crate) struct PathBuilderRef<'a, B: Backend> { - pub(crate) builder: &'a mut B::Path, +pub(crate) struct PathBuilderRef<'a> { + pub(crate) builder: &'a mut Path, pub(crate) transform: Transform2D, } -impl PathBuilderRef<'_, B> { - /// - pub(crate) fn ensure_there_is_a_subpath(&mut self, point: &Point2D) { - if self.builder.get_current_point().is_none() { - self.builder.move_to(*point); - } - } - +impl PathBuilderRef<'_> { /// pub(crate) fn line_to(&mut self, pt: &Point2D) { - // 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y). - self.ensure_there_is_a_subpath(pt); - - let pt = self.transform.transform_point(*pt); - self.builder.line_to(pt); + let pt = self.transform.transform_point(*pt).cast(); + self.builder.line_to(pt.x, pt.y); } pub(crate) fn move_to(&mut self, pt: &Point2D) { - let pt = self.transform.transform_point(*pt); - self.builder.move_to(pt); + let pt = self.transform.transform_point(*pt).cast(); + self.builder.move_to(pt.x, pt.y); } pub(crate) fn rect(&mut self, rect: &Rect) { @@ -101,13 +91,10 @@ impl PathBuilderRef<'_, B> { /// pub(crate) fn quadratic_curve_to(&mut self, cp: &Point2D, endpoint: &Point2D) { - // 2. Ensure there is a subpath for (cpx, cpy). - self.ensure_there_is_a_subpath(cp); - - self.builder.quadratic_curve_to( - &self.transform.transform_point(*cp), - &self.transform.transform_point(*endpoint), - ) + let cp = self.transform.transform_point(*cp).cast(); + let endpoint = self.transform.transform_point(*endpoint).cast(); + self.builder + .quadratic_curve_to(cp.x, cp.y, endpoint.x, endpoint.y) } /// @@ -117,14 +104,11 @@ impl PathBuilderRef<'_, B> { cp2: &Point2D, endpoint: &Point2D, ) { - // 2. Ensure there is a subpath for (cp1x, cp1y). - self.ensure_there_is_a_subpath(cp1); - - self.builder.bezier_curve_to( - &self.transform.transform_point(*cp1), - &self.transform.transform_point(*cp2), - &self.transform.transform_point(*endpoint), - ) + let cp1 = self.transform.transform_point(*cp1).cast(); + let cp2 = self.transform.transform_point(*cp2).cast(); + let endpoint = self.transform.transform_point(*endpoint).cast(); + self.builder + .bezier_curve_to(cp1.x, cp1.y, cp2.x, cp2.y, endpoint.x, endpoint.y) } pub(crate) fn arc( @@ -135,17 +119,23 @@ impl PathBuilderRef<'_, B> { end_angle: f32, ccw: bool, ) { - let center = self.transform.transform_point(*center); - self.builder - .arc(center, radius, start_angle, end_angle, ccw); + let center = self.transform.transform_point(*center).cast(); + let _ = self.builder.arc( + center.x, + center.y, + radius as f64, + start_angle as f64, + end_angle as f64, + ccw, + ); } /// pub(crate) fn arc_to(&mut self, cp1: &Point2D, cp2: &Point2D, radius: f32) { let cp0 = if let (Some(inverse), Some(point)) = - (self.transform.inverse(), self.builder.get_current_point()) + (self.transform.inverse(), self.builder.last_point()) { - inverse.transform_point(Point2D::new(point.x, point.y)) + inverse.transform_point(Point2D::new(point.x, point.y).cast()) } else { *cp1 }; @@ -218,40 +208,21 @@ impl PathBuilderRef<'_, B> { end_angle: f32, ccw: bool, ) { - let center = self.transform.transform_point(*center); - self.builder.ellipse( - center, - radius_x, - radius_y, - rotation_angle, - start_angle, - end_angle, + let center = self.transform.transform_point(*center).cast(); + let _ = self.builder.ellipse( + center.x, + center.y, + radius_x as f64, + radius_y as f64, + rotation_angle as f64, + start_angle as f64, + end_angle as f64, ccw, ); } - pub(crate) fn svg_arc( - &mut self, - radius_x: f32, - radius_y: f32, - rotation_angle: f32, - large_arc: bool, - sweep: bool, - end_point: &Point2D, - ) { - let end_point = self.transform.transform_point(*end_point); - self.builder.svg_arc( - radius_x, - radius_y, - rotation_angle, - large_arc, - sweep, - end_point, - ); - } - pub(crate) fn close(&mut self) { - self.builder.close(); + self.builder.close_path(); } } @@ -334,7 +305,7 @@ pub(crate) enum Filter { pub(crate) struct CanvasData<'a, B: Backend> { backend: B, drawtarget: B::DrawTarget, - path_state: Option>, + path_state: Option, state: CanvasPaintState<'a, B>, saved_states: Vec>, compositor_api: CrossProcessCompositorApi, @@ -746,10 +717,10 @@ impl<'a, B: Backend> CanvasData<'a, B> { /// Turn the [`Self::path_state`] into a user-space path, returning `None` if the /// path transformation matrix is uninvertible. - fn ensure_path(&mut self) -> Option<&B::Path> { + fn ensure_path(&mut self) -> Option<&Path> { // If there's no record of any path yet, create a new path in user-space. if self.path_state.is_none() { - self.path_state = Some(PathState::UserSpacePath(B::Path::new(), None)); + self.path_state = Some(PathState::UserSpacePath(Path::new(), None)); } // If a user-space path exists, create a device-space path based on it if @@ -757,7 +728,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { let new_state = match *self.path_state.as_ref().unwrap() { PathState::UserSpacePath(ref path, Some(ref transform)) => { let mut path = path.clone(); - path.transform(transform); + path.transform(transform.cast()); Some(path) }, PathState::UserSpacePath(..) | PathState::DeviceSpacePath(..) => None, @@ -772,7 +743,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { PathState::DeviceSpacePath(ref mut builder) => { let inverse = self.drawtarget.get_transform().inverse()?; let mut path = builder.clone(); - path.transform(&inverse); + path.transform(inverse.cast()); Some(path) }, PathState::UserSpacePath(..) => None, @@ -809,16 +780,13 @@ impl<'a, B: Backend> CanvasData<'a, B> { ) } - pub(crate) fn fill_path(&mut self, path_segments: &[PathSegment]) { + pub(crate) fn fill_path(&mut self, path: &Path) { if self.state.fill_style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } - let mut path = B::Path::new(); - path.add_segments(path_segments); - self.drawtarget - .fill(&path, &self.state.fill_style, &self.state.draw_options); + .fill(path, &self.state.fill_style, &self.state.draw_options); } pub(crate) fn stroke(&mut self) { @@ -844,20 +812,17 @@ impl<'a, B: Backend> CanvasData<'a, B> { ) } - pub(crate) fn stroke_path(&mut self, path_segments: &[PathSegment]) { + pub(crate) fn stroke_path(&mut self, path: &Path) { if self.state.stroke_style.is_zero_size_gradient() { return; // Paint nothing if gradient size is zero. } - let mut path = B::Path::new(); - path.add_segments(path_segments); - self.maybe_bound_shape_with_pattern( self.state.stroke_style.clone(), &path.bounding_box(), |self_| { self_.drawtarget.stroke( - &path, + path, &self_.state.stroke_style, &self_.state.stroke_opts, &self_.state.draw_options, @@ -874,17 +839,15 @@ impl<'a, B: Backend> CanvasData<'a, B> { self.drawtarget.push_clip(&path); } - pub(crate) fn clip_path(&mut self, path_segments: &[PathSegment]) { - let mut path = B::Path::new(); - path.add_segments(path_segments); - self.drawtarget.push_clip(&path); + pub(crate) fn clip_path(&mut self, path: &Path) { + self.drawtarget.push_clip(path); } pub(crate) fn is_point_in_path( &mut self, x: f64, y: f64, - _fill_rule: FillRule, + fill_rule: FillRule, chan: IpcSender, ) { self.ensure_path(); @@ -892,31 +855,15 @@ impl<'a, B: Backend> CanvasData<'a, B> { Some(PathState::UserSpacePath(path, transform)) => { let target_transform = self.drawtarget.get_transform(); let path_transform = transform.as_ref().unwrap_or(&target_transform); - path.contains_point(x, y, path_transform) + let mut path = path.clone(); + path.transform(path_transform.cast()); + path.is_point_in_path(x, y, fill_rule) }, Some(_) | None => false, }; chan.send(result).unwrap(); } - pub(crate) fn is_point_in_path_( - &mut self, - path_segments: &[PathSegment], - x: f64, - y: f64, - _fill_rule: FillRule, - chan: IpcSender, - ) { - let path_transform = match self.path_state.as_ref() { - Some(PathState::UserSpacePath(_, Some(transform))) => transform, - Some(_) | None => &self.drawtarget.get_transform(), - }; - let mut path = B::Path::new(); - path.add_segments(path_segments); - let result = path.contains_point(x, y, path_transform); - chan.send(result).unwrap(); - } - pub(crate) fn move_to(&mut self, point: &Point2D) { self.path_builder().move_to(point); } @@ -925,9 +872,9 @@ impl<'a, B: Backend> CanvasData<'a, B> { self.path_builder().line_to(point); } - fn path_builder(&mut self) -> PathBuilderRef { + fn path_builder(&mut self) -> PathBuilderRef { if self.path_state.is_none() { - self.path_state = Some(PathState::UserSpacePath(B::Path::new(), None)); + self.path_state = Some(PathState::UserSpacePath(Path::new(), None)); } // Rust is not pleased by returning a reference to a builder in some branches @@ -938,7 +885,7 @@ impl<'a, B: Backend> CanvasData<'a, B> { PathState::DeviceSpacePath(_) => None, PathState::UserSpacePath(ref path, Some(ref transform)) => { let mut path = path.clone(); - path.transform(transform); + path.transform(transform.cast()); Some(PathState::DeviceSpacePath(path)) }, PathState::UserSpacePath(ref path, None) => { diff --git a/components/canvas/canvas_paint_thread.rs b/components/canvas/canvas_paint_thread.rs index eb28fe97037..553171d0233 100644 --- a/components/canvas/canvas_paint_thread.rs +++ b/components/canvas/canvas_paint_thread.rs @@ -156,7 +156,7 @@ impl<'a> CanvasPaintThread<'a> { }, Canvas2dMsg::FillPath(style, path) => { self.canvas(canvas_id).set_fill_style(style); - self.canvas(canvas_id).fill_path(&path[..]); + self.canvas(canvas_id).fill_path(&path); }, Canvas2dMsg::Stroke(style) => { self.canvas(canvas_id).set_stroke_style(style); @@ -164,16 +164,13 @@ impl<'a> CanvasPaintThread<'a> { }, Canvas2dMsg::StrokePath(style, path) => { self.canvas(canvas_id).set_stroke_style(style); - self.canvas(canvas_id).stroke_path(&path[..]); + self.canvas(canvas_id).stroke_path(&path); }, Canvas2dMsg::Clip => self.canvas(canvas_id).clip(), - Canvas2dMsg::ClipPath(path) => self.canvas(canvas_id).clip_path(&path[..]), + Canvas2dMsg::ClipPath(path) => self.canvas(canvas_id).clip_path(&path), Canvas2dMsg::IsPointInCurrentPath(x, y, fill_rule, chan) => self .canvas(canvas_id) .is_point_in_path(x, y, fill_rule, chan), - Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, chan) => self - .canvas(canvas_id) - .is_point_in_path_(&path[..], x, y, fill_rule, chan), Canvas2dMsg::DrawImage(snapshot, dest_rect, source_rect, smoothing_enabled) => { self.canvas(canvas_id).draw_image( snapshot.to_owned(), @@ -333,7 +330,7 @@ impl Canvas<'_> { } } - fn fill_path(&mut self, path: &[PathSegment]) { + fn fill_path(&mut self, path: &Path) { match self { Canvas::Raqote(canvas_data) => canvas_data.fill_path(path), } @@ -345,7 +342,7 @@ impl Canvas<'_> { } } - fn stroke_path(&mut self, path: &[PathSegment]) { + fn stroke_path(&mut self, path: &Path) { match self { Canvas::Raqote(canvas_data) => canvas_data.stroke_path(path), } @@ -363,21 +360,6 @@ impl Canvas<'_> { } } - fn is_point_in_path_( - &mut self, - path: &[PathSegment], - x: f64, - y: f64, - fill_rule: FillRule, - chan: IpcSender, - ) { - match self { - Canvas::Raqote(canvas_data) => { - canvas_data.is_point_in_path_(path, x, y, fill_rule, chan) - }, - } - } - fn clear_rect(&mut self, rect: &Rect) { match self { Canvas::Raqote(canvas_data) => canvas_data.clear_rect(rect), @@ -582,7 +564,7 @@ impl Canvas<'_> { } } - fn clip_path(&mut self, path: &[PathSegment]) { + fn clip_path(&mut self, path: &Path) { match self { Canvas::Raqote(canvas_data) => canvas_data.clip_path(path), } diff --git a/components/canvas/raqote_backend.rs b/components/canvas/raqote_backend.rs index 94a0fc6db9f..f6d72183b3d 100644 --- a/components/canvas/raqote_backend.rs +++ b/components/canvas/raqote_backend.rs @@ -8,20 +8,19 @@ use std::collections::HashMap; use canvas_traits::canvas::*; use compositing_traits::SerializableImageData; use cssparser::color::clamp_unit_f32; -use euclid::default::{Box2D, Point2D, Rect, Size2D, Transform2D, Vector2D}; +use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; use font_kit::font::Font; use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods}; use ipc_channel::ipc::IpcSharedMemory; use log::warn; use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat}; use range::Range; -use raqote::PathOp; +use raqote::PathBuilder; use style::color::AbsoluteColor; use webrender_api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat}; use crate::backend::{ - Backend, DrawOptionsHelpers, GenericDrawTarget, GenericPath, PatternHelpers, - StrokeOptionsHelpers, + Backend, DrawOptionsHelpers, GenericDrawTarget, PatternHelpers, StrokeOptionsHelpers, }; use crate::canvas_data::{CanvasPaintState, Filter, TextRun}; @@ -44,7 +43,6 @@ impl Backend for RaqoteBackend { type CompositionOp = raqote::BlendMode; type DrawTarget = raqote::DrawTarget; type SourceSurface = Vec; // TODO: See if we can avoid the alloc (probably?) - type Path = Path; type GradientStop = raqote::GradientStop; type GradientStops = Vec; @@ -346,7 +344,8 @@ fn create_gradient_stops(gradient_stops: Vec) -> Vec for raqote::DrawTarget { fn clear_rect(&mut self, rect: &Rect) { - let mut pb = raqote::PathBuilder::new(); + let rect = rect.cast(); + let mut pb = canvas_traits::canvas::Path::new(); pb.rect( rect.origin.x, rect.origin.y, @@ -356,7 +355,7 @@ impl GenericDrawTarget for raqote::DrawTarget { let mut options = raqote::DrawOptions::new(); options.blend_mode = raqote::BlendMode::Clear; let pattern = Pattern::Color(0, 0, 0, 0); - >::fill(self, &pb.into(), &pattern, &options); + >::fill(self, &pb, &pattern, &options); } #[allow(unsafe_code)] fn copy_surface( @@ -424,15 +423,15 @@ impl GenericDrawTarget for raqote::DrawTarget { transform, )); - let mut pb = raqote::PathBuilder::new(); + let mut pb = canvas_traits::canvas::Path::new(); pb.rect( - dest.origin.x as f32, - dest.origin.y as f32, - dest.size.width as f32, - dest.size.height as f32, + dest.origin.x, + dest.origin.y, + dest.size.width, + dest.size.height, ); - >::fill(self, &pb.into(), &pattern, draw_options); + >::fill(self, &pb, &pattern, draw_options); } fn draw_surface_with_shadow( &self, @@ -447,11 +446,11 @@ impl GenericDrawTarget for raqote::DrawTarget { } fn fill( &mut self, - path: &::Path, + path: &canvas_traits::canvas::Path, pattern: &Pattern, draw_options: &::DrawOptions, ) { - let path = path.into(); + let path = to_path(path); match draw_options.blend_mode { raqote::BlendMode::Src => { self.clear(raqote::SolidSource::from_unpremultiplied_argb(0, 0, 0, 0)); @@ -546,7 +545,8 @@ impl GenericDrawTarget for raqote::DrawTarget { pattern: &::Pattern<'_>, draw_options: &::DrawOptions, ) { - let mut pb = raqote::PathBuilder::new(); + let rect = rect.cast(); + let mut pb = canvas_traits::canvas::Path::new(); pb.rect( rect.origin.x, rect.origin.y, @@ -554,7 +554,7 @@ impl GenericDrawTarget for raqote::DrawTarget { rect.size.height, ); - >::fill(self, &pb.into(), pattern, draw_options); + >::fill(self, &pb, pattern, draw_options); } fn get_size(&self) -> Size2D { Size2D::new(self.width(), self.height()) @@ -565,8 +565,8 @@ impl GenericDrawTarget for raqote::DrawTarget { fn pop_clip(&mut self) { self.pop_clip(); } - fn push_clip(&mut self, path: &::Path) { - self.push_clip(&path.into()); + fn push_clip(&mut self, path: &canvas_traits::canvas::Path) { + self.push_clip(&to_path(path)); } fn push_clip_rect(&mut self, rect: &Rect) { self.push_clip_rect(rect.to_box2d()); @@ -579,12 +579,17 @@ impl GenericDrawTarget for raqote::DrawTarget { } fn stroke( &mut self, - path: &::Path, + path: &canvas_traits::canvas::Path, pattern: &Pattern, stroke_options: &::StrokeOptions, draw_options: &::DrawOptions, ) { - self.stroke(&path.into(), &source(pattern), stroke_options, draw_options); + self.stroke( + &to_path(path), + &source(pattern), + stroke_options, + draw_options, + ); } fn stroke_rect( &mut self, @@ -665,99 +670,6 @@ impl Clone for Path { } } -impl GenericPath for Path { - fn contains_point(&self, x: f64, y: f64, path_transform: &Transform2D) -> bool { - let path: raqote::Path = self.into(); - path.transform(path_transform) - .contains_point(0.01, x as f32, y as f32) - } - - fn new() -> Self { - Self(Cell::new(raqote::PathBuilder::new())) - } - - fn bezier_curve_to( - &mut self, - control_point1: &Point2D, - control_point2: &Point2D, - control_point3: &Point2D, - ) { - self.0.get_mut().cubic_to( - control_point1.x, - control_point1.y, - control_point2.x, - control_point2.y, - control_point3.x, - control_point3.y, - ); - } - - fn close(&mut self) { - self.0.get_mut().close(); - } - - fn get_current_point(&mut self) -> Option> { - let path: raqote::Path = (&*self).into(); - - path.ops.iter().last().and_then(|op| match op { - PathOp::MoveTo(point) | PathOp::LineTo(point) => Some(Point2D::new(point.x, point.y)), - PathOp::CubicTo(_, _, point) => Some(Point2D::new(point.x, point.y)), - PathOp::QuadTo(_, point) => Some(Point2D::new(point.x, point.y)), - PathOp::Close => path.ops.first().and_then(get_first_point), - }) - } - - fn line_to(&mut self, point: Point2D) { - self.0.get_mut().line_to(point.x, point.y); - } - - fn move_to(&mut self, point: Point2D) { - self.0.get_mut().move_to(point.x, point.y); - } - - fn quadratic_curve_to(&mut self, control_point: &Point2D, end_point: &Point2D) { - self.0 - .get_mut() - .quad_to(control_point.x, control_point.y, end_point.x, end_point.y); - } - - fn transform(&mut self, transform: &Transform2D) { - let path: raqote::Path = (&*self).into(); - self.0.set(path.transform(transform).into()); - } - - fn bounding_box(&self) -> Rect { - let path: raqote::Path = self.into(); - let mut points = vec![]; - for op in path.ops { - match op { - PathOp::MoveTo(p) => points.push(p), - PathOp::LineTo(p) => points.push(p), - PathOp::QuadTo(p1, p2) => { - points.push(p1); - points.push(p2); - }, - PathOp::CubicTo(p1, p2, p3) => { - points.push(p1); - points.push(p2); - points.push(p3); - }, - PathOp::Close => {}, - } - } - Box2D::from_points(points).to_rect().cast() - } -} - -fn get_first_point(op: &PathOp) -> Option> { - match op { - PathOp::MoveTo(point) | PathOp::LineTo(point) => Some(Point2D::new(point.x, point.y)), - PathOp::CubicTo(point, _, _) => Some(Point2D::new(point.x, point.y)), - PathOp::QuadTo(point, _) => Some(Point2D::new(point.x, point.y)), - PathOp::Close => None, - } -} - pub trait ToRaqoteStyle { type Target; @@ -934,3 +846,26 @@ impl ToRaqoteStyle for CompositionStyle { } } } + +fn to_path(path: &canvas_traits::canvas::Path) -> raqote::Path { + let mut pb = PathBuilder::new(); + for cmd in &path.0 { + match cmd { + kurbo::PathEl::MoveTo(kurbo::Point { x, y }) => pb.move_to(x as f32, y as f32), + kurbo::PathEl::LineTo(kurbo::Point { x, y }) => pb.line_to(x as f32, y as f32), + kurbo::PathEl::QuadTo(cp, p) => { + pb.quad_to(cp.x as f32, cp.y as f32, p.x as f32, p.y as f32) + }, + kurbo::PathEl::CurveTo(cp1, cp2, p) => pb.cubic_to( + cp1.x as f32, + cp1.y as f32, + cp2.x as f32, + cp2.y as f32, + p.x as f32, + p.y as f32, + ), + kurbo::PathEl::ClosePath => pb.close(), + } + } + pb.finish() +} diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 553ecd0e7bb..767bf2460a0 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -81,6 +81,7 @@ itertools = { workspace = true } js = { workspace = true } jstraceable_derive = { path = "../jstraceable_derive" } keyboard-types = { workspace = true } +kurbo = { workspace = true } layout_api = { workspace = true } libc = { workspace = true } log = { workspace = true } diff --git a/components/script/canvas_state.rs b/components/script/canvas_state.rs index e77b1ddc092..49767a77dd4 100644 --- a/components/script/canvas_state.rs +++ b/components/script/canvas_state.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use canvas_traits::canvas::{ Canvas2dMsg, CanvasId, CanvasMsg, CompositionOrBlending, Direction, FillOrStrokeStyle, - FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, PathSegment, RadialGradientStyle, + FillRule, LineCapStyle, LineJoinStyle, LinearGradientStyle, Path, RadialGradientStyle, RepetitionStyle, TextAlign, TextBaseline, TextMetrics as CanvasTextMetrics, }; use constellation_traits::ScriptToConstellationMessage; @@ -1799,7 +1799,7 @@ impl CanvasState { } // https://html.spec.whatwg.org/multipage/#dom-context-2d-fill - pub(crate) fn fill_(&self, path: Vec, _fill_rule: CanvasFillRule) { + pub(crate) fn fill_(&self, path: Path, _fill_rule: CanvasFillRule) { // TODO: Process fill rule let style = self.state.borrow().fill_style.to_fill_or_stroke_style(); self.send_canvas_2d_msg(Canvas2dMsg::FillPath(style, path)); @@ -1811,7 +1811,7 @@ impl CanvasState { self.send_canvas_2d_msg(Canvas2dMsg::Stroke(style)); } - pub(crate) fn stroke_(&self, path: Vec) { + pub(crate) fn stroke_(&self, path: Path) { let style = self.state.borrow().stroke_style.to_fill_or_stroke_style(); self.send_canvas_2d_msg(Canvas2dMsg::StrokePath(style, path)); } @@ -1823,7 +1823,7 @@ impl CanvasState { } // https://html.spec.whatwg.org/multipage/#dom-context-2d-clip - pub(crate) fn clip_(&self, path: Vec, _fill_rule: CanvasFillRule) { + pub(crate) fn clip_(&self, path: Path, _fill_rule: CanvasFillRule) { // TODO: Process fill rule self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(path)); } @@ -1853,24 +1853,17 @@ impl CanvasState { // https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath pub(crate) fn is_point_in_path_( &self, - global: &GlobalScope, - path: Vec, + _global: &GlobalScope, + path: Path, x: f64, y: f64, fill_rule: CanvasFillRule, ) -> bool { - if !(x.is_finite() && y.is_finite()) { - return false; - } - let fill_rule = match fill_rule { CanvasFillRule::Nonzero => FillRule::Nonzero, CanvasFillRule::Evenodd => FillRule::Evenodd, }; - let (sender, receiver) = - profiled_ipc::channel::(global.time_profiler_chan().clone()).unwrap(); - self.send_canvas_2d_msg(Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, sender)); - receiver.recv().unwrap() + path.is_point_in_path(x, y, fill_rule) } // https://html.spec.whatwg.org/multipage/#dom-context-2d-scale diff --git a/components/script/dom/path2d.rs b/components/script/dom/path2d.rs index 476889cfbbc..982f4b35883 100644 --- a/components/script/dom/path2d.rs +++ b/components/script/dom/path2d.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; -use canvas_traits::canvas::PathSegment; +use canvas_traits::canvas::Path; use dom_struct::dom_struct; use js::rust::HandleObject; use script_bindings::str::DOMString; @@ -15,20 +15,20 @@ use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::DomRoot; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::CanGc; -use crate::svgpath::PathParser; #[dom_struct] pub(crate) struct Path2D { reflector_: Reflector, + #[ignore_malloc_size_of = "Defined in kurbo."] #[no_trace] - path: RefCell>, + path: RefCell, } impl Path2D { pub(crate) fn new() -> Path2D { Self { reflector_: Reflector::new(), - path: RefCell::new(vec![]), + path: RefCell::new(Path::new()), } } pub(crate) fn new_with_path(other: &Path2D) -> Path2D { @@ -37,26 +37,15 @@ impl Path2D { path: other.path.clone(), } } + pub(crate) fn new_with_str(path: &str) -> Path2D { - let mut path_segments = Vec::new(); - - for segment in PathParser::new(path) { - if let Ok(segment) = segment { - path_segments.push(segment); - } else { - break; - } - } - Self { reflector_: Reflector::new(), - path: RefCell::new(path_segments), + path: RefCell::new(Path::from_svg(path)), } } - pub(crate) fn push(&self, seg: PathSegment) { - self.path.borrow_mut().push(seg); - } - pub(crate) fn segments(&self) -> Vec { + + pub(crate) fn segments(&self) -> Path { self.path.borrow().clone() } } @@ -64,143 +53,49 @@ impl Path2D { impl Path2DMethods for Path2D { /// fn AddPath(&self, other: &Path2D) { + let other = other.segments(); // Step 7. Add all the subpaths in c to a. - if std::ptr::eq(&self.path, &other.path) { - // Note: this is not part of the spec, but it is a workaround to - // avoids borrow conflict when path is same as other.path - self.path.borrow_mut().extend_from_within(..); - } else { - let mut dest = self.path.borrow_mut(); - dest.extend(other.path.borrow().iter().copied()); - } + self.path.borrow_mut().0.extend(other.0); } /// fn ClosePath(&self) { - self.push(PathSegment::ClosePath); + self.path.borrow_mut().close_path(); } /// fn MoveTo(&self, x: f64, y: f64) { - // Step 1. If either of the arguments are infinite or NaN, then return. - if !(x.is_finite() && y.is_finite()) { - return; - } - - // Step 2. Create a new subpath with the specified point as its first (and only) point. - self.push(PathSegment::MoveTo { - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().move_to(x, y); } /// fn LineTo(&self, x: f64, y: f64) { - // Step 1. If either of the arguments are infinite or NaN, then return. - if !(x.is_finite() && y.is_finite()) { - return; - } - - self.push(PathSegment::LineTo { - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().line_to(x, y); } /// fn QuadraticCurveTo(&self, cpx: f64, cpy: f64, x: f64, y: f64) { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) { - return; - } - - self.push(PathSegment::Quadratic { - cpx: cpx as f32, - cpy: cpy as f32, - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().quadratic_curve_to(cpx, cpy, x, y); } /// fn BezierCurveTo(&self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(cp1x.is_finite() && - cp1y.is_finite() && - cp2x.is_finite() && - cp2y.is_finite() && - x.is_finite() && - y.is_finite()) - { - return; - } - - self.push(PathSegment::Bezier { - cp1x: cp1x as f32, - cp1y: cp1y as f32, - cp2x: cp2x as f32, - cp2y: cp2y as f32, - x: x as f32, - y: y as f32, - }); + self.path + .borrow_mut() + .bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y); } /// - fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, r: f64) -> Fallible<()> { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x1.is_finite() && y1.is_finite() && x2.is_finite() && y2.is_finite() && r.is_finite()) - { - return Ok(()); - } - - // Step 3. If radius is negative, then throw an "IndexSizeError" DOMException. - if r < 0.0 { - return Err(Error::IndexSize); - } - - self.push(PathSegment::ArcTo { - cp1x: x1 as f32, - cp1y: y1 as f32, - cp2x: x2 as f32, - cp2y: y2 as f32, - radius: r as f32, - }); - Ok(()) + fn ArcTo(&self, x1: f64, y1: f64, x2: f64, y2: f64, radius: f64) -> Fallible<()> { + self.path + .borrow_mut() + .arc_to(x1, y1, x2, y2, radius) + .map_err(|_| Error::IndexSize) } /// fn Rect(&self, x: f64, y: f64, w: f64, h: f64) { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) { - return; - } - // Step 2. Create a new subpath containing just the four points - // (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order, - // with those four points connected by straight lines. - self.push(PathSegment::MoveTo { - x: x as f32, - y: y as f32, - }); - self.push(PathSegment::LineTo { - x: (x + w) as f32, - y: y as f32, - }); - self.push(PathSegment::LineTo { - x: (x + w) as f32, - y: (y + h) as f32, - }); - self.push(PathSegment::LineTo { - x: x as f32, - y: (y + h) as f32, - }); - // Step 3. Mark the subpath as closed. - self.push(PathSegment::ClosePath); - - // Step 4. Create a new subpath with the point (x, y) as the only point in the subpath. - self.push(PathSegment::MoveTo { - x: x as f32, - y: y as f32, - }); + self.path.borrow_mut().rect(x, y, w, h); } /// @@ -208,37 +103,15 @@ impl Path2DMethods for Path2D { &self, x: f64, y: f64, - r: f64, - start: f64, - end: f64, - anticlockwise: bool, + radius: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, ) -> Fallible<()> { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x.is_finite() && - y.is_finite() && - r.is_finite() && - start.is_finite() && - end.is_finite()) - { - return Ok(()); - } - - // Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException. - if r < 0.0 { - return Err(Error::IndexSize); - } - - self.push(PathSegment::Ellipse { - x: x as f32, - y: y as f32, - radius_x: r as f32, - radius_y: r as f32, - rotation: 0., - start_angle: start as f32, - end_angle: end as f32, - anticlockwise, - }); - Ok(()) + self.path + .borrow_mut() + .arc(x, y, radius, start_angle, end_angle, counterclockwise) + .map_err(|_| Error::IndexSize) } /// @@ -246,41 +119,26 @@ impl Path2DMethods for Path2D { &self, x: f64, y: f64, - rx: f64, - ry: f64, - rotation: f64, - start: f64, - end: f64, - anticlockwise: bool, + radius_x: f64, + radius_y: f64, + rotation_angle: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, ) -> Fallible<()> { - // Step 1. If any of the arguments are infinite or NaN, then return. - if !(x.is_finite() && - y.is_finite() && - rx.is_finite() && - ry.is_finite() && - rotation.is_finite() && - start.is_finite() && - end.is_finite()) - { - return Ok(()); - } - - // Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException. - if rx < 0.0 || ry < 0.0 { - return Err(Error::IndexSize); - } - - self.push(PathSegment::Ellipse { - x: x as f32, - y: y as f32, - radius_x: rx as f32, - radius_y: ry as f32, - rotation: rotation as f32, - start_angle: start as f32, - end_angle: end as f32, - anticlockwise, - }); - Ok(()) + self.path + .borrow_mut() + .ellipse( + x, + y, + radius_x, + radius_y, + rotation_angle, + start_angle, + end_angle, + counterclockwise, + ) + .map_err(|_| Error::IndexSize) } /// diff --git a/components/script/lib.rs b/components/script/lib.rs index 742f952d9d6..73564d85c38 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -74,8 +74,6 @@ mod drag_data_store; mod links; mod xpath; -mod svgpath; - pub use init::init; pub(crate) use script_bindings::DomTypes; pub use script_runtime::JSEngineSetup; diff --git a/components/script/svgpath/mod.rs b/components/script/svgpath/mod.rs deleted file mode 100644 index f3093e3d282..00000000000 --- a/components/script/svgpath/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -mod number; -mod path; -mod stream; - -#[derive(Debug, Default, Eq, PartialEq)] -pub struct Error; - -pub(crate) use path::PathParser; -pub(crate) use stream::Stream; diff --git a/components/script/svgpath/number.rs b/components/script/svgpath/number.rs deleted file mode 100644 index b199b357868..00000000000 --- a/components/script/svgpath/number.rs +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2018 the SVG Types Authors -// Copyright 2025 the Servo Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use std::str::FromStr; - -use crate::svgpath::{Error, Stream}; - -/// An [SVG number](https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumber). -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Number(pub f32); - -impl std::str::FromStr for Number { - type Err = Error; - - fn from_str(text: &str) -> Result { - let mut s = Stream::from(text); - let n = s.parse_number()?; - s.skip_spaces(); - if !s.at_end() { - return Err(Error); - } - - Ok(Self(n)) - } -} - -impl Stream<'_> { - /// Parses number from the stream. - /// - /// This method will detect a number length and then - /// will pass a substring to the `f64::from_str` method. - /// - /// - pub fn parse_number(&mut self) -> Result { - // Strip off leading whitespaces. - self.skip_spaces(); - - if self.at_end() { - return Err(Error); - } - - self.parse_number_impl().map_err(|_| Error) - } - - fn parse_number_impl(&mut self) -> Result { - let start = self.pos(); - - let mut c = self.curr_byte()?; - - // Consume sign. - if matches!(c, b'+' | b'-') { - self.advance(1); - c = self.curr_byte()?; - } - - // Consume integer. - match c { - b'0'..=b'9' => self.skip_digits(), - b'.' => {}, - _ => return Err(Error), - } - - // Consume fraction. - if let Ok(b'.') = self.curr_byte() { - self.advance(1); - self.skip_digits(); - } - - if let Ok(c) = self.curr_byte() { - if matches!(c, b'e' | b'E') { - let c2 = self.next_byte()?; - // Check for `em`/`ex`. - if c2 != b'm' && c2 != b'x' { - self.advance(1); - - match self.curr_byte()? { - b'+' | b'-' => { - self.advance(1); - self.skip_digits(); - }, - b'0'..=b'9' => self.skip_digits(), - _ => { - return Err(Error); - }, - } - } - } - } - - let s = self.slice_back(start); - - // Use the default f32 parser now. - if let Ok(n) = f32::from_str(s) { - // inf, nan, etc. are an error. - if n.is_finite() { - return Ok(n); - } - } - - Err(Error) - } - - /// Parses number from a list of numbers. - pub fn parse_list_number(&mut self) -> Result { - if self.at_end() { - return Err(Error); - } - - let n = self.parse_number()?; - self.skip_spaces(); - self.parse_list_separator(); - Ok(n) - } -} - -/// A pull-based [``] parser. -/// -/// # Examples -/// -/// ``` -/// use svgtypes::NumberListParser; -/// -/// let mut p = NumberListParser::from("10, 20 -50"); -/// assert_eq!(p.next().unwrap().unwrap(), 10.0); -/// assert_eq!(p.next().unwrap().unwrap(), 20.0); -/// assert_eq!(p.next().unwrap().unwrap(), -50.0); -/// assert_eq!(p.next().is_none(), true); -/// ``` -/// -/// [``]: https://www.w3.org/TR/SVG2/types.html#InterfaceSVGNumberList -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct NumberListParser<'a>(Stream<'a>); - -impl<'a> From<&'a str> for NumberListParser<'a> { - #[inline] - fn from(v: &'a str) -> Self { - NumberListParser(Stream::from(v)) - } -} - -impl Iterator for NumberListParser<'_> { - type Item = Result; - - fn next(&mut self) -> Option { - if self.0.at_end() { - None - } else { - let v = self.0.parse_list_number(); - if v.is_err() { - self.0.jump_to_end(); - } - - Some(v) - } - } -} - -#[rustfmt::skip] -#[cfg(test)] -mod tests { - use crate::svgpath::Stream; - - macro_rules! test_p { - ($name:ident, $text:expr, $result:expr) => ( - #[test] - fn $name() { - let mut s = Stream::from($text); - assert_eq!(s.parse_number().unwrap(), $result); - } - ) - } - - test_p!(parse_1, "0", 0.0); - test_p!(parse_2, "1", 1.0); - test_p!(parse_3, "-1", -1.0); - test_p!(parse_4, " -1 ", -1.0); - test_p!(parse_5, " 1 ", 1.0); - test_p!(parse_6, ".4", 0.4); - test_p!(parse_7, "-.4", -0.4); - test_p!(parse_8, "-.4text", -0.4); - test_p!(parse_9, "-.01 text", -0.01); - test_p!(parse_10, "-.01 4", -0.01); - test_p!(parse_11, ".0000000000008", 0.0000000000008); - test_p!(parse_12, "1000000000000", 1000000000000.0); - test_p!(parse_13, "123456.123456", 123456.123456); - test_p!(parse_14, "+10", 10.0); - test_p!(parse_15, "1e2", 100.0); - test_p!(parse_16, "1e+2", 100.0); - test_p!(parse_17, "1E2", 100.0); - test_p!(parse_18, "1e-2", 0.01); - test_p!(parse_19, "1ex", 1.0); - test_p!(parse_20, "1em", 1.0); - test_p!(parse_21, "12345678901234567890", 12345678901234567000.0); - test_p!(parse_22, "0.", 0.0); - test_p!(parse_23, "1.3e-2", 0.013); - // test_number!(parse_24, "1e", 1.0); // TODO: this -} diff --git a/components/script/svgpath/path.rs b/components/script/svgpath/path.rs deleted file mode 100644 index 7d97df22cd2..00000000000 --- a/components/script/svgpath/path.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2021 the SVG Types Authors -// Copyright 2025 the Servo Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use canvas_traits::canvas::PathSegment; - -use crate::svgpath::{Error, Stream}; - -pub struct PathParser<'a> { - stream: Stream<'a>, - state: State, - last_cmd: u8, -} - -impl<'a> PathParser<'a> { - pub fn new(string: &'a str) -> Self { - Self { - stream: Stream::from(string), - state: State::default(), - last_cmd: b' ', - } - } -} - -impl Iterator for PathParser<'_> { - type Item = Result; - - fn next(&mut self) -> Option { - self.stream.skip_spaces(); - - let Ok(curr_byte) = self.stream.curr_byte() else { - return None; - }; - - let cmd = if self.last_cmd == b' ' { - if let move_to @ (b'm' | b'M') = curr_byte { - self.stream.advance(1); - move_to - } else { - return Some(Err(Error)); - } - } else if curr_byte.is_ascii_alphabetic() { - self.stream.advance(1); - curr_byte - } else { - match self.last_cmd { - b'm' => b'l', - b'M' => b'L', - b'z' | b'Z' => return Some(Err(Error)), - cmd => cmd, - } - }; - - self.last_cmd = cmd; - Some(to_point(&mut self.stream, cmd, &mut self.state)) - } -} - -#[derive(Default)] -pub struct State { - start: (f32, f32), - pos: (f32, f32), - quad: (f32, f32), - cubic: (f32, f32), -} - -pub fn to_point(s: &mut Stream<'_>, cmd: u8, state: &mut State) -> Result { - let abs = cmd.is_ascii_uppercase(); - let cmd = cmd.to_ascii_lowercase(); - let (dx, dy) = if abs { (0., 0.) } else { state.pos }; - let seg = match cmd { - b'm' => PathSegment::MoveTo { - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'l' => PathSegment::LineTo { - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'h' => PathSegment::LineTo { - x: s.parse_list_number()? + dx, - y: state.pos.1, - }, - b'v' => PathSegment::LineTo { - x: state.pos.0, - y: s.parse_list_number()? + dy, - }, - b'c' => PathSegment::Bezier { - cp1x: s.parse_list_number()? + dx, - cp1y: s.parse_list_number()? + dy, - cp2x: s.parse_list_number()? + dx, - cp2y: s.parse_list_number()? + dy, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b's' => PathSegment::Bezier { - cp1x: state.cubic.0, - cp1y: state.cubic.1, - cp2x: s.parse_list_number()? + dx, - cp2y: s.parse_list_number()? + dy, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'q' => PathSegment::Quadratic { - cpx: s.parse_list_number()? + dx, - cpy: s.parse_list_number()? + dy, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b't' => PathSegment::Quadratic { - cpx: state.quad.0, - cpy: state.quad.1, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'a' => PathSegment::SvgArc { - radius_x: s.parse_list_number()?, - radius_y: s.parse_list_number()?, - rotation: s.parse_list_number()?, - large_arc: s.parse_flag()?, - sweep: s.parse_flag()?, - x: s.parse_list_number()? + dx, - y: s.parse_list_number()? + dy, - }, - b'z' => PathSegment::ClosePath, - _ => return Err(crate::svgpath::Error), - }; - - match seg { - PathSegment::MoveTo { x, y } => { - state.start = (x, y); - state.pos = (x, y); - state.quad = (x, y); - state.cubic = (x, y); - }, - PathSegment::LineTo { x, y } | PathSegment::SvgArc { x, y, .. } => { - state.pos = (x, y); - state.quad = (x, y); - state.cubic = (x, y); - }, - PathSegment::Bezier { - cp2x, cp2y, x, y, .. - } => { - state.pos = (x, y); - state.quad = (x, y); - state.cubic = (x * 2.0 - cp2x, y * 2.0 - cp2y); - }, - PathSegment::Quadratic { cpx, cpy, x, y, .. } => { - state.pos = (x, y); - state.quad = (x * 2.0 - cpx, y * 2.0 - cpy); - state.cubic = (x, y); - }, - PathSegment::ClosePath => { - state.pos = state.start; - state.quad = state.start; - state.cubic = state.start; - }, - _ => {}, - } - - Ok(seg) -} - -#[rustfmt::skip] -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! test { - ($name:ident, $text:expr, $( $seg:expr ),*) => ( - #[test] - fn $name() { - let mut s = PathParser::new($text); - $( - assert_eq!(s.next().unwrap().unwrap(), $seg); - )* - - if let Some(res) = s.next() { - assert!(res.is_err()); - } - } - ) - } - - test!(null, "", ); - test!(not_a_path, "q", ); - test!(not_a_move_to, "L 20 30", ); - test!(stop_on_err_1, "M 10 20 L 30 40 L 50", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 } - ); - - test!(move_to_1, "M 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 }); - test!(move_to_2, "m 10 20", PathSegment::MoveTo { x: 10.0, y: 20.0 }); - test!(move_to_3, "M 10 20 30 40 50 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 60.0 } - ); - test!(move_to_4, "M 10 20 30 40 50 60 M 70 80 90 100 110 120", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 60.0 }, - PathSegment::MoveTo { x: 70.0, y: 80.0 }, - PathSegment::LineTo { x: 90.0, y: 100.0 }, - PathSegment::LineTo { x: 110.0, y: 120.0 } - ); - - test!(arc_to_1, "M 10 20 A 5 5 30 1 1 20 20", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::SvgArc { - radius_x: 5.0, radius_y: 5.0, - rotation: 30.0, - large_arc: true, sweep: true, - x: 20.0, y: 20.0 - } - ); - - test!(arc_to_2, "M 10 20 a 5 5 30 0 0 20 20", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::SvgArc { - radius_x: 5.0, radius_y: 5.0, - rotation: 30.0, - large_arc: false, sweep: false, - x: 30.0, y: 40.0 - } - ); - - test!(arc_to_10, "M10-20A5.5.3-4 010-.1", - PathSegment::MoveTo { x: 10.0, y: -20.0 }, - PathSegment::SvgArc { - radius_x: 5.5, radius_y: 0.3, - rotation: -4.0, - large_arc: false, sweep: true, - x: 0.0, y: -0.1 - } - ); - - test!(separator_1, "M 10 20 L 5 15 C 10 20 30 40 50 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_2, "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_3, "M 10,20 L 5,15 C 10,20 30,40 50,60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_4, "M10, 20 L5, 15 C10, 20 30 40 50 60", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 5.0, y: 15.0 }, - PathSegment::Bezier { - cp1x: 10.0, cp1y: 20.0, - cp2x: 30.0, cp2y: 40.0, - x: 50.0, y: 60.0, - } - ); - - test!(separator_5, "M10 20V30H40V50H60Z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 10.0, y: 30.0 }, - PathSegment::LineTo { x: 40.0, y: 30.0 }, - PathSegment::LineTo { x: 40.0, y: 50.0 }, - PathSegment::LineTo { x: 60.0, y: 50.0 }, - PathSegment::ClosePath - ); - - test!(all_segments_1, "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160 - Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 40.0 }, - PathSegment::LineTo { x: 50.0, y: 60.0 }, - PathSegment::Bezier { - cp1x: 70.0, cp1y: 80.0, - cp2x: 90.0, cp2y: 100.0, - x: 110.0, y: 120.0, - }, - PathSegment::Bezier { - cp1x: 130.0, cp1y: 140.0, - cp2x: 130.0, cp2y: 140.0, - x: 150.0, y: 160.0, - }, - PathSegment::Quadratic { - cpx: 170.0, cpy: 180.0, - x: 190.0, y: 200.0, - }, - PathSegment::Quadratic { - cpx: 210.0, cpy: 220.0, - x: 210.0, y: 220.0, - }, - PathSegment::SvgArc { - radius_x: 50.0, radius_y: 50.0, - rotation: 30.0, - large_arc: true, sweep: true, - x: 230.0, y: 240.0 - }, - PathSegment::ClosePath - ); - - test!(all_segments_2, "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160 - q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 40.0, y: 60.0 }, - PathSegment::LineTo { x: 90.0, y: 60.0 }, - PathSegment::LineTo { x: 90.0, y: 120.0 }, - PathSegment::Bezier { - cp1x: 160.0, cp1y: 200.0, - cp2x: 180.0, cp2y: 220.0, - x: 200.0, y: 240.0, - }, - PathSegment::Bezier { - cp1x: 220.0, cp1y: 260.0, //? - cp2x: 330.0, cp2y: 380.0, - x: 350.0, y: 400.0, - }, - PathSegment::Quadratic { - cpx: 520.0, cpy: 580.0, - x: 540.0, y: 600.0, - }, - PathSegment::Quadratic { - cpx: 560.0, cpy: 620.0, //? - x: 750.0, y: 820.0 - }, - PathSegment::SvgArc { - radius_x: 50.0, radius_y: 50.0, - rotation: 30.0, - large_arc: true, sweep: true, - x: 980.0, y: 1060.0 - }, - PathSegment::ClosePath - ); - - test!(close_path_1, "M10 20 L 30 40 ZM 100 200 L 300 400", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::ClosePath, - PathSegment::MoveTo { x: 100.0, y: 200.0 }, - PathSegment::LineTo { x: 300.0, y: 400.0 } - ); - - test!(close_path_2, "M10 20 L 30 40 zM 100 200 L 300 400", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::ClosePath, - PathSegment::MoveTo { x: 100.0, y: 200.0 }, - PathSegment::LineTo { x: 300.0, y: 400.0 } - ); - - test!(close_path_3, "M10 20 L 30 40 Z Z Z", - PathSegment::MoveTo { x: 10.0, y: 20.0 }, - PathSegment::LineTo { x: 30.0, y: 40.0 }, - PathSegment::ClosePath, - PathSegment::ClosePath, - PathSegment::ClosePath - ); - - // first token should be EndOfStream - test!(invalid_1, "M\t.", ); - - // ClosePath can't be followed by a number - test!(invalid_2, "M 0 0 Z 2", - PathSegment::MoveTo { x: 0.0, y: 0.0 }, - PathSegment::ClosePath - ); - - // ClosePath can be followed by any command - test!(invalid_3, "M 0 0 Z H 10", - PathSegment::MoveTo { x: 0.0, y: 0.0 }, - PathSegment::ClosePath, - PathSegment::LineTo { x: 10.0, y: 0.0 } - ); -} diff --git a/components/script/svgpath/stream.rs b/components/script/svgpath/stream.rs deleted file mode 100644 index 2ed044ba3b6..00000000000 --- a/components/script/svgpath/stream.rs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2018 the SVG Types Authors -// Copyright 2025 the Servo Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::svgpath::Error; - -/// A streaming text parsing interface. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct Stream<'a> { - text: &'a str, - pos: usize, -} - -impl<'a> From<&'a str> for Stream<'a> { - #[inline] - fn from(text: &'a str) -> Self { - Stream { text, pos: 0 } - } -} - -impl<'a> Stream<'a> { - /// Returns the current position in bytes. - #[inline] - pub fn pos(&self) -> usize { - self.pos - } - - /// Sets current position equal to the end. - /// - /// Used to indicate end of parsing on error. - #[inline] - pub fn jump_to_end(&mut self) { - self.pos = self.text.len(); - } - - /// Checks if the stream is reached the end. - /// - /// Any [`pos()`] value larger than original text length indicates stream end. - /// - /// Accessing stream after reaching end via safe methods will produce - /// an error. - /// - /// Accessing stream after reaching end via *_unchecked methods will produce - /// a Rust's bound checking error. - /// - /// [`pos()`]: #method.pos - #[inline] - pub fn at_end(&self) -> bool { - self.pos >= self.text.len() - } - - /// Returns a byte from a current stream position. - #[inline] - pub fn curr_byte(&self) -> Result { - if self.at_end() { - return Err(Error); - } - - Ok(self.curr_byte_unchecked()) - } - - /// Returns a byte from a current stream position. - /// - /// # Panics - /// - /// - if the current position is after the end of the data - #[inline] - pub fn curr_byte_unchecked(&self) -> u8 { - self.text.as_bytes()[self.pos] - } - - /// Checks that current byte is equal to provided. - /// - /// Returns `false` if no bytes left. - #[inline] - pub fn is_curr_byte_eq(&self, c: u8) -> bool { - if !self.at_end() { - self.curr_byte_unchecked() == c - } else { - false - } - } - - /// Returns a next byte from a current stream position. - #[inline] - pub fn next_byte(&self) -> Result { - if self.pos + 1 >= self.text.len() { - return Err(Error); - } - - Ok(self.text.as_bytes()[self.pos + 1]) - } - - /// Advances by `n` bytes. - #[inline] - pub fn advance(&mut self, n: usize) { - debug_assert!(self.pos + n <= self.text.len()); - self.pos += n; - } - - /// Skips whitespaces. - /// - /// Accepted values: `' ' \n \r \t`. - pub fn skip_spaces(&mut self) { - while !self.at_end() && matches!(self.curr_byte_unchecked(), b' ' | b'\t' | b'\n' | b'\r') { - self.advance(1); - } - } - - /// Consumes bytes by the predicate. - pub fn skip_bytes(&mut self, f: F) - where - F: Fn(&Stream<'_>, u8) -> bool, - { - while !self.at_end() { - let c = self.curr_byte_unchecked(); - if f(self, c) { - self.advance(1); - } else { - break; - } - } - } - - /// Slices data from `pos` to the current position. - #[inline] - pub fn slice_back(&self, pos: usize) -> &'a str { - &self.text[pos..self.pos] - } - - /// Skips digits. - pub fn skip_digits(&mut self) { - self.skip_bytes(|_, c| c.is_ascii_digit()); - } - - #[inline] - pub(crate) fn parse_list_separator(&mut self) { - if self.is_curr_byte_eq(b',') { - self.advance(1); - } - } - - // By the SVG spec 'large-arc' and 'sweep' must contain only one char - // and can be written without any separators, e.g.: 10 20 30 01 10 20. - pub(crate) fn parse_flag(&mut self) -> Result { - self.skip_spaces(); - - let c = self.curr_byte()?; - match c { - b'0' | b'1' => { - self.advance(1); - if self.is_curr_byte_eq(b',') { - self.advance(1); - } - self.skip_spaces(); - - Ok(c == b'1') - }, - _ => Err(Error), - } - } -} diff --git a/components/shared/canvas/Cargo.toml b/components/shared/canvas/Cargo.toml index 463d52b8c87..80ea22fc398 100644 --- a/components/shared/canvas/Cargo.toml +++ b/components/shared/canvas/Cargo.toml @@ -20,6 +20,7 @@ crossbeam-channel = { workspace = true } euclid = { workspace = true } glow = { workspace = true } ipc-channel = { workspace = true } +kurbo = { workspace = true, features = ["serde"] } malloc_size_of = { workspace = true } malloc_size_of_derive = { workspace = true } pixels = { path = "../../pixels" } diff --git a/components/shared/canvas/canvas.rs b/components/shared/canvas/canvas.rs index cc46be5cde2..f043ad26b9c 100644 --- a/components/shared/canvas/canvas.rs +++ b/components/shared/canvas/canvas.rs @@ -5,8 +5,11 @@ use std::default::Default; use std::str::FromStr; +use euclid::Angle; +use euclid::approxeq::ApproxEq; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; use ipc_channel::ipc::IpcSender; +use kurbo::{Affine, BezPath, ParamCurveNearest as _, PathEl, Point, Shape, Triangle}; use malloc_size_of_derive::MallocSizeOf; use pixels::IpcSnapshot; use serde::{Deserialize, Serialize}; @@ -14,57 +17,387 @@ use strum::{Display, EnumString}; use style::color::AbsoluteColor; use style::properties::style_structs::Font as FontStyleStruct; -#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] -pub enum PathSegment { - ClosePath, - MoveTo { - x: f32, - y: f32, - }, - LineTo { - x: f32, - y: f32, - }, - Quadratic { - cpx: f32, - cpy: f32, - x: f32, - y: f32, - }, - Bezier { - cp1x: f32, - cp1y: f32, - cp2x: f32, - cp2y: f32, - x: f32, - y: f32, - }, - ArcTo { - cp1x: f32, - cp1y: f32, - cp2x: f32, - cp2y: f32, - radius: f32, - }, - Ellipse { - x: f32, - y: f32, - radius_x: f32, - radius_y: f32, - rotation: f32, - start_angle: f32, - end_angle: f32, - anticlockwise: bool, - }, - SvgArc { - radius_x: f32, - radius_y: f32, - rotation: f32, - large_arc: bool, - sweep: bool, - x: f32, - y: f32, - }, +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Path(pub BezPath); + +pub struct IndexSizeError; + +impl Path { + pub fn new() -> Self { + Self(BezPath::new()) + } + + pub fn from_svg(s: &str) -> Self { + Self(BezPath::from_svg(s).unwrap_or_default()) + } + + pub fn transform(&mut self, transform: Transform2D) { + self.0.apply_affine(Affine::new(transform.to_array())); + } + + /// + pub fn ensure_there_is_a_subpath(&mut self, x: f64, y: f64) { + // The user agent must check to see if the path has its need new subpath flag set. + if self.0.elements().is_empty() { + // If it does, then the user agent must create a new subpath with the point (x, y) + // as its first (and only) point, + // as if the moveTo() method had been called, + // and must then unset the path's need new subpath flag. + self.0.move_to((x, y)); + } + } + + /// + pub fn close_path(&mut self) { + // must do nothing if the object's path has no subpaths + if matches!(self.0.elements().last(), None | Some(PathEl::ClosePath)) { + return; + } + // Otherwise, it must mark the last subpath as closed, + // create a new subpath whose first point is the same as the previous subpath's first point, + // and finally add this new subpath to the path. + self.0.close_path(); + } + + /// + pub fn move_to(&mut self, x: f64, y: f64) { + // Step 1. If either of the arguments are infinite or NaN, then return. + if !(x.is_finite() && y.is_finite()) { + return; + } + + // Step 2. Create a new subpath with the specified point as its first (and only) point. + self.0.move_to((x, y)); + } + + /// + pub fn line_to(&mut self, x: f64, y: f64) { + // Step 1. If either of the arguments are infinite or NaN, then return. + if !(x.is_finite() && y.is_finite()) { + return; + } + + // Step 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y). + self.ensure_there_is_a_subpath(x, y); + + // Step 3. Otherwise, connect the last point in the subpath to the given point (x, y) using a straight line, + // and then add the given point (x, y) to the subpath. + self.0.line_to((x, y)); + } + + /// + pub fn quadratic_curve_to(&mut self, cpx: f64, cpy: f64, x: f64, y: f64) { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(cpx.is_finite() && cpy.is_finite() && x.is_finite() && y.is_finite()) { + return; + } + + // Step 2. Ensure there is a subpath for (cpx, cpy). + self.ensure_there_is_a_subpath(cpx, cpy); + + // 3. Connect the last point in the subpath to the given point (x, y) + // using a quadratic Bézier curve with control point (cpx, cpy). [BEZIER] + // 4. Add the given point (x, y) to the subpath. + self.0.quad_to((cpx, cpy), (x, y)); + } + + /// + pub fn bezier_curve_to(&mut self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(cp1x.is_finite() && + cp1y.is_finite() && + cp2x.is_finite() && + cp2y.is_finite() && + x.is_finite() && + y.is_finite()) + { + return; + } + + // Step 2. Ensure there is a subpath for (cp1x, cp1y). + self.ensure_there_is_a_subpath(cp1x, cp1y); + + // Step 3. Connect the last point in the subpath to the given point (x, y) + // using a cubic Bézier curve with control points (cp1x, cp1y) and (cp2x, cp2y). [BEZIER] + // Step 4. Add the point (x, y) to the subpath. + self.0.curve_to((cp1x, cp1y), (cp2x, cp2y), (x, y)); + } + + /// + pub fn arc_to( + &mut self, + x1: f64, + y1: f64, + x2: f64, + y2: f64, + radius: f64, + ) -> Result<(), IndexSizeError> { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(x1.is_finite() && + y1.is_finite() && + x2.is_finite() && + y2.is_finite() && + radius.is_finite()) + { + return Ok(()); + } + + // Step 2. Ensure there is a subpath for (x1, y1). + self.ensure_there_is_a_subpath(x1, y1); + + // Step 3. If either radius is negative, then throw an "IndexSizeError" DOMException. + if radius.is_sign_negative() { + return Err(IndexSizeError); + } + + // Step 4. Let the point (x0, y0) be the last point in the subpath. + let Point { x: x0, y: y0 } = self.last_point().unwrap(); + + // Step 5. If the point (x0, y0) is equal to the point (x1, y1), + // or if the point (x1, y1) is equal to the point (x2, y2), + // or if radius is zero, then add the point (x1, y1) to the subpath, + // and connect that point to the previous point (x0, y0) by a straight line. + if ((x0, y0) == (x1, y1)) || ((x1, y1) == (x2, y2)) || radius.approx_eq(&0.0) { + self.0.line_to((x1, y1)); + return Ok(()); + } + + // Step 6. Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) + // all lie on a single straight line, then add the point (x1, y1) to the subpath, + // and connect that point to the previous point (x0, y0) by a straight line. + let direction = Triangle::from_coords((x0, y0), (x1, y1), (x2, y2)).area(); + if direction == 0.0 { + self.0.line_to((x1, y1)); + return Ok(()); + } + + // Step 7. Otherwise, let The Arc be the shortest arc given by circumference of the circle + // that has radius radius, and that has one point tangent to the half-infinite line + // that crosses the point (x0, y0) and ends at the point (x1, y1), + // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1) + // and crosses the point (x2, y2). + // The points at which this circle touches these two lines are called the start + // and end tangent points respectively. + // Connect the point (x0, y0) to the start tangent point by a straight line, + // adding the start tangent point to the subpath, + // and then connect the start tangent point to the end tangent point by The Arc, + // adding the end tangent point to the subpath. + + let a2 = (x0 - x1).powi(2) + (y0 - y1).powi(2); + let b2 = (x1 - x2).powi(2) + (y1 - y2).powi(2); + let d = { + let c2 = (x0 - x2).powi(2) + (y0 - y2).powi(2); + let cosx = (a2 + b2 - c2) / (2.0 * (a2 * b2).sqrt()); + let sinx = (1.0 - cosx.powi(2)).sqrt(); + radius / ((1.0 - cosx) / sinx) + }; + + // first tangent point + let anx = (x1 - x0) / a2.sqrt(); + let any = (y1 - y0) / a2.sqrt(); + let tp1 = Point2D::new(x1 - anx * d, y1 - any * d); + + // second tangent point + let bnx = (x1 - x2) / b2.sqrt(); + let bny = (y1 - y2) / b2.sqrt(); + let tp2 = Point2D::new(x1 - bnx * d, y1 - bny * d); + + // arc center and angles + let anticlockwise = direction < 0.0; + let cx = tp1.x + any * radius * if anticlockwise { 1.0 } else { -1.0 }; + let cy = tp1.y - anx * radius * if anticlockwise { 1.0 } else { -1.0 }; + let angle_start = (tp1.y - cy).atan2(tp1.x - cx); + let angle_end = (tp2.y - cy).atan2(tp2.x - cx); + + self.0.line_to((tp1.x, tp2.x)); + + self.arc(cx, cy, radius, angle_start, angle_end, anticlockwise) + } + + pub fn last_point(&mut self) -> Option { + // https://github.com/linebender/kurbo/pull/462 + match self.0.elements().last()? { + PathEl::ClosePath => self + .0 + .elements() + .iter() + .rev() + .skip(1) + .take_while(|el| !matches!(el, PathEl::ClosePath)) + .last() + .and_then(|el| el.end_point()), + other => other.end_point(), + } + } + + #[allow(clippy::too_many_arguments)] + /// + pub fn arc( + &mut self, + x: f64, + y: f64, + radius: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, + ) -> Result<(), IndexSizeError> { + // ellipse() with both radii are equal and rotation is 0. + self.ellipse( + x, + y, + radius, + radius, + 0., + start_angle, + end_angle, + counterclockwise, + ) + } + + #[allow(clippy::too_many_arguments)] + /// + pub fn ellipse( + &mut self, + x: f64, + y: f64, + radius_x: f64, + radius_y: f64, + rotation_angle: f64, + start_angle: f64, + end_angle: f64, + counterclockwise: bool, + ) -> Result<(), IndexSizeError> { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(x.is_finite() && + y.is_finite() && + radius_x.is_finite() && + radius_y.is_finite() && + rotation_angle.is_finite() && + start_angle.is_finite() && + end_angle.is_finite()) + { + return Ok(()); + } + + // Step 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException. + if radius_x.is_sign_negative() || radius_y.is_sign_negative() { + return Err(IndexSizeError); + } + + let mut start = Angle::radians(start_angle); + let mut end = Angle::radians(end_angle); + + // Wrap angles mod 2 * PI if necessary + if !counterclockwise && start > end + Angle::two_pi() || + counterclockwise && end > start + Angle::two_pi() + { + start = start.positive(); + end = end.positive(); + } + + // Calculate the total arc we're going to sweep. + let sweep = match counterclockwise { + true => { + if end - start == Angle::two_pi() { + -Angle::two_pi() + } else if end > start { + -(Angle::two_pi() - (end - start)) + } else { + -(start - end) + } + }, + false => { + if start - end == Angle::two_pi() { + Angle::two_pi() + } else if start > end { + Angle::two_pi() - (start - end) + } else { + end - start + } + }, + }; + + let arc = kurbo::Arc::new( + (x, y), + (radius_x, radius_y), + start.radians, + sweep.radians, + rotation_angle, + ); + + let mut iter = arc.path_elements(0.01); + let kurbo::PathEl::MoveTo(start_point) = iter.next().unwrap() else { + unreachable!() + }; + + self.line_to(start_point.x, start_point.y); + + if sweep.radians.abs() > 1e-3 { + self.0.extend(iter); + } + + Ok(()) + } + + /// + pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) { + // Step 1. If any of the arguments are infinite or NaN, then return. + if !(x.is_finite() && y.is_finite() && w.is_finite() && h.is_finite()) { + return; + } + + // Step 2. Create a new subpath containing just the four points + // (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order, + // with those four points connected by straight lines. + self.0.move_to((x, y)); + self.0.line_to((x + w, y)); + self.0.line_to((x + w, y + h)); + self.0.line_to((x, y + h)); + + // Step 3. Mark the subpath as closed. + self.0.close_path(); + + // Step 4. Create a new subpath with the point (x, y) as the only point in the subpath. + self.0.move_to((x, y)); + } + + /// + pub fn is_point_in_path(&self, x: f64, y: f64, fill_rule: FillRule) -> bool { + let p = Point::new(x, y); + // Step 1. If x or y are infinite or NaN, then return false. + if !p.is_finite() { + return false; + } + + // Step 2. If the point given by the x and y coordinates, + // when treated as coordinates in the canvas coordinate space unaffected by the current transformation, + // is inside the intended path for path as determined by the fill rule indicated by fillRule, + // then return true. + // Open subpaths must be implicitly closed when computing the area inside the path, + // without affecting the actual subpaths. + let mut path = self.clone(); + path.close_path(); + let winding = path.0.winding(p); + let is_inside = match fill_rule { + FillRule::Nonzero => winding != 0, + FillRule::Evenodd => (winding % 2) != 0, + }; + if is_inside { + return true; + } + // Points on the path itself must be considered to be inside the path. + path.0 + .segments() + .any(|seg| seg.nearest(p, 0.00001).distance_sq < 0.00001) + } + + pub fn bounding_box(&self) -> Rect { + let rect = self.0.control_box(); + Rect::new( + Point2D::new(rect.origin().x, rect.origin().y), + Size2D::new(rect.width(), rect.height()), + ) + } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -95,17 +428,16 @@ pub enum Canvas2dMsg { BezierCurveTo(Point2D, Point2D, Point2D), ClearRect(Rect), Clip, - ClipPath(Vec), + ClipPath(Path), ClosePath, Ellipse(Point2D, f32, f32, f32, f32, f32, bool), Fill(FillOrStrokeStyle), - FillPath(FillOrStrokeStyle, Vec), + FillPath(FillOrStrokeStyle, Path), FillText(String, f64, f64, Option, FillOrStrokeStyle, bool), FillRect(Rect, FillOrStrokeStyle), GetImageData(Rect, Size2D, IpcSender), GetTransform(IpcSender>), IsPointInCurrentPath(f64, f64, FillRule, IpcSender), - IsPointInPath(Vec, f64, f64, FillRule, IpcSender), LineTo(Point2D), MoveTo(Point2D), MeasureText(String, IpcSender), @@ -116,7 +448,7 @@ pub enum Canvas2dMsg { SaveContext, StrokeRect(Rect, FillOrStrokeStyle), Stroke(FillOrStrokeStyle), - StrokePath(FillOrStrokeStyle, Vec), + StrokePath(FillOrStrokeStyle, Path), SetLineWidth(f32), SetLineCap(LineCapStyle), SetLineJoin(LineJoinStyle), diff --git a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.arc.shape.1.html.ini b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.arc.shape.1.html.ini deleted file mode 100644 index f5b107a7d84..00000000000 --- a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.arc.shape.1.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[2d.path.arc.shape.1.html] - [arc() from 0 to pi does not draw anything in the wrong half] - expected: FAIL diff --git a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.isPointInpath.multi.path.html.ini b/tests/wpt/meta/html/canvas/element/path-objects/2d.path.isPointInpath.multi.path.html.ini deleted file mode 100644 index 7be04136c27..00000000000 --- a/tests/wpt/meta/html/canvas/element/path-objects/2d.path.isPointInpath.multi.path.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[2d.path.isPointInpath.multi.path.html] - [Verify the winding rule in isPointInPath works for path object.] - expected: FAIL - diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.html.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.html.ini deleted file mode 100644 index bd75f8ab39b..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[2d.path.arc.shape.1.html] - [arc() from 0 to pi does not draw anything in the wrong half] - expected: FAIL - diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.worker.js.ini deleted file mode 100644 index c7698ce2f86..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.arc.shape.1.worker.js.ini +++ /dev/null @@ -1,4 +0,0 @@ -[2d.path.arc.shape.1.worker.html] - [arc() from 0 to pi does not draw anything in the wrong half] - expected: FAIL - diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.html.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.html.ini deleted file mode 100644 index 6267ee28a52..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[2d.path.isPointInpath.multi.path.html] - [Verify the winding rule in isPointInPath works for path object.] - expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.worker.js.ini deleted file mode 100644 index 43b5909c290..00000000000 --- a/tests/wpt/meta/html/canvas/offscreen/path-objects/2d.path.isPointInpath.multi.path.worker.js.ini +++ /dev/null @@ -1,3 +0,0 @@ -[2d.path.isPointInpath.multi.path.worker.html] - [Verify the winding rule in isPointInPath works for path object.] - expected: FAIL