mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 05:18:19 +00:00
Path tool: sliding point insertion (#1581)
* #1578 sliding point works(1st approx) TODO: * don't move too close to the side points * double click works incorrect? * do we need to jump from segment to segment? * #1578 disallow move slide point too close to side points * fix double click + ctrl insertion * #1578 select insertion point (except `ctrl` case) * #1578 far depends on line width & more accurate seg finding * #1578 insert point on most top suitable selected layer * #1581 draw insertion point by overlay + `Esc` abort * #1581 sharp stay unchanged on double click * #1581 fix incorrect handle of scaling * #1581 `square` selection point & too close in px(instead of magic) * #1581 bug fix: insertion point on unselection * #1581 use `color: Option` instead of `SelectionType` * Some code review, still need to review shape_editor.rs * #1581 insert sharp point on a straight segment Also correct insertion on quadratic segments `ManipulatorGroup::have_handle` have such form because `handle = Some(self.anchor)` often used instead of `handle = None` * Final code review pass * Code review pass --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
a412a77062
commit
f25038067e
11 changed files with 446 additions and 108 deletions
|
@ -45,6 +45,8 @@ pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
|
|||
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 5.;
|
||||
pub const SELECTION_THRESHOLD: f64 = 10.;
|
||||
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
|
||||
pub const INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE: f64 = 50.;
|
||||
pub const INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE: f64 = 5.;
|
||||
|
||||
// Pen tool
|
||||
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
|
||||
|
|
|
@ -181,7 +181,12 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
|
||||
entry!(KeyDown(Delete); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDown(Backspace); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: Shift }),
|
||||
entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::MouseDown { ctrl: Control, shift: Shift }),
|
||||
entry!(KeyDown(Rmb); action_dispatch=PathToolMessage::RightClick),
|
||||
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
|
||||
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
|
||||
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
|
||||
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift }),
|
||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyDown(KeyA); modifiers=[Control], action_dispatch=PathToolMessage::SelectAllPoints),
|
||||
|
@ -190,7 +195,7 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter {
|
||||
add_to_selection: Shift
|
||||
}),
|
||||
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::InsertPoint),
|
||||
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSharp),
|
||||
entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
|
||||
|
|
|
@ -48,7 +48,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape
|
|||
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
|
||||
}
|
||||
|
||||
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)));
|
||||
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::Anchor)), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,14 +66,14 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
|
|||
let anchor = first_manipulator.anchor;
|
||||
let anchor_position = transform.transform_point2(anchor);
|
||||
|
||||
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)));
|
||||
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(first_manipulator.id, SelectedType::Anchor)), None);
|
||||
};
|
||||
|
||||
if let Some(last_manipulator) = manipulator_groups.last() {
|
||||
let anchor = last_manipulator.anchor;
|
||||
let anchor_position = transform.transform_point2(anchor);
|
||||
|
||||
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)));
|
||||
overlay_context.square(anchor_position, is_selected(selected, ManipulatorPointId::new(last_manipulator.id, SelectedType::Anchor)), None);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,21 +54,23 @@ impl OverlayContext {
|
|||
.expect("draw circle");
|
||||
|
||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&fill));
|
||||
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
|
||||
self.render_context.fill();
|
||||
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
|
||||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
pub fn square(&mut self, position: DVec2, selected: bool) {
|
||||
pub fn square(&mut self, position: DVec2, selected: bool, color_selected: Option<&str>) {
|
||||
let color_selected = color_selected.unwrap_or(COLOR_OVERLAY_BLUE);
|
||||
|
||||
self.render_context.begin_path();
|
||||
let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.;
|
||||
self.render_context
|
||||
.rect(corner.x.round(), corner.y.round(), MANIPULATOR_GROUP_MARKER_SIZE, MANIPULATOR_GROUP_MARKER_SIZE);
|
||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
||||
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(&fill));
|
||||
let fill = if selected { color_selected } else { COLOR_OVERLAY_WHITE };
|
||||
self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
|
||||
self.render_context.fill();
|
||||
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
|
||||
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_selected));
|
||||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
|
|
|
@ -194,6 +194,14 @@ pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> O
|
|||
Some((text, font, font_size))
|
||||
}
|
||||
|
||||
pub fn get_stroke_width(layer: LayerNodeIdentifier, network: &NodeNetwork) -> Option<f32> {
|
||||
if let TaggedValue::F32(width) = NodeGraphLayer::new(layer, network)?.find_input("Stroke", 2)? {
|
||||
Some(*width)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a specified layer uses an upstream node matching the given name.
|
||||
pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, document_network: &NodeNetwork, node_name: &str) -> bool {
|
||||
NodeGraphLayer::new(layer, document_network).is_some_and(|layer| layer.find_node_inputs(node_name).is_some())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::graph_modification_utils;
|
||||
use super::snapping::{group_smooth, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
||||
use crate::consts::DRAG_THRESHOLD;
|
||||
use crate::consts::{DRAG_THRESHOLD, INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE};
|
||||
use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource};
|
||||
|
@ -9,6 +9,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{get_
|
|||
|
||||
use bezier_rs::{Bezier, ManipulatorGroup, TValue};
|
||||
use graph_craft::document::NodeNetwork;
|
||||
use graphene_core::transform::Transform;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
||||
|
||||
|
@ -64,6 +65,190 @@ pub struct ManipulatorPointInfo {
|
|||
|
||||
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<ManipulatorGroupId, Option<f64>>>;
|
||||
|
||||
struct ClosestSegmentInfo {
|
||||
pub bezier: Bezier,
|
||||
pub t: f64,
|
||||
pub bezier_point_to_viewport: DVec2,
|
||||
pub layer_scale: DVec2,
|
||||
}
|
||||
|
||||
pub struct ClosestSegment {
|
||||
layer: LayerNodeIdentifier,
|
||||
start: ManipulatorGroupId,
|
||||
end: ManipulatorGroupId,
|
||||
bezier: Bezier,
|
||||
t: f64,
|
||||
t_min: f64,
|
||||
t_max: f64,
|
||||
scale: f64,
|
||||
stroke_width: f64,
|
||||
bezier_point_to_viewport: DVec2,
|
||||
has_start_handle: bool,
|
||||
has_end_handle: bool,
|
||||
}
|
||||
|
||||
impl ClosestSegment {
|
||||
fn new(info: ClosestSegmentInfo, layer: LayerNodeIdentifier, document_network: &NodeNetwork, start: ManipulatorGroup<ManipulatorGroupId>, end: ManipulatorGroup<ManipulatorGroupId>) -> Self {
|
||||
// 0.5 is half the line (center to side) but it's convenient to allow targetting slightly more than half the line width
|
||||
const STROKE_WIDTH_PERCENT: f64 = 0.7;
|
||||
|
||||
let bezier = info.bezier;
|
||||
let t = info.t;
|
||||
let (t_min, t_max) = ClosestSegment::t_min_max(&bezier, info.layer_scale);
|
||||
let stroke_width = graph_modification_utils::get_stroke_width(layer, document_network).unwrap_or(1.) as f64 * STROKE_WIDTH_PERCENT;
|
||||
let bezier_point_to_viewport = info.bezier_point_to_viewport;
|
||||
let has_start_handle = start.has_out_handle();
|
||||
let has_end_handle = end.has_in_handle();
|
||||
|
||||
Self {
|
||||
layer,
|
||||
start: start.id,
|
||||
end: end.id,
|
||||
bezier,
|
||||
t,
|
||||
t_min,
|
||||
t_max,
|
||||
scale: 1.,
|
||||
stroke_width,
|
||||
bezier_point_to_viewport,
|
||||
has_start_handle,
|
||||
has_end_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layer(&self) -> LayerNodeIdentifier {
|
||||
self.layer
|
||||
}
|
||||
|
||||
pub fn closest_point_to_viewport(&self) -> DVec2 {
|
||||
self.bezier_point_to_viewport
|
||||
}
|
||||
|
||||
fn t_min_max(bezier: &Bezier, layer_scale: DVec2) -> (f64, f64) {
|
||||
let length = bezier.apply_transformation(|point| point * layer_scale).length(Some(100));
|
||||
let too_close_t = (INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE / length).min(0.5);
|
||||
|
||||
let t_min_euclidean = too_close_t;
|
||||
let t_max_euclidean = 1. - too_close_t;
|
||||
|
||||
// We need parametric values because they are faster to calculate
|
||||
let t_min = bezier.euclidean_to_parametric(t_min_euclidean, 0.001);
|
||||
let t_max = bezier.euclidean_to_parametric(t_max_euclidean, 0.001);
|
||||
|
||||
(t_min, t_max)
|
||||
}
|
||||
|
||||
/// Updates this [`ClosestSegment`] with the viewport-space location of the closest point on the segment to the given mouse position.
|
||||
pub fn update_closest_point(&mut self, document_metadata: &DocumentMetadata, mouse_position: DVec2) {
|
||||
let transform = document_metadata.transform_to_viewport(self.layer);
|
||||
let layer_m_pos = transform.inverse().transform_point2(mouse_position);
|
||||
|
||||
self.scale = document_metadata.document_to_viewport.decompose_scale().x.max(1.);
|
||||
|
||||
// Linear approximation of parametric t-value ranges:
|
||||
let t_min = self.t_min / self.scale;
|
||||
let t_max = 1. - ((1. - self.t_max) / self.scale);
|
||||
let t = self.bezier.project(layer_m_pos, None).max(t_min).min(t_max);
|
||||
self.t = t;
|
||||
|
||||
let bezier_point = self.bezier.evaluate(TValue::Parametric(t));
|
||||
let bezier_point = transform.transform_point2(bezier_point);
|
||||
self.bezier_point_to_viewport = bezier_point;
|
||||
}
|
||||
|
||||
pub fn distance_squared(&self, mouse_position: DVec2) -> f64 {
|
||||
self.bezier_point_to_viewport.distance_squared(mouse_position)
|
||||
}
|
||||
|
||||
pub fn split(&self) -> [Bezier; 2] {
|
||||
self.bezier.split(TValue::Parametric(self.t))
|
||||
}
|
||||
|
||||
pub fn too_far(&self, mouse_position: DVec2, tolerance: f64) -> bool {
|
||||
let dist_sq = self.distance_squared(mouse_position);
|
||||
let stroke_width = self.scale * self.stroke_width;
|
||||
let stroke_width_sq = stroke_width * stroke_width;
|
||||
let tolerance_sq = tolerance * tolerance;
|
||||
(stroke_width_sq + tolerance_sq) < dist_sq
|
||||
}
|
||||
|
||||
pub fn adjust_start_handle(&self, responses: &mut VecDeque<Message>) {
|
||||
if !self.has_start_handle {
|
||||
return;
|
||||
}
|
||||
|
||||
let [first, _] = self.split();
|
||||
let point = ManipulatorPointId::new(self.start, SelectedType::OutHandle);
|
||||
|
||||
// `first.handle_start()` should always be expected
|
||||
let position = first.handle_start().unwrap_or(first.start());
|
||||
|
||||
let out_handle = GraphOperationMessage::Vector {
|
||||
layer: self.layer,
|
||||
modification: VectorDataModification::SetManipulatorPosition { point, position },
|
||||
};
|
||||
responses.add(out_handle);
|
||||
}
|
||||
|
||||
pub fn adjust_end_handle(&self, responses: &mut VecDeque<Message>) {
|
||||
if !self.has_end_handle {
|
||||
return;
|
||||
}
|
||||
|
||||
let [_, second] = self.split();
|
||||
let point = ManipulatorPointId::new(self.end, SelectedType::InHandle);
|
||||
|
||||
// `second.handle_end()` should not be expected in the quadratic case
|
||||
let position = if second.handles.is_cubic() { second.handle_end() } else { second.handle_start() };
|
||||
let position = position.unwrap_or(second.end());
|
||||
|
||||
let in_handle = GraphOperationMessage::Vector {
|
||||
layer: self.layer,
|
||||
modification: VectorDataModification::SetManipulatorPosition { point, position },
|
||||
};
|
||||
responses.add(in_handle);
|
||||
}
|
||||
|
||||
/// Inserts the point that this [`ClosestSegment`] currently has. Returns the [`ManipulatorGroupId`] of the inserted point.
|
||||
pub fn insert_point(&self, responses: &mut VecDeque<Message>) -> ManipulatorGroupId {
|
||||
let [first, second] = self.split();
|
||||
|
||||
let layer = self.layer;
|
||||
let anchor = first.end();
|
||||
|
||||
// `first.handle_end()` should not be expected in the quadratic case
|
||||
let in_handle = if first.handles.is_cubic() { first.handle_end() } else { first.handle_start() };
|
||||
let out_handle = second.handle_start();
|
||||
let (in_handle, out_handle) = match (self.has_start_handle, self.has_end_handle) {
|
||||
(false, false) => (None, None),
|
||||
(false, true) => (in_handle, if second.handles.is_cubic() { out_handle } else { None }),
|
||||
(true, false) => (if first.handles.is_cubic() { in_handle } else { None }, out_handle),
|
||||
(true, true) => (in_handle, out_handle),
|
||||
};
|
||||
|
||||
let manipulator_group = ManipulatorGroup::new(anchor, in_handle, out_handle);
|
||||
let modification = VectorDataModification::AddManipulatorGroup {
|
||||
manipulator_group,
|
||||
after_id: self.start,
|
||||
};
|
||||
let insert = GraphOperationMessage::Vector { layer, modification };
|
||||
responses.add(insert);
|
||||
|
||||
manipulator_group.id
|
||||
}
|
||||
|
||||
pub fn adjusted_insert(&self, responses: &mut VecDeque<Message>) -> ManipulatorGroupId {
|
||||
self.adjust_start_handle(responses);
|
||||
self.adjust_end_handle(responses);
|
||||
self.insert_point(responses)
|
||||
}
|
||||
|
||||
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, add_to_selection: bool) {
|
||||
let id = self.adjusted_insert(responses);
|
||||
shape_editor.select_anchor_point_by_id(self.layer, id, add_to_selection)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
|
||||
impl ShapeState {
|
||||
// Snap, returning a viewport delta
|
||||
|
@ -97,7 +282,7 @@ impl ShapeState {
|
|||
} else {
|
||||
SnapSource::Geometry(GeometrySnapSource::Sharp)
|
||||
};
|
||||
let Some(position) = handle.get_position(&group) else { continue };
|
||||
let Some(position) = handle.get_position(group) else { continue };
|
||||
let mut point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
|
||||
|
||||
let mut push_neighbor = |group: ManipulatorGroup<ManipulatorGroupId>| {
|
||||
|
@ -133,23 +318,30 @@ impl ShapeState {
|
|||
document.metadata.document_to_viewport.transform_vector2(offset)
|
||||
}
|
||||
|
||||
/// Select the first point within the selection threshold.
|
||||
pub fn select_anchor_point_by_id(&mut self, layer: LayerNodeIdentifier, id: ManipulatorGroupId, add_to_selection: bool) {
|
||||
if !add_to_selection {
|
||||
self.deselect_all();
|
||||
}
|
||||
let point = ManipulatorPointId::new(id, SelectedType::Anchor);
|
||||
let Some(selected_state) = self.selected_shape_state.get_mut(&layer) else { return };
|
||||
selected_state.select_point(point);
|
||||
}
|
||||
|
||||
/// Select/deselect the first point within the selection threshold.
|
||||
/// Returns a tuple of the points if found and the offset, or `None` otherwise.
|
||||
pub fn select_point(
|
||||
pub fn change_point_selection(
|
||||
&mut self,
|
||||
document_network: &NodeNetwork,
|
||||
document_metadata: &DocumentMetadata,
|
||||
mouse_position: DVec2,
|
||||
select_threshold: f64,
|
||||
add_to_selection: bool,
|
||||
) -> Option<SelectedPointsInfo> {
|
||||
) -> Option<Option<SelectedPointsInfo>> {
|
||||
if self.selected_shape_state.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(document_network, document_metadata, mouse_position, select_threshold) {
|
||||
trace!("Selecting... manipulator point: {manipulator_point_id:?}");
|
||||
|
||||
let subpaths = get_subpaths(layer, document_network)?;
|
||||
let manipulator_group = get_manipulator_groups(subpaths).find(|group| group.id == manipulator_point_id.group)?;
|
||||
let point_position = manipulator_point_id.manipulator_type.get_position(manipulator_group)?;
|
||||
|
@ -160,6 +352,9 @@ impl ShapeState {
|
|||
// Should we select or deselect the point?
|
||||
let new_selected = if already_selected { !add_to_selection } else { true };
|
||||
|
||||
// Offset to snap the selected point to the cursor
|
||||
let offset = mouse_position - document_metadata.transform_to_viewport(layer).transform_point2(point_position);
|
||||
|
||||
// This is selecting the manipulator only for now, next to generalize to points
|
||||
if new_selected {
|
||||
let retain_existing_selection = add_to_selection || already_selected;
|
||||
|
@ -171,21 +366,18 @@ impl ShapeState {
|
|||
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||
selected_shape_state.select_point(manipulator_point_id);
|
||||
|
||||
// Offset to snap the selected point to the cursor
|
||||
let offset = mouse_position - document_metadata.transform_to_viewport(layer).transform_point2(point_position);
|
||||
|
||||
let points = self
|
||||
.selected_shape_state
|
||||
.iter()
|
||||
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
|
||||
.collect();
|
||||
|
||||
return Some(SelectedPointsInfo { points, offset });
|
||||
return Some(Some(SelectedPointsInfo { points, offset }));
|
||||
} else {
|
||||
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||
selected_shape_state.deselect_point(manipulator_point_id);
|
||||
|
||||
return None;
|
||||
return Some(None);
|
||||
}
|
||||
}
|
||||
None
|
||||
|
@ -219,6 +411,13 @@ impl ShapeState {
|
|||
self.selected_shape_state.keys()
|
||||
}
|
||||
|
||||
/// iterate over all selected layers in order from top to bottom
|
||||
/// # WARN
|
||||
/// iterate over all layers of the document
|
||||
pub fn sorted_selected_layers<'a>(&'a self, document_metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
|
||||
document_metadata.all_layers().filter(|layer| self.selected_shape_state.contains_key(layer))
|
||||
}
|
||||
|
||||
pub fn has_selected_layers(&self) -> bool {
|
||||
!self.selected_shape_state.is_empty()
|
||||
}
|
||||
|
@ -228,7 +427,6 @@ impl ShapeState {
|
|||
self.iter(document_network).flat_map(|subpaths| get_manipulator_groups(subpaths))
|
||||
}
|
||||
|
||||
// Sets the selected points to all points for the corresponding intersection
|
||||
pub fn select_all_anchors(&mut self, document_network: &NodeNetwork, layer: LayerNodeIdentifier) {
|
||||
let Some(subpaths) = get_subpaths(layer, document_network) else { return };
|
||||
let Some(state) = self.selected_shape_state.get_mut(&layer) else { return };
|
||||
|
@ -895,24 +1093,21 @@ impl ShapeState {
|
|||
}
|
||||
|
||||
/// Find the `t` value along the path segment we have clicked upon, together with that segment ID.
|
||||
fn closest_segment(
|
||||
&self,
|
||||
document_network: &NodeNetwork,
|
||||
document_metadata: &DocumentMetadata,
|
||||
layer: LayerNodeIdentifier,
|
||||
position: glam::DVec2,
|
||||
tolerance: f64,
|
||||
) -> Option<(ManipulatorGroupId, ManipulatorGroupId, Bezier, f64)> {
|
||||
fn closest_segment(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, layer: LayerNodeIdentifier, position: glam::DVec2, tolerance: f64) -> Option<ClosestSegment> {
|
||||
let transform = document_metadata.transform_to_viewport(layer);
|
||||
let layer_pos = transform.inverse().transform_point2(position);
|
||||
let projection_options = bezier_rs::ProjectionOptions { lut_size: 5, ..Default::default() };
|
||||
|
||||
let mut result = None;
|
||||
let scale = document_metadata.document_to_viewport.decompose_scale().x;
|
||||
let tolerance = tolerance + 0.5 * scale; // make more talerance at large scale
|
||||
let lut_size = ((5. + scale) as usize).min(20); // need more precision at large scale
|
||||
let projection_options = bezier_rs::ProjectionOptions { lut_size, ..Default::default() };
|
||||
|
||||
let mut closest = None;
|
||||
let mut closest_distance_squared: f64 = tolerance * tolerance;
|
||||
|
||||
let subpaths = get_subpaths(layer, document_network)?;
|
||||
|
||||
for subpath in subpaths {
|
||||
for (subpath_index, subpath) in subpaths.iter().enumerate() {
|
||||
for (manipulator_index, bezier) in subpath.iter().enumerate() {
|
||||
let t = bezier.project(layer_pos, Some(projection_options));
|
||||
let layerspace = bezier.evaluate(TValue::Parametric(t));
|
||||
|
@ -922,50 +1117,41 @@ impl ShapeState {
|
|||
|
||||
if distance_squared < closest_distance_squared {
|
||||
closest_distance_squared = distance_squared;
|
||||
let start = subpath.manipulator_groups()[manipulator_index];
|
||||
let end = subpath.manipulator_groups()[(manipulator_index + 1) % subpath.len()];
|
||||
result = Some((start.id, end.id, bezier, t));
|
||||
|
||||
let info = ClosestSegmentInfo {
|
||||
bezier,
|
||||
t,
|
||||
// needs for correct length calc when there is non 1x1 layer scale
|
||||
layer_scale: transform.decompose_scale() / scale,
|
||||
bezier_point_to_viewport: screenspace,
|
||||
};
|
||||
closest = Some(((subpath_index, manipulator_index), info))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
closest.map(|((subpath_index, manipulator_index), info)| {
|
||||
let subpath = &subpaths[subpath_index];
|
||||
let start = subpath.manipulator_groups()[manipulator_index];
|
||||
let end = subpath.manipulator_groups()[(manipulator_index + 1) % subpath.len()];
|
||||
ClosestSegment::new(info, layer, document_network, start, end)
|
||||
})
|
||||
}
|
||||
|
||||
/// find closest to the position segment on selected layers. If there is more than one layers with close enough segment it return upper from them
|
||||
pub fn upper_closest_segment(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, position: glam::DVec2, tolerance: f64) -> Option<ClosestSegment> {
|
||||
let closest_seg = |layer| self.closest_segment(document_network, document_metadata, layer, position, tolerance);
|
||||
match self.selected_shape_state.len() {
|
||||
0 => None,
|
||||
1 => self.selected_layers().next().copied().and_then(closest_seg),
|
||||
_ => self.sorted_selected_layers(document_metadata).find_map(closest_seg),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the splitting of a curve to insert new points (which can be activated by double clicking on a curve with the Path tool).
|
||||
pub fn split(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, position: glam::DVec2, tolerance: f64, responses: &mut VecDeque<Message>) {
|
||||
for &layer in self.selected_layers() {
|
||||
if let Some((start, end, bezier, t)) = self.closest_segment(document_network, document_metadata, layer, position, tolerance) {
|
||||
let [first, second] = bezier.split(TValue::Parametric(t));
|
||||
|
||||
// Adjust the first manipulator group's out handle
|
||||
let point = ManipulatorPointId::new(start, SelectedType::OutHandle);
|
||||
let position = first.handle_start().unwrap_or(first.start());
|
||||
let out_handle = GraphOperationMessage::Vector {
|
||||
layer,
|
||||
modification: VectorDataModification::SetManipulatorPosition { point, position },
|
||||
};
|
||||
responses.add(out_handle);
|
||||
|
||||
// Insert a new manipulator group between the existing ones
|
||||
let manipulator_group = ManipulatorGroup::new(first.end(), first.handle_end(), second.handle_start());
|
||||
let insert = GraphOperationMessage::Vector {
|
||||
layer,
|
||||
modification: VectorDataModification::AddManipulatorGroup { manipulator_group, after_id: start },
|
||||
};
|
||||
responses.add(insert);
|
||||
|
||||
// Adjust the last manipulator group's in handle
|
||||
let point = ManipulatorPointId::new(end, SelectedType::InHandle);
|
||||
let position = second.handle_end().unwrap_or(second.end());
|
||||
let in_handle = GraphOperationMessage::Vector {
|
||||
layer,
|
||||
modification: VectorDataModification::SetManipulatorPosition { point, position },
|
||||
};
|
||||
responses.add(in_handle);
|
||||
|
||||
return;
|
||||
}
|
||||
if let Some(segment) = self.upper_closest_segment(document_network, document_metadata, position, tolerance) {
|
||||
segment.adjusted_insert(responses);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -334,7 +334,7 @@ impl SnapManager {
|
|||
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
|
||||
|
||||
overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.);
|
||||
overlay_context.square(viewport, true);
|
||||
overlay_context.square(viewport, true, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -246,7 +246,7 @@ impl BoundingBoxManager {
|
|||
overlay_context.quad(self.transform * Quad::from_box(self.bounds));
|
||||
|
||||
for position in self.evaluate_transform_handle_positions() {
|
||||
overlay_context.square(position, false);
|
||||
overlay_context.square(position, false, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::consts::{DRAG_THRESHOLD, SELECTION_THRESHOLD, SELECTION_TOLERANCE};
|
||||
use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE};
|
||||
use crate::messages::portfolio::document::overlays::utility_functions::path_overlays;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_mirror_handles, get_subpaths};
|
||||
use crate::messages::tool::common_functionality::shape_editor::{ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
|
||||
use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager};
|
||||
|
||||
use graph_craft::document::NodeNetwork;
|
||||
|
@ -35,18 +35,24 @@ pub enum PathToolMessage {
|
|||
BreakPath,
|
||||
Delete,
|
||||
DeleteAndBreakPath,
|
||||
DragStart {
|
||||
add_to_selection: Key,
|
||||
},
|
||||
DragStop {
|
||||
shift_mirror_distance: Key,
|
||||
},
|
||||
Enter {
|
||||
add_to_selection: Key,
|
||||
},
|
||||
InsertPoint,
|
||||
Escape,
|
||||
FlipSharp,
|
||||
GRS {
|
||||
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
|
||||
key: Key,
|
||||
},
|
||||
ManipulatorAngleMakeSharp,
|
||||
ManipulatorAngleMakeSmooth,
|
||||
MouseDown {
|
||||
ctrl: Key,
|
||||
shift: Key,
|
||||
},
|
||||
NudgeSelectedPoints {
|
||||
delta_x: f64,
|
||||
delta_y: f64,
|
||||
|
@ -55,6 +61,7 @@ pub enum PathToolMessage {
|
|||
alt: Key,
|
||||
shift: Key,
|
||||
},
|
||||
RightClick,
|
||||
SelectAllPoints,
|
||||
SelectedPointUpdated,
|
||||
SelectedPointXChanged {
|
||||
|
@ -155,8 +162,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(PathToolMessageDiscriminant;
|
||||
InsertPoint,
|
||||
DragStart,
|
||||
FlipSharp,
|
||||
MouseDown,
|
||||
Delete,
|
||||
NudgeSelectedPoints,
|
||||
Enter,
|
||||
|
@ -165,7 +172,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||
DeleteAndBreakPath,
|
||||
),
|
||||
Dragging => actions!(PathToolMessageDiscriminant;
|
||||
InsertPoint,
|
||||
FlipSharp,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Delete,
|
||||
|
@ -174,7 +181,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||
DeleteAndBreakPath,
|
||||
),
|
||||
DrawingBox => actions!(PathToolMessageDiscriminant;
|
||||
InsertPoint,
|
||||
FlipSharp,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Delete,
|
||||
|
@ -183,6 +190,15 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||
BreakPath,
|
||||
DeleteAndBreakPath,
|
||||
),
|
||||
InsertPoint => actions!(PathToolMessageDiscriminant;
|
||||
Enter,
|
||||
MouseDown,
|
||||
PointerMove,
|
||||
Escape,
|
||||
Delete,
|
||||
RightClick,
|
||||
GRS,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,6 +220,12 @@ enum PathToolFsmState {
|
|||
Ready,
|
||||
Dragging,
|
||||
DrawingBox,
|
||||
InsertPoint,
|
||||
}
|
||||
|
||||
enum InsertEndKind {
|
||||
Abort,
|
||||
Add { shift: bool },
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -216,30 +238,88 @@ struct PathToolData {
|
|||
/// Describes information about the selected point(s), if any, across one or multiple shapes and manipulator point types (anchor or handle).
|
||||
/// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected.
|
||||
selection_status: SelectionStatus,
|
||||
segment: Option<ClosestSegment>,
|
||||
double_click_handled: bool,
|
||||
}
|
||||
|
||||
impl PathToolData {
|
||||
fn start_insertion(&mut self, responses: &mut VecDeque<Message>, segment: ClosestSegment) -> PathToolFsmState {
|
||||
if self.segment.is_some() {
|
||||
warn!("Segment was `Some(..)` before `start_insertion`")
|
||||
}
|
||||
self.segment = Some(segment);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
PathToolFsmState::InsertPoint
|
||||
}
|
||||
|
||||
fn update_insertion(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, mouse_position: DVec2) -> PathToolFsmState {
|
||||
if let Some(closed_segment) = &mut self.segment {
|
||||
closed_segment.update_closest_point(&document.metadata, mouse_position);
|
||||
if closed_segment.too_far(mouse_position, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE) {
|
||||
self.end_insertion(shape_editor, responses, InsertEndKind::Abort)
|
||||
} else {
|
||||
PathToolFsmState::InsertPoint
|
||||
}
|
||||
} else {
|
||||
warn!("Segment was `None` on `update_insertion`");
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
}
|
||||
|
||||
fn end_insertion(&mut self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, kind: InsertEndKind) -> PathToolFsmState {
|
||||
match self.segment.as_mut() {
|
||||
None => {
|
||||
warn!("Segment was `None` before `end_insertion`")
|
||||
}
|
||||
Some(closed_segment) => {
|
||||
if let InsertEndKind::Add { shift } = kind {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
closed_segment.adjusted_insert_and_select(shape_editor, responses, shift);
|
||||
responses.add(DocumentMessage::CommitTransaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.segment = None;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
shift: bool,
|
||||
shape_editor: &mut ShapeState,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
add_to_selection: bool,
|
||||
direct_insert_without_sliding: bool,
|
||||
) -> PathToolFsmState {
|
||||
self.double_click_handled = false;
|
||||
self.opposing_handle_lengths = None;
|
||||
let _selected_layers = shape_editor.selected_layers().cloned().collect::<Vec<_>>();
|
||||
|
||||
let document_network = document.network();
|
||||
let document_metadata = document.metadata();
|
||||
|
||||
// Select the first point within the threshold (in pixels)
|
||||
if let Some(selected_points) = shape_editor.select_point(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, shift) {
|
||||
self.start_dragging_point(selected_points, input, document, responses);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
if let Some(selected_points) = shape_editor.change_point_selection(document_network, document_metadata, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) {
|
||||
if let Some(selected_points) = selected_points {
|
||||
self.start_dragging_point(selected_points, input, document, responses);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
PathToolFsmState::Dragging
|
||||
}
|
||||
// We didn't find a point nearby, so consider selecting the nearest shape instead
|
||||
// We didn't find a point nearby, so now we'll try to add a point into the closest path segment
|
||||
else if let Some(closed_segment) = shape_editor.upper_closest_segment(document_network, document_metadata, input.mouse.position, SELECTION_TOLERANCE) {
|
||||
if direct_insert_without_sliding {
|
||||
self.start_insertion(responses, closed_segment);
|
||||
self.end_insertion(shape_editor, responses, InsertEndKind::Add { shift: add_to_selection })
|
||||
} else {
|
||||
self.start_insertion(responses, closed_segment)
|
||||
}
|
||||
}
|
||||
// We didn't find a segment path, so consider selecting the nearest shape instead
|
||||
else if let Some(layer) = document.click(input.mouse.position, &document.network) {
|
||||
if shift {
|
||||
if add_to_selection {
|
||||
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
|
||||
} else {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||
|
@ -336,21 +416,59 @@ impl Fsm for PathToolFsmState {
|
|||
(_, PathToolMessage::Overlays(mut overlay_context)) => {
|
||||
path_overlays(document, shape_editor, &mut overlay_context);
|
||||
|
||||
if self == Self::DrawingBox {
|
||||
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]))
|
||||
} else if self == Self::Dragging {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
match self {
|
||||
Self::DrawingBox => {
|
||||
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]));
|
||||
}
|
||||
Self::Dragging => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
}
|
||||
Self::InsertPoint => {
|
||||
let state = tool_data.update_insertion(shape_editor, document, responses, input.mouse.position);
|
||||
|
||||
if let Some(closest_segment) = &tool_data.segment {
|
||||
overlay_context.square(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW));
|
||||
}
|
||||
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
return state;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
|
||||
self
|
||||
}
|
||||
// Mouse down
|
||||
(_, PathToolMessage::DragStart { add_to_selection }) => {
|
||||
let shift = input.keyboard.get(add_to_selection as usize);
|
||||
|
||||
tool_data.mouse_down(shift, shape_editor, document, input, responses)
|
||||
// `Self::InsertPoint` case:
|
||||
(Self::InsertPoint, PathToolMessage::MouseDown { .. } | PathToolMessage::Enter { .. }) => {
|
||||
tool_data.double_click_handled = true;
|
||||
let shift = input.keyboard.get(Key::Shift as usize);
|
||||
tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { shift })
|
||||
}
|
||||
(Self::InsertPoint, PathToolMessage::PointerMove { .. }) => {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
// `tool_data.update_insertion` would be called on `OverlaysMessage::Draw`
|
||||
// we anyway should to call it on `::Draw` because we can change scale by ctrl+scroll without `::PointerMove`
|
||||
self
|
||||
}
|
||||
(Self::InsertPoint, PathToolMessage::Escape | PathToolMessage::Delete | PathToolMessage::RightClick) => tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort),
|
||||
(Self::InsertPoint, PathToolMessage::GRS { key: propagate }) => {
|
||||
// MAYBE: use `InputMapperMessage::KeyDown(..)` instead
|
||||
match propagate {
|
||||
Key::KeyG => responses.add(TransformLayerMessage::BeginGrab),
|
||||
Key::KeyR => responses.add(TransformLayerMessage::BeginRotate),
|
||||
Key::KeyS => responses.add(TransformLayerMessage::BeginScale),
|
||||
_ => warn!("Unexpected GRS key"),
|
||||
}
|
||||
tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort)
|
||||
}
|
||||
|
||||
// Mouse down
|
||||
(_, PathToolMessage::MouseDown { ctrl, shift }) => {
|
||||
let add_to_selection = input.keyboard.get(shift as usize);
|
||||
let direct_insert_without_sliding = input.keyboard.get(ctrl as usize);
|
||||
tool_data.mouse_down(shape_editor, document, input, responses, add_to_selection, direct_insert_without_sliding)
|
||||
}
|
||||
(PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { .. }) => {
|
||||
tool_data.previous_mouse_position = input.mouse.position;
|
||||
|
@ -407,7 +525,7 @@ impl Fsm for PathToolFsmState {
|
|||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point));
|
||||
if clicked_selected {
|
||||
shape_editor.deselect_all();
|
||||
shape_editor.select_point(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false);
|
||||
shape_editor.change_point_selection(&document.network, &document.metadata, input.mouse.position, SELECTION_THRESHOLD, false);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
|
@ -434,19 +552,15 @@ impl Fsm for PathToolFsmState {
|
|||
shape_editor.delete_point_and_break_path(&document.network, responses);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::InsertPoint) => {
|
||||
// First we try and flip the sharpness (if they have clicked on an anchor)
|
||||
if !shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses) {
|
||||
// If not, then we try and split the path that may have been clicked upon
|
||||
shape_editor.split(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||
(_, PathToolMessage::FlipSharp) => {
|
||||
if !tool_data.double_click_handled {
|
||||
shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
}
|
||||
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::Abort) => {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::PointerMove { .. }) => self,
|
||||
|
@ -511,6 +625,10 @@ impl Fsm for PathToolFsmState {
|
|||
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
||||
HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(),
|
||||
])]),
|
||||
PathToolFsmState::InsertPoint => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point")]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel Insertion").prepend_slash()]),
|
||||
]),
|
||||
};
|
||||
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
|
|
|
@ -31,6 +31,11 @@ pub enum BezierHandles {
|
|||
handle_end: DVec2,
|
||||
},
|
||||
}
|
||||
impl BezierHandles {
|
||||
pub fn is_cubic(&self) -> bool {
|
||||
matches!(self, Self::Cubic { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dyn-any")]
|
||||
unsafe impl dyn_any::StaticType for BezierHandles {
|
||||
|
|
|
@ -117,6 +117,18 @@ impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId>
|
|||
std::mem::swap(&mut self.in_handle, &mut self.out_handle);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_in_handle(&self) -> bool {
|
||||
self.in_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn has_out_handle(&self) -> bool {
|
||||
self.out_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn has_handle(anchor: DVec2, handle: DVec2) -> bool {
|
||||
!((handle.x - anchor.x).abs() < f64::EPSILON && (handle.y - anchor.y).abs() < f64::EPSILON)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue