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

* Collapse ClickTargetType to a single multi-contour BezPath variant

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

View File

@@ -42,6 +42,7 @@ use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode;
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::misc::dvec2_to_point;
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_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| {
targets.iter().all(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => {
let mut subpath = subpath.clone();
subpath.apply_transform(layer_transform);
subpath.is_inside_subpath(&viewport_polygon, None, None)
ClickTargetType::Path(path) => {
let mut path = path.clone();
path.apply_affine(Affine::new(layer_transform.to_cols_array()));
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) => {
let mut point = *point;
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 {
let segments = click_targets
.filter_map(|target| {
if let ClickTargetType::Subpath(subpath) = target.target_type() {
Some(subpath.iter())
} else {
None
}
})
.filter_map(|target| if let ClickTargetType::Path(path) = target.target_type() { Some(path.segments()) } else { None })
.flatten()
.map(|bezier| Affine::new(transform.to_cols_array()) * bezier);
BezPath::from_path_segments(segments)

View File

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

View File

@@ -15,7 +15,6 @@ use glam::{DAffine2, DVec2};
use graphene_std::ATTR_TRANSFORM;
use graphene_std::list::List;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::text::{TextAlign, TextContext, TypesettingConfig};
use graphene_std::vector::click_target::ClickTargetType;
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.
/// 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) {
self.internal().fill_path(subpaths, transform, color);
pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
self.internal().fill_path(bezpath, transform, color);
}
/// 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.
pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
self.internal().fill_path_pattern(subpaths, transform, color);
pub fn fill_path_pattern(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
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]) {
@@ -978,49 +977,25 @@ impl OverlayContextInternal {
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();
for subpath in subpaths {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
let snap_start = |context: &Self, point: kurbo::Point| {
let snapped = context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
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 {
continue;
};
let start_point = transform.transform_point2(point_to_dvec2(first.start()));
let start_point = self.snap_to_physical_pixel(start_point);
path.move_to(kurbo::Point::new(start_point.x, start_point.y));
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();
for element in bezpath.elements() {
match *element {
kurbo::PathEl::MoveTo(point) => path.move_to(snap_start(self, point)),
kurbo::PathEl::LineTo(point) => path.line_to(snap_center(self, point)),
kurbo::PathEl::QuadTo(a, b) => path.quad_to(snap_center(self, a), snap_center(self, b)),
kurbo::PathEl::CurveTo(a, b, c) => path.curve_to(snap_center(self, a), snap_center(self, b), snap_center(self, c)),
kurbo::PathEl::ClosePath => 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.
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 {
match target_type.borrow() {
ClickTargetType::FreePoint(point) => {
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
}
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
}
}
if !subpaths.is_empty() {
let path = self.push_path(subpaths.iter(), transform);
if !combined.is_empty() {
let path = self.push_path(&combined, transform);
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
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.
/// 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) {
let path = self.push_path(subpaths, transform);
fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
let path = self.push_path(bezpath, transform);
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.
/// 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_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);
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);

View File

@@ -13,11 +13,10 @@ use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, PI, TAU};
use glam::{DAffine2, DVec2};
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2};
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 wasm_bindgen::{JsCast, JsValue};
use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d};
@@ -931,50 +930,33 @@ impl OverlayContext {
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.render_context.begin_path();
for subpath in subpaths {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
let Some(&first) = curves.peek() else {
continue;
};
let snap_start = |context: &Self, point: kurbo::Point| context.snap_to_physical_pixel(transform.transform_point2(point_to_dvec2(point)));
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()));
let start_point = self.snap_to_physical_pixel(start_point);
self.render_context.move_to(start_point.x, start_point.y);
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);
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);
}
for element in bezpath.elements() {
match *element {
kurbo::PathEl::MoveTo(point) => {
let point = snap_start(self, point);
self.render_context.move_to(point.x, point.y);
}
}
if subpath.closed() {
self.render_context.close_path();
kurbo::PathEl::LineTo(point) => {
let point = snap_center(self, point);
self.render_context.line_to(point.x, point.y);
}
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.
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() {
ClickTargetType::FreePoint(point) => {
self.manipulator_anchor(transform.transform_point2(point.position), false, None);
}
ClickTargetType::Subpath(subpath) => subpaths.push(subpath.clone()),
ClickTargetType::CompoundPath(compound) => subpaths.extend(compound.iter().cloned()),
ClickTargetType::Path(bezpath) => combined.extend(bezpath.elements().iter().copied()),
});
if !subpaths.is_empty() {
self.push_path(subpaths.iter(), transform);
if !combined.is_empty() {
self.push_path(&combined, transform);
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
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.
/// 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) {
self.push_path(subpaths, transform);
pub fn fill_path(&mut self, bezpath: &BezPath, transform: DAffine2, color: &str) {
self.push_path(bezpath, transform);
self.render_context.set_fill_style_str(color);
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.
/// 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_HEIGHT: usize = 4;
@@ -1047,7 +1028,7 @@ impl OverlayContext {
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();
self.push_path(subpaths, transform);
self.push_path(bezpath, transform);
self.render_context.set_fill_style_canvas_pattern(&pattern);
self.render_context.fill();

View File

@@ -8,10 +8,10 @@ use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::Appearance;
use graphene_std::math::quad::Quad;
use graphene_std::subpath;
use graphene_std::transform::Footprint;
use graphene_std::vector::Vector;
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::num::NonZeroU64;
use std::sync::Arc;
@@ -188,11 +188,13 @@ impl DocumentMetadata {
self.visual_targets(layer)?
.iter()
.filter_map(|click_target| match click_target.target_type() {
ClickTargetType::Subpath(subpath) => subpath.loose_bounding_box_with_transform(transform),
ClickTargetType::CompoundPath(subpaths) => subpaths
.iter()
.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)]),
ClickTargetType::Path(path) => {
let mut transformed = path.clone();
transformed.apply_affine(Affine::new(transform.to_cols_array()));
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),
})
.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)
}
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &subpath::Subpath<PointId>> {
self.visual_targets(layer).unwrap_or(&[]).iter().flat_map(|target| match target.target_type() {
ClickTargetType::Subpath(subpath) => std::slice::from_ref(subpath),
ClickTargetType::CompoundPath(subpaths) => subpaths.as_slice(),
ClickTargetType::FreePoint(_) => &[],
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &BezPath> {
self.visual_targets(layer).unwrap_or(&[]).iter().filter_map(|target| match target.target_type() {
ClickTargetType::Path(path) => Some(path),
ClickTargetType::FreePoint(_) => None,
})
}

View File

@@ -43,10 +43,9 @@ use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, No
use graphene_std::Appearance;
use graphene_std::ContextDependencies;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::transform::Footprint;
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 memo_network::MemoNetwork;
use serde_json::{Value, json};

View File

@@ -397,12 +397,12 @@ impl NodeNetworkInterface {
if *import_index == 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);
} else {
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 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 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_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);
remove_imports_exports.insert_custom_output_port(*import_index, remove_import);
}
@@ -417,12 +417,12 @@ impl NodeNetworkInterface {
if *export_index == 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);
} else {
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 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 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_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);
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 radius = 3.;
let subpath = Subpath::new_rounded_rectangle(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
let path = rounded_rectangle_path(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
let node_click_target = ClickTarget::new_with_path(path, 0.);
DocumentNodeClickTargets {
node_click_target,
@@ -1032,22 +1032,22 @@ impl NodeNetworkInterface {
// Update visibility button click target
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,
[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)
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 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,
[3.; 4],
);
Some(ClickTarget::new_with_subpath(subpath, 0.))
Some(ClickTarget::new_with_path(path, 0.))
} else {
None
};
@@ -1057,12 +1057,12 @@ impl NodeNetworkInterface {
const GRIP_WIDTH: f64 = 8.;
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 subpath = Subpath::new_rounded_rectangle(
let path = rounded_rectangle_path(
DVec2::new(-GRIP_WIDTH, -ICON_HALF_EXTENT) + grip_offset_right_edge,
DVec2::new(0., ICON_HALF_EXTENT) + grip_offset_right_edge,
[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
// (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.
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 subpath = Subpath::new_rounded_rectangle(DVec2::new(name_left, name_top), DVec2::new(name_right, name_bottom), [3.; 4]);
Some(ClickTarget::new_with_subpath(subpath, 0.))
let path = rounded_rectangle_path(DVec2::new(name_left, name_top), DVec2::new(name_right, name_bottom), [3.; 4]);
Some(ClickTarget::new_with_path(path, 0.))
} else {
None
}
@@ -1104,8 +1104,8 @@ impl NodeNetworkInterface {
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.);
const CORNER_RADIUS: f64 = 10.;
let subpath = Subpath::new_rounded_rectangle(chain_top_left, node_bottom_right, [CORNER_RADIUS; 4]);
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
let path = rounded_rectangle_path(chain_top_left, node_bottom_right, [CORNER_RADIUS; 4]);
let node_click_target = ClickTarget::new_with_path(path, 0.);
DocumentNodeClickTargets {
node_click_target,

View File

@@ -33,8 +33,8 @@ impl NodeNetworkInterface {
let nodes = network_metadata.persistent_metadata.node_metadata.keys().copied().collect::<Vec<_>>();
self.with_import_export_ports(network_path, |import_export_click_targets| {
for port in import_export_click_targets.click_targets() {
if let ClickTargetType::Subpath(subpath) = port.target_type() {
connector_click_targets.push(subpath.to_bezpath().to_svg());
if let ClickTargetType::Path(path) = port.target_type() {
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| {
let mut node_path = String::new();
if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() {
node_path.push_str(subpath.to_bezpath().to_svg().as_str())
if let ClickTargetType::Path(path) = node_click_targets.node_click_target.target_type() {
node_path.push_str(path.to_svg().as_str())
}
all_node_click_targets.push((node_id, node_path));
for port in node_click_targets.port_click_targets.click_targets() {
if let ClickTargetType::Subpath(subpath) = port.target_type() {
connector_click_targets.push(subpath.to_bezpath().to_svg());
if let ClickTargetType::Path(path) = port.target_type() {
connector_click_targets.push(path.to_svg());
}
}
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
// Visibility button (eye icon)
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
icon_click_targets.push(subpath.to_bezpath().to_svg());
if let ClickTargetType::Path(path) = layer_metadata.visibility_click_target.target_type() {
icon_click_targets.push(path.to_svg());
}
// Lock button (padlock icon), only when the layer is locked
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)
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
icon_click_targets.push(subpath.to_bezpath().to_svg());
if let ClickTargetType::Path(path) = layer_metadata.grip_click_target.target_type() {
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 rect = Subpath::<PointId>::new_rectangle(bounds[0], bounds[1]);
let all_nodes_bounding_box = rect.to_bezpath().to_svg();
let all_nodes_bounding_box = rectangle_path(bounds[0], bounds[1]).to_svg();
let mut modify_import_export = Vec::new();
self.with_modify_import_export(network_path, |modify_import_export_click_targets| {
@@ -90,8 +89,8 @@ impl NodeNetworkInterface {
.click_targets()
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
{
if let ClickTargetType::Subpath(subpath) = click_target.target_type() {
modify_import_export.push(subpath.to_bezpath().to_svg());
if let ClickTargetType::Path(path) = click_target.target_type() {
modify_import_export.push(path.to_svg());
}
}
});
@@ -342,8 +341,8 @@ impl NodeNetworkInterface {
return None;
};
let bounding_box_subpath = Subpath::<PointId>::new_rectangle(bounds[0], bounds[1]);
bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport)
let node_graph_to_viewport = 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>) {

View File

@@ -38,9 +38,12 @@ impl NodeNetworkInterface {
let vector = self.upstream_path_node_vector(layer)?;
let mut targets = Vec::new();
let subpaths: Vec<Subpath<PointId>> = vector.stroke_bezier_paths().collect();
if !subpaths.is_empty() {
targets.push(ClickTargetType::CompoundPath(subpaths));
let mut combined = BezPath::new();
for subpath in vector.stroke_bezier_paths() {
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() {

View File

@@ -1,5 +1,29 @@
use super::*;
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)]
pub enum FlowType {
@@ -227,8 +251,8 @@ impl Ports {
}
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.));
self.insert_custom_input_port(input_index, ClickTarget::new_with_subpath(subpath, 0.));
let path = ellipse_path(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
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) {
@@ -236,8 +260,8 @@ impl Ports {
}
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.));
self.insert_custom_output_port(output_index, ClickTarget::new_with_subpath(subpath, 0.));
let path = ellipse_path(center - DVec2::new(8., 8.), center + DVec2::new(8., 8.));
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) {

View File

@@ -17,6 +17,7 @@ use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
use kurbo::{BezPath, PathEl, Shape};
@@ -445,9 +446,11 @@ pub fn star_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessa
let diameter: f64 = radius1 * 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
@@ -462,9 +465,9 @@ pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMe
let points = sides as u64;
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
@@ -475,15 +478,11 @@ pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessag
return;
};
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::Subpath(Subpath::new_arc(
radius,
start_angle / 360. * std::f64::consts::TAU,
sweep_angle / 360. * std::f64::consts::TAU,
arc_type,
))];
let arc = Subpath::<PointId>::new_arc(radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, arc_type);
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(arc.to_bezpath())];
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

View File

@@ -13,7 +13,7 @@ use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_po
use graphene_std::vector::algorithms::intersection::filtered_segment_intersections;
use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::misc::point_to_dvec2;
use kurbo::{Affine, ParamCurve, PathSeg};
use kurbo::{Affine, BezPath, ParamCurve, PathEl, PathSeg};
#[derive(Clone, Debug, Default)]
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)) {
let mut push_candidates = |subpath: &Subpath<PointId>, transform: DAffine2| {
for (start_index, curve) in subpath.iter().enumerate() {
let document_curve = Affine::new(transform.to_cols_array()) * curve;
let start = subpath.manipulator_groups()[start_index].id;
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) {
continue;
// Post-solidified outline (the layer's recorded geometry). Its anchors carry no point IDs,
// so while this layer's manipulators are being dragged the whole outline is skipped instead of filtering per manipulator.
if !snap_data.ignore_bounds(layer) {
for bezpath in document.metadata().layer_outline(layer) {
for curve in bezpath.segments() {
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,
@@ -102,7 +96,20 @@ impl LayerSnapper {
let path_aware_transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
if path_aware_transform.is_finite() {
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 {
let anchor = manipulators.anchor;
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);
}
} else {
// Post-solidified outline (the layer's recorded geometry)
if document.metadata().layer_outline(layer).next().is_some() {
// 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 !snap_data.ignore_bounds(layer) {
let to_document = document.metadata().transform_to_document(layer);
for subpath in document.metadata().layer_outline(layer) {
subpath_anchor_snap_points(layer, subpath, snap_data, points, to_document);
for bezpath in document.metadata().layer_outline(layer) {
bezpath_anchor_snap_points(layer, bezpath, snap_data, points, to_document);
}
}

View File

@@ -6,9 +6,9 @@ use crate::messages::tool::common_functionality::color_selector::solid;
use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_upstream_color_value_node_id, gradient_chain_target_input, replaceable_paint_chain};
use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color;
use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::style::FillChoice;
use kurbo::{BezPath, DEFAULT_ACCURACY, Rect, Shape};
#[derive(Default, ExtractField)]
pub struct FillTool {
@@ -150,9 +150,13 @@ impl Fsm for FillToolFsmState {
if paints_whole_expanse(layer, &document.network_interface) {
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 {
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,
/// 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
.ancestors(document.metadata())
.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)) {
Some([min, max]) => Subpath::new_rectangle(min, max),
None => Subpath::new_rectangle(DVec2::ZERO, viewport_size),
}
let [min, max] = containing_artboard
.and_then(|artboard| document.metadata().bounding_box_viewport(artboard))
.unwrap_or([DVec2::ZERO, viewport_size]);
Rect::from_points(dvec2_to_point(min), dvec2_to_point(max)).to_path(DEFAULT_ACCURACY)
}
#[cfg(test)]

View File

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