Collapse ClickTargetType to a single multi-contour BezPath variant (#4456)

* Collapse ClickTargetType to a single multi-contour BezPath variant

* Restrict no-stroke point hits to a path's closed contours
This commit is contained in:
Keavon Chambers
2026-08-18 14:31:52 -07:00
committed by GitHub
parent c111308c67
commit 8f1b2bed5f
16 changed files with 438 additions and 360 deletions

View File

@@ -42,6 +42,7 @@ use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect; use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::misc::dvec2_to_point; use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::style::RenderMode; use graphene_std::vector::style::RenderMode;
@@ -1876,18 +1877,15 @@ impl DocumentMessageHandler {
let layer_click_targets = self.network_interface.document_metadata().click_targets(*layer); let layer_click_targets = self.network_interface.document_metadata().click_targets(*layer);
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer); let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
let viewport_polygon_bezpath = viewport_polygon.to_bezpath();
layer_click_targets.is_some_and(|targets| { layer_click_targets.is_some_and(|targets| {
targets.iter().all(|target| match target.target_type() { targets.iter().all(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => { ClickTargetType::Path(path) => {
let mut subpath = subpath.clone(); let mut path = path.clone();
subpath.apply_transform(layer_transform); path.apply_affine(Affine::new(layer_transform.to_cols_array()));
subpath.is_inside_subpath(&viewport_polygon, None, None) bezpath_is_inside_bezpath(&path, &viewport_polygon_bezpath, None, None)
} }
ClickTargetType::CompoundPath(subpaths) => subpaths.iter().all(|subpath| {
let mut subpath = subpath.clone();
subpath.apply_transform(layer_transform);
subpath.is_inside_subpath(&viewport_polygon, None, None)
}),
ClickTargetType::FreePoint(point) => { ClickTargetType::FreePoint(point) => {
let mut point = *point; let mut point = *point;
point.apply_transform(layer_transform); point.apply_transform(layer_transform);
@@ -3814,13 +3812,7 @@ fn quad_to_kurbo(quad: Quad) -> BezPath {
fn click_targets_to_kurbo<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> BezPath { fn click_targets_to_kurbo<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> BezPath {
let segments = click_targets let segments = click_targets
.filter_map(|target| { .filter_map(|target| if let ClickTargetType::Path(path) = target.target_type() { Some(path.segments()) } else { None })
if let ClickTargetType::Subpath(subpath) = target.target_type() {
Some(subpath.iter())
} else {
None
}
})
.flatten() .flatten()
.map(|bezier| Affine::new(transform.to_cols_array()) * bezier); .map(|bezier| Affine::new(transform.to_cols_array()) * bezier);
BezPath::from_path_segments(segments) BezPath::from_path_segments(segments)

View File

@@ -109,12 +109,10 @@ fn overlay_bezier_handle_specific_point(
let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
match segment_to_handles(&segment) { match segment_to_handles(&segment) {
BezierHandles::Quadratic { handle } => { BezierHandles::Quadratic { handle } if not_under_anchor(handle, segment_start) && not_under_anchor(handle, segment_end) => {
if not_under_anchor(handle, segment_start) && not_under_anchor(handle, segment_end) { let anchor = if start == point_to_render { segment_start } else { segment_end };
let anchor = if start == point_to_render { segment_start } else { segment_end }; overlay_context.line(handle, anchor, None, None);
overlay_context.line(handle, anchor, None, None); overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None);
}
} }
BezierHandles::Cubic { handle_start, handle_end } => { BezierHandles::Cubic { handle_start, handle_end } => {
if not_under_anchor(handle_start, segment_start) && (point_to_render == start) { if not_under_anchor(handle_start, segment_start) && (point_to_render == start) {

View File

@@ -15,7 +15,6 @@ use glam::{DAffine2, DVec2};
use graphene_std::ATTR_TRANSFORM; use graphene_std::ATTR_TRANSFORM;
use graphene_std::list::List; use graphene_std::list::List;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig}; use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2; use graphene_std::vector::misc::point_to_dvec2;
@@ -404,14 +403,14 @@ impl OverlayContext {
/// Fills the area inside the path. Assumes `color` is in gamma space. /// Fills the area inside the path. Assumes `color` is in gamma space.
/// Used by the Pen tool to show the path being closed. /// Used by the Pen tool to show the path being closed.
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) { pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
self.internal().fill_path(subpaths, transform, color); self.internal().fill_path(bezpath, transform, color);
} }
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string. /// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
/// Used by the fill tool to show the area to be filled. /// Used by the fill tool to show the area to be filled.
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) { pub fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
self.internal().fill_path_pattern(subpaths, transform, color); self.internal().fill_path_pattern(bezpath, transform, color);
} }
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
@@ -978,49 +977,25 @@ impl OverlayContextInternal {
path.push(bezier.as_path_el()); path.push(bezier.as_path_el());
} }
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) -> BezPath { fn push_path(&mut self, bezpath: &BezPath, transform: DAffine2) -> BezPath {
let mut path = BezPath::new(); let mut path = BezPath::new();
for subpath in subpaths { let snap_start = |context: &Self, point: kurbo::Point| {
let subpath = subpath.borrow(); let snapped = context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
let mut curves = subpath.iter().peekable(); kurbo::Point::new(snapped.x, snapped.y)
};
let snap_center = |context: &Self, point: kurbo::Point| {
let snapped = context.snap_to_physical_pixel_center(transform.transform_point2(point_to_dvec2(point)));
kurbo::Point::new(snapped.x, snapped.y)
};
let Some(first) = curves.peek() else { for element in bezpath.elements() {
continue; match *element {
}; kurbo::PathEl::MoveTo(point) => path.move_to(snap_start(self, point)),
kurbo::PathEl::LineTo(point) => path.line_to(snap_center(self, point)),
let start_point = transform.transform_point2(point_to_dvec2(first.start())); kurbo::PathEl::QuadTo(a, b) => path.quad_to(snap_center(self, a), snap_center(self, b)),
let start_point = self.snap_to_physical_pixel(start_point); kurbo::PathEl::CurveTo(a, b, c) => path.curve_to(snap_center(self, a), snap_center(self, b), snap_center(self, c)),
path.move_to(kurbo::Point::new(start_point.x, start_point.y)); kurbo::PathEl::ClosePath => path.close_path(),
for curve in curves {
match curve {
PathSeg::Line(line) => {
let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = self.snap_to_physical_pixel_center(a);
path.line_to(kurbo::Point::new(a.x, a.y));
}
PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = self.snap_to_physical_pixel_center(a);
let b = self.snap_to_physical_pixel_center(b);
path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y));
}
PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = self.snap_to_physical_pixel_center(a);
let b = self.snap_to_physical_pixel_center(b);
let c = self.snap_to_physical_pixel_center(c);
path.curve_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y), kurbo::Point::new(c.x, c.y));
}
}
}
if subpath.closed() {
path.close_path();
} }
} }
@@ -1029,20 +1004,19 @@ impl OverlayContextInternal {
/// Used by the Select tool to outline a path or a free point when selected or hovered. /// Used by the Select tool to outline a path or a free point when selected or hovered.
fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) { fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
let mut subpaths: Vec<subpath::Subpath<PointId>> = vec![]; let mut combined = BezPath::new();
for target_type in target_types { for target_type in target_types {
match target_type.borrow() { match target_type.borrow() {
ClickTargetType::FreePoint(point) => { ClickTargetType::FreePoint(point) => {
self.manipulator_anchor(transform.transform_point2(point.position), false, None); self.manipulator_anchor(transform.transform_point2(point.position), false, None);
} }
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()), ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
} }
} }
if !subpaths.is_empty() { if !combined.is_empty() {
let path = self.push_path(subpaths.iter(), transform); let path = self.push_path(&combined, transform);
let color = color.unwrap_or(COLOR_OVERLAY_BLUE); let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path); self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path);
@@ -1051,15 +1025,15 @@ impl OverlayContextInternal {
/// Fills the area inside the path. Assumes `color` is in gamma space. /// Fills the area inside the path. Assumes `color` is in gamma space.
/// Used by the Pen tool to show the path being closed. /// Used by the Pen tool to show the path being closed.
fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) { fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
let path = self.push_path(subpaths, transform); let path = self.push_path(bezpath, transform);
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path); self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path);
} }
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string. /// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
/// Used by the fill tool to show the area to be filled. /// Used by the fill tool to show the area to be filled.
fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) { fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
const PATTERN_WIDTH: u32 = 4; const PATTERN_WIDTH: u32 = 4;
const PATTERN_HEIGHT: u32 = 4; const PATTERN_HEIGHT: u32 = 4;
@@ -1096,7 +1070,7 @@ impl OverlayContextInternal {
}, },
}; };
let path = self.push_path(subpaths, transform); let path = self.push_path(bezpath, transform);
let brush = peniko::Brush::Image(image); let brush = peniko::Brush::Image(image);
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path); self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);

View File

@@ -13,11 +13,10 @@ use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU}; use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2}; use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
use graphene_std::vector::{PointId, SegmentId, Vector}; use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg}; use kurbo::{self, Affine, BezPath, CubicBez, ParamCurve, PathSeg};
use std::collections::HashMap; use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
@@ -931,50 +930,33 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
fn push_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) { fn push_path(&mut self, bezpath: &BezPath, transform: DAffine2) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();
self.render_context.begin_path(); self.render_context.begin_path();
for subpath in subpaths {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
let Some(&first) = curves.peek() else { let snap_start = |context: &Self, point: kurbo::Point| context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
continue; let snap_center = |context: &Self, point: kurbo::Point| context.snap_to_physical_pixel_center(transform.transform_point2(point_to_dvec2(point)));
};
let start_point = transform.transform_point2(point_to_dvec2(first.start())); for element in bezpath.elements() {
let start_point = self.snap_to_physical_pixel(start_point); match *element {
self.render_context.move_to(start_point.x, start_point.y); kurbo::PathEl::MoveTo(point) => {
let point = snap_start(self, point);
for curve in curves { self.render_context.move_to(point.x, point.y);
match curve {
PathSeg::Line(line) => {
let a = transform.transform_point2(point_to_dvec2(line.p1));
let a = self.snap_to_physical_pixel_center(a);
self.render_context.line_to(a.x, a.y);
}
PathSeg::Quad(quad_bez) => {
let a = transform.transform_point2(point_to_dvec2(quad_bez.p1));
let b = transform.transform_point2(point_to_dvec2(quad_bez.p2));
let a = self.snap_to_physical_pixel_center(a);
let b = self.snap_to_physical_pixel_center(b);
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
}
PathSeg::Cubic(cubic_bez) => {
let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1));
let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2));
let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3));
let a = self.snap_to_physical_pixel_center(a);
let b = self.snap_to_physical_pixel_center(b);
let c = self.snap_to_physical_pixel_center(c);
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
}
} }
} kurbo::PathEl::LineTo(point) => {
let point = snap_center(self, point);
if subpath.closed() { self.render_context.line_to(point.x, point.y);
self.render_context.close_path(); }
kurbo::PathEl::QuadTo(a, b) => {
let (a, b) = (snap_center(self, a), snap_center(self, b));
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y);
}
kurbo::PathEl::CurveTo(a, b, c) => {
let (a, b, c) = (snap_center(self, a), snap_center(self, b), snap_center(self, c));
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y);
}
kurbo::PathEl::ClosePath => self.render_context.close_path(),
} }
} }
@@ -983,18 +965,17 @@ impl OverlayContext {
/// Used by the Select tool to outline a path or a free point when selected or hovered. /// Used by the Select tool to outline a path or a free point when selected or hovered.
pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) { pub fn outline(&mut self, target_types: impl Iterator<Item = impl Borrow<ClickTargetType>>, transform: DAffine2, color: Option<&str>) {
let mut subpaths: Vec<Subpath<PointId>> = vec![]; let mut combined = BezPath::new();
target_types.for_each(|target_type| match target_type.borrow() { target_types.for_each(|target_type| match target_type.borrow() {
ClickTargetType::FreePoint(point) => { ClickTargetType::FreePoint(point) => {
self.manipulator_anchor(transform.transform_point2(point.position), false, None); self.manipulator_anchor(transform.transform_point2(point.position), false, None);
} }
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()), ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
}); });
if !subpaths.is_empty() { if !combined.is_empty() {
self.push_path(subpaths.iter(), transform); self.push_path(&combined, transform);
let color = color.unwrap_or(COLOR_OVERLAY_BLUE); let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
self.render_context.set_stroke_style_str(color); self.render_context.set_stroke_style_str(color);
@@ -1005,8 +986,8 @@ impl OverlayContext {
/// Fills the area inside the path. Assumes `color` is in gamma space. /// Fills the area inside the path. Assumes `color` is in gamma space.
/// Used by the Pen tool to show the path being closed. /// Used by the Pen tool to show the path being closed.
pub fn fill_path(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) { pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
self.push_path(subpaths, transform); self.push_path(bezpath, transform);
self.render_context.set_fill_style_str(color); self.render_context.set_fill_style_str(color);
self.render_context.fill(); self.render_context.fill();
@@ -1014,7 +995,7 @@ impl OverlayContext {
/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string. /// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
/// Used by the fill tool to show the area to be filled. /// Used by the fill tool to show the area to be filled.
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) { pub fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
const PATTERN_WIDTH: usize = 4; const PATTERN_WIDTH: usize = 4;
const PATTERN_HEIGHT: usize = 4; const PATTERN_HEIGHT: usize = 4;
@@ -1047,7 +1028,7 @@ impl OverlayContext {
pattern_context.put_image_data(&image_data, 0, 0).unwrap(); pattern_context.put_image_data(&image_data, 0, 0).unwrap();
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();
self.push_path(subpaths, transform); self.push_path(bezpath, transform);
self.render_context.set_fill_style_canvas_pattern(&pattern); self.render_context.set_fill_style_canvas_pattern(&pattern);
self.render_context.fill(); self.render_context.fill();

View File

@@ -8,10 +8,10 @@ use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Appearance; use graphene_std::Appearance;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::Vector;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, Vector}; use kurbo::{Affine, BezPath};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64; use std::num::NonZeroU64;
use std::sync::Arc; use std::sync::Arc;
@@ -188,11 +188,13 @@ impl DocumentMetadata {
self.visual_targets(layer)? self.visual_targets(layer)?
.iter() .iter()
.filter_map(|click_target| match click_target.target_type() { .filter_map(|click_target| match click_target.target_type() {
ClickTargetType::Subpath(subpath) => subpath.loose_bounding_box_with_transform(transform), ClickTargetType::Path(path) => {
ClickTargetType::CompoundPath(subpaths) => subpaths let mut transformed = path.clone();
.iter() transformed.apply_affine(Affine::new(transform.to_cols_array()));
.filter_map(|subpath| subpath.loose_bounding_box_with_transform(transform))
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)]), let control_box = transformed.control_box();
(!transformed.is_empty()).then(|| [DVec2::new(control_box.min_x(), control_box.min_y()), DVec2::new(control_box.max_x(), control_box.max_y())])
}
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform), ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
}) })
.reduce(Quad::combine_bounds) .reduce(Quad::combine_bounds)
@@ -251,11 +253,10 @@ impl DocumentMetadata {
self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds) self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds)
} }
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> { pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &BezPath> {
self.visual_targets(layer).unwrap_or(&[]).iter().flat_map(|target| match target.target_type() { self.visual_targets(layer).unwrap_or(&[]).iter().filter_map(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => std::slice::from_ref(subpath), ClickTargetType::Path(path) => Some(path),
ClickTargetType::CompoundPath(subpaths) => subpaths.as_slice(), ClickTargetType::FreePoint(_) => None,
ClickTargetType::FreePoint(_) => &[],
}) })
} }

