Experimental vector meshes (#2223)

* Experimental vector meshes

* Clarify limitations in label and tooltip

* Restore old traversal direction

* Fix Bezier-rs crashes

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
James Lindsay 2025-01-26 01:13:35 +00:00 committed by GitHub
parent 26d66298cf
commit 93880abc4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 102 additions and 78 deletions

1
Cargo.lock generated
View File

@ -2397,6 +2397,7 @@ dependencies = [
"serde_json", "serde_json",
"specta", "specta",
"spirv-std", "spirv-std",
"tinyvec",
"tokio", "tokio",
"usvg", "usvg",
"vello", "vello",

View File

@ -228,6 +228,7 @@ impl Dispatcher {
input: &self.message_handlers.input_preprocessor_message_handler, input: &self.message_handlers.input_preprocessor_message_handler,
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data, persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
node_graph: &self.message_handlers.portfolio_message_handler.executor, node_graph: &self.message_handlers.portfolio_message_handler.executor,
preferences: &self.message_handlers.preferences_message_handler,
}; };
self.message_handlers.tool_message_handler.process_message(message, &mut queue, data); self.message_handlers.tool_message_handler.process_message(message, &mut queue, data);

View File

@ -47,7 +47,7 @@ impl PreferencesDialogMessageHandler {
TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(), TextLabel::new("Zoom with Scroll").table_align(true).tooltip(zoom_with_scroll_tooltip).widget_holder(),
]; ];
let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)"; let vello_tooltip = "Use the experimental Vello renderer (your browser must support WebGPU)";
let renderer_section = vec![TextLabel::new("Renderer").italic(true).widget_holder()]; let renderer_section = vec![TextLabel::new("Experimental").italic(true).widget_holder()];
let use_vello = vec![ let use_vello = vec![
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu()) CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
.tooltip(vello_tooltip) .tooltip(vello_tooltip)
@ -55,7 +55,7 @@ impl PreferencesDialogMessageHandler {
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into()) .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.widget_holder(), .widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Vello (Experimental)") TextLabel::new("Vello Renderer")
.table_align(true) .table_align(true)
.tooltip(vello_tooltip) .tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu()) .disabled(!preferences.supports_wgpu())
@ -85,11 +85,22 @@ impl PreferencesDialogMessageHandler {
// .widget_holder(), // .widget_holder(),
// ]; // ];
let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills.";
let vector_meshes = vec![
CheckboxInput::new(preferences.vector_meshes)
.tooltip(vector_mesh_tooltip)
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::VectorMeshes { enabled: checkbox_input.checked }.into())
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Vector Meshes").table_align(true).tooltip(vector_mesh_tooltip).widget_holder(),
];
Layout::WidgetLayout(WidgetLayout::new(vec![ Layout::WidgetLayout(WidgetLayout::new(vec![
LayoutGroup::Row { widgets: input_section }, LayoutGroup::Row { widgets: input_section },
LayoutGroup::Row { widgets: zoom_with_scroll }, LayoutGroup::Row { widgets: zoom_with_scroll },
LayoutGroup::Row { widgets: renderer_section }, LayoutGroup::Row { widgets: renderer_section },
LayoutGroup::Row { widgets: use_vello }, LayoutGroup::Row { widgets: use_vello },
LayoutGroup::Row { widgets: vector_meshes },
// LayoutGroup::Row { widgets: imaginate_server_hostname }, // LayoutGroup::Row { widgets: imaginate_server_hostname },
// LayoutGroup::Row { widgets: imaginate_refresh_frequency }, // LayoutGroup::Row { widgets: imaginate_refresh_frequency },
])) ]))

View File

