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",
"specta",
"spirv-std",
"tinyvec",
"tokio",
"usvg",
"vello",

View file

@ -228,6 +228,7 @@ impl Dispatcher {
input: &self.message_handlers.input_preprocessor_message_handler,
persistent_data: &self.message_handlers.portfolio_message_handler.persistent_data,
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);

View file

@ -47,7 +47,7 @@ impl PreferencesDialogMessageHandler {
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 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![
CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu())
.tooltip(vello_tooltip)
@ -55,7 +55,7 @@ impl PreferencesDialogMessageHandler {
.on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into())
.widget_holder(),
Separator::new(SeparatorType::Unrelated).widget_holder(),
TextLabel::new("Vello (Experimental)")
TextLabel::new("Vello Renderer")
.table_align(true)
.tooltip(vello_tooltip)
.disabled(!preferences.supports_wgpu())
@ -85,11 +85,22 @@ impl PreferencesDialogMessageHandler {
// .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![
LayoutGroup::Row { widgets: input_section },
LayoutGroup::Row { widgets: zoom_with_scroll },
LayoutGroup::Row { widgets: renderer_section },
LayoutGroup::Row { widgets: use_vello },
LayoutGroup::Row { widgets: vector_meshes },
// LayoutGroup::Row { widgets: imaginate_server_hostname },
// LayoutGroup::Row { widgets: imaginate_refresh_frequency },
]))

View file

@ -1,7 +1,7 @@
use super::utility_types::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;
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
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()) {
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
continue;
@ -72,7 +72,7 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
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));
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 position = transform.transform_point2(position);
overlay_context.manipulator_anchor(position, is_selected(selected, ManipulatorPointId::Anchor(point)), None);

View file

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

View file

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

View file

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

View file

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

View file

@ -191,13 +191,14 @@ impl Fsm for FreehandToolFsmState {
global_tool_data,
input,
shape_editor,
preferences,
..
} = tool_action_data;
let ToolMessage::Freehand(event) = event else { return self };
match (self, event) {
(_, 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
}
@ -210,7 +211,8 @@ impl Fsm for FreehandToolFsmState {
// Extend an endpoint of the selected path
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.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 next_handle_start = self.next_handle_start;
let handle_start = self.latest_point()?.handle_start;
let mouse = snap_data.input.mouse.position;
let Some(handle_end) = self.handle_end else {
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;
return None;
};
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);
// Get close path
@ -298,7 +298,7 @@ impl PenToolData {
let vector_data = document.network_interface.compute_modified_vector(layer)?;
let start = self.latest_point()?.id;
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 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);
@ -359,7 +359,7 @@ impl PenToolData {
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 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 vector_data = document.network_interface.compute_modified_vector(layer)?;
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 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);
@ -456,7 +456,15 @@ impl PenToolData {
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 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);
@ -464,7 +472,8 @@ impl PenToolData {
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
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
self.add_point(LastPoint {
id: point,
@ -533,6 +542,7 @@ impl Fsm for PenToolFsmState {
global_tool_data,
input,
shape_editor,
preferences,
..
} = tool_action_data;
@ -638,7 +648,7 @@ impl Fsm for PenToolFsmState {
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {
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
PenToolFsmState::DraggingHandle
@ -660,7 +670,7 @@ impl Fsm for PenToolFsmState {
if tool_data.buffering_merged_vector {
tool_data.buffering_merged_vector = false;
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;
PenToolFsmState::DraggingHandle
} else {
@ -673,7 +683,7 @@ impl Fsm for PenToolFsmState {
let layers = LayerNodeIdentifier::ROOT_PARENT
.descendants(document.metadata())
.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 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) {
@ -696,7 +706,7 @@ impl Fsm for PenToolFsmState {
self
}
(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),
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
tool_data.modifiers = ModifierState {
@ -724,7 +734,7 @@ impl Fsm for PenToolFsmState {
break_handle: input.keyboard.key(break_handle),
};
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);
// Auto-panning
@ -783,7 +793,7 @@ impl Fsm for PenToolFsmState {
if tool_data.point_index > 0 {
tool_data.point_index -= 1;
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)
} else {
responses.add(PenToolMessage::Abort);
@ -792,7 +802,7 @@ impl Fsm for PenToolFsmState {
}
(_, PenToolMessage::Redo) => {
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 {
0 => PenToolFsmState::Ready,
_ => PenToolFsmState::PlacingAnchor,

View file

@ -227,6 +227,7 @@ impl Fsm for SplineToolFsmState {
global_tool_data,
input,
shape_editor,
preferences,
..
} = tool_action_data;
@ -234,7 +235,7 @@ impl Fsm for SplineToolFsmState {
match (self, event) {
(_, SplineToolMessage::CanvasTransformed) => self,
(_, 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);
self
}
@ -250,7 +251,7 @@ impl Fsm for SplineToolFsmState {
// Extend an endpoint of the selected path
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.points.push((point, position));
tool_data.next_point = position;
@ -304,7 +305,7 @@ impl Fsm for SplineToolFsmState {
if tool_data.layer.is_none() {
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);
return SplineToolFsmState::Ready;
}
@ -318,7 +319,7 @@ impl Fsm for SplineToolFsmState {
(SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => {
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 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
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`.
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 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());
// 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| {
preview_point.is_some_and(|pp| pp == cp) || cp == endpoint
});
let closest_point = closest_point(
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 };
// 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 shape_editor: &'a mut ShapeState,
pub node_graph: &'a NodeGraphExecutor,
}
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 preferences: &'a PreferencesMessageHandler,
}
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.,
};
// 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 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_left = left - center;
let in_segment = self.get_segment(self.len_segments() - 1).unwrap();
let in_tangent = in_segment.tangent(TValue::Parametric(1.));
let in_segment = self.len_segments().checked_sub(1).and_then(|segment| self.get_segment(segment));
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 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. });
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
half = { version = "2.4.1", default-features = false, features = ["bytemuck"] }
tinyvec = { version = "1" }
# Optional workspace dependencies
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))
}
/// Points connected to a single segment
pub fn single_connected_points(&self) -> impl Iterator<Item = PointId> + '_ {
self.point_domain
.ids()
.iter()
.enumerate()
.filter(|(index, _)| self.segment_domain.connected_count(*index) == 1)
.map(|(_, &id)| id)
/// Points that can be extended from.
///
/// This is usually only points with exactly one connection unless vector meshes are enabled.
pub fn extendable_points(&self, vector_meshes: bool) -> impl Iterator<Item = PointId> + '_ {
let point_ids = self.point_domain.ids().iter().enumerate();
point_ids.filter(move |(index, _)| vector_meshes || 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.

View file

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