mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 00:08:12 +08: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:
co-authored by
Keavon Chambers
parent
a412a77062
commit
f25038067e
@@ -45,6 +45,8 @@ pub const BOUNDS_ROTATE_THRESHOLD: f64 = 20.;
|
|||||||
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 5.;
|
pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 5.;
|
||||||
pub const SELECTION_THRESHOLD: f64 = 10.;
|
pub const SELECTION_THRESHOLD: f64 = 10.;
|
||||||
pub const HIDE_HANDLE_DISTANCE: f64 = 3.;
|
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
|
// Pen tool
|
||||||
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
|
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(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
|
||||||
entry!(KeyDown(Delete); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
|
entry!(KeyDown(Delete); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||||
entry!(KeyDown(Backspace); 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!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift }),
|
||||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||||
entry!(KeyDown(KeyA); modifiers=[Control], action_dispatch=PathToolMessage::SelectAllPoints),
|
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 {
|
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter {
|
||||||
add_to_selection: Shift
|
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); 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=[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 }),
|
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.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 = first_manipulator.anchor;
|
||||||
let anchor_position = transform.transform_point2(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() {
|
if let Some(last_manipulator) = manipulator_groups.last() {
|
||||||
let anchor = last_manipulator.anchor;
|
let anchor = last_manipulator.anchor;
|
||||||
let anchor_position = transform.transform_point2(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");
|
.expect("draw circle");
|
||||||
|
|
||||||
let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
|
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.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_OVERLAY_BLUE));
|
||||||
self.render_context.stroke();
|
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();
|
self.render_context.begin_path();
|
||||||
let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.;
|
let corner = position - DVec2::splat(MANIPULATOR_GROUP_MARKER_SIZE) / 2.;
|
||||||
self.render_context
|
self.render_context
|
||||||
.rect(corner.x.round(), corner.y.round(), MANIPULATOR_GROUP_MARKER_SIZE, MANIPULATOR_GROUP_MARKER_SIZE);
|
.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 };
|
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.set_fill_style(&wasm_bindgen::JsValue::from_str(fill));
|
||||||
self.render_context.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();
|
self.render_context.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ pub fn get_text(layer: LayerNodeIdentifier, document_network: &NodeNetwork) -> O
|
|||||||
Some((text, font, font_size))
|
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.
|
/// 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 {
|
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())
|
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::graph_modification_utils;
|
||||||
use super::snapping::{group_smooth, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
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::node_graph::VectorDataModification;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||||
use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource};
|
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 bezier_rs::{Bezier, ManipulatorGroup, TValue};
|
||||||
use graph_craft::document::NodeNetwork;
|
use graph_craft::document::NodeNetwork;
|
||||||
|
use graphene_core::transform::Transform;
|
||||||
use graphene_core::uuid::ManipulatorGroupId;
|
use graphene_core::uuid::ManipulatorGroupId;
|
||||||
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
use graphene_core::vector::{ManipulatorPointId, SelectedType};
|
||||||
|
|
||||||
@@ -64,6 +65,190 @@ pub struct ManipulatorPointInfo {
|
|||||||
|
|
||||||
pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<ManipulatorGroupId, Option<f64>>>;
|
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
|
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
|
||||||
impl ShapeState {
|
impl ShapeState {
|
||||||
// Snap, returning a viewport delta
|
// Snap, returning a viewport delta
|
||||||
@@ -97,7 +282,7 @@ impl ShapeState {
|
|||||||
} else {
|
} else {
|
||||||
SnapSource::Geometry(GeometrySnapSource::Sharp)
|
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 point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
|
||||||
|
|
||||||
let mut push_neighbor = |group: ManipulatorGroup<ManipulatorGroupId>| {
|
let mut push_neighbor = |group: ManipulatorGroup<ManipulatorGroupId>| {
|
||||||
@@ -133,23 +318,30 @@ impl ShapeState {
|
|||||||
document.metadata.document_to_viewport.transform_vector2(offset)
|
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.
|
/// 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,
|
&mut self,
|
||||||
document_network: &NodeNetwork,
|
document_network: &NodeNetwork,
|
||||||
document_metadata: &DocumentMetadata,
|
document_metadata: &DocumentMetadata,
|
||||||
mouse_position: DVec2,
|
mouse_position: DVec2,
|
||||||
select_threshold: f64,
|
select_threshold: f64,
|
||||||
add_to_selection: bool,
|
add_to_selection: bool,
|
||||||
) -> Option<SelectedPointsInfo> {
|
) -> Option<Option<SelectedPointsInfo>> {
|
||||||
if self.selected_shape_state.is_empty() {
|
if self.selected_shape_state.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(document_network, document_metadata, mouse_position, select_threshold) {
|
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 subpaths = get_subpaths(layer, document_network)?;
|
||||||
let manipulator_group = get_manipulator_groups(subpaths).find(|group| group.id == manipulator_point_id.group)?;
|
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)?;
|
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?
|
// Should we select or deselect the point?
|
||||||
let new_selected = if already_selected { !add_to_selection } else { true };
|
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
|
// This is selecting the manipulator only for now, next to generalize to points
|
||||||
if new_selected {
|
if new_selected {
|
||||||
let retain_existing_selection = add_to_selection || already_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)?;
|
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||||
selected_shape_state.select_point(manipulator_point_id);
|
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
|
let points = self
|
||||||
.selected_shape_state
|
.selected_shape_state
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
|
.flat_map(|(layer, state)| state.selected_points.iter().map(|&point_id| ManipulatorPointInfo { layer: *layer, point_id }))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
return Some(SelectedPointsInfo { points, offset });
|
return Some(Some(SelectedPointsInfo { points, offset }));
|
||||||
} else {
|
} else {
|
||||||
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
let selected_shape_state = self.selected_shape_state.get_mut(&layer)?;
|
||||||
selected_shape_state.deselect_point(manipulator_point_id);
|
selected_shape_state.deselect_point(manipulator_point_id);
|
||||||
|
|
||||||
return None;
|
return Some(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -219,6 +411,13 @@ impl ShapeState {
|
|||||||
self.selected_shape_state.keys()
|
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 {
|
pub fn has_selected_layers(&self) -> bool {
|
||||||
!self.selected_shape_state.is_empty()
|
!self.selected_shape_state.is_empty()
|
||||||
}
|
}
|
||||||
@@ -228,7 +427,6 @@ impl ShapeState {
|
|||||||
self.iter(document_network).flat_map(|subpaths| get_manipulator_groups(subpaths))
|
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) {
|
pub fn select_all_anchors(&mut self, document_network: &NodeNetwork, layer: LayerNodeIdentifier) {
|
||||||
let Some(subpaths) = get_subpaths(layer, document_network) else { return };
|
let Some(subpaths) = get_subpaths(layer, document_network) else { return };
|
||||||
let Some(state) = self.selected_shape_state.get_mut(&layer) 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.
|
/// Find the `t` value along the path segment we have clicked upon, together with that segment ID.
|
||||||
fn closest_segment(
|
fn closest_segment(&self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, layer: LayerNodeIdentifier, position: glam::DVec2, tolerance: f64) -> Option<ClosestSegment> {
|
||||||
&self,
|
|
||||||
document_network: &NodeNetwork,
|
|
||||||
document_metadata: &DocumentMetadata,
|
|
||||||
layer: LayerNodeIdentifier,
|
|
||||||
position: glam::DVec2,
|
|
||||||
tolerance: f64,
|
|
||||||
) -> Option<(ManipulatorGroupId, ManipulatorGroupId, Bezier, f64)> {
|
|
||||||
let transform = document_metadata.transform_to_viewport(layer);
|
let transform = document_metadata.transform_to_viewport(layer);
|
||||||
let layer_pos = transform.inverse().transform_point2(position);
|
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 mut closest_distance_squared: f64 = tolerance * tolerance;
|
||||||
|
|
||||||
let subpaths = get_subpaths(layer, document_network)?;
|
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() {
|
for (manipulator_index, bezier) in subpath.iter().enumerate() {
|
||||||
let t = bezier.project(layer_pos, Some(projection_options));
|
let t = bezier.project(layer_pos, Some(projection_options));
|
||||||
let layerspace = bezier.evaluate(TValue::Parametric(t));
|
let layerspace = bezier.evaluate(TValue::Parametric(t));
|
||||||
@@ -922,50 +1117,41 @@ impl ShapeState {
|
|||||||
|
|
||||||
if distance_squared < closest_distance_squared {
|
if distance_squared < closest_distance_squared {
|
||||||
closest_distance_squared = distance_squared;
|
closest_distance_squared = distance_squared;
|
||||||
let start = subpath.manipulator_groups()[manipulator_index];
|
|
||||||
let end = subpath.manipulator_groups()[(manipulator_index + 1) % subpath.len()];
|
let info = ClosestSegmentInfo {
|
||||||
result = Some((start.id, end.id, bezier, t));
|
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).
|
/// 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>) {
|
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(segment) = self.upper_closest_segment(document_network, document_metadata, position, tolerance) {
|
||||||
if let Some((start, end, bezier, t)) = self.closest_segment(document_network, document_metadata, layer, position, tolerance) {
|
segment.adjusted_insert(responses);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ impl SnapManager {
|
|||||||
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
|
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.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));
|
overlay_context.quad(self.transform * Quad::from_box(self.bounds));
|
||||||
|
|
||||||
for position in self.evaluate_transform_handle_positions() {
|
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 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_functions::path_overlays;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
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::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 crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager};
|
||||||
|
|
||||||
use graph_craft::document::NodeNetwork;
|
use graph_craft::document::NodeNetwork;
|
||||||
@@ -35,18 +35,24 @@ pub enum PathToolMessage {
|
|||||||
BreakPath,
|
BreakPath,
|
||||||
Delete,
|
Delete,
|
||||||
DeleteAndBreakPath,
|
DeleteAndBreakPath,
|
||||||
DragStart {
|
|
||||||
add_to_selection: Key,
|
|
||||||
},
|
|
||||||
DragStop {
|
DragStop {
|
||||||
shift_mirror_distance: Key,
|
shift_mirror_distance: Key,
|
||||||
},
|
},
|
||||||
Enter {
|
Enter {
|
||||||
add_to_selection: Key,
|
add_to_selection: Key,
|
||||||
},
|
},
|
||||||
InsertPoint,
|
Escape,
|
||||||
|
FlipSharp,
|
||||||
|
GRS {
|
||||||
|
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
|
||||||
|
key: Key,
|
||||||
|
},
|
||||||
ManipulatorAngleMakeSharp,
|
ManipulatorAngleMakeSharp,
|
||||||
ManipulatorAngleMakeSmooth,
|
ManipulatorAngleMakeSmooth,
|
||||||
|
MouseDown {
|
||||||
|
ctrl: Key,
|
||||||
|
shift: Key,
|
||||||
|
},
|
||||||
NudgeSelectedPoints {
|
NudgeSelectedPoints {
|
||||||
delta_x: f64,
|
delta_x: f64,
|
||||||
delta_y: f64,
|
delta_y: f64,
|
||||||
@@ -55,6 +61,7 @@ pub enum PathToolMessage {
|
|||||||
alt: Key,
|
alt: Key,
|
||||||
shift: Key,
|
shift: Key,
|
||||||
},
|
},
|
||||||
|
RightClick,
|
||||||
SelectAllPoints,
|
SelectAllPoints,
|
||||||
SelectedPointUpdated,
|
SelectedPointUpdated,
|
||||||
SelectedPointXChanged {
|
SelectedPointXChanged {
|
||||||
@@ -155,8 +162,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
|
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(PathToolMessageDiscriminant;
|
Ready => actions!(PathToolMessageDiscriminant;
|
||||||
InsertPoint,
|
FlipSharp,
|
||||||
DragStart,
|
MouseDown,
|
||||||
Delete,
|
Delete,
|
||||||
NudgeSelectedPoints,
|
NudgeSelectedPoints,
|
||||||
Enter,
|
Enter,
|
||||||
@@ -165,7 +172,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
DeleteAndBreakPath,
|
DeleteAndBreakPath,
|
||||||
),
|
),
|
||||||
Dragging => actions!(PathToolMessageDiscriminant;
|
Dragging => actions!(PathToolMessageDiscriminant;
|
||||||
InsertPoint,
|
FlipSharp,
|
||||||
DragStop,
|
DragStop,
|
||||||
PointerMove,
|
PointerMove,
|
||||||
Delete,
|
Delete,
|
||||||
@@ -174,7 +181,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
DeleteAndBreakPath,
|
DeleteAndBreakPath,
|
||||||
),
|
),
|
||||||
DrawingBox => actions!(PathToolMessageDiscriminant;
|
DrawingBox => actions!(PathToolMessageDiscriminant;
|
||||||
InsertPoint,
|
FlipSharp,
|
||||||
DragStop,
|
DragStop,
|
||||||
PointerMove,
|
PointerMove,
|
||||||
Delete,
|
Delete,
|
||||||
@@ -183,6 +190,15 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
BreakPath,
|
BreakPath,
|
||||||
DeleteAndBreakPath,
|
DeleteAndBreakPath,
|
||||||
),
|
),
|
||||||
|
InsertPoint => actions!(PathToolMessageDiscriminant;
|
||||||
|
Enter,
|
||||||
|
MouseDown,
|
||||||
|
PointerMove,
|
||||||
|
Escape,
|
||||||
|
Delete,
|
||||||
|
RightClick,
|
||||||
|
GRS,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,6 +220,12 @@ enum PathToolFsmState {
|
|||||||
Ready,
|
Ready,
|
||||||
Dragging,
|
Dragging,
|
||||||
DrawingBox,
|
DrawingBox,
|
||||||
|
InsertPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InsertEndKind {
|
||||||
|
Abort,
|
||||||
|
Add { shift: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[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).
|
/// 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.
|
/// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected.
|
||||||
selection_status: SelectionStatus,
|
selection_status: SelectionStatus,
|
||||||
|
segment: Option<ClosestSegment>,
|
||||||
|
double_click_handled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PathToolData {
|
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(
|
fn mouse_down(
|
||||||
&mut self,
|
&mut self,
|
||||||
shift: bool,
|
|
||||||
shape_editor: &mut ShapeState,
|
shape_editor: &mut ShapeState,
|
||||||
document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
input: &InputPreprocessorMessageHandler,
|
input: &InputPreprocessorMessageHandler,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
|
add_to_selection: bool,
|
||||||
|
direct_insert_without_sliding: bool,
|
||||||
) -> PathToolFsmState {
|
) -> PathToolFsmState {
|
||||||
|
self.double_click_handled = false;
|
||||||
self.opposing_handle_lengths = None;
|
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)
|
// 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) {
|
if let Some(selected_points) = shape_editor.change_point_selection(document_network, document_metadata, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) {
|
||||||
self.start_dragging_point(selected_points, input, document, responses);
|
if let Some(selected_points) = selected_points {
|
||||||
responses.add(OverlaysMessage::Draw);
|
self.start_dragging_point(selected_points, input, document, responses);
|
||||||
|
responses.add(OverlaysMessage::Draw);
|
||||||
|
}
|
||||||
PathToolFsmState::Dragging
|
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) {
|
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()] });
|
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
|
||||||
} else {
|
} else {
|
||||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
|
||||||
@@ -336,21 +416,59 @@ impl Fsm for PathToolFsmState {
|
|||||||
(_, PathToolMessage::Overlays(mut overlay_context)) => {
|
(_, PathToolMessage::Overlays(mut overlay_context)) => {
|
||||||
path_overlays(document, shape_editor, &mut overlay_context);
|
path_overlays(document, shape_editor, &mut overlay_context);
|
||||||
|
|
||||||
if self == Self::DrawingBox {
|
match self {
|
||||||
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]))
|
Self::DrawingBox => {
|
||||||
} else if self == Self::Dragging {
|
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]));
|
||||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
}
|
||||||
|
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);
|
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||||
|
|
||||||
self
|
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 { .. }) => {
|
(PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { .. }) => {
|
||||||
tool_data.previous_mouse_position = input.mouse.position;
|
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));
|
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == Some(point));
|
||||||
if clicked_selected {
|
if clicked_selected {
|
||||||
shape_editor.deselect_all();
|
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);
|
responses.add(OverlaysMessage::Draw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,19 +552,15 @@ impl Fsm for PathToolFsmState {
|
|||||||
shape_editor.delete_point_and_break_path(&document.network, responses);
|
shape_editor.delete_point_and_break_path(&document.network, responses);
|
||||||
PathToolFsmState::Ready
|
PathToolFsmState::Ready
|
||||||
}
|
}
|
||||||
(_, PathToolMessage::InsertPoint) => {
|
(_, PathToolMessage::FlipSharp) => {
|
||||||
// First we try and flip the sharpness (if they have clicked on an anchor)
|
if !tool_data.double_click_handled {
|
||||||
if !shape_editor.flip_sharp(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses) {
|
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
|
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||||
shape_editor.split(&document.network, &document.metadata, input.mouse.position, SELECTION_TOLERANCE, responses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(_, PathToolMessage::Abort) => {
|
(_, PathToolMessage::Abort) => {
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
|
|
||||||
PathToolFsmState::Ready
|
PathToolFsmState::Ready
|
||||||
}
|
}
|
||||||
(_, PathToolMessage::PointerMove { .. }) => self,
|
(_, PathToolMessage::PointerMove { .. }) => self,
|
||||||
@@ -511,6 +625,10 @@ impl Fsm for PathToolFsmState {
|
|||||||
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
||||||
HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(),
|
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 });
|
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ pub enum BezierHandles {
|
|||||||
handle_end: DVec2,
|
handle_end: DVec2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
impl BezierHandles {
|
||||||
|
pub fn is_cubic(&self) -> bool {
|
||||||
|
matches!(self, Self::Cubic { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "dyn-any")]
|
#[cfg(feature = "dyn-any")]
|
||||||
unsafe impl dyn_any::StaticType for BezierHandles {
|
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);
|
std::mem::swap(&mut self.in_handle, &mut self.out_handle);
|
||||||
self
|
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)]
|
#[derive(Copy, Clone)]
|
||||||
|
|||||||
Reference in New Issue
Block a user