diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 1556699e..d9af75f7 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -1,11 +1,13 @@ -use super::utility_types::OverlayContext; +use super::utility_types::{DrawHandles, OverlayContext}; use crate::consts::HIDE_HANDLE_DISTANCE; use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState}; use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler}; use graphene_core::vector::ManipulatorPointId; +use graphene_std::vector::{PointId, SegmentId}; -use glam::DVec2; +use bezier_rs::{Bezier, BezierHandles}; +use glam::{DAffine2, DVec2}; use wasm_bindgen::JsCast; pub fn overlay_canvas_element() -> Option { @@ -23,41 +25,143 @@ pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d { create_context().expect("Failed to get canvas context") } -pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) { - for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { - let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { - continue; - }; - //let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); - let transform = document.metadata().transform_to_viewport(layer); - let selected = shape_editor.selected_shape_state.get(&layer); - let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); - overlay_context.outline_vector(&vector_data, transform); +pub fn selected_segments(document: &DocumentMessageHandler, shape_editor: &mut ShapeState) -> Vec { + let selected_points = shape_editor.selected_points(); + let selected_anchors = selected_points + .filter_map(|point_id| if let ManipulatorPointId::Anchor(p) = point_id { Some(*p) } else { None }) + .collect::>(); - for (segment_id, bezier, _start, _end) in vector_data.segment_bezier_iter() { - let bezier = bezier.apply_transformation(|point| transform.transform_point2(point)); - let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; - match bezier.handles { - bezier_rs::BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => { - overlay_context.line(handle, bezier.start, None); - overlay_context.line(handle, bezier.end, None); - overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)), None); - } - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { - if not_under_anchor(handle_start, bezier.start) { - overlay_context.line(handle_start, bezier.start, None); - overlay_context.manipulator_handle(handle_start, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)), None); - } - if not_under_anchor(handle_end, bezier.end) { - overlay_context.line(handle_end, bezier.end, None); - overlay_context.manipulator_handle(handle_end, is_selected(selected, ManipulatorPointId::EndHandle(segment_id)), None); - } - } - _ => {} + // Collect the segments whose handles are selected + let mut selected_segments = shape_editor + .selected_points() + .filter_map(|point_id| match point_id { + ManipulatorPointId::PrimaryHandle(segment_id) | ManipulatorPointId::EndHandle(segment_id) => Some(*segment_id), + ManipulatorPointId::Anchor(_) => None, + }) + .collect::>(); + + // TODO: Currently if there are two duplicate layers, both of their segments get overlays + // Adding segments which are are connected to selected anchors + for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + + for (segment_id, _bezier, start, end) in vector_data.segment_bezier_iter() { + if selected_anchors.contains(&start) || selected_anchors.contains(&end) { + selected_segments.push(segment_id); } } + } + + selected_segments +} + +fn overlay_bezier_handles(bezier: Bezier, segment_id: SegmentId, transform: DAffine2, is_selected: impl Fn(ManipulatorPointId) -> bool, overlay_context: &mut OverlayContext) { + let bezier = bezier.apply_transformation(|point| transform.transform_point2(point)); + let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; + + match bezier.handles { + BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => { + overlay_context.line(handle, bezier.start, None); + overlay_context.line(handle, bezier.end, None); + overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None); + } + BezierHandles::Cubic { handle_start, handle_end } => { + if not_under_anchor(handle_start, bezier.start) { + overlay_context.line(handle_start, bezier.start, None); + overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None); + } + if not_under_anchor(handle_end, bezier.end) { + overlay_context.line(handle_end, bezier.end, None); + overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None); + } + } + _ => {} + } +} + +pub fn overlay_bezier_handle_specific_point( + bezier: Bezier, + segment_id: SegmentId, + (start, end): (PointId, PointId), + point_to_render: PointId, + transform: DAffine2, + is_selected: impl Fn(ManipulatorPointId) -> bool, + overlay_context: &mut OverlayContext, +) { + let bezier = bezier.apply_transformation(|point| transform.transform_point2(point)); + let not_under_anchor = |position: DVec2, anchor: DVec2| position.distance_squared(anchor) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE; + + match bezier.handles { + BezierHandles::Quadratic { handle } => { + if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) { + let end = if start == point_to_render { bezier.start } else { bezier.end }; + overlay_context.line(handle, end, None); + overlay_context.manipulator_handle(handle, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None); + } + } + BezierHandles::Cubic { handle_start, handle_end } => { + if not_under_anchor(handle_start, bezier.start) && (point_to_render == start) { + overlay_context.line(handle_start, bezier.start, None); + overlay_context.manipulator_handle(handle_start, is_selected(ManipulatorPointId::PrimaryHandle(segment_id)), None); + } + if not_under_anchor(handle_end, bezier.end) && (point_to_render == end) { + overlay_context.line(handle_end, bezier.end, None); + overlay_context.manipulator_handle(handle_end, is_selected(ManipulatorPointId::EndHandle(segment_id)), None); + } + } + _ => {} + } +} + +pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandles, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) { + for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + let transform = document.metadata().transform_to_viewport(layer); + overlay_context.outline_vector(&vector_data, transform); + + let selected = shape_editor.selected_shape_state.get(&layer); + let is_selected = |point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point)); + + let opposite_handles_data: Vec<(PointId, SegmentId)> = shape_editor.selected_points().filter_map(|point_id| vector_data.adjacent_segment(point_id)).collect(); + + match draw_handles { + DrawHandles::All => { + vector_data.segment_bezier_iter().for_each(|(segment_id, bezier, _start, _end)| { + overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context); + }); + } + DrawHandles::SelectedAnchors(ref selected_segments) => { + vector_data + .segment_bezier_iter() + .filter(|(segment_id, ..)| selected_segments.contains(segment_id)) + .for_each(|(segment_id, bezier, _start, _end)| { + overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context); + }); + + for (segment_id, bezier, start, end) in vector_data.segment_bezier_iter() { + if let Some((corresponding_anchor, _)) = opposite_handles_data.iter().find(|(_, adj_segment_id)| adj_segment_id == &segment_id) { + overlay_bezier_handle_specific_point(bezier, segment_id, (start, end), *corresponding_anchor, transform, is_selected, overlay_context); + } + } + } + DrawHandles::FrontierHandles(ref segment_endpoints) => { + vector_data + .segment_bezier_iter() + .filter(|(segment_id, ..)| segment_endpoints.contains_key(segment_id)) + .for_each(|(segment_id, bezier, start, end)| { + if segment_endpoints.get(&segment_id).unwrap().len() == 1 { + let point_to_render = segment_endpoints.get(&segment_id).unwrap()[0]; + overlay_bezier_handle_specific_point(bezier, segment_id, (start, end), point_to_render, transform, is_selected, overlay_context); + } else { + overlay_bezier_handles(bezier, segment_id, transform, is_selected, overlay_context); + } + }); + } + DrawHandles::None => {} + } + for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { - overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(selected, ManipulatorPointId::Anchor(id)), None); + overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(ManipulatorPointId::Anchor(id)), None); } } } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 6773f135..a16eda48 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -6,11 +6,12 @@ use crate::messages::prelude::Message; use bezier_rs::{Bezier, Subpath}; use graphene_core::renderer::Quad; -use graphene_std::vector::{PointId, VectorData}; +use graphene_std::vector::{PointId, SegmentId, VectorData}; use core::borrow::Borrow; use core::f64::consts::TAU; use glam::{DAffine2, DVec2}; +use std::collections::HashMap; use wasm_bindgen::JsValue; pub type OverlayProvider = fn(OverlayContext) -> Message; @@ -482,3 +483,10 @@ pub enum Pivot { Middle, End, } + +pub enum DrawHandles { + All, + SelectedAnchors(Vec), + FrontierHandles(HashMap>), + None, +} diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index ea7b7da2..a87fccbf 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -3,8 +3,8 @@ use super::tool_prelude::*; use crate::consts::{ COLOR_OVERLAY_BLUE, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, }; -use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; -use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments}; +use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::preferences::SelectionMode; @@ -15,8 +15,8 @@ use crate::messages::tool::common_functionality::shape_editor::{ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; use graphene_core::renderer::Quad; -use graphene_core::vector::ManipulatorPointId; -use graphene_std::vector::NoHashBuilder; +use graphene_core::vector::{ManipulatorPointId, PointId}; +use graphene_std::vector::{NoHashBuilder, SegmentId}; use std::vec; @@ -24,6 +24,12 @@ use std::vec; pub struct PathTool { fsm_state: PathToolFsmState, tool_data: PathToolData, + options: PathToolOptions, +} + +#[derive(Default)] +pub struct PathToolOptions { + path_overlay_mode: PathOverlayMode, } #[impl_message(Message, ToolMessage, Path)] @@ -89,6 +95,20 @@ pub enum PathToolMessage { new_y: f64, }, SwapSelectedHandles, + UpdateOptions(PathOptionsUpdate), +} + +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum PathOverlayMode { + AllHandles = 0, + #[default] + SelectedPointHandles = 1, + FrontierHandles = 2, +} + +#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum PathOptionsUpdate { + OverlayModeType(PathOverlayMode), } impl ToolMetadata for PathTool { @@ -170,15 +190,31 @@ impl LayoutHolder for PathTool { .tooltip(colinear_handles_tooltip) .widget_holder(); + let path_overlay_mode_widget = RadioInput::new(vec![ + RadioEntryData::new("1") + .label("1") + .on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::AllHandles)).into()), + RadioEntryData::new("2") + .label("2") + .on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::SelectedPointHandles)).into()), + RadioEntryData::new("3") + .label("3") + .on_update(move |_| PathToolMessage::UpdateOptions(PathOptionsUpdate::OverlayModeType(PathOverlayMode::FrontierHandles)).into()), + ]) + .selected_index(Some(self.options.path_overlay_mode as u32)) + .widget_holder(); + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![ x_location, related_seperator.clone(), y_location, - unrelated_seperator, + unrelated_seperator.clone(), colinear_handle_checkbox, related_seperator, colinear_handles_label, + unrelated_seperator, + path_overlay_mode_widget, ], }])) } @@ -189,6 +225,12 @@ impl<'a> MessageHandler> for PathToo let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated); match message { + ToolMessage::Path(PathToolMessage::UpdateOptions(action)) => match action { + PathOptionsUpdate::OverlayModeType(overlay_mode_type) => { + self.options.path_overlay_mode = overlay_mode_type; + responses.add(OverlaysMessage::Draw); + } + }, ToolMessage::Path(PathToolMessage::ClosePath) => { responses.add(DocumentMessage::AddTransaction); tool_data.shape_editor.close_selected_path(tool_data.document, responses); @@ -204,7 +246,7 @@ impl<'a> MessageHandler> for PathToo } } _ => { - self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true); + self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); } } @@ -321,6 +363,8 @@ struct PathToolData { auto_panning: AutoPanning, saved_points_before_anchor_select_toggle: Vec, select_anchor_toggled: bool, + saved_points_before_handle_drag: Vec, + handle_drag_toggle: bool, dragging_state: DraggingState, current_selected_handle_id: Option, angle: f64, @@ -430,12 +474,27 @@ impl PathToolData { self.drag_start_pos = input.mouse.position; + let old_selection = shape_editor.selected_points().cloned().collect::>(); + // Select the first point within the threshold (in pixels) if let Some(selected_points) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, extend_selection) { responses.add(DocumentMessage::StartTransaction); if let Some(selected_points) = selected_points { self.drag_start_pos = input.mouse.position; + + // If selected points contain only handles and there was some selection before, then it is stored and becomes restored upon release + let mut dragging_only_handles = true; + for point in &selected_points.points { + if matches!(point.point_id, ManipulatorPointId::Anchor(_)) { + dragging_only_handles = false; + break; + } + } + if dragging_only_handles && !self.handle_drag_toggle && !old_selection.is_empty() { + self.saved_points_before_handle_drag = old_selection; + } + self.start_dragging_point(selected_points, input, document, shape_editor); responses.add(OverlaysMessage::Draw); } @@ -687,9 +746,9 @@ impl PathToolData { impl Fsm for PathToolFsmState { type ToolData = PathToolData; - type ToolOptions = (); + type ToolOptions = PathToolOptions; - fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { let ToolActionHandlerData { document, input, shape_editor, .. } = tool_action_data; let ToolMessage::Path(event) = event else { return self }; match (self, event) { @@ -704,7 +763,51 @@ impl Fsm for PathToolFsmState { self } (_, PathToolMessage::Overlays(mut overlay_context)) => { - path_overlays(document, shape_editor, &mut overlay_context); + // TODO: find the segment ids of which the selected points are a part of + + match tool_options.path_overlay_mode { + PathOverlayMode::AllHandles => { + path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); + } + PathOverlayMode::SelectedPointHandles => { + let selected_segments = selected_segments(document, shape_editor); + + path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context); + } + PathOverlayMode::FrontierHandles => { + let selected_segments = selected_segments(document, shape_editor); + + // Match the behavior of `PathOverlayMode::SelectedPointHandles` when only one point is selected + if shape_editor.selected_points().count() == 1 { + path_overlays(document, DrawHandles::SelectedAnchors(selected_segments), shape_editor, &mut overlay_context); + } else { + let mut segment_endpoints: HashMap> = HashMap::new(); + + for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + + // The points which are part of only one segment will be rendered + let mut selected_segments_by_point: HashMap> = HashMap::new(); + + for (segment_id, _bezier, start, end) in vector_data.segment_bezier_iter() { + if selected_segments.contains(&segment_id) { + selected_segments_by_point.entry(start).or_default().push(segment_id); + selected_segments_by_point.entry(end).or_default().push(segment_id); + } + } + + for (point, attached_segments) in selected_segments_by_point { + if attached_segments.len() == 1 { + segment_endpoints.entry(attached_segments[0]).or_default().push(point); + } + } + } + + // Now frontier anchors can be sent for rendering overlays + path_overlays(document, DrawHandles::FrontierHandles(segment_endpoints), shape_editor, &mut overlay_context); + } + } + } match self { Self::Drawing { selection_shape } => { @@ -839,6 +942,21 @@ impl Fsm for PathToolFsmState { lock_angle, }, ) => { + let mut selected_only_handles = true; + + let selected_points = shape_editor.selected_points(); + + for point in selected_points { + if matches!(point, ManipulatorPointId::Anchor(_)) { + selected_only_handles = false; + break; + } + } + + if !tool_data.saved_points_before_handle_drag.is_empty() && (tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) { + tool_data.handle_drag_toggle = true; + } + if tool_data.selection_status.is_none() { if let Some(layer) = document.click(input) { shape_editor.select_all_anchors_in_layer(document, layer); @@ -1027,6 +1145,14 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::DragStop { extend_selection, .. }) => { + if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { + shape_editor.deselect_all_points(); + shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); + + tool_data.saved_points_before_handle_drag.clear(); + tool_data.handle_drag_toggle = false; + } + if tool_data.select_anchor_toggled { shape_editor.deselect_all_points(); shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 9ec62cae..d59af4ff 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -2,7 +2,7 @@ use super::tool_prelude::*; use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE}; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; 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::{DrawHandles, OverlayContext}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; @@ -27,6 +27,7 @@ pub struct PenOptions { line_weight: f64, fill: ToolColorOptions, stroke: ToolColorOptions, + pen_overlay_mode: PenOverlayMode, } impl Default for PenOptions { @@ -35,6 +36,7 @@ impl Default for PenOptions { line_weight: DEFAULT_STROKE_WIDTH, fill: ToolColorOptions::new_secondary(), stroke: ToolColorOptions::new_primary(), + pen_overlay_mode: PenOverlayMode::FrontierHandles, } } } @@ -75,6 +77,12 @@ enum PenToolFsmState { GRSHandle, } +#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum PenOverlayMode { + AllHandles = 0, + FrontierHandles = 1, +} + #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum PenOptionsUpdate { FillColor(Option), @@ -83,6 +91,7 @@ pub enum PenOptionsUpdate { StrokeColor(Option), StrokeColorType(ToolColorType), WorkingColors(Option, Option), + OverlayModeType(PenOverlayMode), } impl ToolMetadata for PenTool { @@ -126,9 +135,26 @@ impl LayoutHolder for PenTool { |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(), )); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.push(create_weight_widget(self.options.line_weight)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + widgets.push( + RadioInput::new(vec![ + RadioEntryData::new("1") + .label("1") + .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::AllHandles)).into()), + RadioEntryData::new("2") + .label("2") + .on_update(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::OverlayModeType(PenOverlayMode::FrontierHandles)).into()), + ]) + .selected_index(Some(self.options.pen_overlay_mode as u32)) + .widget_holder(), + ); + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } @@ -140,6 +166,10 @@ impl<'a> MessageHandler> for PenTool return; }; match action { + PenOptionsUpdate::OverlayModeType(overlay_mode_type) => { + self.options.pen_overlay_mode = overlay_mode_type; + responses.add(OverlaysMessage::Draw); + } PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, PenOptionsUpdate::FillColor(color) => { self.options.fill.custom_color = color; @@ -795,7 +825,7 @@ impl Fsm for PenToolFsmState { self } (PenToolFsmState::Ready, PenToolMessage::Overlays(mut overlay_context)) => { - path_overlays(document, shape_editor, &mut overlay_context); + path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); self } @@ -839,7 +869,19 @@ impl Fsm for PenToolFsmState { overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5)); } - path_overlays(document, shape_editor, &mut overlay_context); + match tool_options.pen_overlay_mode { + PenOverlayMode::AllHandles => { + path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); + } + PenOverlayMode::FrontierHandles => { + // Find the last segment ID to have its handles drawn + if let Some(latest_segment) = tool_data.latest_point().and_then(|point| point.in_segment) { + path_overlays(document, DrawHandles::SelectedAnchors(vec![latest_segment]), shape_editor, &mut overlay_context); + } else { + path_overlays(document, DrawHandles::None, shape_editor, &mut overlay_context); + }; + } + } if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) { // Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out) @@ -852,7 +894,7 @@ impl Fsm for PenToolFsmState { } } else { // Draw the whole path and its manipulators when the user is clicking-and-dragging out from the most recently placed anchor to set its outgoing handle, during which it would otherwise not have its overlays drawn - path_overlays(document, shape_editor, &mut overlay_context); + path_overlays(document, DrawHandles::All, shape_editor, &mut overlay_context); } if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, next_handle_start) { diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index bd5cf324..8502b717 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -293,6 +293,26 @@ impl VectorData { None } } + + pub fn adjacent_segment(&self, manipulator_id: &ManipulatorPointId) -> Option<(PointId, SegmentId)> { + match manipulator_id { + ManipulatorPointId::PrimaryHandle(segment_id) => { + // For start handle, find segments ending at our start point + let (start_point_id, _, _) = self.segment_points_from_id(*segment_id)?; + let start_index = self.point_domain.resolve_id(start_point_id)?; + + self.segment_domain.end_connected(start_index).find(|&id| id != *segment_id).map(|id| (start_point_id, id)) + } + ManipulatorPointId::EndHandle(segment_id) => { + // For end handle, find segments starting at our end point + let (_, end_point_id, _) = self.segment_points_from_id(*segment_id)?; + let end_index = self.point_domain.resolve_id(end_point_id)?; + + self.segment_domain.start_connected(end_index).find(|&id| id != *segment_id).map(|id| (end_point_id, id)) + } + ManipulatorPointId::Anchor(_) => None, + } + } } impl Default for VectorData {