@ -1,7 +1,7 @@
use super::utility_types::OverlayContext; use super::utility_types::OverlayContext;
use crate::consts::HIDE_HANDLE_DISTANCE; use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState}; use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler; use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
use graphene_core::vector::ManipulatorPointId; use graphene_core::vector::ManipulatorPointId;
@ -62,7 +62,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape
} }
} }
pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext) { pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &mut ShapeState, overlay_context: &mut OverlayContext, preferences: &PreferencesMessageHandler) {
for layer in document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()) { 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 { let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
continue; continue;
@ -72,7 +72,7 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
let selected = shape_editor.selected_shape_state.get(&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)); let is_selected = |selected: Option<&SelectedLayerState>, point: ManipulatorPointId| selected.is_some_and(|selected| selected.is_selected(point));
for point in vector_data.single_connected_points() { for point in vector_data.extendable_points(preferences.vector_meshes) {
let Some(position) = vector_data.point_domain.position_from_id(point) else { continue }; let Some(position) = vector_data.point_domain.position_from_id(point) else { continue };
let position = transform.transform_point2(position); let position = transform.transform_point2(position);
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None); overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);

View File

@ -9,5 +9,6 @@ pub enum PreferencesMessage {
ImaginateRefreshFrequency { seconds: f64 }, ImaginateRefreshFrequency { seconds: f64 },
UseVello { use_vello: bool }, UseVello { use_vello: bool },
ImaginateServerHostname { hostname: String }, ImaginateServerHostname { hostname: String },
VectorMeshes { enabled: bool },
ModifyLayout { zoom_with_scroll: bool }, ModifyLayout { zoom_with_scroll: bool },
} }

View File

@ -8,6 +8,7 @@ pub struct PreferencesMessageHandler {
pub imaginate_refresh_frequency: f64, pub imaginate_refresh_frequency: f64,
pub zoom_with_scroll: bool, pub zoom_with_scroll: bool,
pub use_vello: bool, pub use_vello: bool,
pub vector_meshes: bool,
} }
impl PreferencesMessageHandler { impl PreferencesMessageHandler {
@ -34,6 +35,7 @@ impl Default for PreferencesMessageHandler {
imaginate_refresh_frequency: 1., imaginate_refresh_frequency: 1.,
zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll), zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll),
use_vello, use_vello,
vector_meshes: false,
} }
} }
} }
@ -88,6 +90,9 @@ impl MessageHandler<PreferencesMessage, ()> for PreferencesMessageHandler {
responses.add(PortfolioMessage::ImaginateCheckServerStatus); responses.add(PortfolioMessage::ImaginateCheckServerStatus);
responses.add(PortfolioMessage::EditorPreferences); responses.add(PortfolioMessage::EditorPreferences);
} }
PreferencesMessage::VectorMeshes { enabled } => {
self.vector_meshes = enabled;
}
PreferencesMessage::ModifyLayout { zoom_with_scroll } => { PreferencesMessage::ModifyLayout { zoom_with_scroll } => {
self.zoom_with_scroll = zoom_with_scroll; self.zoom_with_scroll = zoom_with_scroll;

View File

@ -6,8 +6,14 @@ use graphene_std::vector::PointId;
use glam::DVec2; use glam::DVec2;
/// 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(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64, layers: impl Iterator<Item = LayerNodeIdentifier>) -> Option<(LayerNodeIdentifier, PointId, DVec2)> { pub fn should_extend(
closest_point(document, goal, tolerance, layers, |_| false) document: &DocumentMessageHandler,
goal: DVec2,
tolerance: f64,
layers: impl Iterator<Item = LayerNodeIdentifier>,
preferences: &PreferencesMessageHandler,
) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
closest_point(document, goal, tolerance, layers, |_| false, preferences)
} }
/// Determine the closest point to the goal point under max_distance. /// Determine the closest point to the goal point under max_distance.
@ -18,6 +24,7 @@ pub fn closest_point<T>(
max_distance: f64, max_distance: f64,
layers: impl Iterator<Item = LayerNodeIdentifier>, layers: impl Iterator<Item = LayerNodeIdentifier>,
exclude: T, exclude: T,
preferences: &PreferencesMessageHandler,
) -> Option<(LayerNodeIdentifier, PointId, DVec2)> ) -> Option<(LayerNodeIdentifier, PointId, DVec2)>
where where
T: Fn(PointId) -> bool, T: Fn(PointId) -> bool,
@ -29,7 +36,7 @@ where
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
continue; continue;
}; };
for id in vector_data.single_connected_points() { for id in vector_data.extendable_points(preferences.vector_meshes) {
if exclude(id) { if exclude(id) {
continue; continue;
} }

View File

@ -18,6 +18,7 @@ pub struct ToolMessageData<'a> {
pub input: &'a InputPreprocessorMessageHandler, pub input: &'a InputPreprocessorMessageHandler,
pub persistent_data: &'a PersistentData, pub persistent_data: &'a PersistentData,
pub node_graph: &'a NodeGraphExecutor, pub node_graph: &'a NodeGraphExecutor,
pub preferences: &'a PreferencesMessageHandler,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -36,6 +37,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
input, input,
persistent_data, persistent_data,
node_graph, node_graph,
preferences,
} = data; } = data;
let font_cache = &persistent_data.font_cache; let font_cache = &persistent_data.font_cache;
@ -86,6 +88,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
font_cache, font_cache,
shape_editor: &mut self.shape_editor, shape_editor: &mut self.shape_editor,
node_graph, node_graph,
preferences,
}; };
if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort { if let Some(tool_abort_message) = tool.event_to_message_map().tool_abort {
@ -180,6 +183,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
font_cache, font_cache,
shape_editor: &mut self.shape_editor, shape_editor: &mut self.shape_editor,
node_graph, node_graph,
preferences,
}; };
// Set initial hints and cursor // Set initial hints and cursor
@ -269,6 +273,7 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
font_cache, font_cache,
shape_editor: &mut self.shape_editor, shape_editor: &mut self.shape_editor,
node_graph, node_graph,
preferences,
}; };
if matches!(tool_message, ToolMessage::UpdateHints) { if matches!(tool_message, ToolMessage::UpdateHints) {
if self.transform_layer_handler.is_transforming() { if self.transform_layer_handler.is_transforming() {

View File

@ -191,13 +191,14 @@ impl Fsm for FreehandToolFsmState {
global_tool_data, global_tool_data,
input, input,
shape_editor, shape_editor,
preferences,
.. ..
} = tool_action_data; } = tool_action_data;
let ToolMessage::Freehand(event) = event else { return self }; let ToolMessage::Freehand(event) = event else { return self };
match (self, event) { match (self, event) {
(_, FreehandToolMessage::Overlays(mut overlay_context)) => { (_, FreehandToolMessage::Overlays(mut overlay_context)) => {
path_endpoint_overlays(document, shape_editor, &mut overlay_context); path_endpoint_overlays(document, shape_editor, &mut overlay_context, tool_action_data.preferences);
self self
} }
@ -210,7 +211,8 @@ impl Fsm for FreehandToolFsmState {
// Extend an endpoint of the selected path // Extend an endpoint of the selected path
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
if let Some((layer, point, position)) = should_extend(document, input.mouse.position, crate::consts::SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata())) { let tolerance = crate::consts::SNAP_POINT_TOLERANCE;
if let Some((layer, point, position)) = should_extend(document, input.mouse.position, tolerance, selected_nodes.selected_layers(document.metadata()), preferences) {
tool_data.layer = Some(layer); tool_data.layer = Some(layer);
tool_data.end_point = Some((position, point)); tool_data.end_point = Some((position, point));

View File

@ -275,19 +275,19 @@ impl PenToolData {
} }
} }
fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { fn finish_placing_handle(&mut self, snap_data: SnapData, transform: DAffine2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document; let document = snap_data.document;
let next_handle_start = self.next_handle_start; let next_handle_start = self.next_handle_start;
let handle_start = self.latest_point()?.handle_start; let handle_start = self.latest_point()?.handle_start;
let mouse = snap_data.input.mouse.position; let mouse = snap_data.input.mouse.position;
let Some(handle_end) = self.handle_end else { let Some(handle_end) = self.handle_end else {
self.handle_end = Some(next_handle_start); self.handle_end = Some(next_handle_start);
self.place_anchor(snap_data, transform, mouse, responses); self.place_anchor(snap_data, transform, mouse, preferences, responses);
self.latest_point_mut()?.handle_start = next_handle_start; self.latest_point_mut()?.handle_start = next_handle_start;
return None; return None;
}; };
let next_point = self.next_point; let next_point = self.next_point;
self.place_anchor(snap_data, transform, mouse, responses); self.place_anchor(snap_data, transform, mouse, preferences, responses);
let handles = [handle_start - self.latest_point()?.pos, handle_end - next_point].map(Some); let handles = [handle_start - self.latest_point()?.pos, handle_end - next_point].map(Some);
// Get close path // Get close path
@ -298,7 +298,7 @@ impl PenToolData {
let vector_data = document.network_interface.compute_modified_vector(layer)?; let vector_data = document.network_interface.compute_modified_vector(layer)?;
let start = self.latest_point()?.id; let start = self.latest_point()?.id;
let transform = document.metadata().document_to_viewport * transform; let transform = document.metadata().document_to_viewport * transform;
for id in vector_data.single_connected_points().filter(|&point| point != start) { for id in vector_data.extendable_points(preferences.vector_meshes).filter(|&point| point != start) {
let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue }; let Some(pos) = vector_data.point_domain.position_from_id(id) else { continue };
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point));
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);
@ -359,7 +359,7 @@ impl PenToolData {
Some(PenToolFsmState::DraggingHandle) Some(PenToolFsmState::DraggingHandle)
} }
fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
let document = snap_data.document; let document = snap_data.document;
let relative = self.latest_point().map(|point| point.pos); let relative = self.latest_point().map(|point| point.pos);
@ -370,7 +370,7 @@ impl PenToolData {
let layer = selected_layers.next().filter(|_| selected_layers.next().is_none())?; let layer = selected_layers.next().filter(|_| selected_layers.next().is_none())?;
let vector_data = document.network_interface.compute_modified_vector(layer)?; let vector_data = document.network_interface.compute_modified_vector(layer)?;
let transform = document.metadata().document_to_viewport * transform; let transform = document.metadata().document_to_viewport * transform;
for point in vector_data.single_connected_points() { for point in vector_data.extendable_points(preferences.vector_meshes) {
let Some(pos) = vector_data.point_domain.position_from_id(point) else { continue }; let Some(pos) = vector_data.point_domain.position_from_id(point) else { continue };
let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point)); let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point));
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);
@ -456,7 +456,15 @@ impl PenToolData {
transform.inverse().transform_point2(document_pos) transform.inverse().transform_point2(document_pos)
} }
fn create_initial_point(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, tool_options: &PenOptions, append: bool) { fn create_initial_point(
&mut self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
tool_options: &PenOptions,
append: bool,
preferences: &PreferencesMessageHandler,
) {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
@ -464,7 +472,8 @@ impl PenToolData {
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
self.handle_end = None; self.handle_end = None;
if let Some((layer, point, position)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata())) { let tolerance = crate::consts::SNAP_POINT_TOLERANCE;
if let Some((layer, point, position)) = should_extend(document, viewport, tolerance, selected_nodes.selected_layers(document.metadata()), preferences) {
// Perform extension of an existing path // Perform extension of an existing path
self.add_point(LastPoint { self.add_point(LastPoint {
id: point, id: point,
@ -533,6 +542,7 @@ impl Fsm for PenToolFsmState {
global_tool_data, global_tool_data,
input, input,
shape_editor, shape_editor,
preferences,
.. ..
} = tool_action_data; } = tool_action_data;
@ -638,7 +648,7 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
tool_data.create_initial_point(document, input, responses, tool_options, input.keyboard.key(append_to_selected)); tool_data.create_initial_point(document, input, responses, tool_options, input.keyboard.key(append_to_selected), preferences);
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle // Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle
@ -660,7 +670,7 @@ impl Fsm for PenToolFsmState {
if tool_data.buffering_merged_vector { if tool_data.buffering_merged_vector {
tool_data.buffering_merged_vector = false; tool_data.buffering_merged_vector = false;
tool_data.bend_from_previous_point(SnapData::new(document, input), transform); tool_data.bend_from_previous_point(SnapData::new(document, input), transform);
tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses); tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses);
tool_data.buffering_merged_vector = false; tool_data.buffering_merged_vector = false;
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle
} else { } else {
@ -673,7 +683,7 @@ impl Fsm for PenToolFsmState {
let layers = LayerNodeIdentifier::ROOT_PARENT let layers = LayerNodeIdentifier::ROOT_PARENT
.descendants(document.metadata()) .descendants(document.metadata())
.filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[])); .filter(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]));
if let Some((other_layer, _, _)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, layers) { if let Some((other_layer, _, _)) = should_extend(document, viewport, crate::consts::SNAP_POINT_TOLERANCE, layers, preferences) {
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
let mut selected_layers = selected_nodes.selected_layers(document.metadata()); let mut selected_layers = selected_nodes.selected_layers(document.metadata());
if let Some(current_layer) = selected_layers.next().filter(|current_layer| selected_layers.next().is_none() && *current_layer != other_layer) { if let Some(current_layer) = selected_layers.next().filter(|current_layer| selected_layers.next().is_none() && *current_layer != other_layer) {
@ -696,7 +706,7 @@ impl Fsm for PenToolFsmState {
self self
} }
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data (PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
.finish_placing_handle(SnapData::new(document, input), transform, responses) .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor), .unwrap_or(PenToolFsmState::PlacingAnchor),
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { (PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
tool_data.modifiers = ModifierState { tool_data.modifiers = ModifierState {
@ -724,7 +734,7 @@ impl Fsm for PenToolFsmState {
break_handle: input.keyboard.key(break_handle), break_handle: input.keyboard.key(break_handle),
}; };
let state = tool_data let state = tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses) .place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses)
.unwrap_or(PenToolFsmState::Ready); .unwrap_or(PenToolFsmState::Ready);
// Auto-panning // Auto-panning
@ -783,7 +793,7 @@ impl Fsm for PenToolFsmState {
if tool_data.point_index > 0 { if tool_data.point_index > 0 {
tool_data.point_index -= 1; tool_data.point_index -= 1;
tool_data tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses) .place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor) .unwrap_or(PenToolFsmState::PlacingAnchor)
} else { } else {
responses.add(PenToolMessage::Abort); responses.add(PenToolMessage::Abort);
@ -792,7 +802,7 @@ impl Fsm for PenToolFsmState {
} }
(_, PenToolMessage::Redo) => { (_, PenToolMessage::Redo) => {
tool_data.point_index = (tool_data.point_index + 1).min(tool_data.latest_points.len().saturating_sub(1)); tool_data.point_index = (tool_data.point_index + 1).min(tool_data.latest_points.len().saturating_sub(1));
tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses); tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses);
match tool_data.point_index { match tool_data.point_index {
0 => PenToolFsmState::Ready, 0 => PenToolFsmState::Ready,
_ => PenToolFsmState::PlacingAnchor, _ => PenToolFsmState::PlacingAnchor,

View File

@ -227,6 +227,7 @@ impl Fsm for SplineToolFsmState {
global_tool_data, global_tool_data,
input, input,
shape_editor, shape_editor,
preferences,
.. ..
} = tool_action_data; } = tool_action_data;
@ -234,7 +235,7 @@ impl Fsm for SplineToolFsmState {
match (self, event) { match (self, event) {
(_, SplineToolMessage::CanvasTransformed) => self, (_, SplineToolMessage::CanvasTransformed) => self,
(_, SplineToolMessage::Overlays(mut overlay_context)) => { (_, SplineToolMessage::Overlays(mut overlay_context)) => {
path_endpoint_overlays(document, shape_editor, &mut overlay_context); path_endpoint_overlays(document, shape_editor, &mut overlay_context, preferences);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self self
} }
@ -250,7 +251,7 @@ impl Fsm for SplineToolFsmState {
// Extend an endpoint of the selected path // Extend an endpoint of the selected path
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata())) { if let Some((layer, point, position)) = should_extend(document, viewport, SNAP_POINT_TOLERANCE, selected_nodes.selected_layers(document.metadata()), preferences) {
tool_data.layer = Some(layer); tool_data.layer = Some(layer);
tool_data.points.push((point, position)); tool_data.points.push((point, position));
tool_data.next_point = position; tool_data.next_point = position;
@ -304,7 +305,7 @@ impl Fsm for SplineToolFsmState {
if tool_data.layer.is_none() { if tool_data.layer.is_none() {
return SplineToolFsmState::Ready; return SplineToolFsmState::Ready;
}; };
if join_path(document, input.mouse.position, tool_data, responses) { if join_path(document, input.mouse.position, tool_data, preferences, responses) {
responses.add(DocumentMessage::EndTransaction); responses.add(DocumentMessage::EndTransaction);
return SplineToolFsmState::Ready; return SplineToolFsmState::Ready;
} }
@ -318,7 +319,7 @@ impl Fsm for SplineToolFsmState {
(SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => { (SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => {
let Some(layer) = tool_data.layer else { return SplineToolFsmState::Ready }; let Some(layer) = tool_data.layer else { return SplineToolFsmState::Ready };
let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp); let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp);
let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore); let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore, preferences);
// Endpoints snapping // Endpoints snapping
if let Some((_, _, point)) = join_point { if let Some((_, _, point)) = join_point {
@ -402,7 +403,7 @@ impl Fsm for SplineToolFsmState {
} }
/// Return `true` only if new segment is inserted to connect two end points in the selected layer otherwise `false`. /// Return `true` only if new segment is inserted to connect two end points in the selected layer otherwise `false`.
fn join_path(document: &DocumentMessageHandler, mouse_pos: DVec2, tool_data: &mut SplineToolData, responses: &mut VecDeque<Message>) -> bool { fn join_path(document: &DocumentMessageHandler, mouse_pos: DVec2, tool_data: &mut SplineToolData, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> bool {
let Some(&(endpoint, _)) = tool_data.points.last() else { return false }; let Some(&(endpoint, _)) = tool_data.points.last() else { return false };
let preview_point = tool_data.preview_point; let preview_point = tool_data.preview_point;
@ -410,9 +411,14 @@ fn join_path(document: &DocumentMessageHandler, mouse_pos: DVec2, tool_data: &mu
let selected_layers = selected_nodes.selected_layers(document.metadata()); let selected_layers = selected_nodes.selected_layers(document.metadata());
// Get the closest point to mouse position which is not preview_point or end_point. // Get the closest point to mouse position which is not preview_point or end_point.
let closest_point = closest_point(document, mouse_pos, PATH_JOIN_THRESHOLD, selected_layers, |cp| { let closest_point = closest_point(
preview_point.is_some_and(|pp| pp == cp) || cp == endpoint document,
}); mouse_pos,
PATH_JOIN_THRESHOLD,
selected_layers,
|cp| preview_point.is_some_and(|pp| pp == cp) || cp == endpoint,
preferences,
);
let Some((layer, join_point, _)) = closest_point else { return false }; let Some((layer, join_point, _)) = closest_point else { return false };
// Last end point inserted was the preview point and segment therefore we delete it before joining the end_point & join_point. // Last end point inserted was the preview point and segment therefore we delete it before joining the end_point & join_point.

View File

@ -26,27 +26,7 @@ pub struct ToolActionHandlerData<'a> {
pub font_cache: &'a FontCache, pub font_cache: &'a FontCache,
pub shape_editor: &'a mut ShapeState, pub shape_editor: &'a mut ShapeState,
pub node_graph: &'a NodeGraphExecutor, pub node_graph: &'a NodeGraphExecutor,
} pub preferences: &'a PreferencesMessageHandler,
impl<'a> ToolActionHandlerData<'a> {
pub fn new(
document: &'a mut DocumentMessageHandler,
document_id: DocumentId,
global_tool_data: &'a DocumentToolData,
input: &'a InputPreprocessorMessageHandler,
font_cache: &'a FontCache,
shape_editor: &'a mut ShapeState,
node_graph: &'a NodeGraphExecutor,
) -> Self {
Self {
document,
document_id,
global_tool_data,
input,
font_cache,
shape_editor,
node_graph,
}
}
} }
pub trait ToolCommon: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionHandlerData<'a>> + LayoutHolder + ToolTransition + ToolMetadata {} pub trait ToolCommon: for<'a, 'b> MessageHandler<ToolMessage, &'b mut ToolActionHandlerData<'a>> + LayoutHolder + ToolTransition + ToolMetadata {}

View File

@ -428,7 +428,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
_ => 4., _ => 4.,
}; };
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`? // TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
let in_segment = self.get_segment(self.len_segments() - 1)?; let in_segment = self.get_segment(self.len_segments().checked_sub(1)?)?;
let out_segment = other.get_segment(0)?; let out_segment = other.get_segment(0)?;
let in_tangent = in_segment.tangent(TValue::Parametric(1.)); let in_tangent = in_segment.tangent(TValue::Parametric(1.));
@ -483,13 +483,13 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
let center_to_right = right - center; let center_to_right = right - center;
let center_to_left = left - center; let center_to_left = left - center;
let in_segment = self.get_segment(self.len_segments() - 1).unwrap(); let in_segment = self.len_segments().checked_sub(1).and_then(|segment| self.get_segment(segment));
let in_tangent = in_segment.tangent(TValue::Parametric(1.)); let in_tangent = in_segment.map(|in_segment| in_segment.tangent(TValue::Parametric(1.)));
let mut angle = center_to_right.angle_to(center_to_left) / 2.; let mut angle = center_to_right.angle_to(center_to_left) / 2.;
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
if (arc_point - left).angle_to(in_tangent).abs() > PI / 2. { if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > PI / 2. {
angle = angle - PI * (if angle < 0. { -1. } else { 1. }); angle = angle - PI * (if angle < 0. { -1. } else { 1. });
arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
} }

View File

@ -55,6 +55,7 @@ glam = { workspace = true, default-features = false, features = [
# Required dependencies # Required dependencies
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] } half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }
tinyvec = { version = "1" }
# Optional workspace dependencies # Optional workspace dependencies
dyn-any = { workspace = true, optional = true } dyn-any = { workspace = true, optional = true }

View File

@ -243,14 +243,12 @@ impl VectorData {
self.point_domain.resolve_id(point).map_or(0, |point| self.segment_domain.connected_count(point)) self.point_domain.resolve_id(point).map_or(0, |point| self.segment_domain.connected_count(point))
} }
/// Points connected to a single segment /// Points that can be extended from.
pub fn single_connected_points(&self) -> impl Iterator<Item = PointId> + '_ { ///
self.point_domain /// This is usually only points with exactly one connection unless vector meshes are enabled.
.ids() pub fn extendable_points(&self, vector_meshes: bool) -> impl Iterator<Item = PointId> + '_ {
.iter() let point_ids = self.point_domain.ids().iter().enumerate();
.enumerate() point_ids.filter(move |(index, _)| vector_meshes || self.segment_domain.connected_count(*index) == 1).map(|(_, &id)| id)
.filter(|(index, _)| self.segment_domain.connected_count(*index) == 1)
.map(|(_, &id)| id)
} }
/// Computes if all the connected handles are colinear for an anchor, or if that handle is colinear for a handle. /// Computes if all the connected handles are colinear for an anchor, or if that handle is colinear for a handle.

View File

@ -654,7 +654,7 @@ impl super::VectorData {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
struct StrokePathIterPointSegmentMetadata { struct StrokePathIterPointSegmentMetadata {
segment_index: usize, segment_index: usize,
start_from_end: bool, start_from_end: bool,
@ -675,28 +675,24 @@ impl StrokePathIterPointSegmentMetadata {
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
struct StrokePathIterPointMetadata([Option<StrokePathIterPointSegmentMetadata>; 2]); struct StrokePathIterPointMetadata(tinyvec::TinyVec<[StrokePathIterPointSegmentMetadata; 2]>);
impl StrokePathIterPointMetadata { impl StrokePathIterPointMetadata {
fn set(&mut self, value: StrokePathIterPointSegmentMetadata) { fn set(&mut self, value: StrokePathIterPointSegmentMetadata) {
if self.0[0].is_none() { self.0.insert(0, value);
self.0[0] = Some(value)
} else if self.0[1].is_none() {
self.0[1] = Some(value);
} else {
panic!("Mesh networks are not supported");
}
} }
#[must_use] #[must_use]
fn connected(&self) -> usize { fn connected(&self) -> usize {
self.0.iter().filter(|val| val.is_some()).count() self.0.len()
} }
#[must_use] #[must_use]
fn take_first(&mut self) -> Option<StrokePathIterPointSegmentMetadata> { fn take_first(&mut self) -> Option<StrokePathIterPointSegmentMetadata> {
self.0[0].take().or_else(|| self.0[1].take()) self.0.pop()
} }
fn take_eq(&mut self, target: StrokePathIterPointSegmentMetadata) -> bool { fn take_eq(&mut self, target: StrokePathIterPointSegmentMetadata) -> bool {
self.0[0].take_if(|&mut value| value == target).or_else(|| self.0[1].take_if(|&mut value| value == target)).is_some() let has_taken = self.0.contains(&target);
self.0.retain(|value| *value != target);
has_taken
} }
} }