View File

@@ -43,10 +43,9 @@ use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, No
use graphene_std::Appearance; use graphene_std::Appearance;
use graphene_std::ContextDependencies; use graphene_std::ContextDependencies;
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType, FreePoint}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType, FreePoint};
use graphene_std::vector::{PointId, Vector, VectorModificationType}; use graphene_std::vector::{Vector, VectorModificationType};
use kurbo::BezPath; use kurbo::BezPath;
use memo_network::MemoNetwork; use memo_network::MemoNetwork;
use serde_json::{Value, json}; use serde_json::{Value, json};

View File

@@ -397,12 +397,12 @@ impl NodeNetworkInterface {
if *import_index == 0 { if *import_index == 0 {
let remove_import_center = reorder_import_center + DVec2::new(-4., 0.); let remove_import_center = reorder_import_center + DVec2::new(-4., 0.);
let remove_import = ClickTarget::new_with_subpath(Subpath::new_rectangle(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.); let remove_import = ClickTarget::new_with_path(rectangle_path(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.);
remove_imports_exports.insert_custom_output_port(*import_index, remove_import); remove_imports_exports.insert_custom_output_port(*import_index, remove_import);
} else { } else {
let remove_import_center = reorder_import_center + DVec2::new(-12., 0.); let remove_import_center = reorder_import_center + DVec2::new(-12., 0.);
let reorder_import = ClickTarget::new_with_subpath(Subpath::new_rectangle(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.); let reorder_import = ClickTarget::new_with_path(rectangle_path(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.);
let remove_import = ClickTarget::new_with_subpath(Subpath::new_rectangle(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.); let remove_import = ClickTarget::new_with_path(rectangle_path(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.);
reorder_imports_exports.insert_custom_output_port(*import_index, reorder_import); reorder_imports_exports.insert_custom_output_port(*import_index, reorder_import);
remove_imports_exports.insert_custom_output_port(*import_index, remove_import); remove_imports_exports.insert_custom_output_port(*import_index, remove_import);
} }
@@ -417,12 +417,12 @@ impl NodeNetworkInterface {
if *export_index == 0 { if *export_index == 0 {
let remove_export_center = reorder_export_center + DVec2::new(4., 0.); let remove_export_center = reorder_export_center + DVec2::new(4., 0.);
let remove_export = ClickTarget::new_with_subpath(Subpath::new_rectangle(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.); let remove_export = ClickTarget::new_with_path(rectangle_path(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.);
remove_imports_exports.insert_custom_input_port(*export_index, remove_export); remove_imports_exports.insert_custom_input_port(*export_index, remove_export);
} else { } else {
let remove_export_center = reorder_export_center + DVec2::new(12., 0.); let remove_export_center = reorder_export_center + DVec2::new(12., 0.);
let reorder_export = ClickTarget::new_with_subpath(Subpath::new_rectangle(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.); let reorder_export = ClickTarget::new_with_path(rectangle_path(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.);
let remove_export = ClickTarget::new_with_subpath(Subpath::new_rectangle(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.); let remove_export = ClickTarget::new_with_path(rectangle_path(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.);
reorder_imports_exports.insert_custom_input_port(*export_index, reorder_export); reorder_imports_exports.insert_custom_input_port(*export_index, reorder_export);
remove_imports_exports.insert_custom_input_port(*export_index, remove_export); remove_imports_exports.insert_custom_input_port(*export_index, remove_export);
} }
@@ -1001,8 +1001,8 @@ impl NodeNetworkInterface {
let node_click_target_bottom_right = node_click_target_top_left + DVec2::new(width as f64, height as f64); let node_click_target_bottom_right = node_click_target_top_left + DVec2::new(width as f64, height as f64);
let radius = 3.; let radius = 3.;
let subpath = Subpath::new_rounded_rectangle(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]); let path = rounded_rectangle_path(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); let node_click_target = ClickTarget::new_with_path(path, 0.);
DocumentNodeClickTargets { DocumentNodeClickTargets {
node_click_target, node_click_target,
@@ -1032,22 +1032,22 @@ impl NodeNetworkInterface {
// Update visibility button click target // Update visibility button click target
let visibility_offset = node_top_left + DVec2::new(width as f64, LAYER_VERTICAL_CENTER); let visibility_offset = node_top_left + DVec2::new(width as f64, LAYER_VERTICAL_CENTER);
let subpath = Subpath::new_rounded_rectangle( let path = rounded_rectangle_path(
DVec2::new(-ICON_HALF_EXTENT, -ICON_HALF_EXTENT) + visibility_offset, DVec2::new(-ICON_HALF_EXTENT, -ICON_HALF_EXTENT) + visibility_offset,
DVec2::new(ICON_HALF_EXTENT, ICON_HALF_EXTENT) + visibility_offset, DVec2::new(ICON_HALF_EXTENT, ICON_HALF_EXTENT) + visibility_offset,
[3.; 4], [3.; 4],
); );
let visibility_click_target = ClickTarget::new_with_subpath(subpath, 0.); let visibility_click_target = ClickTarget::new_with_path(path, 0.);
// Update lock button click target, positioned one grid unit to the left of the visibility button (only when locked) // Update lock button click target, positioned one grid unit to the left of the visibility button (only when locked)
let lock_click_target = if locked { let lock_click_target = if locked {
let lock_offset = node_top_left + DVec2::new(width as f64 - GRID_SIZE as f64, LAYER_VERTICAL_CENTER); let lock_offset = node_top_left + DVec2::new(width as f64 - GRID_SIZE as f64, LAYER_VERTICAL_CENTER);
let subpath = Subpath::new_rounded_rectangle( let path = rounded_rectangle_path(
DVec2::new(-ICON_HALF_EXTENT, -ICON_HALF_EXTENT) + lock_offset, DVec2::new(-ICON_HALF_EXTENT, -ICON_HALF_EXTENT) + lock_offset,
DVec2::new(ICON_HALF_EXTENT, ICON_HALF_EXTENT) + lock_offset, DVec2::new(ICON_HALF_EXTENT, ICON_HALF_EXTENT) + lock_offset,
[3.; 4], [3.; 4],
); );
Some(ClickTarget::new_with_subpath(subpath, 0.)) Some(ClickTarget::new_with_path(path, 0.))
} else { } else {
None None
}; };
@@ -1057,12 +1057,12 @@ impl NodeNetworkInterface {
const GRIP_WIDTH: f64 = 8.; const GRIP_WIDTH: f64 = 8.;
let icons_width = if locked { GRID_SIZE as f64 } else { 0. }; let icons_width = if locked { GRID_SIZE as f64 } else { 0. };
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - ICON_HALF_EXTENT - icons_width, LAYER_VERTICAL_CENTER); let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - ICON_HALF_EXTENT - icons_width, LAYER_VERTICAL_CENTER);
let subpath = Subpath::new_rounded_rectangle( let path = rounded_rectangle_path(
DVec2::new(-GRIP_WIDTH, -ICON_HALF_EXTENT) + grip_offset_right_edge, DVec2::new(-GRIP_WIDTH, -ICON_HALF_EXTENT) + grip_offset_right_edge,
DVec2::new(0., ICON_HALF_EXTENT) + grip_offset_right_edge, DVec2::new(0., ICON_HALF_EXTENT) + grip_offset_right_edge,
[0.; 4], [0.; 4],
); );
let grip_click_target = ClickTarget::new_with_subpath(subpath, 0.); let grip_click_target = ClickTarget::new_with_path(path, 0.);
// Update display-name text click target, used to detect double-click rename. Sized to the text bounds // Update display-name text click target, used to detect double-click rename. Sized to the text bounds
// (not the surrounding `.details` area) so the rest of the layer still drills into the subgraph on double-click. // (not the surrounding `.details` area) so the rest of the layer still drills into the subgraph on double-click.
@@ -1091,8 +1091,8 @@ impl NodeNetworkInterface {
// The 1-grid-tall name strip is centered vertically in the 2-grid-tall layer. // The 1-grid-tall name strip is centered vertically in the 2-grid-tall layer.
let name_top = node_top_left.y + HALF_GRID_SIZE as f64; let name_top = node_top_left.y + HALF_GRID_SIZE as f64;
let name_bottom = node_top_left.y + GRID_SIZE as f64 + HALF_GRID_SIZE as f64; let name_bottom = node_top_left.y + GRID_SIZE as f64 + HALF_GRID_SIZE as f64;
let subpath = Subpath::new_rounded_rectangle(DVec2::new(name_left, name_top), DVec2::new(name_right, name_bottom), [3.; 4]); let path = rounded_rectangle_path(DVec2::new(name_left, name_top), DVec2::new(name_right, name_bottom), [3.; 4]);
Some(ClickTarget::new_with_subpath(subpath, 0.)) Some(ClickTarget::new_with_path(path, 0.))
} else { } else {
None None
} }
@@ -1104,8 +1104,8 @@ impl NodeNetworkInterface {
let node_bottom_right = node_top_left + DVec2::new(width as f64, height as f64); let node_bottom_right = node_top_left + DVec2::new(width as f64, height as f64);
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * GRID_SIZE) as f64, 0.); let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * GRID_SIZE) as f64, 0.);
const CORNER_RADIUS: f64 = 10.; const CORNER_RADIUS: f64 = 10.;
let subpath = Subpath::new_rounded_rectangle(chain_top_left, node_bottom_right, [CORNER_RADIUS; 4]); let path = rounded_rectangle_path(chain_top_left, node_bottom_right, [CORNER_RADIUS; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); let node_click_target = ClickTarget::new_with_path(path, 0.);
DocumentNodeClickTargets { DocumentNodeClickTargets {
node_click_target, node_click_target,

View File

@@ -33,8 +33,8 @@ impl NodeNetworkInterface {
let nodes = network_metadata.persistent_metadata.node_metadata.keys().copied().collect::<Vec<_>>(); let nodes = network_metadata.persistent_metadata.node_metadata.keys().copied().collect::<Vec<_>>();
self.with_import_export_ports(network_path, |import_export_click_targets| { self.with_import_export_ports(network_path, |import_export_click_targets| {
for port in import_export_click_targets.click_targets() { for port in import_export_click_targets.click_targets() {
if let ClickTargetType::Subpath(subpath) = port.target_type() { if let ClickTargetType::Path(path) = port.target_type() {
connector_click_targets.push(subpath.to_bezpath().to_svg()); connector_click_targets.push(path.to_svg());
} }
} }
}); });
@@ -42,29 +42,29 @@ impl NodeNetworkInterface {
self.with_node_click_targets(&node_id, network_path, |node_click_targets| { self.with_node_click_targets(&node_id, network_path, |node_click_targets| {
let mut node_path = String::new(); let mut node_path = String::new();
if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() { if let ClickTargetType::Path(path) = node_click_targets.node_click_target.target_type() {
node_path.push_str(subpath.to_bezpath().to_svg().as_str()) node_path.push_str(path.to_svg().as_str())
} }
all_node_click_targets.push((node_id, node_path)); all_node_click_targets.push((node_id, node_path));
for port in node_click_targets.port_click_targets.click_targets() { for port in node_click_targets.port_click_targets.click_targets() {
if let ClickTargetType::Subpath(subpath) = port.target_type() { if let ClickTargetType::Path(path) = port.target_type() {
connector_click_targets.push(subpath.to_bezpath().to_svg()); connector_click_targets.push(path.to_svg());
} }
} }
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata { if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
// Visibility button (eye icon) // Visibility button (eye icon)
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() { if let ClickTargetType::Path(path) = layer_metadata.visibility_click_target.target_type() {
icon_click_targets.push(subpath.to_bezpath().to_svg()); icon_click_targets.push(path.to_svg());
} }
// Lock button (padlock icon), only when the layer is locked // Lock button (padlock icon), only when the layer is locked
if let Some(lock_click_target) = &layer_metadata.lock_click_target if let Some(lock_click_target) = &layer_metadata.lock_click_target
&& let ClickTargetType::Subpath(subpath) = lock_click_target.target_type() && let ClickTargetType::Path(path) = lock_click_target.target_type()
{ {
icon_click_targets.push(subpath.to_bezpath().to_svg()); icon_click_targets.push(path.to_svg());
} }
// Drag grip (dotted symbol) // Drag grip (dotted symbol)
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() { if let ClickTargetType::Path(path) = layer_metadata.grip_click_target.target_type() {
icon_click_targets.push(subpath.to_bezpath().to_svg()); icon_click_targets.push(path.to_svg());
} }
} }
}); });
@@ -80,8 +80,7 @@ impl NodeNetworkInterface {
}); });
let bounds = self.all_nodes_bounding_box(network_path).unwrap_or([DVec2::ZERO, DVec2::ZERO]); let bounds = self.all_nodes_bounding_box(network_path).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let rect = Subpath::<PointId>::new_rectangle(bounds[0], bounds[1]); let all_nodes_bounding_box = rectangle_path(bounds[0], bounds[1]).to_svg();
let all_nodes_bounding_box = rect.to_bezpath().to_svg();
let mut modify_import_export = Vec::new(); let mut modify_import_export = Vec::new();
self.with_modify_import_export(network_path, |modify_import_export_click_targets| { self.with_modify_import_export(network_path, |modify_import_export_click_targets| {
@@ -90,8 +89,8 @@ impl NodeNetworkInterface {
.click_targets() .click_targets()
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets()) .chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
{ {
if let ClickTargetType::Subpath(subpath) = click_target.target_type() { if let ClickTargetType::Path(path) = click_target.target_type() {
modify_import_export.push(subpath.to_bezpath().to_svg()); modify_import_export.push(path.to_svg());
} }
} }
}); });
@@ -342,8 +341,8 @@ impl NodeNetworkInterface {
return None; return None;
}; };
let bounding_box_subpath = Subpath::<PointId>::new_rectangle(bounds[0], bounds[1]); let node_graph_to_viewport = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport;
bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport) Some((node_graph_to_viewport * Quad::from_box(bounds)).bounding_box())
} }
pub fn collect_layer_widths(&self, network_path: &[NodeId]) -> (HashMap<NodeId, u32>, HashMap<NodeId, u32>, HashMap<NodeId, bool>) { pub fn collect_layer_widths(&self, network_path: &[NodeId]) -> (HashMap<NodeId, u32>, HashMap<NodeId, u32>, HashMap<NodeId, bool>) {

View File

@@ -38,9 +38,12 @@ impl NodeNetworkInterface {
let vector = self.upstream_path_node_vector(layer)?; let vector = self.upstream_path_node_vector(layer)?;
let mut targets = Vec::new(); let mut targets = Vec::new();
let subpaths: Vec<Subpath<PointId>> = vector.stroke_bezier_paths().collect(); let mut combined = BezPath::new();
if !subpaths.is_empty() { for subpath in vector.stroke_bezier_paths() {
targets.push(ClickTargetType::CompoundPath(subpaths)); combined.extend(subpath.to_bezpath().elements().iter().copied());
}
if !combined.is_empty() {
targets.push(ClickTargetType::Path(combined));
} }
for &point_id in vector.point_domain.ids() { for &point_id in vector.point_domain.ids() {

View File

@@ -1,5 +1,29 @@
use super::*; use super::*;
use graphene_std::ParameterRef; use graphene_std::ParameterRef;
use graphene_std::vector::misc::dvec2_to_point;
use kurbo::{DEFAULT_ACCURACY, Shape};
/// Axis-aligned rectangle spanning the two opposite corners, used for node graph chrome click targets.
pub fn rectangle_path(corner1: DVec2, corner2: DVec2) -> BezPath {
kurbo::Rect::from_points(dvec2_to_point(corner1), dvec2_to_point(corner2)).to_path(DEFAULT_ACCURACY)
}
/// Same as [`rectangle_path`] but with per-corner radii ordered top left, top right, bottom right, bottom left.
pub fn rounded_rectangle_path(corner1: DVec2, corner2: DVec2, radii: [f64; 4]) -> BezPath {
let rect = kurbo::Rect::from_points(dvec2_to_point(corner1), dvec2_to_point(corner2));
if radii.iter().all(|radius| *radius == 0.) {
return rect.to_path(DEFAULT_ACCURACY);
}
let [top_left, top_right, bottom_right, bottom_left] = radii;
kurbo::RoundedRect::from_rect(rect, kurbo::RoundedRectRadii::new(top_left, top_right, bottom_right, bottom_left)).to_path(DEFAULT_ACCURACY)
}
/// Ellipse inscribed in the box spanning the two opposite corners.
pub fn ellipse_path(corner1: DVec2, corner2: DVec2) -> BezPath {
let rect = kurbo::Rect::from_points(dvec2_to_point(corner1), dvec2_to_point(corner2));
kurbo::Ellipse::new(rect.center(), (rect.width() / 2., rect.height() / 2.), 0.).to_path(DEFAULT_ACCURACY)
}
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum FlowType { pub enum FlowType {
@@ -227,8 +251,8 @@ impl Ports {
} }
pub(crate) fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) { pub(crate) fn insert_input_port_at_center(&mut self, input_index: usize, center: DVec2) {
let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.)); let path = ellipse_path(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
self.insert_custom_input_port(input_index, ClickTarget::new_with_subpath(subpath, 0.)); self.insert_custom_input_port(input_index, ClickTarget::new_with_path(path, 0.));
} }
pub(crate) fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) { pub(crate) fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) {
@@ -236,8 +260,8 @@ impl Ports {
} }
pub(crate) fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) { pub(crate) fn insert_output_port_at_center(&mut self, output_index: usize, center: DVec2) {
let subpath = Subpath::new_ellipse(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.)); let path = ellipse_path(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
self.insert_custom_output_port(output_index, ClickTarget::new_with_subpath(subpath, 0.)); self.insert_custom_output_port(output_index, ClickTarget::new_with_path(path, 0.));
} }
pub(crate) fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) { pub(crate) fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) {

View File

@@ -17,6 +17,7 @@ use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput; use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point}; use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
use kurbo::{BezPath, PathEl, Shape}; use kurbo::{BezPath, PathEl, Shape};
@@ -445,9 +446,11 @@ pub fn star_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessa
let diameter: f64 = radius1 * 2.; let diameter: f64 = radius1 * 2.;
let inner_diameter = radius2 * 2.; let inner_diameter = radius2 * 2.;
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))]; let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(
Subpath::<PointId>::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter).to_bezpath(),
)];
overlay_context.outline(subpath.iter(), viewport, None); overlay_context.outline(targets.iter(), viewport, None);
} }
/// Outlines the geometric shape made by polygon-node /// Outlines the geometric shape made by polygon-node
@@ -462,9 +465,9 @@ pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMe
let points = sides as u64; let points = sides as u64;
let radius: f64 = radius * 2.; let radius: f64 = radius * 2.;
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))]; let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(Subpath::<PointId>::new_regular_polygon(DVec2::splat(-radius), points, radius).to_bezpath())];
overlay_context.outline(subpath.iter(), viewport, None); overlay_context.outline(targets.iter(), viewport, None);
} }
/// Outlines the geometric shape made by an Arc node /// Outlines the geometric shape made by an Arc node
@@ -475,15 +478,11 @@ pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessag
return; return;
}; };
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_arc( let arc = Subpath::<PointId>::new_arc(radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, arc_type);
radius, let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(arc.to_bezpath())];
start_angle / 360. * std::f64::consts::TAU,
sweep_angle / 360. * std::f64::consts::TAU,
arc_type,
))];
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
overlay_context.outline(subpath.iter(), viewport, None); overlay_context.outline(targets.iter(), viewport, None);
} }
/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications /// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications

