mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 16:18:11 +08:00
Add molding segments to the Path tool (#2660)
* Moulding of cubic bezier * Implemented falloff with interpolation * remove conflict * Move falloff to consts * Spelling * Refine falloff param and bug fix * Code review * Add colinear disable modes to molding degment feat * Clean comments and unused code * Code refactor * Fix error * Change colinear toggle behaviour * Code review * Remove KeyC feat + Fix overlay * Dynamic hints in path tool * Revamp molding logic * Code review * Remove unused Bezier algorithms (maybe useful for future reference) --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
4696004dc9
commit
f72263f4f8
@@ -218,7 +218,7 @@ pub fn input_mappings() -> Mapping {
|
|||||||
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
|
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
|
||||||
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
|
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
|
||||||
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
|
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
|
||||||
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt }),
|
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt }),
|
||||||
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
|
||||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
|
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
|
||||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
|
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::graph_modification_utils::merge_layers;
|
use super::graph_modification_utils::merge_layers;
|
||||||
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
||||||
use super::utility_functions::calculate_segment_angle;
|
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position};
|
||||||
use crate::consts::HANDLE_LENGTH_FACTOR;
|
use crate::consts::HANDLE_LENGTH_FACTOR;
|
||||||
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
|
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||||
@@ -282,6 +282,71 @@ impl ClosestSegment {
|
|||||||
.unwrap_or(DVec2::ZERO);
|
.unwrap_or(DVec2::ZERO);
|
||||||
tangent.perp()
|
tangent.perp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Molding the bezier curve.
|
||||||
|
/// Returns adjacent handles' [`HandleId`] if colinearity is broken temporarily.
|
||||||
|
pub fn mold_handle_positions(
|
||||||
|
&self,
|
||||||
|
document: &DocumentMessageHandler,
|
||||||
|
responses: &mut VecDeque<Message>,
|
||||||
|
(c1, c2): (DVec2, DVec2),
|
||||||
|
new_b: DVec2,
|
||||||
|
break_colinear_molding: bool,
|
||||||
|
temporary_adjacent_handles_while_molding: Option<[Option<HandleId>; 2]>,
|
||||||
|
) -> Option<[Option<HandleId>; 2]> {
|
||||||
|
let transform = document.metadata().transform_to_viewport(self.layer);
|
||||||
|
|
||||||
|
let start = self.bezier.start;
|
||||||
|
let end = self.bezier.end;
|
||||||
|
|
||||||
|
// Apply the drag delta to the segment's handles
|
||||||
|
let b = self.bezier_point_to_viewport;
|
||||||
|
let delta = transform.inverse().transform_vector2(new_b - b);
|
||||||
|
let (nc1, nc2) = (c1 + delta, c2 + delta);
|
||||||
|
|
||||||
|
let handle1 = HandleId::primary(self.segment);
|
||||||
|
let handle2 = HandleId::end(self.segment);
|
||||||
|
let layer = self.layer;
|
||||||
|
|
||||||
|
let modification_type = handle1.set_relative_position(nc1 - start);
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
|
||||||
|
let modification_type = handle2.set_relative_position(nc2 - end);
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
|
||||||
|
// If adjacent segments have colinear handles, their direction is changed but their handle lengths is preserved
|
||||||
|
// TODO: Find something which is more appropriate
|
||||||
|
let vector_data = document.network_interface.compute_modified_vector(self.layer())?;
|
||||||
|
|
||||||
|
if break_colinear_molding {
|
||||||
|
// Disable G1 continuity
|
||||||
|
let other_handles = [
|
||||||
|
restore_previous_handle_position(handle1, c1, start, &vector_data, layer, responses),
|
||||||
|
restore_previous_handle_position(handle2, c2, end, &vector_data, layer, responses),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Store other HandleId in tool data to regain colinearity later
|
||||||
|
if temporary_adjacent_handles_while_molding.is_some() {
|
||||||
|
temporary_adjacent_handles_while_molding
|
||||||
|
} else {
|
||||||
|
Some(other_handles)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Move the colinear handles so that colinearity is maintained
|
||||||
|
adjust_handle_colinearity(handle1, start, nc1, &vector_data, layer, responses);
|
||||||
|
adjust_handle_colinearity(handle2, end, nc2, &vector_data, layer, responses);
|
||||||
|
|
||||||
|
if let Some(adjacent_handles) = temporary_adjacent_handles_while_molding {
|
||||||
|
if let Some(other_handle1) = adjacent_handles[0] {
|
||||||
|
restore_g1_continuity(handle1, other_handle1, nc1, start, &vector_data, layer, responses);
|
||||||
|
}
|
||||||
|
if let Some(other_handle2) = adjacent_handles[1] {
|
||||||
|
restore_g1_continuity(handle2, other_handle2, nc2, end, &vector_data, layer, responses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
|
|||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphene_core::renderer::Quad;
|
use graphene_core::renderer::Quad;
|
||||||
use graphene_core::text::{FontCache, load_face};
|
use graphene_core::text::{FontCache, load_face};
|
||||||
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorData};
|
use graphene_std::vector::{HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
|
||||||
|
|
||||||
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
|
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
|
||||||
pub fn should_extend(
|
pub fn should_extend(
|
||||||
@@ -95,6 +95,65 @@ pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data:
|
|||||||
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
|
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn adjust_handle_colinearity(handle: HandleId, anchor_position: DVec2, target_control_point: DVec2, vector_data: &VectorData, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||||
|
let Some(other_handle) = vector_data.other_colinear_handle(handle) else { return };
|
||||||
|
let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(direction) = (anchor_position - target_control_point).try_normalize() else { return };
|
||||||
|
|
||||||
|
let new_relative_position = (handle_position - anchor_position).length() * direction;
|
||||||
|
let modification_type = other_handle.set_relative_position(new_relative_position);
|
||||||
|
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_previous_handle_position(
|
||||||
|
handle: HandleId,
|
||||||
|
original_c: DVec2,
|
||||||
|
anchor_position: DVec2,
|
||||||
|
vector_data: &VectorData,
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
responses: &mut VecDeque<Message>,
|
||||||
|
) -> Option<HandleId> {
|
||||||
|
let other_handle = vector_data.other_colinear_handle(handle)?;
|
||||||
|
let handle_position = other_handle.to_manipulator_point().get_position(vector_data)?;
|
||||||
|
let direction = (anchor_position - original_c).try_normalize()?;
|
||||||
|
|
||||||
|
let old_relative_position = (handle_position - anchor_position).length() * direction;
|
||||||
|
let modification_type = other_handle.set_relative_position(old_relative_position);
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
|
||||||
|
let handles = [handle, other_handle];
|
||||||
|
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
|
||||||
|
Some(other_handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_g1_continuity(
|
||||||
|
handle: HandleId,
|
||||||
|
other_handle: HandleId,
|
||||||
|
control_point: DVec2,
|
||||||
|
anchor_position: DVec2,
|
||||||
|
vector_data: &VectorData,
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
responses: &mut VecDeque<Message>,
|
||||||
|
) {
|
||||||
|
let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(direction) = (anchor_position - control_point).try_normalize() else { return };
|
||||||
|
|
||||||
|
let new_relative_position = (handle_position - anchor_position).length() * direction;
|
||||||
|
let modification_type = other_handle.set_relative_position(new_relative_position);
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
|
||||||
|
let handles = [handle, other_handle];
|
||||||
|
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true };
|
||||||
|
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||||
|
}
|
||||||
|
|
||||||
/// Check whether a point is visible in the current overlay mode.
|
/// Check whether a point is visible in the current overlay mode.
|
||||||
pub fn is_visible_point(
|
pub fn is_visible_point(
|
||||||
manipulator_point_id: ManipulatorPointId,
|
manipulator_point_id: ManipulatorPointId,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ pub enum PathToolMessage {
|
|||||||
snap_angle: Key,
|
snap_angle: Key,
|
||||||
lock_angle: Key,
|
lock_angle: Key,
|
||||||
delete_segment: Key,
|
delete_segment: Key,
|
||||||
|
break_colinear_molding: Key,
|
||||||
},
|
},
|
||||||
PointerOutsideViewport {
|
PointerOutsideViewport {
|
||||||
equidistant: Key,
|
equidistant: Key,
|
||||||
@@ -88,6 +89,7 @@ pub enum PathToolMessage {
|
|||||||
snap_angle: Key,
|
snap_angle: Key,
|
||||||
lock_angle: Key,
|
lock_angle: Key,
|
||||||
delete_segment: Key,
|
delete_segment: Key,
|
||||||
|
break_colinear_molding: Key,
|
||||||
},
|
},
|
||||||
RightClick,
|
RightClick,
|
||||||
SelectAllAnchors,
|
SelectAllAnchors,
|
||||||
@@ -306,6 +308,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||||||
Escape,
|
Escape,
|
||||||
RightClick,
|
RightClick,
|
||||||
),
|
),
|
||||||
|
PathToolFsmState::MoldingSegment => actions!(PathToolMessageDiscriminant;
|
||||||
|
PointerMove,
|
||||||
|
DragStop,
|
||||||
|
RightClick,
|
||||||
|
Escape,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,6 +350,7 @@ enum PathToolFsmState {
|
|||||||
Drawing {
|
Drawing {
|
||||||
selection_shape: SelectionShapeType,
|
selection_shape: SelectionShapeType,
|
||||||
},
|
},
|
||||||
|
MoldingSegment,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@@ -379,6 +388,9 @@ struct PathToolData {
|
|||||||
alt_dragging_from_anchor: bool,
|
alt_dragging_from_anchor: bool,
|
||||||
angle_locked: bool,
|
angle_locked: bool,
|
||||||
temporary_colinear_handles: bool,
|
temporary_colinear_handles: bool,
|
||||||
|
molding_info: Option<(DVec2, DVec2)>,
|
||||||
|
molding_segment: bool,
|
||||||
|
temporary_adjacent_handles_while_molding: Option<[Option<HandleId>; 2]>,
|
||||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||||
adjacent_anchor_offset: Option<DVec2>,
|
adjacent_anchor_offset: Option<DVec2>,
|
||||||
}
|
}
|
||||||
@@ -562,19 +574,17 @@ impl PathToolData {
|
|||||||
else if let Some(closed_segment) = &mut self.segment {
|
else if let Some(closed_segment) = &mut self.segment {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
|
||||||
if self.delete_segment_pressed {
|
// Calculating and storing handle positions
|
||||||
if let Some(vector_data) = document.network_interface.compute_modified_vector(closed_segment.layer()) {
|
let handle1 = ManipulatorPointId::PrimaryHandle(closed_segment.segment());
|
||||||
shape_editor.dissolve_segment(responses, closed_segment.layer(), &vector_data, closed_segment.segment(), closed_segment.points());
|
let handle2 = ManipulatorPointId::EndHandle(closed_segment.segment());
|
||||||
responses.add(DocumentMessage::EndTransaction);
|
|
||||||
|
if let Some(vector_data) = document.network_interface.compute_modified_vector(closed_segment.layer()) {
|
||||||
|
if let (Some(pos1), Some(pos2)) = (handle1.get_position(&vector_data), handle2.get_position(&vector_data)) {
|
||||||
|
self.molding_info = Some((pos1, pos2))
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
closed_segment.adjusted_insert_and_select(shape_editor, responses, extend_selection);
|
|
||||||
responses.add(DocumentMessage::EndTransaction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.segment = None;
|
PathToolFsmState::MoldingSegment
|
||||||
|
|
||||||
PathToolFsmState::Ready
|
|
||||||
}
|
}
|
||||||
// We didn't find a segment, so consider selecting the nearest shape instead
|
// We didn't find a segment, so consider selecting the nearest shape instead
|
||||||
else if let Some(layer) = document.click(input) {
|
else if let Some(layer) = document.click(input) {
|
||||||
@@ -1045,6 +1055,9 @@ impl Fsm for PathToolFsmState {
|
|||||||
|
|
||||||
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
|
||||||
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
|
let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data;
|
||||||
|
|
||||||
|
update_dynamic_hints(self, responses, shape_editor, document, tool_data);
|
||||||
|
|
||||||
let ToolMessage::Path(event) = event else { return self };
|
let ToolMessage::Path(event) = event else { return self };
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(_, PathToolMessage::SelectionChanged) => {
|
(_, PathToolMessage::SelectionChanged) => {
|
||||||
@@ -1127,6 +1140,32 @@ impl Fsm for PathToolFsmState {
|
|||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::Ready => {
|
Self::Ready => {
|
||||||
|
// Check if there is no point nearby
|
||||||
|
if shape_editor
|
||||||
|
.find_nearest_visible_point_indices(
|
||||||
|
&document.network_interface,
|
||||||
|
input.mouse.position,
|
||||||
|
SELECTION_THRESHOLD,
|
||||||
|
tool_options.path_overlay_mode,
|
||||||
|
tool_data.frontier_handles_info.clone(),
|
||||||
|
)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
tool_data.segment = None;
|
||||||
|
}
|
||||||
|
// If already hovering on a segment, then recalculate its closest point
|
||||||
|
else if let Some(closest_segment) = &mut tool_data.segment {
|
||||||
|
closest_segment.update_closest_point(document.metadata(), input.mouse.position);
|
||||||
|
|
||||||
|
if closest_segment.too_far(input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
|
||||||
|
tool_data.segment = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not, check that if there is some closest segment or not
|
||||||
|
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
|
||||||
|
tool_data.segment = Some(closest_segment);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(closest_segment) = &tool_data.segment {
|
if let Some(closest_segment) = &tool_data.segment {
|
||||||
let perp = closest_segment.calculate_perp(document);
|
let perp = closest_segment.calculate_perp(document);
|
||||||
let point = closest_segment.closest_point(document.metadata());
|
let point = closest_segment.closest_point(document.metadata());
|
||||||
@@ -1195,6 +1234,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Self::MoldingSegment => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||||
@@ -1241,6 +1281,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
||||||
@@ -1260,6 +1301,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
PathToolMessage::PointerMove {
|
PathToolMessage::PointerMove {
|
||||||
@@ -1269,6 +1311,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
];
|
];
|
||||||
@@ -1285,6 +1328,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
let mut selected_only_handles = true;
|
let mut selected_only_handles = true;
|
||||||
@@ -1356,6 +1400,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
PathToolMessage::PointerMove {
|
PathToolMessage::PointerMove {
|
||||||
@@ -1365,6 +1410,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
];
|
];
|
||||||
@@ -1372,6 +1418,29 @@ impl Fsm for PathToolFsmState {
|
|||||||
|
|
||||||
PathToolFsmState::Dragging(tool_data.dragging_state)
|
PathToolFsmState::Dragging(tool_data.dragging_state)
|
||||||
}
|
}
|
||||||
|
(PathToolFsmState::MoldingSegment, PathToolMessage::PointerMove { break_colinear_molding, .. }) => {
|
||||||
|
if tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
|
||||||
|
tool_data.molding_segment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let break_colinear_molding = input.keyboard.get(break_colinear_molding as usize);
|
||||||
|
|
||||||
|
// Logic for molding segment
|
||||||
|
if let Some(segment) = &mut tool_data.segment {
|
||||||
|
if let Some(molding_segment_handles) = tool_data.molding_info {
|
||||||
|
tool_data.temporary_adjacent_handles_while_molding = segment.mold_handle_positions(
|
||||||
|
document,
|
||||||
|
responses,
|
||||||
|
molding_segment_handles,
|
||||||
|
input.mouse.position,
|
||||||
|
break_colinear_molding,
|
||||||
|
tool_data.temporary_adjacent_handles_while_molding,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathToolFsmState::MoldingSegment
|
||||||
|
}
|
||||||
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
||||||
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
|
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
|
||||||
|
|
||||||
@@ -1383,33 +1452,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
tool_data.adjacent_anchor_offset = None;
|
tool_data.adjacent_anchor_offset = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a point nearby, then remove the overlay
|
responses.add(OverlaysMessage::Draw);
|
||||||
if shape_editor
|
|
||||||
.find_nearest_visible_point_indices(
|
|
||||||
&document.network_interface,
|
|
||||||
input.mouse.position,
|
|
||||||
SELECTION_THRESHOLD,
|
|
||||||
tool_options.path_overlay_mode,
|
|
||||||
tool_data.frontier_handles_info.clone(),
|
|
||||||
)
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
tool_data.segment = None;
|
|
||||||
responses.add(OverlaysMessage::Draw)
|
|
||||||
}
|
|
||||||
// If already hovering on a segment, then recalculate its closest point
|
|
||||||
else if let Some(closest_segment) = &mut tool_data.segment {
|
|
||||||
closest_segment.update_closest_point(document.metadata(), input.mouse.position);
|
|
||||||
if closest_segment.too_far(input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
|
|
||||||
tool_data.segment = None;
|
|
||||||
}
|
|
||||||
responses.add(OverlaysMessage::Draw)
|
|
||||||
}
|
|
||||||
// If not, check that if there is some closest segment or not
|
|
||||||
else if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SEGMENT_INSERTION_DISTANCE) {
|
|
||||||
tool_data.segment = Some(closest_segment);
|
|
||||||
responses.add(OverlaysMessage::Draw)
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -1438,6 +1481,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
// Auto-panning
|
// Auto-panning
|
||||||
@@ -1449,6 +1493,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
PathToolMessage::PointerMove {
|
PathToolMessage::PointerMove {
|
||||||
@@ -1458,6 +1503,7 @@ impl Fsm for PathToolFsmState {
|
|||||||
snap_angle,
|
snap_angle,
|
||||||
lock_angle,
|
lock_angle,
|
||||||
delete_segment,
|
delete_segment,
|
||||||
|
break_colinear_molding,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
];
|
];
|
||||||
@@ -1524,6 +1570,17 @@ impl Fsm for PathToolFsmState {
|
|||||||
tool_data.snap_manager.cleanup(responses);
|
tool_data.snap_manager.cleanup(responses);
|
||||||
PathToolFsmState::Ready
|
PathToolFsmState::Ready
|
||||||
}
|
}
|
||||||
|
(PathToolFsmState::MoldingSegment, PathToolMessage::Escape | PathToolMessage::RightClick) => {
|
||||||
|
// Undo the molding and go back to the state before
|
||||||
|
tool_data.molding_info = None;
|
||||||
|
tool_data.molding_segment = false;
|
||||||
|
tool_data.temporary_adjacent_handles_while_molding = None;
|
||||||
|
|
||||||
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
|
tool_data.snap_manager.cleanup(responses);
|
||||||
|
|
||||||
|
PathToolFsmState::Ready
|
||||||
|
}
|
||||||
// Mouse up
|
// Mouse up
|
||||||
(PathToolFsmState::Drawing { selection_shape }, PathToolMessage::DragStop { extend_selection, shrink_selection }) => {
|
(PathToolFsmState::Drawing { selection_shape }, PathToolMessage::DragStop { extend_selection, shrink_selection }) => {
|
||||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||||
@@ -1579,6 +1636,29 @@ impl Fsm for PathToolFsmState {
|
|||||||
tool_data.frontier_handles_info.clone(),
|
tool_data.frontier_handles_info.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if let Some(segment) = &mut tool_data.segment {
|
||||||
|
if !drag_occurred && !tool_data.molding_segment {
|
||||||
|
if tool_data.delete_segment_pressed {
|
||||||
|
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) {
|
||||||
|
shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points());
|
||||||
|
responses.add(DocumentMessage::EndTransaction);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
segment.adjusted_insert_and_select(shape_editor, responses, extend_selection);
|
||||||
|
responses.add(DocumentMessage::EndTransaction);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
responses.add(DocumentMessage::EndTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
tool_data.segment = None;
|
||||||
|
tool_data.molding_info = None;
|
||||||
|
tool_data.molding_segment = false;
|
||||||
|
tool_data.temporary_adjacent_handles_while_molding = None;
|
||||||
|
|
||||||
|
return PathToolFsmState::Ready;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((layer, nearest_point)) = nearest_point {
|
if let Some((layer, nearest_point)) = nearest_point {
|
||||||
if !drag_occurred && extend_selection {
|
if !drag_occurred && extend_selection {
|
||||||
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
|
||||||
@@ -1756,98 +1836,8 @@ impl Fsm for PathToolFsmState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
fn update_hints(&self, _responses: &mut VecDeque<Message>) {
|
||||||
let hint_data = match self {
|
// Moved logic to update_dynamic_hints
|
||||||
PathToolFsmState::Ready => HintData(vec![
|
|
||||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
|
||||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
|
|
||||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
|
||||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
|
|
||||||
// TODO: Only show if at least one anchor is selected, and dynamically show either "Smooth" or "Sharp" based on the current state
|
|
||||||
HintGroup(vec![
|
|
||||||
HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"),
|
|
||||||
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"),
|
|
||||||
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"),
|
|
||||||
]),
|
|
||||||
// TODO: Only show the following hints if at least one point is selected
|
|
||||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
|
|
||||||
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
|
||||||
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
|
|
||||||
HintGroup(vec![
|
|
||||||
HintInfo::keys([Key::Delete], "Delete Selected"),
|
|
||||||
// TODO: Only show the following hints if at least one anchor is selected
|
|
||||||
HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus(),
|
|
||||||
HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus(),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
PathToolFsmState::Dragging(dragging_state) => {
|
|
||||||
let colinear = dragging_state.colinear;
|
|
||||||
let mut dragging_hint_data = HintData(Vec::new());
|
|
||||||
dragging_hint_data
|
|
||||||
.0
|
|
||||||
.push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]));
|
|
||||||
|
|
||||||
let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor");
|
|
||||||
let toggle_group = match dragging_state.point_select_state {
|
|
||||||
PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => {
|
|
||||||
let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Dragged Handle")];
|
|
||||||
hints.push(HintInfo::keys(
|
|
||||||
[Key::KeyC],
|
|
||||||
if colinear == ManipulatorAngle::Colinear {
|
|
||||||
"Break Colinear Handles"
|
|
||||||
} else {
|
|
||||||
"Make Handles Colinear"
|
|
||||||
},
|
|
||||||
));
|
|
||||||
hints
|
|
||||||
}
|
|
||||||
PointSelectState::Anchor => Vec::new(),
|
|
||||||
};
|
|
||||||
let hold_group = match dragging_state.point_select_state {
|
|
||||||
PointSelectState::HandleNoPair => {
|
|
||||||
let mut hints = vec![];
|
|
||||||
if colinear != ManipulatorAngle::Free {
|
|
||||||
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
|
|
||||||
}
|
|
||||||
hints.push(HintInfo::keys([Key::Shift], "15° Increments"));
|
|
||||||
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
|
|
||||||
hints.push(drag_anchor);
|
|
||||||
hints
|
|
||||||
}
|
|
||||||
PointSelectState::HandleWithPair => {
|
|
||||||
let mut hints = vec![];
|
|
||||||
if colinear != ManipulatorAngle::Free {
|
|
||||||
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
|
|
||||||
}
|
|
||||||
hints.push(HintInfo::keys([Key::Shift], "15° Increments"));
|
|
||||||
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
|
|
||||||
hints.push(drag_anchor);
|
|
||||||
hints
|
|
||||||
}
|
|
||||||
PointSelectState::Anchor => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !toggle_group.is_empty() {
|
|
||||||
dragging_hint_data.0.push(HintGroup(toggle_group));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hold_group.is_empty() {
|
|
||||||
dragging_hint_data.0.push(HintGroup(hold_group));
|
|
||||||
}
|
|
||||||
|
|
||||||
dragging_hint_data
|
|
||||||
}
|
|
||||||
PathToolFsmState::Drawing { .. } => HintData(vec![
|
|
||||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
|
||||||
HintGroup(vec![
|
|
||||||
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
|
||||||
HintInfo::keys([Key::Shift], "Extend").prepend_plus(),
|
|
||||||
HintInfo::keys([Key::Alt], "Subtract").prepend_plus(),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||||
@@ -2074,3 +2064,130 @@ fn calculate_adjacent_anchor_tangent(
|
|||||||
_ => (None, None),
|
_ => (None, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Message>, _shape_editor: &mut ShapeState, document: &DocumentMessageHandler, tool_data: &PathToolData) {
|
||||||
|
// Condinting based on currently selected segment if it has any one g1 continuous handle
|
||||||
|
|
||||||
|
let hint_data = match state {
|
||||||
|
PathToolFsmState::Ready => HintData(vec![
|
||||||
|
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
||||||
|
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
|
||||||
|
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||||
|
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
|
||||||
|
// TODO: Only show if at least one anchor is selected, and dynamically show either "Smooth" or "Sharp" based on the current state
|
||||||
|
HintGroup(vec![
|
||||||
|
HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"),
|
||||||
|
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"),
|
||||||
|
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"),
|
||||||
|
]),
|
||||||
|
// TODO: Only show the following hints if at least one point is selected
|
||||||
|
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
|
||||||
|
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
||||||
|
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
|
||||||
|
HintGroup(vec![
|
||||||
|
HintInfo::keys([Key::Delete], "Delete Selected"),
|
||||||
|
// TODO: Only show the following hints if at least one anchor is selected
|
||||||
|
HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus(),
|
||||||
|
HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
PathToolFsmState::Dragging(dragging_state) => {
|
||||||
|
let colinear = dragging_state.colinear;
|
||||||
|
let mut dragging_hint_data = HintData(Vec::new());
|
||||||
|
dragging_hint_data
|
||||||
|
.0
|
||||||
|
.push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]));
|
||||||
|
|
||||||
|
let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor");
|
||||||
|
let toggle_group = match dragging_state.point_select_state {
|
||||||
|
PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => {
|
||||||
|
let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Dragged Handle")];
|
||||||
|
hints.push(HintInfo::keys(
|
||||||
|
[Key::KeyC],
|
||||||
|
if colinear == ManipulatorAngle::Colinear {
|
||||||
|
"Break Colinear Handles"
|
||||||
|
} else {
|
||||||
|
"Make Handles Colinear"
|
||||||
|
},
|
||||||
|
));
|
||||||
|
hints
|
||||||
|
}
|
||||||
|
PointSelectState::Anchor => Vec::new(),
|
||||||
|
};
|
||||||
|
let hold_group = match dragging_state.point_select_state {
|
||||||
|
PointSelectState::HandleNoPair => {
|
||||||
|
let mut hints = vec![];
|
||||||
|
if colinear != ManipulatorAngle::Free {
|
||||||
|
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
|
||||||
|
}
|
||||||
|
hints.push(HintInfo::keys([Key::Shift], "15° Increments"));
|
||||||
|
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
|
||||||
|
hints.push(drag_anchor);
|
||||||
|
hints
|
||||||
|
}
|
||||||
|
PointSelectState::HandleWithPair => {
|
||||||
|
let mut hints = vec![];
|
||||||
|
if colinear != ManipulatorAngle::Free {
|
||||||
|
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
|
||||||
|
}
|
||||||
|
hints.push(HintInfo::keys([Key::Shift], "15° Increments"));
|
||||||
|
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
|
||||||
|
hints.push(drag_anchor);
|
||||||
|
hints
|
||||||
|
}
|
||||||
|
PointSelectState::Anchor => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !toggle_group.is_empty() {
|
||||||
|
dragging_hint_data.0.push(HintGroup(toggle_group));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hold_group.is_empty() {
|
||||||
|
dragging_hint_data.0.push(HintGroup(hold_group));
|
||||||
|
}
|
||||||
|
|
||||||
|
dragging_hint_data
|
||||||
|
}
|
||||||
|
PathToolFsmState::Drawing { .. } => HintData(vec![
|
||||||
|
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||||
|
HintGroup(vec![
|
||||||
|
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
||||||
|
HintInfo::keys([Key::Shift], "Extend").prepend_plus(),
|
||||||
|
HintInfo::keys([Key::Alt], "Subtract").prepend_plus(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
PathToolFsmState::MoldingSegment => {
|
||||||
|
let mut has_colinear_anchors = false;
|
||||||
|
|
||||||
|
if let Some(segment) = &tool_data.segment {
|
||||||
|
let handle1 = HandleId::primary(segment.segment());
|
||||||
|
let handle2 = HandleId::end(segment.segment());
|
||||||
|
|
||||||
|
if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) {
|
||||||
|
let other_handle1 = vector_data.other_colinear_handle(handle1);
|
||||||
|
let other_handle2 = vector_data.other_colinear_handle(handle2);
|
||||||
|
if other_handle1.is_some() || other_handle2.is_some() {
|
||||||
|
has_colinear_anchors = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let handles_stored = if let Some(other_handles) = tool_data.temporary_adjacent_handles_while_molding {
|
||||||
|
other_handles[0].is_some() || other_handles[1].is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let molding_disable_possible = has_colinear_anchors || handles_stored;
|
||||||
|
|
||||||
|
let mut molding_hints = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])];
|
||||||
|
|
||||||
|
if molding_disable_possible {
|
||||||
|
molding_hints.push(HintGroup(vec![HintInfo::keys([Key::Alt], "Break Colinear Handles")]));
|
||||||
|
}
|
||||||
|
|
||||||
|
HintData(molding_hints)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user