Add overlays for free-floating anchors on hovered/selected vector layers (#2630)
* Add selection overlay for free-floating anchors * Add hover overlay for free-floating anchors * Refactor outline_free_floating anchor * Add single-anchor click targets on VectorData * Modify ClickTarget to adapt for Subpath and PointGroup * Fix Rust formatting * Remove debug statements * Add point groups support in VectorDataTable::add_upstream_click_targets * Improve overlay for free floating anchors * Remove datatype for nodes_to_shift * Fix formatting in select_tool.rs * Lints * Code review * Remove references to point_group * Refactor ManipulatorGroup for FreePoint in ClickTargetGroup * Rename ClickTargetGroup to ClickTargetType * Refactor outline_free_floating_anchors into outline * Adapt TransformCage to disable dragging and rotating on a single anchor layer * Fix hover on single points * Fix comments * Lints * Code review pass --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
477a3f6670
commit
878f5d3bf7
|
|
@ -89,6 +89,8 @@ pub const MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR: f64 = 40.;
|
|||
///
|
||||
/// The motion of the user's cursor by an `x` pixel offset results in `x * scale_factor` pixels of offset on the other side.
|
||||
pub const MAXIMUM_ALT_SCALE_FACTOR: f64 = 25.;
|
||||
/// The width or height that the transform cage needs before it is considered to have no width or height.
|
||||
pub const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4;
|
||||
|
||||
// SKEW TRIANGLES
|
||||
pub const SKEW_TRIANGLE_SIZE: f64 = 7.;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ use graphene_core::raster::BlendMode;
|
|||
use graphene_core::raster_types::RasterDataTable;
|
||||
use graphene_core::vector::style::ViewMode;
|
||||
use graphene_std::raster_types::Raster;
|
||||
use graphene_std::renderer::{ClickTarget, Quad};
|
||||
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
|
||||
use graphene_std::vector::{PointId, path_bool_lib};
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -1637,10 +1637,17 @@ impl DocumentMessageHandler {
|
|||
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
|
||||
|
||||
layer_click_targets.is_some_and(|targets| {
|
||||
targets.iter().all(|target| {
|
||||
let mut subpath = target.subpath().clone();
|
||||
subpath.apply_transform(layer_transform);
|
||||
subpath.is_inside_subpath(&viewport_polygon, None, None)
|
||||
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::FreePoint(point) => {
|
||||
let mut point = point.clone();
|
||||
point.apply_transform(layer_transform);
|
||||
viewport_polygon.contains_point(point.position)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -2880,7 +2887,14 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'
|
|||
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
|
||||
};
|
||||
click_targets
|
||||
.flat_map(|target| target.subpath().iter())
|
||||
.filter_map(|target| {
|
||||
if let ClickTargetType::Subpath(subpath) = target.target_type() {
|
||||
Some(subpath.iter())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.map(|bezier| segment(bezier.apply_transformation(|x| transform.transform_point2(x))))
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use core::f64::consts::{FRAC_PI_2, TAU};
|
|||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core::Color;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_std::renderer::ClickTargetType;
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorData};
|
||||
use std::collections::HashMap;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
|
@ -647,13 +648,24 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
/// Used by the Select tool to outline a path selected or hovered.
|
||||
pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: Option<&str>) {
|
||||
self.push_path(subpaths, transform);
|
||||
/// 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<bezier_rs::Subpath<PointId>> = vec![];
|
||||
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.set_stroke_style_str(color);
|
||||
self.render_context.stroke();
|
||||
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()),
|
||||
});
|
||||
|
||||
if !subpaths.is_empty() {
|
||||
self.push_path(subpaths.iter(), transform);
|
||||
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
self.render_context.set_stroke_style_str(color);
|
||||
self.render_context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills the area inside the path. Assumes `color` is in gamma space.
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ use crate::messages::portfolio::document::graph_operation::transform_utils;
|
|||
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::renderer::ClickTarget;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::renderer::{ClickTarget, ClickTargetType, Quad};
|
||||
use graphene_core::transform::Footprint;
|
||||
use graphene_std::vector::{PointId, VectorData};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
|
@ -134,7 +133,10 @@ impl DocumentMetadata {
|
|||
pub fn bounding_box_with_transform(&self, layer: LayerNodeIdentifier, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.click_targets(layer)?
|
||||
.iter()
|
||||
.filter_map(|click_target| click_target.subpath().bounding_box_with_transform(transform))
|
||||
.filter_map(|click_target| match click_target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.bounding_box_with_transform(transform),
|
||||
ClickTargetType::FreePoint(_) => click_target.bounding_box_with_transform(transform),
|
||||
})
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +179,16 @@ impl DocumentMetadata {
|
|||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
|
||||
static EMPTY: Vec<ClickTarget> = Vec::new();
|
||||
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
|
||||
click_targets.iter().map(ClickTarget::subpath)
|
||||
click_targets.iter().filter_map(|target| match target.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => Some(subpath),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn layer_with_free_points_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &ClickTargetType> {
|
||||
static EMPTY: Vec<ClickTarget> = Vec::new();
|
||||
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
|
||||
click_targets.iter().map(|target| target.target_type())
|
||||
}
|
||||
|
||||
pub fn is_clip(&self, node: NodeId) -> bool {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use glam::{DAffine2, DVec2, IVec2};
|
|||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
|
||||
use graph_craft::{Type, concrete};
|
||||
use graphene_std::renderer::{ClickTarget, Quad};
|
||||
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::vector::{PointId, VectorData, VectorModificationType};
|
||||
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
|
||||
|
|
@ -2120,7 +2120,7 @@ impl NodeNetworkInterface {
|
|||
let bounding_box_top_right = DVec2::new((all_nodes_bounding_box[1].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_right;
|
||||
let export_top_right: DVec2 = DVec2::new(viewport_top_right.x.max(bounding_box_top_right.x), viewport_top_right.y.min(bounding_box_top_right.y));
|
||||
let add_export_center = export_top_right + DVec2::new(0., network.exports.len() as f64 * 24.);
|
||||
let add_export = ClickTarget::new(
|
||||
let add_export = ClickTarget::new_with_subpath(
|
||||
Subpath::new_rounded_rect(add_export_center - DVec2::new(12., 12.), add_export_center + DVec2::new(12., 12.), [3.; 4]),
|
||||
0.,
|
||||
);
|
||||
|
|
@ -2146,7 +2146,7 @@ impl NodeNetworkInterface {
|
|||
let bounding_box_top_left = DVec2::new((all_nodes_bounding_box[0].x / 24. + 0.5).floor() * 24., (all_nodes_bounding_box[0].y / 24. + 0.5).floor() * 24.) + offset_from_top_left;
|
||||
let import_top_left = DVec2::new(viewport_top_left.x.min(bounding_box_top_left.x), viewport_top_left.y.min(bounding_box_top_left.y));
|
||||
let add_import_center = import_top_left + DVec2::new(0., self.number_of_displayed_imports(network_path) as f64 * 24.);
|
||||
let add_import = ClickTarget::new(
|
||||
let add_import = ClickTarget::new_with_subpath(
|
||||
Subpath::new_rounded_rect(add_import_center - DVec2::new(12., 12.), add_import_center + DVec2::new(12., 12.), [3.; 4]),
|
||||
0.,
|
||||
);
|
||||
|
|
@ -2165,8 +2165,8 @@ impl NodeNetworkInterface {
|
|||
let reorder_import_center = (import_bounding_box[0] + import_bounding_box[1]) / 2. + DVec2::new(-12., 0.);
|
||||
let remove_import_center = reorder_import_center + DVec2::new(-12., 0.);
|
||||
|
||||
let reorder_import = ClickTarget::new(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.);
|
||||
let remove_import = ClickTarget::new(Subpath::new_rect(remove_import_center - DVec2::new(8., 8.), remove_import_center + DVec2::new(8., 8.)), 0.);
|
||||
let reorder_import = ClickTarget::new_with_subpath(Subpath::new_rect(reorder_import_center - DVec2::new(3., 4.), reorder_import_center + DVec2::new(3., 4.)), 0.);
|
||||
let remove_import = ClickTarget::new_with_subpath(Subpath::new_rect(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);
|
||||
|
|
@ -2180,8 +2180,8 @@ impl NodeNetworkInterface {
|
|||
let reorder_export_center = (export_bounding_box[0] + export_bounding_box[1]) / 2. + DVec2::new(12., 0.);
|
||||
let remove_export_center = reorder_export_center + DVec2::new(12., 0.);
|
||||
|
||||
let reorder_export = ClickTarget::new(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.);
|
||||
let remove_export = ClickTarget::new(Subpath::new_rect(remove_export_center - DVec2::new(8., 8.), remove_export_center + DVec2::new(8., 8.)), 0.);
|
||||
let reorder_export = ClickTarget::new_with_subpath(Subpath::new_rect(reorder_export_center - DVec2::new(3., 4.), reorder_export_center + DVec2::new(3., 4.)), 0.);
|
||||
let remove_export = ClickTarget::new_with_subpath(Subpath::new_rect(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);
|
||||
|
|
@ -2572,7 +2572,7 @@ impl NodeNetworkInterface {
|
|||
|
||||
let radius = 3.;
|
||||
let subpath = bezier_rs::Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]);
|
||||
let node_click_target = ClickTarget::new(subpath, 0.);
|
||||
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
|
||||
|
||||
DocumentNodeClickTargets {
|
||||
node_click_target,
|
||||
|
|
@ -2597,12 +2597,12 @@ impl NodeNetworkInterface {
|
|||
// Update visibility button click target
|
||||
let visibility_offset = node_top_left + DVec2::new(width as f64, 24.);
|
||||
let subpath = Subpath::new_rounded_rect(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]);
|
||||
let visibility_click_target = ClickTarget::new(subpath, 0.);
|
||||
let visibility_click_target = ClickTarget::new_with_subpath(subpath, 0.);
|
||||
|
||||
// Update grip button click target, which is positioned to the left of the left most icon
|
||||
let grip_offset_right_edge = node_top_left + DVec2::new(width as f64 - (GRID_SIZE as f64) / 2., 24.);
|
||||
let subpath = Subpath::new_rounded_rect(DVec2::new(-8., -12.) + grip_offset_right_edge, DVec2::new(0., 12.) + grip_offset_right_edge, [0.; 4]);
|
||||
let grip_click_target = ClickTarget::new(subpath, 0.);
|
||||
let grip_click_target = ClickTarget::new_with_subpath(subpath, 0.);
|
||||
|
||||
// Create layer click target, which is contains the layer and the chain background
|
||||
let chain_width_grid_spaces = self.chain_width(node_id, network_path);
|
||||
|
|
@ -2611,7 +2611,7 @@ impl NodeNetworkInterface {
|
|||
let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.);
|
||||
let radius = 10.;
|
||||
let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]);
|
||||
let node_click_target = ClickTarget::new(subpath, 0.);
|
||||
let node_click_target = ClickTarget::new_with_subpath(subpath, 0.);
|
||||
|
||||
DocumentNodeClickTargets {
|
||||
node_click_target,
|
||||
|
|
@ -2804,20 +2804,29 @@ impl NodeNetworkInterface {
|
|||
if let (Some(import_export_click_targets), Some(node_click_targets)) = (self.import_export_ports(network_path).cloned(), self.node_click_targets(&node_id, network_path)) {
|
||||
let mut node_path = String::new();
|
||||
|
||||
let _ = node_click_targets.node_click_target.subpath().subpath_to_svg(&mut node_path, DAffine2::IDENTITY);
|
||||
if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() {
|
||||
let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY);
|
||||
}
|
||||
all_node_click_targets.push((node_id, node_path));
|
||||
for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) {
|
||||
let mut port_path = String::new();
|
||||
let _ = port.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
port_click_targets.push(port_path);
|
||||
if let ClickTargetType::Subpath(subpath) = port.target_type() {
|
||||
let mut port_path = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
port_click_targets.push(port_path);
|
||||
}
|
||||
}
|
||||
if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata {
|
||||
let mut port_path = String::new();
|
||||
let _ = layer_metadata.visibility_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
icon_click_targets.push(port_path);
|
||||
let mut port_path = String::new();
|
||||
let _ = layer_metadata.grip_click_target.subpath().subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
icon_click_targets.push(port_path);
|
||||
if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() {
|
||||
let mut port_path = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
icon_click_targets.push(port_path);
|
||||
}
|
||||
|
||||
if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() {
|
||||
let mut port_path = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY);
|
||||
icon_click_targets.push(port_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -2872,9 +2881,11 @@ impl NodeNetworkInterface {
|
|||
.chain(modify_import_export_click_targets.remove_imports_exports.click_targets())
|
||||
.chain(modify_import_export_click_targets.reorder_imports_exports.click_targets())
|
||||
{
|
||||
let mut remove_string = String::new();
|
||||
let _ = click_target.subpath().subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
|
||||
modify_import_export.push(remove_string);
|
||||
if let ClickTargetType::Subpath(subpath) = click_target.target_type() {
|
||||
let mut remove_string = String::new();
|
||||
let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY);
|
||||
modify_import_export.push(remove_string);
|
||||
}
|
||||
}
|
||||
}
|
||||
FrontendClickTargets {
|
||||
|
|
@ -3174,8 +3185,8 @@ impl NodeNetworkInterface {
|
|||
self.document_metadata
|
||||
.click_targets
|
||||
.get(&layer)
|
||||
.map(|click| click.iter().map(ClickTarget::subpath))
|
||||
.map(|subpaths| VectorData::from_subpaths(subpaths, true))
|
||||
.map(|click| click.iter().map(ClickTarget::target_type))
|
||||
.map(|target_types| VectorData::from_target_types(target_types, true))
|
||||
}
|
||||
|
||||
/// Loads the structure of layer nodes from a node graph.
|
||||
|
|
@ -5893,7 +5904,7 @@ impl Ports {
|
|||
|
||||
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(subpath, 0.));
|
||||
self.insert_custom_input_port(input_index, ClickTarget::new_with_subpath(subpath, 0.));
|
||||
}
|
||||
|
||||
fn insert_custom_input_port(&mut self, input_index: usize, click_target: ClickTarget) {
|
||||
|
|
@ -5902,7 +5913,7 @@ impl Ports {
|
|||
|
||||
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(subpath, 0.));
|
||||
self.insert_custom_output_port(output_index, ClickTarget::new_with_subpath(subpath, 0.));
|
||||
}
|
||||
|
||||
fn insert_custom_output_port(&mut self, output_index: usize, click_target: ClickTarget) {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,11 @@ impl SelectedLayerState {
|
|||
}
|
||||
|
||||
pub fn selected_points_count(&self) -> usize {
|
||||
self.selected_points.len()
|
||||
let count = self.selected_points.iter().fold(0, |acc, point| {
|
||||
let is_ignored = (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors);
|
||||
acc + if is_ignored { 0 } else { 1 }
|
||||
});
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -449,7 +449,11 @@ impl SnapManager {
|
|||
if let Some(ind) = &self.indicator {
|
||||
for layer in &ind.outline_layers {
|
||||
let &Some(layer) = layer else { continue };
|
||||
overlay_context.outline(snap_data.document.metadata().layer_outline(layer), snap_data.document.metadata().transform_to_viewport(layer), None);
|
||||
overlay_context.outline(
|
||||
snap_data.document.metadata().layer_with_free_points_outline(layer),
|
||||
snap_data.document.metadata().transform_to_viewport(layer),
|
||||
None,
|
||||
);
|
||||
}
|
||||
if let Some(quad) = ind.target_bounds {
|
||||
overlay_context.quad(to_viewport * quad, None, None);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
|
||||
use crate::consts::{
|
||||
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS,
|
||||
MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE, SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET,
|
||||
SKEW_TRIANGLE_SIZE,
|
||||
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY,
|
||||
MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE,
|
||||
SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||
};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
|
|
@ -52,6 +52,8 @@ enum TransformCageSizeCategory {
|
|||
Narrow,
|
||||
/// - 
|
||||
Flat,
|
||||
/// A single point in space with no width or height.
|
||||
Point,
|
||||
}
|
||||
|
||||
impl SelectedEdges {
|
||||
|
|
@ -635,7 +637,12 @@ impl BoundingBoxManager {
|
|||
fn overlay_display_category(&self) -> TransformCageSizeCategory {
|
||||
let quad = self.transform * Quad::from_box(self.bounds);
|
||||
|
||||
// Check if the area is essentially zero because either the width or height is smaller than an epsilon
|
||||
// Check if the bounds are essentially the same because the width and height are smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT
|
||||
if self.is_bounds_point() {
|
||||
return TransformCageSizeCategory::Point;
|
||||
}
|
||||
|
||||
// Check if the area is essentially zero because either the width or height is smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT
|
||||
if self.is_bounds_flat() {
|
||||
return TransformCageSizeCategory::Flat;
|
||||
}
|
||||
|
|
@ -661,7 +668,12 @@ impl BoundingBoxManager {
|
|||
|
||||
/// Determine if these bounds are flat ([`TransformCageSizeCategory::Flat`]), which means that the width and/or height is essentially zero and the bounds are a line with effectively no area. This can happen on actual lines (axis-aligned, i.e. drawn horizontally or vertically) or when an element is scaled to zero in X or Y. A flat transform cage can still be rotated by a transformation, but its local space remains flat.
|
||||
fn is_bounds_flat(&self) -> bool {
|
||||
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any()
|
||||
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).any()
|
||||
}
|
||||
|
||||
/// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on points when an element is scaled to zero in both X and Y, or if an element is just a single anchor point. A point transform cage cannot be rotated by a transformation, and its local space remains a point.
|
||||
fn is_bounds_point(&self) -> bool {
|
||||
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).all()
|
||||
}
|
||||
|
||||
/// Determine if the given point in viewport space falls within the bounds of `self`.
|
||||
|
|
@ -699,7 +711,7 @@ impl BoundingBoxManager {
|
|||
let [edge_min_x, edge_min_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR);
|
||||
let [midpoint_threshold_x, midpoint_threshold_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS);
|
||||
|
||||
if min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y && cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y {
|
||||
if (min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y) && (cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y) {
|
||||
let mut top = (cursor.y - min.y).abs() < threshold_y;
|
||||
let mut bottom = (max.y - cursor.y).abs() < threshold_y;
|
||||
let mut left = (cursor.x - min.x).abs() < threshold_x;
|
||||
|
|
@ -741,11 +753,11 @@ impl BoundingBoxManager {
|
|||
}
|
||||
|
||||
// On bounds with no width/height, disallow transformation in the relevant axis
|
||||
if width < f64::EPSILON * 1000. {
|
||||
if width < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
if height < f64::EPSILON * 1000. {
|
||||
if height < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT {
|
||||
top = false;
|
||||
bottom = false;
|
||||
}
|
||||
|
|
@ -767,9 +779,12 @@ impl BoundingBoxManager {
|
|||
let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_ROTATE_THRESHOLD);
|
||||
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||
|
||||
let flat = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
|
||||
let flat = self.is_bounds_flat();
|
||||
let point = self.is_bounds_point();
|
||||
let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y;
|
||||
if flat {
|
||||
if point {
|
||||
false
|
||||
} else if flat {
|
||||
[self.bounds[0], self.bounds[1]].iter().any(within_square_bounds)
|
||||
} else {
|
||||
self.evaluate_transform_handle_positions().iter().any(within_square_bounds)
|
||||
|
|
|
|||
|
|
@ -527,10 +527,11 @@ impl Fsm for SelectToolFsmState {
|
|||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
{
|
||||
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), None);
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None);
|
||||
|
||||
if is_layer_fed_by_node_of_name(layer, &document.network_interface, "Text") {
|
||||
let transformed_quad = document.metadata().transform_to_viewport(layer) * text_bounding_box(layer, document, font_cache);
|
||||
let transformed_quad = layer_to_viewport * text_bounding_box(layer, document, font_cache);
|
||||
overlay_context.dashed_quad(transformed_quad, None, None, Some(7.), Some(5.), None);
|
||||
}
|
||||
}
|
||||
|
|
@ -573,13 +574,14 @@ impl Fsm for SelectToolFsmState {
|
|||
let not_selected_click = click.filter(|&hovered_layer| !document.network_interface.selected_nodes().selected_layers_contains(hovered_layer, document.metadata()));
|
||||
if let Some(layer) = not_selected_click {
|
||||
if overlay_context.visibility_settings.hover_outline() {
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||
let mut hover_overlay_draw = |layer: LayerNodeIdentifier, color: Option<&str>| {
|
||||
if layer.has_children(document.metadata()) {
|
||||
if let Some(bounds) = document.metadata().bounding_box_viewport(layer) {
|
||||
overlay_context.quad(Quad::from_box(bounds), color, None);
|
||||
}
|
||||
} else {
|
||||
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), color);
|
||||
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, color);
|
||||
}
|
||||
};
|
||||
let layer = match tool_data.nested_selection_behavior {
|
||||
|
|
@ -817,7 +819,8 @@ impl Fsm for SelectToolFsmState {
|
|||
if overlay_context.visibility_settings.selection_outline() {
|
||||
// Draws a temporary outline on the layers that will be selected by the current box/lasso area
|
||||
for layer in layers_to_outline {
|
||||
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), None);
|
||||
let layer_to_viewport = document.metadata().transform_to_viewport(layer);
|
||||
overlay_context.outline(document.metadata().layer_with_free_points_outline(layer), layer_to_viewport, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,22 +20,63 @@ use std::fmt::Write;
|
|||
#[cfg(feature = "vello")]
|
||||
use vello::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FreePoint {
|
||||
pub id: PointId,
|
||||
pub position: DVec2,
|
||||
}
|
||||
|
||||
impl FreePoint {
|
||||
pub fn new(id: PointId, position: DVec2) -> Self {
|
||||
Self { id, position }
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, transform: DAffine2) {
|
||||
self.position = transform.transform_point2(self.position);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ClickTargetType {
|
||||
Subpath(bezier_rs::Subpath<PointId>),
|
||||
FreePoint(FreePoint),
|
||||
}
|
||||
|
||||
/// Represents a clickable target for the layer
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ClickTarget {
|
||||
subpath: bezier_rs::Subpath<PointId>,
|
||||
target_type: ClickTargetType,
|
||||
stroke_width: f64,
|
||||
bounding_box: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
impl ClickTarget {
|
||||
pub fn new(subpath: bezier_rs::Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
pub fn new_with_subpath(subpath: bezier_rs::Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
let bounding_box = subpath.loose_bounding_box();
|
||||
Self { subpath, stroke_width, bounding_box }
|
||||
Self {
|
||||
target_type: ClickTargetType::Subpath(subpath),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subpath(&self) -> &bezier_rs::Subpath<PointId> {
|
||||
&self.subpath
|
||||
pub fn new_with_free_point(point: FreePoint) -> Self {
|
||||
const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.;
|
||||
let stroke_width = 10.;
|
||||
let bounding_box = Some([
|
||||
point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
]);
|
||||
|
||||
Self {
|
||||
target_type: ClickTargetType::FreePoint(point),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_type(&self) -> &ClickTargetType {
|
||||
&self.target_type
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
|
|
@ -47,12 +88,26 @@ impl ClickTarget {
|
|||
}
|
||||
|
||||
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
|
||||
self.subpath.apply_transform(affine_transform);
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref mut subpath) => {
|
||||
subpath.apply_transform(affine_transform);
|
||||
}
|
||||
ClickTargetType::FreePoint(ref mut point) => {
|
||||
point.apply_transform(affine_transform);
|
||||
}
|
||||
}
|
||||
self.update_bbox();
|
||||
}
|
||||
|
||||
fn update_bbox(&mut self) {
|
||||
self.bounding_box = self.subpath.bounding_box();
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref subpath) => {
|
||||
self.bounding_box = subpath.bounding_box();
|
||||
}
|
||||
ClickTargetType::FreePoint(ref point) => {
|
||||
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the path
|
||||
|
|
@ -66,19 +121,24 @@ impl ClickTarget {
|
|||
let inverse = layer_transform.inverse();
|
||||
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
|
||||
|
||||
// Check if outlines intersect
|
||||
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
|
||||
if self.subpath.iter().any(outline_intersects) {
|
||||
return true;
|
||||
}
|
||||
// Check if selection is entirely within the shape
|
||||
if self.subpath.closed() && bezier_iter().next().is_some_and(|bezier| self.subpath.contains_point(bezier.start)) {
|
||||
return true;
|
||||
}
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => {
|
||||
// Check if outlines intersect
|
||||
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
|
||||
if subpath.iter().any(outline_intersects) {
|
||||
return true;
|
||||
}
|
||||
// Check if selection is entirely within the shape
|
||||
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if shape is entirely within selection
|
||||
let any_point_from_subpath = self.subpath.manipulator_groups().first().map(|group| group.anchor);
|
||||
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
|
||||
// Check if shape is entirely within selection
|
||||
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
|
||||
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
|
||||
}
|
||||
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (accounting for stroke size)
|
||||
|
|
@ -107,7 +167,10 @@ impl ClickTarget {
|
|||
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
|
||||
{
|
||||
// Check if the point is within the shape
|
||||
self.subpath.closed() && self.subpath.contains_point(point)
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
|
||||
ClickTargetType::FreePoint(free_point) => free_point.position == point,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
@ -653,10 +716,23 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
subpath
|
||||
};
|
||||
|
||||
// For free-floating anchors, we need to add a click target for each
|
||||
let single_anchors_targets = instance.point_domain.ids().iter().filter_map(|&point_id| {
|
||||
if instance.connected_count(point_id) == 0 {
|
||||
let anchor = instance.point_domain.position_from_id(point_id).unwrap_or_default();
|
||||
let point = FreePoint::new(point_id, anchor);
|
||||
|
||||
Some(ClickTarget::new_with_free_point(point))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let click_targets = instance
|
||||
.stroke_bezier_paths()
|
||||
.map(fill)
|
||||
.map(|subpath| ClickTarget::new(subpath, stroke_width))
|
||||
.map(|subpath| ClickTarget::new_with_subpath(subpath, stroke_width))
|
||||
.chain(single_anchors_targets.into_iter())
|
||||
.collect::<Vec<ClickTarget>>();
|
||||
|
||||
metadata.click_targets.insert(element_id, click_targets);
|
||||
|
|
@ -680,10 +756,25 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
subpath
|
||||
};
|
||||
click_targets.extend(instance.instance.stroke_bezier_paths().map(fill).map(|subpath| {
|
||||
let mut click_target = ClickTarget::new(subpath, stroke_width);
|
||||
let mut click_target = ClickTarget::new_with_subpath(subpath, stroke_width);
|
||||
click_target.apply_transform(*instance.transform);
|
||||
click_target
|
||||
}));
|
||||
|
||||
// For free-floating anchors, we need to add a click target for each
|
||||
let single_anchors_targets = instance.instance.point_domain.ids().iter().filter_map(|&point_id| {
|
||||
if instance.instance.connected_count(point_id) > 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default();
|
||||
let point = FreePoint::new(point_id, anchor);
|
||||
|
||||
let mut click_target = ClickTarget::new_with_free_point(point);
|
||||
click_target.apply_transform(*instance.transform);
|
||||
Some(click_target)
|
||||
});
|
||||
click_targets.extend(single_anchors_targets);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -785,7 +876,7 @@ impl GraphicElementRendered for Artboard {
|
|||
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
|
||||
if let Some(element_id) = element_id {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]);
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
metadata.local_transforms.insert(element_id, DAffine2::from_translation(self.location.as_dvec2()));
|
||||
if self.clip {
|
||||
|
|
@ -798,7 +889,7 @@ impl GraphicElementRendered for Artboard {
|
|||
|
||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let subpath_rectangle = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||
click_targets.push(ClickTarget::new(subpath_rectangle, 0.));
|
||||
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
|
||||
}
|
||||
|
||||
fn contains_artboard(&self) -> bool {
|
||||
|
|
@ -909,7 +1000,7 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
|||
let Some(element_id) = element_id else { return };
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]);
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
// TODO: Find a way to handle more than one row of the graphical data table
|
||||
if let Some(image) = self.instance_ref_iter().next() {
|
||||
|
|
@ -919,7 +1010,7 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
|||
|
||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
|
||||
}
|
||||
|
||||
fn to_graphic_element(&self) -> GraphicElement {
|
||||
|
|
@ -976,7 +1067,7 @@ impl GraphicElementRendered for RasterDataTable<GPU> {
|
|||
let Some(element_id) = element_id else { return };
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]);
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
// TODO: Find a way to handle more than one row of the graphical data table
|
||||
if let Some(image) = self.instance_ref_iter().next() {
|
||||
|
|
@ -986,7 +1077,7 @@ impl GraphicElementRendered for RasterDataTable<GPU> {
|
|||
|
||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ mod modification;
|
|||
use super::misc::{dvec2_to_point, point_to_dvec2};
|
||||
use super::style::{PathStyle, Stroke};
|
||||
use crate::instances::Instances;
|
||||
use crate::renderer::{ClickTargetType, FreePoint};
|
||||
use crate::{AlphaBlending, Color, GraphicGroupTable};
|
||||
pub use attributes::*;
|
||||
use bezier_rs::ManipulatorGroup;
|
||||
|
|
@ -115,11 +116,6 @@ impl core::hash::Hash for VectorData {
|
|||
}
|
||||
|
||||
impl VectorData {
|
||||
/// Construct some new vector data from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
|
||||
Self::from_subpaths([subpath], false)
|
||||
}
|
||||
|
||||
/// Push a subpath to the vector data
|
||||
pub fn append_subpath(&mut self, subpath: impl Borrow<bezier_rs::Subpath<PointId>>, preserve_id: bool) {
|
||||
let subpath: &bezier_rs::Subpath<PointId> = subpath.borrow();
|
||||
|
|
@ -135,6 +131,8 @@ impl VectorData {
|
|||
let mut segment_id = self.segment_domain.next_id();
|
||||
let mut last_point = None;
|
||||
let mut first_point = None;
|
||||
|
||||
// Construct a bezier segment from the two manipulators on the subpath.
|
||||
for pair in subpath.manipulator_groups().windows(2) {
|
||||
let start = last_point.unwrap_or_else(|| {
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) {
|
||||
|
|
@ -178,11 +176,28 @@ impl VectorData {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn append_free_point(&mut self, point: &FreePoint, preserve_id: bool) {
|
||||
let mut point_id = self.point_domain.next_id();
|
||||
|
||||
// Use the current point ID if it's not already in the domain, otherwise generate a new one
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&point.id) {
|
||||
point.id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
self.point_domain.push(id, point.position);
|
||||
}
|
||||
|
||||
/// Appends a Kurbo BezPath to the vector data.
|
||||
pub fn append_bezpath(&mut self, bezpath: kurbo::BezPath) {
|
||||
AppendBezpath::append_bezpath(self, bezpath);
|
||||
}
|
||||
|
||||
/// Construct some new vector data from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
|
||||
Self::from_subpaths([subpath], false)
|
||||
}
|
||||
|
||||
/// Construct some new vector data from subpaths with an identity transform and black fill.
|
||||
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
|
||||
let mut vector_data = Self::default();
|
||||
|
|
@ -194,6 +209,19 @@ impl VectorData {
|
|||
vector_data
|
||||
}
|
||||
|
||||
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self {
|
||||
let mut vector_data = Self::default();
|
||||
|
||||
for target_type in target_types.into_iter() {
|
||||
match target_type.borrow() {
|
||||
ClickTargetType::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id),
|
||||
ClickTargetType::FreePoint(point) => vector_data.append_free_point(point, preserve_id),
|
||||
}
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the bezpaths without any transform
|
||||
pub fn bounding_box_rect(&self) -> Option<Rect> {
|
||||
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
||||
|
|
|
|||
Loading…
Reference in New Issue