View File

@@ -13,7 +13,7 @@ use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_po
use graphene_std::vector::algorithms::intersection::filtered_segment_intersections; use graphene_std::vector::algorithms::intersection::filtered_segment_intersections;
use graphene_std::vector::misc::dvec2_to_point; use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::misc::point_to_dvec2; use graphene_std::vector::misc::point_to_dvec2;
use kurbo::{Affine, ParamCurve, PathSeg}; use kurbo::{Affine, BezPath, ParamCurve, PathEl, PathSeg};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct LayerSnapper { pub struct LayerSnapper {
@@ -74,26 +74,20 @@ impl LayerSnapper {
} }
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) { if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) {
let mut push_candidates = |subpath: &Subpath<PointId>, transform: DAffine2| { // Post-solidified outline (the layer's recorded geometry). Its anchors carry no point IDs,
for (start_index, curve) in subpath.iter().enumerate() { // so while this layer's manipulators are being dragged the whole outline is skipped instead of filtering per manipulator.
let document_curve = Affine::new(transform.to_cols_array()) * curve; if !snap_data.ignore_bounds(layer) {
let start = subpath.manipulator_groups()[start_index].id; for bezpath in document.metadata().layer_outline(layer) {
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) { for curve in bezpath.segments() {
continue; self.paths_to_snap.push(SnapCandidatePath {
document_curve: Affine::new(transform.to_cols_array()) * curve,
layer,
start: PointId::new(),
target: SnapTarget::Path(PathSnapTarget::AlongPath),
bounds: None,
});
} }
self.paths_to_snap.push(SnapCandidatePath {
document_curve,
layer,
start,
target: SnapTarget::Path(PathSnapTarget::AlongPath),
bounds: None,
});
} }
};
// Post-solidified outline (the layer's recorded geometry)
for subpath in document.metadata().layer_outline(layer) {
push_candidates(subpath, transform);
} }
// Pre-solidified centerline (the Path tool's view) when an upstream Path node exists, // Pre-solidified centerline (the Path tool's view) when an upstream Path node exists,
@@ -102,7 +96,20 @@ impl LayerSnapper {
let path_aware_transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface); let path_aware_transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
if path_aware_transform.is_finite() { if path_aware_transform.is_finite() {
for subpath in vector.stroke_bezier_paths() { for subpath in vector.stroke_bezier_paths() {
push_candidates(&subpath, path_aware_transform); for (start_index, curve) in subpath.iter().enumerate() {
let start = subpath.manipulator_groups()[start_index].id;
let end = subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id;
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, end) {
continue;
}
self.paths_to_snap.push(SnapCandidatePath {
document_curve: Affine::new(path_aware_transform.to_cols_array()) * curve,
layer,
start,
target: SnapTarget::Path(PathSnapTarget::AlongPath),
bounds: None,
});
}
} }
} }
} }
@@ -608,6 +615,96 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
} }
} }
fn bezpath_anchor_snap_points(layer: LayerNodeIdentifier, bezpath: &BezPath, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
let document = snap_data.document;
// Split the path into contours so endpoint and wraparound handling stays per-subpath
let mut contours = Vec::new();
let mut current = BezPath::new();
for element in bezpath.elements() {
if matches!(element, PathEl::MoveTo(_)) && !current.elements().is_empty() {
contours.push(std::mem::take(&mut current));
}
current.push(*element);
}
if !current.elements().is_empty() {
contours.push(current);
}
for contour in contours {
let closed = matches!(contour.elements().last(), Some(PathEl::ClosePath));
let segments: Vec<PathSeg> = contour.segments().collect();
if segments.is_empty() {
continue;
}
// Midpoints of linear segments
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::LineMidpoint)) {
for &segment in &segments {
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
return;
}
let curve = pathseg_points(segment);
let in_handle = curve.p1.map(|handle| handle - curve.p0).filter(handle_not_under(to_document));
let out_handle = curve.p2.map(|handle| handle - curve.p3).filter(handle_not_under(to_document));
if in_handle.is_none() && out_handle.is_none() {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(curve.p0 * 0.5 + curve.p3 * 0.5),
SnapSource::Path(PathSnapSource::LineMidpoint),
SnapTarget::Path(PathSnapTarget::LineMidpoint),
Some(layer),
));
}
}
}
// Anchors
let anchor_count = segments.len() + if closed { 0 } else { 1 };
for index in 0..anchor_count {
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
return;
}
let anchor = if index < segments.len() {
pathseg_points(segments[index]).p0
} else {
pathseg_points(segments[index - 1]).p3
};
let in_handle = if index > 0 {
pathseg_points(segments[index - 1]).p2
} else if closed {
pathseg_points(segments[segments.len() - 1]).p2
} else {
None
};
let out_handle = if index < segments.len() { pathseg_points(segments[index]).p1 } else { None };
let handle_in = in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
let handle_out = out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));
let anchor_is_endpoint = !closed && (index == 0 || index == anchor_count - 1);
let colinear = !anchor_is_endpoint && handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_to(handle_out) < 1e-5));
if colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles)) {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(anchor),
SnapSource::Path(PathSnapSource::AnchorPointWithColinearHandles),
SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles),
Some(layer),
));
} else if !colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles)) {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(anchor),
SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles),
SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles),
Some(layer),
));
}
}
}
}
pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool { pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
let anchor = manipulators.anchor; let anchor = manipulators.anchor;
let handle_in = manipulators.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); let handle_in = manipulators.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
@@ -633,11 +730,11 @@ pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, p
get_layer_snap_points(child, snap_data, points); get_layer_snap_points(child, snap_data, points);
} }
} else { } else {
// Post-solidified outline (the layer's recorded geometry) // Post-solidified outline (the layer's recorded geometry), skipped wholesale while this layer's manipulators are being dragged since the outline carries no point IDs
if document.metadata().layer_outline(layer).next().is_some() { if !snap_data.ignore_bounds(layer) {
let to_document = document.metadata().transform_to_document(layer); let to_document = document.metadata().transform_to_document(layer);
for subpath in document.metadata().layer_outline(layer) { for bezpath in document.metadata().layer_outline(layer) {
subpath_anchor_snap_points(layer, subpath, snap_data, points, to_document); bezpath_anchor_snap_points(layer, bezpath, snap_data, points, to_document);
} }
} }

View File

@@ -6,9 +6,9 @@ use crate::messages::tool::common_functionality::color_selector::solid;
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_upstream_color_value_node_id, gradient_chain_target_input, replaceable_paint_chain}; use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_upstream_color_value_node_id, gradient_chain_target_input, replaceable_paint_chain};
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
use graphene_std::subpath::Subpath; use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::PointId;
use graphene_std::vector::style::FillChoice; use graphene_std::vector::style::FillChoice;
use kurbo::{BezPath, DEFAULT_ACCURACY, Rect, Shape};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct FillTool { pub struct FillTool {
@@ -150,9 +150,13 @@ impl Fsm for FillToolFsmState {
if paints_whole_expanse(layer, &document.network_interface) { if paints_whole_expanse(layer, &document.network_interface) {
let expanse = whole_expanse_rect(layer, document, overlay_context.viewport.size().into_dvec2()); let expanse = whole_expanse_rect(layer, document, overlay_context.viewport.size().into_dvec2());
overlay_context.fill_path_pattern(std::iter::once(expanse), DAffine2::IDENTITY, &color_hex); overlay_context.fill_path_pattern(&expanse, DAffine2::IDENTITY, &color_hex);
} else { } else {
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &color_hex); let mut outline = BezPath::new();
for path in document.metadata().layer_outline(layer) {
outline.extend(path.elements().iter().copied());
}
overlay_context.fill_path_pattern(&outline, document.metadata().transform_to_viewport(layer), &color_hex);
} }
} }
@@ -244,15 +248,16 @@ fn paints_whole_expanse(layer: LayerNodeIdentifier, network_interface: &NodeNetw
/// The viewport-space area a whole-expanse color paints: the artboard containing the layer, since the color fills it, /// The viewport-space area a whole-expanse color paints: the artboard containing the layer, since the color fills it,
/// or else the visible viewport for a layer living outside any artboard. /// or else the visible viewport for a layer living outside any artboard.
fn whole_expanse_rect(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport_size: DVec2) -> Subpath<PointId> { fn whole_expanse_rect(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport_size: DVec2) -> BezPath {
let containing_artboard = layer let containing_artboard = layer
.ancestors(document.metadata()) .ancestors(document.metadata())
.find(|&ancestor| ancestor != LayerNodeIdentifier::ROOT_PARENT && document.network_interface.is_artboard(&ancestor.to_node(), &[])); .find(|&ancestor| ancestor != LayerNodeIdentifier::ROOT_PARENT && document.network_interface.is_artboard(&ancestor.to_node(), &[]));
match containing_artboard.and_then(|artboard| document.metadata().bounding_box_viewport(artboard)) { let [min, max] = containing_artboard
Some([min, max]) => Subpath::new_rectangle(min, max), .and_then(|artboard| document.metadata().bounding_box_viewport(artboard))
None => Subpath::new_rectangle(DVec2::ZERO, viewport_size), .unwrap_or([DVec2::ZERO, viewport_size]);
}
Rect::from_points(dvec2_to_point(min), dvec2_to_point(max)).to_path(DEFAULT_ACCURACY)
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -22,7 +22,7 @@ use graphene_std::subpath::pathseg_points;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
use graphene_std::vector::style::FillChoice; use graphene_std::vector::style::FillChoice;
use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType}; use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType};
use kurbo::{CubicBez, PathSeg}; use kurbo::{BezPath, CubicBez, PathSeg};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct PenTool { pub struct PenTool {
@@ -1857,21 +1857,22 @@ impl Fsm for PenToolFsmState {
let grouped_segments = vector.auto_join_paths(); let grouped_segments = vector.auto_join_paths();
let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id));
let subpaths: Vec<_> = closed_paths let mut fill_region = BezPath::new();
.filter_map(|path| { for path in closed_paths {
let segments = path.edges.iter().filter_map(|edge| { let segments = path.edges.iter().filter_map(|edge| {
vector vector
.segment_domain .segment_domain
.iter() .iter()
.find(|(id, _, _, _)| id == &edge.id) .find(|(id, _, _, _)| id == &edge.id)
.map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) .map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) })
}); });
vector.subpath_from_segments_ignore_discontinuities(segments) if let Some(subpath) = vector.subpath_from_segments_ignore_discontinuities(segments) {
}) fill_region.extend(subpath.to_bezpath().elements().iter().copied());
.collect(); }
}
let fill_color = COLOR_OVERLAY_BLUE_05; let fill_color = COLOR_OVERLAY_BLUE_05;
overlay_context.fill_path(subpaths.iter(), transform, fill_color); overlay_context.fill_path(&fill_region, transform, fill_color);
} }
} }
} }

