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::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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user