mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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(_) => &[],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user