View File

@@ -26,9 +26,10 @@ use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{Gradient, GradientForm}; use graphic_types::vector_types::gradient::{Gradient, GradientForm};
use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::misc::dvec2_to_point;
use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
use graphic_types::{Appearance, Artboard, Cover, Coverage, FillAndStroke, Graphic, Vector}; use graphic_types::{Appearance, Artboard, Cover, Coverage, FillAndStroke, Graphic, Vector};
use kurbo::{Affine, BezPath, Cap, Join, Shape, StrokeOpts}; use kurbo::{Affine, BezPath, Cap, Join, PathEl, Shape, StrokeOpts};
use num_traits::Zero; use num_traits::Zero;
use skrifa::instance::{LocationRef, NormalizedCoord, Size}; use skrifa::instance::{LocationRef, NormalizedCoord, Size};
use skrifa::outline::{DrawSettings, OutlinePen}; use skrifa::outline::{DrawSettings, OutlinePen};
@@ -1361,8 +1362,9 @@ impl Render for List<Artboard> {
let element_id = layer_path.iter_element_values().next_back().copied(); let element_id = layer_path.iter_element_values().next_back().copied();
if let Some(element_id) = element_id { if let Some(element_id) = element_id {
let subpath = Subpath::new_rectangle(DVec2::ZERO, dimensions); metadata
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]); .click_targets
.insert(element_id, vec![ClickTarget::new_with_path(rectangle_path(DVec2::ZERO, dimensions), 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
metadata.local_transforms.insert(element_id, DAffine2::from_translation(location)); metadata.local_transforms.insert(element_id, DAffine2::from_translation(location));
if clip { if clip {
@@ -1381,8 +1383,7 @@ impl Render for List<Artboard> {
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) { fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>, _inherited_appearance: Option<&Appearance>) {
for index in 0..self.len() { for index in 0..self.len() {
let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index); let dimensions: DVec2 = self.attribute_cloned_or_default(ATTR_DIMENSIONS, index);
let subpath_rectangle = Subpath::new_rectangle(DVec2::ZERO, dimensions); click_targets.push(ClickTarget::new_with_path(rectangle_path(DVec2::ZERO, dimensions), 0.));
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
} }
} }
@@ -2210,20 +2211,20 @@ impl Render for List<Vector> {
} }
} }
/// Build one `CompoundPath` (non-zero fill rule, so holes like the inside of an "O" work /// Build one multi-contour `Path` (non-zero fill rule, so holes like the inside of an "O" work
/// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append. /// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append.
fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, appearance: Option<&Appearance>, geometry: &Vector, transform: DAffine2) { fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, appearance: Option<&Appearance>, geometry: &Vector, transform: DAffine2) {
// A coverage whose paint is `Graphic::None` exists but paints nothing, so it does not close subpaths for hit testing // A coverage whose paint is `Graphic::None` exists but paints nothing, so it does not close subpaths for hit testing
let filled = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill)); let filled = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill));
let mut subpaths: Vec<Subpath<_>> = geometry.stroke_bezier_paths().collect(); let mut bezpaths: Vec<BezPath> = geometry.stroke_bezpath_iter().filter(|bezpath| !bezpath.elements().is_empty()).collect();
let all_subpaths_closed = subpaths.iter().all(|subpath| subpath.closed()); let all_contours_closed = bezpaths.iter().all(|bezpath| matches!(bezpath.elements().last(), Some(PathEl::ClosePath)));
// Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side, // Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side,
// so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths. // so they need double the click inflation. Alignment is only honored by the renderer for fully-closed paths.
let stroke_width = appearance.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke)).map_or(0., |coverage| { let stroke_width = appearance.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke)).map_or(0., |coverage| {
let stroke = coverage.stroke_params(); let stroke = coverage.stroke_params();
if stroke.align.is_not_centered() && all_subpaths_closed { if stroke.align.is_not_centered() && all_contours_closed {
stroke.weight * 2. stroke.weight * 2.
} else { } else {
stroke.weight stroke.weight
@@ -2231,13 +2232,20 @@ fn extend_targets_from_vector(targets: &mut Vec<ClickTarget>, appearance: Option
}); });
if filled { if filled {
for subpath in &mut subpaths { for bezpath in &mut bezpaths {
subpath.set_closed(true); if !matches!(bezpath.elements().last(), Some(PathEl::ClosePath)) {
bezpath.close_path();
}
} }
} }
if !subpaths.is_empty() { if !bezpaths.is_empty() {
let mut click_target = ClickTarget::new_with_compound_path(subpaths, stroke_width); let mut combined_path = BezPath::new();
for bezpath in bezpaths {
combined_path.extend(bezpath);
}
let mut click_target = ClickTarget::new_with_path(combined_path, stroke_width);
click_target.apply_transform(transform); click_target.apply_transform(transform);
targets.push(click_target); targets.push(click_target);
} }
@@ -2412,9 +2420,9 @@ fn render_raster_cpu_item_to_vello(item: ItemRef<'_, Raster<CPU>>, scene: &mut S
/// plus the first item's transform and any merged-layers snapshot when a first item exists. /// plus the first item's transform and any merged-layers snapshot when a first item exists.
fn collect_raster_metadata<T>(first_row: Option<ItemRef<'_, T>>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) { fn collect_raster_metadata<T>(first_row: Option<ItemRef<'_, T>>, metadata: &mut RenderMetadata, footprint: Footprint, element_id: Option<NodeId>) {
let Some(element_id) = element_id else { return }; let Some(element_id) = element_id else { return };
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); metadata
.click_targets
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.).into()]); .insert(element_id, vec![ClickTarget::new_with_path(rectangle_path(DVec2::ZERO, DVec2::ONE), 0.).into()]);
metadata.upstream_footprints.insert(element_id, footprint); metadata.upstream_footprints.insert(element_id, footprint);
// TODO: Find a way to handle more than one item of the `List<Raster<...>>` // TODO: Find a way to handle more than one item of the `List<Raster<...>>`
if let Some(item) = first_row { if let Some(item) = first_row {
@@ -2437,10 +2445,10 @@ fn collect_raster_metadata<T>(first_row: Option<ItemRef<'_, T>>, metadata: &mut
/// Adds the unit-square click target every raster item presents, placed by the item's transform. /// Adds the unit-square click target every raster item presents, placed by the item's transform.
fn add_unit_square_click_target(transform: DAffine2, click_targets: &mut Vec<ClickTarget>) { fn add_unit_square_click_target(transform: DAffine2, click_targets: &mut Vec<ClickTarget>) {
// The unit square is the raster's own space, so its placement only exists in the item transform // The unit square is the raster's own space, so its placement only exists in the item transform
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let mut path = rectangle_path(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); path.apply_affine(Affine::new(transform.to_cols_array()));
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.)); click_targets.push(ClickTarget::new_with_path(path, 0.));
} }
impl Render for List<Raster<CPU>> { impl Render for List<Raster<CPU>> {
@@ -2630,11 +2638,28 @@ fn render_color_item_to_vello(item: ItemRef<'_, Color>, scene: &mut Scene, rende
} }
} }
/// The closed rectangular path spanning the two opposite corners, used for the box-shaped click targets.
fn rectangle_path(corner1: DVec2, corner2: DVec2) -> BezPath {
kurbo::Rect::from_points(dvec2_to_point(corner1), dvec2_to_point(corner2)).to_path(kurbo::DEFAULT_ACCURACY)
}
/// A gradient's control geometry in its local space: the unit circle a radial gradient's transform carries to its drawn ellipse, or the (0,0) to (1,0) gradient line for a linear one. /// A gradient's control geometry in its local space: the unit circle a radial gradient's transform carries to its drawn ellipse, or the (0,0) to (1,0) gradient line for a linear one.
fn gradient_control_outline(gradient_form: GradientForm) -> Subpath<graphic_types::vector_types::vector::PointId> { fn gradient_control_outline(gradient_form: GradientForm) -> BezPath {
match gradient_form { match gradient_form {
GradientForm::Linear => Subpath::new_line(DVec2::ZERO, DVec2::X), GradientForm::Linear => BezPath::from_path_segments(std::iter::once(kurbo::PathSeg::Line(kurbo::Line::new(dvec2_to_point(DVec2::ZERO), dvec2_to_point(DVec2::X))))),
GradientForm::Radial => Subpath::new_ellipse(DVec2::splat(-1.), DVec2::splat(1.)), GradientForm::Radial => {
// Four-cubic kappa circle with anchors on the axes, so the tight bounding box is exactly the unit square
// <https://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve#Using_four_curves>
const KAPPA: f64 = 4. / 3. * (std::f64::consts::SQRT_2 - 1.);
let mut path = BezPath::new();
path.move_to((1., 0.));
path.curve_to((1., KAPPA), (KAPPA, 1.), (0., 1.));
path.curve_to((-KAPPA, 1.), (-1., KAPPA), (-1., 0.));
path.curve_to((-1., -KAPPA), (-KAPPA, -1.), (0., -1.));
path.curve_to((KAPPA, -1.), (1., -KAPPA), (1., 0.));
path.close_path();
path
}
} }
} }
@@ -2867,7 +2892,7 @@ fn collect_gradient_items_metadata<'a>(items: impl Iterator<Item = ItemRef<'a, G
// The first item's transform is the reference all targets bake against // The first item's transform is the reference all targets bake against
let item_zero_inverse = *item_zero_inverse.get_or_insert_with(|| if transform_is_invertible(item_transform) { item_transform.inverse() } else { DAffine2::IDENTITY }); let item_zero_inverse = *item_zero_inverse.get_or_insert_with(|| if transform_is_invertible(item_transform) { item_transform.inverse() } else { DAffine2::IDENTITY });
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.); let mut target = ClickTarget::new_with_path(gradient_control_outline(gradient_form), 0.);
target.apply_transform(item_zero_inverse * item_transform); target.apply_transform(item_zero_inverse * item_transform);
let target = Arc::new(target); let target = Arc::new(target);
@@ -2895,7 +2920,7 @@ fn add_gradient_item_click_targets(item: ItemRef<'_, Gradient>, click_targets: &
} }
let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.); let mut target = ClickTarget::new_with_path(gradient_control_outline(gradient_form), 0.);
target.apply_transform(transform); target.apply_transform(transform);
click_targets.push(target); click_targets.push(target);
} }
@@ -2905,7 +2930,7 @@ fn add_gradient_item_outline_targets(item: ItemRef<'_, Gradient>, outlines: &mut
let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM); let gradient_form: GradientForm = item.attribute_cloned_or_default(ATTR_GRADIENT_FORM);
let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let mut target = ClickTarget::new_with_subpath(gradient_control_outline(gradient_form), 0.); let mut target = ClickTarget::new_with_path(gradient_control_outline(gradient_form), 0.);
target.apply_transform(transform); target.apply_transform(transform);
outlines.push(target); outlines.push(target);
} }
@@ -3273,8 +3298,7 @@ fn collect_text_items_metadata<'a>(items: impl Iterator<Item = ItemRef<'a, Strin
} }
let Some((size, item_transform)) = text_item_size_and_transform(item) else { continue }; let Some((size, item_transform)) = text_item_size_and_transform(item) else { continue };
let subpath = Subpath::new_rectangle(DVec2::ZERO, size); let mut target = ClickTarget::new_with_path(rectangle_path(DVec2::ZERO, size), 0.);
let mut target = ClickTarget::new_with_subpath(subpath, 0.);
target.apply_transform(item_zero_inverse * item_transform); target.apply_transform(item_zero_inverse * item_transform);
accumulated_click_targets.entry(element_id).or_default().push(Arc::new(target)); accumulated_click_targets.entry(element_id).or_default().push(Arc::new(target));
} }
@@ -3289,8 +3313,7 @@ fn collect_text_items_metadata<'a>(items: impl Iterator<Item = ItemRef<'a, Strin
/// Collects one text item's laid-out rectangle as a click target. /// Collects one text item's laid-out rectangle as a click target.
fn add_text_item_click_targets(item: ItemRef<'_, String>, click_targets: &mut Vec<ClickTarget>) { fn add_text_item_click_targets(item: ItemRef<'_, String>, click_targets: &mut Vec<ClickTarget>) {
let Some((size, transform)) = text_item_size_and_transform(item) else { return }; let Some((size, transform)) = text_item_size_and_transform(item) else { return };
let subpath = Subpath::new_rectangle(DVec2::ZERO, size); let mut target = ClickTarget::new_with_path(rectangle_path(DVec2::ZERO, size), 0.);
let mut target = ClickTarget::new_with_subpath(subpath, 0.);
target.apply_transform(transform); target.apply_transform(transform);
click_targets.push(target); click_targets.push(target);
} }

View File

@@ -3,16 +3,42 @@ use std::sync::{Arc, RwLock};
use super::algorithms::{bezpath_algorithms::bezpath_is_inside_bezpath, intersection::filtered_segment_intersections}; use super::algorithms::{bezpath_algorithms::bezpath_is_inside_bezpath, intersection::filtered_segment_intersections};
use super::misc::dvec2_to_point; use super::misc::dvec2_to_point;
use crate::math::QuadExt; use crate::math::QuadExt;
use crate::subpath::Subpath;
use crate::vector::PointId; use crate::vector::PointId;
use crate::vector::misc::point_to_dvec2;
use core_types::math::quad::Quad; use core_types::math::quad::Quad;
use core_types::transform::Transform; use core_types::transform::Transform;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use kurbo::{Affine, BezPath, ParamCurve, PathSeg, Shape}; use kurbo::{Affine, BezPath, ParamCurve, PathEl, PathSeg, Shape};
type BoundingBox = Option<[DVec2; 2]>; type BoundingBox = Option<[DVec2; 2]>;
/// Per-segment tight bounding box union of the transformed path, or None if the path has no segments.
fn bezpath_bounding_box_with_transform(bezpath: &BezPath, transform: DAffine2) -> BoundingBox {
let affine = Affine::new(transform.to_cols_array());
bezpath
.segments()
.map(|segment| (affine * segment).bounding_box())
.reduce(|a, b| a.union(b))
.map(|rect| [DVec2::new(rect.min_x(), rect.min_y()), DVec2::new(rect.max_x(), rect.max_y())])
}
/// The explicitly closed contours of the path, which together form its fillable region.
fn closed_contours(bezpath: &BezPath) -> BezPath {
let elements = bezpath.elements();
let mut kept = Vec::new();
let mut contour_start = 0;
for (index, element) in elements.iter().enumerate() {
if matches!(element, PathEl::MoveTo(_)) {
contour_start = index;
}
if matches!(element, PathEl::ClosePath) {
kept.extend_from_slice(&elements[contour_start..=index]);
}
}
BezPath::from_vec(kept)
}
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FreePoint { pub struct FreePoint {
@@ -33,11 +59,10 @@ impl FreePoint {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ClickTargetType { pub enum ClickTargetType {
Subpath(Subpath<PointId>), /// One or more contours tested as one compound shape using the non-zero fill rule, so holes
FreePoint(FreePoint),
/// Multiple subpaths tested as one compound shape using the non-zero fill rule, so holes
/// (e.g. the inside of an "O") correctly count as outside the fill. /// (e.g. the inside of an "O") correctly count as outside the fill.
CompoundPath(Vec<Subpath<PointId>>), Path(BezPath),
FreePoint(FreePoint),
} }
/// Fixed-size ring buffer cache for rotated bounding boxes. /// Fixed-size ring buffer cache for rotated bounding boxes.
@@ -93,9 +118,9 @@ impl BoundingBoxCache {
} }
/// Computes and caches bounding box for the given rotation, then applies scale/translation. /// Computes and caches bounding box for the given rotation, then applies scale/translation.
/// Returns the final transformed bounds. /// Returns the final transformed bounds.
fn add_to_cache(&mut self, subpath: &Subpath<PointId>, rotation: f64, scale: DVec2, translation: DVec2, fingerprint: u8) -> BoundingBox { fn add_to_cache(&mut self, bezpath: &BezPath, rotation: f64, scale: DVec2, translation: DVec2, fingerprint: u8) -> BoundingBox {
// Compute bounds for pure rotation (expensive operation we want to cache) // Compute bounds for pure rotation (expensive operation we want to cache)
let bounds = subpath.bounding_box_with_transform(DAffine2::from_angle(rotation)); let bounds = bezpath_bounding_box_with_transform(bezpath, DAffine2::from_angle(rotation));
if bounds.is_none() { if bounds.is_none() {
return bounds; return bounds;
@@ -137,23 +162,12 @@ impl PartialEq for ClickTarget {
} }
impl ClickTarget { impl ClickTarget {
pub fn new_with_subpath(subpath: Subpath<PointId>, stroke_width: f64) -> Self { pub fn new_with_path(path: BezPath, stroke_width: f64) -> Self {
let bounding_box = subpath.loose_bounding_box(); // The control-point hull serves as the loose bounding box
let control_box = path.control_box();
let bounding_box = (!path.elements().is_empty()).then(|| [DVec2::new(control_box.min_x(), control_box.min_y()), DVec2::new(control_box.max_x(), control_box.max_y())]);
Self { Self {
target_type: ClickTargetType::Subpath(subpath), target_type: ClickTargetType::Path(path),
stroke_width,
bounding_box,
bounding_box_cache: Default::default(),
}
}
pub fn new_with_compound_path(subpaths: Vec<Subpath<PointId>>, stroke_width: f64) -> Self {
let bounding_box = subpaths
.iter()
.filter_map(|subpath| subpath.loose_bounding_box())
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)]);
Self {
target_type: ClickTargetType::CompoundPath(subpaths),
stroke_width, stroke_width,
bounding_box, bounding_box,
bounding_box_cache: Default::default(), bounding_box_cache: Default::default(),
@@ -190,10 +204,10 @@ impl ClickTarget {
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> BoundingBox { pub fn bounding_box_with_transform(&self, transform: DAffine2) -> BoundingBox {
match self.target_type { match self.target_type {
ClickTargetType::Subpath(ref subpath) => { ClickTargetType::Path(ref path) => {
// Bypass cache for skewed transforms since rotation decomposition isn't valid // Bypass cache for skewed transforms since rotation decomposition isn't valid
if transform.has_skew() { if transform.has_skew() {
return subpath.bounding_box_with_transform(transform); return bezpath_bounding_box_with_transform(path, transform);
} }
// Decompose transform into rotation, scale, translation for caching strategy // Decompose transform into rotation, scale, translation for caching strategy
@@ -213,12 +227,8 @@ impl ClickTarget {
// Cache miss - compute and store new entry // Cache miss - compute and store new entry
let mut write_lock = self.bounding_box_cache.write().unwrap(); let mut write_lock = self.bounding_box_cache.write().unwrap();
write_lock.add_to_cache(subpath, rotation, scale, translation, fingerprint) write_lock.add_to_cache(path, rotation, scale, translation, fingerprint)
} }
ClickTargetType::CompoundPath(ref subpaths) => subpaths
.iter()
.filter_map(|subpath| subpath.bounding_box_with_transform(transform))
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)]),
// TODO: use point for calculation of bbox // TODO: use point for calculation of bbox
ClickTargetType::FreePoint(_) => self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)]), ClickTargetType::FreePoint(_) => self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)]),
} }
@@ -226,13 +236,8 @@ impl ClickTarget {
pub fn apply_transform(&mut self, affine_transform: DAffine2) { pub fn apply_transform(&mut self, affine_transform: DAffine2) {
match self.target_type { match self.target_type {
ClickTargetType::Subpath(ref mut subpath) => { ClickTargetType::Path(ref mut path) => {
subpath.apply_transform(affine_transform); path.apply_affine(Affine::new(affine_transform.to_cols_array()));
}
ClickTargetType::CompoundPath(ref mut subpaths) => {
for subpath in subpaths {
subpath.apply_transform(affine_transform);
}
} }
ClickTargetType::FreePoint(ref mut point) => { ClickTargetType::FreePoint(ref mut point) => {
point.apply_transform(affine_transform); point.apply_transform(affine_transform);
@@ -243,14 +248,8 @@ impl ClickTarget {
fn update_bbox(&mut self) { fn update_bbox(&mut self) {
match self.target_type { match self.target_type {
ClickTargetType::Subpath(ref subpath) => { ClickTargetType::Path(ref path) => {
self.bounding_box = subpath.bounding_box(); self.bounding_box = bezpath_bounding_box_with_transform(path, DAffine2::IDENTITY);
}
ClickTargetType::CompoundPath(ref subpaths) => {
self.bounding_box = subpaths
.iter()
.filter_map(|subpath| subpath.bounding_box())
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)]);
} }
ClickTargetType::FreePoint(ref point) => { ClickTargetType::FreePoint(ref point) => {
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]); self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
@@ -270,43 +269,26 @@ impl ClickTarget {
let mut bezier_iter = || bezier_iter().map(|bezier| Affine::new(inverse.to_cols_array()) * bezier); let mut bezier_iter = || bezier_iter().map(|bezier| Affine::new(inverse.to_cols_array()) * bezier);
match self.target_type() { match self.target_type() {
ClickTargetType::Subpath(subpath) => { ClickTargetType::Path(path) => {
// Check if outlines intersect
let outline_intersects = |path_segment: PathSeg| bezier_iter().any(|line| !filtered_segment_intersections(path_segment, line, None, None).is_empty());
if subpath.iter().any(outline_intersects) {
return true;
}
// Check if selection is entirely within the shape
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(point_to_dvec2(bezier.start()))) {
return true;
}
let mut selection = BezPath::from_path_segments(bezier_iter());
selection.close_path();
// Check if shape is entirely within selection
bezpath_is_inside_bezpath(&subpath.to_bezpath(), &selection, None, None)
}
ClickTargetType::CompoundPath(subpaths) => {
// Outline intersection (catches strokes and both filled/unfilled shapes) // Outline intersection (catches strokes and both filled/unfilled shapes)
let outline_intersects = |path_segment: PathSeg| bezier_iter().any(|line| !filtered_segment_intersections(path_segment, line, None, None).is_empty()); let outline_intersects = |path_segment: PathSeg| bezier_iter().any(|line| !filtered_segment_intersections(path_segment, line, None, None).is_empty());
if subpaths.iter().flat_map(|subpath| subpath.iter()).any(outline_intersects) { if path.segments().any(outline_intersects) {
return true; return true;
} }
// Selection point inside compound fill (non-zero rule). // Selection point inside the fill (non-zero rule).
// Only closed subpaths contribute to the fill region; open segments would otherwise produce spurious winding on one side of the segment. // Only closed contours contribute to the fill region; open segments would otherwise produce spurious winding on one side of the segment.
let combined: BezPath = subpaths.iter().filter(|subpath| subpath.closed()).flat_map(|subpath| subpath.to_bezpath()).collect(); let fill_region = closed_contours(path);
if !combined.is_empty() && bezier_iter().next().is_some_and(|bezier| combined.contains(bezier.start())) { if !fill_region.is_empty() && bezier_iter().next().is_some_and(|segment| fill_region.contains(segment.start())) {
return true; return true;
} }
// Build closed selection path, then check if all contours are entirely within it // Build closed selection path, then check if the whole shape is entirely within it
let mut selection = BezPath::from_path_segments(bezier_iter()); let mut selection = BezPath::from_path_segments(bezier_iter());
selection.close_path(); selection.close_path();
subpaths.iter().all(|subpath| bezpath_is_inside_bezpath(&subpath.to_bezpath(), &selection, None, None)) bezpath_is_inside_bezpath(path, &selection, None, None)
} }
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: PathSeg| bezier.winding(dvec2_to_point(point.position))).sum::<i32>() != 0, ClickTargetType::FreePoint(point) => bezier_iter().map(|segment: PathSeg| segment.winding(dvec2_to_point(point.position))).sum::<i32>() != 0,
} }
} }
@@ -337,11 +319,7 @@ impl ClickTarget {
{ {
// Check if the point is within the shape // Check if the point is within the shape
match self.target_type() { match self.target_type() {
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point), ClickTargetType::Path(path) => closed_contours(path).contains(dvec2_to_point(point)),
ClickTargetType::CompoundPath(subpaths) => {
let combined: BezPath = subpaths.iter().flat_map(|subpath| subpath.to_bezpath()).collect();
combined.contains(dvec2_to_point(point))
}
ClickTargetType::FreePoint(free_point) => free_point.position == point, ClickTargetType::FreePoint(free_point) => free_point.position == point,
} }
} else { } else {
@@ -353,10 +331,14 @@ impl ClickTarget {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::subpath::Subpath;
use glam::DVec2; use glam::DVec2;
use kurbo::{DEFAULT_ACCURACY, Rect};
use std::f64::consts::PI; use std::f64::consts::PI;
fn rectangle_path(corner1: DVec2, corner2: DVec2) -> BezPath {
Rect::new(corner1.x, corner1.y, corner2.x, corner2.y).to_path(DEFAULT_ACCURACY)
}
#[test] #[test]
fn test_bounding_box_cache_fingerprint_generation() { fn test_bounding_box_cache_fingerprint_generation() {
// Test that fingerprints have MSB set and use only 7 bits for data // Test that fingerprints have MSB set and use only 7 bits for data
@@ -386,8 +368,8 @@ mod tests {
fn test_bounding_box_cache_basic_operations() { fn test_bounding_box_cache_basic_operations() {
let mut cache = BoundingBoxCache::default(); let mut cache = BoundingBoxCache::default();
// Create a simple rectangle subpath for testing // Create a simple rectangle path for testing
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(100., 50.)); let path = rectangle_path(DVec2::ZERO, DVec2::new(100., 50.));
let rotation = PI / 4.; let rotation = PI / 4.;
let scale = DVec2::new(2., 2.); let scale = DVec2::new(2., 2.);
@@ -398,7 +380,7 @@ mod tests {
assert!(cache.try_read(rotation, scale, translation, fingerprint).is_none()); assert!(cache.try_read(rotation, scale, translation, fingerprint).is_none());
// Add to cache // Add to cache
let result = cache.add_to_cache(&subpath, rotation, scale, translation, fingerprint); let result = cache.add_to_cache(&path, rotation, scale, translation, fingerprint);
assert!(result.is_some()); assert!(result.is_some());
// Should now be able to read from cache // Should now be able to read from cache
@@ -410,7 +392,7 @@ mod tests {
#[test] #[test]
fn test_bounding_box_cache_ring_buffer_behavior() { fn test_bounding_box_cache_ring_buffer_behavior() {
let mut cache = BoundingBoxCache::default(); let mut cache = BoundingBoxCache::default();
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(10., 10.)); let path = rectangle_path(DVec2::ZERO, DVec2::new(10., 10.));
let scale = DVec2::ONE; let scale = DVec2::ONE;
let translation = DVec2::ZERO; let translation = DVec2::ZERO;
@@ -419,7 +401,7 @@ mod tests {
for rotation in &rotations { for rotation in &rotations {
let fingerprint = BoundingBoxCache::rotation_fingerprint(*rotation); let fingerprint = BoundingBoxCache::rotation_fingerprint(*rotation);
cache.add_to_cache(&subpath, *rotation, scale, translation, fingerprint); cache.add_to_cache(&path, *rotation, scale, translation, fingerprint);
} }
// First two entries should be overwritten (cache size is 8) // First two entries should be overwritten (cache size is 8)
@@ -435,8 +417,8 @@ mod tests {
#[test] #[test]
fn test_click_target_bounding_box_caching() { fn test_click_target_bounding_box_caching() {
// Create a click target with a simple rectangle // Create a click target with a simple rectangle
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(100., 50.)); let path = rectangle_path(DVec2::ZERO, DVec2::new(100., 50.));
let click_target = ClickTarget::new_with_subpath(subpath, 1.); let click_target = ClickTarget::new_with_path(path, 1.);
let rotation = PI / 6.; let rotation = PI / 6.;
let scale = DVec2::new(1.5, 1.5); let scale = DVec2::new(1.5, 1.5);
@@ -472,8 +454,8 @@ mod tests {
#[test] #[test]
fn test_click_target_skew_bypass_cache() { fn test_click_target_skew_bypass_cache() {
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(100., 50.)); let path = rectangle_path(DVec2::ZERO, DVec2::new(100., 50.));
let click_target = ClickTarget::new_with_subpath(subpath.clone(), 1.); let click_target = ClickTarget::new_with_path(path.clone(), 1.);
// Create a transform with skew (non-uniform scaling in different directions) // Create a transform with skew (non-uniform scaling in different directions)
let skew_transform = DAffine2::from_cols_array(&[2., 0.5, 0., 1., 10., 20.]); let skew_transform = DAffine2::from_cols_array(&[2., 0.5, 0., 1., 10., 20.]);
@@ -481,14 +463,14 @@ mod tests {
// Should bypass cache and compute directly // Should bypass cache and compute directly
let result = click_target.bounding_box_with_transform(skew_transform); let result = click_target.bounding_box_with_transform(skew_transform);
let expected = subpath.bounding_box_with_transform(skew_transform); let expected = bezpath_bounding_box_with_transform(&path, skew_transform);
assert_eq!(result, expected); assert_eq!(result, expected);
} }
#[test] #[test]
fn test_cache_fingerprint_collision_handling() { fn test_cache_fingerprint_collision_handling() {
let mut cache = BoundingBoxCache::default(); let mut cache = BoundingBoxCache::default();
let subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::new(10., 10.)); let path = rectangle_path(DVec2::ZERO, DVec2::new(10., 10.));
let scale = DVec2::ONE; let scale = DVec2::ONE;
let translation = DVec2::ZERO; let translation = DVec2::ZERO;
@@ -501,7 +483,7 @@ mod tests {
// If we found a collision, test that exact rotation matching still works // If we found a collision, test that exact rotation matching still works
if fp1 == fp2 && rotation1 != rotation2 { if fp1 == fp2 && rotation1 != rotation2 {
// Add first rotation // Add first rotation
cache.add_to_cache(&subpath, rotation1, scale, translation, fp1); cache.add_to_cache(&path, rotation1, scale, translation, fp1);
// Should find the exact rotation // Should find the exact rotation
assert!(cache.try_read(rotation1, scale, translation, fp1).is_some()); assert!(cache.try_read(rotation1, scale, translation, fp1).is_some());