diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 34b9ffa1e6..2191161bcb 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -30,7 +30,9 @@ ron = ["dep:ron"] # Local dependencies graphite-proc-macros = { path = "../proc-macros" } graph-craft = { path = "../node-graph/graph-craft" } -interpreted-executor = { path = "../node-graph/interpreted-executor", features = ["serde"] } +interpreted-executor = { path = "../node-graph/interpreted-executor", features = [ + "serde", +] } graphene-core = { path = "../node-graph/gcore" } graphene-std = { path = "../node-graph/gstd", features = ["serde"] } @@ -58,6 +60,9 @@ web-sys = { workspace = true, features = [ "Element", "HtmlCanvasElement", "CanvasRenderingContext2d", + "CanvasPattern", + "OffscreenCanvas", + "OffscreenCanvasRenderingContext2d", "TextMetrics", ] } diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 0448726b0a..b75940bf09 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -282,6 +282,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Enter); action_dispatch=SplineToolMessage::Confirm), // // FillToolMessage + entry!(PointerMove; refresh_keys=[Shift], action_dispatch=FillToolMessage::PointerMove), entry!(KeyDown(MouseLeft); action_dispatch=FillToolMessage::FillPrimaryColor), entry!(KeyDown(MouseLeft); modifiers=[Shift], action_dispatch=FillToolMessage::FillSecondaryColor), entry!(KeyUp(MouseLeft); action_dispatch=FillToolMessage::PointerUp), diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index 2ddee78dab..0a6f0b6902 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -47,7 +47,7 @@ impl MessageHandler> for OverlaysMessag let [a, b, c, d, e, f] = DAffine2::from_scale(DVec2::splat(device_pixel_ratio)).to_cols_array(); let _ = context.set_transform(a, b, c, d, e, f); - context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y); + context.clear_rect(0., 0., canvas.width().into(), canvas.height().into()); let _ = context.reset_transform(); if overlays_visible { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 79cc7f20c3..2ddb8169db 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -8,10 +8,12 @@ use bezier_rs::{Bezier, Subpath}; use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, TAU}; use glam::{DAffine2, DVec2}; +use graphene_core::Color; use graphene_core::renderer::Quad; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; -use wasm_bindgen::JsValue; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; pub type OverlayProvider = fn(OverlayContext) -> Message; @@ -447,6 +449,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + /// Used by the Pen and Path tools to outline the path of the shape. pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { self.start_dpi_aware_transform(); @@ -465,6 +468,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + /// Used by the Pen tool in order to show how the bezier curve would look like. pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { self.start_dpi_aware_transform(); @@ -493,7 +497,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -540,10 +544,63 @@ impl OverlayContext { } } + self.end_dpi_aware_transform(); + } + + /// Used by the Select tool to outline a path selected or hovered. + pub fn outline(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + self.push_path(subpaths, transform); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.stroke(); + } - self.end_dpi_aware_transform(); + /// Fills the area inside the path. Assumes `color` is in gamma space. + /// Used by the Pen tool to show the path being closed. + pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { + self.push_path(subpaths, transform); + + self.render_context.set_fill_style_str(color); + self.render_context.fill(); + } + + /// Fills the area inside the path with a pattern. Assumes `color` is in gamma space. + /// Used by the fill tool to show the area to be filled. + pub fn fill_path_pattern(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color) { + const PATTERN_WIDTH: usize = 4; + const PATTERN_HEIGHT: usize = 4; + + let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas + .get_context("2d") + .ok() + .flatten() + .expect("Failed to get canvas context") + .dyn_into() + .expect("Context should be a canvas 2d context"); + + // 4x4 pixels, 4 components (RGBA) per pixel + let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; + + // ┌▄▄┬──┬──┬──┐ + // ├▀▀┼──┼──┼──┤ + // ├──┼──┼▄▄┼──┤ + // ├──┼──┼▀▀┼──┤ + // └──┴──┴──┴──┘ + let pixels = [(0, 0), (2, 2)]; + for &(x, y) in &pixels { + let index = (x + y * PATTERN_WIDTH as usize) * 4; + data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb()); + } + + let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap(); + pattern_context.put_image_data(&image_data, 0., 0.).unwrap(); + let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); + + self.push_path(subpaths, transform); + + self.render_context.set_fill_style_canvas_pattern(&pattern); + self.render_context.fill(); } pub fn get_width(&self, text: &str) -> f64 { diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 91aa73ac41..b70affe51b 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -218,7 +218,7 @@ impl MessageHandler> for ToolMessageHandler { let document_data = &mut self.tool_state.document_tool_data; document_data.primary_color = color; - self.tool_state.document_tool_data.update_working_colors(responses); // TODO: Make this an event + document_data.update_working_colors(responses); // TODO: Make this an event } ToolMessage::SelectRandomPrimaryColor => { // Select a random primary color (rgba) based on an UUID diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 6bca37c0df..0eae252bcd 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,18 +1,23 @@ use super::tool_prelude::*; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; use graphene_core::vector::style::Fill; + #[derive(Default)] pub struct FillTool { fsm_state: FillToolFsmState, } #[impl_message(Message, ToolMessage, Fill)] -#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive(PartialEq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)] pub enum FillToolMessage { // Standard messages Abort, + WorkingColorChanged, + Overlays(OverlayContext), // Tool-specific messages + PointerMove, PointerUp, FillPrimaryColor, FillSecondaryColor, @@ -45,8 +50,10 @@ impl<'a> MessageHandler> for FillToo FillToolFsmState::Ready => actions!(FillToolMessageDiscriminant; FillPrimaryColor, FillSecondaryColor, + PointerMove, ), FillToolFsmState::Filling => actions!(FillToolMessageDiscriminant; + PointerMove, PointerUp, Abort, ), @@ -58,6 +65,8 @@ impl ToolTransition for FillTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(FillToolMessage::Abort.into()), + working_color_changed: Some(FillToolMessage::WorkingColorChanged.into()), + overlay_provider: Some(|overlay_context| FillToolMessage::Overlays(overlay_context).into()), ..Default::default() } } @@ -82,6 +91,23 @@ impl Fsm for FillToolFsmState { let ToolMessage::Fill(event) = event else { return self }; match (self, event) { + (_, FillToolMessage::Overlays(mut overlay_context)) => { + // Choose the working color to preview + let use_secondary = input.keyboard.get(Key::Shift as usize); + let preview_color = if use_secondary { global_tool_data.secondary_color } else { global_tool_data.primary_color }; + + // Get the layer the user is hovering over + if let Some(layer) = document.click(input) { + overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &preview_color); + } + + self + } + (_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => { + // Generate the hover outline + responses.add(OverlaysMessage::Draw); + self + } (FillToolFsmState::Ready, color_event) => { let Some(layer_identifier) = document.click(input) else { return self; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 97ee44ace1..537f3fd091 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1030,7 +1030,7 @@ impl Fsm for PathToolFsmState { match self { Self::Drawing { selection_shape } => { - let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) .unwrap() .with_alpha(0.05) .to_rgba_hex_srgb(); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index b7783c50ab..0f9e7cc0c2 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE}; +use crate::consts::{COLOR_OVERLAY_BLUE, DEFAULT_STROKE_WIDTH, HIDE_HANDLE_DISTANCE, LINE_ROTATE_SNAP_ANGLE}; use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; @@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles}; use graph_craft::document::NodeId; use graphene_core::Color; use graphene_core::vector::{PointId, VectorModificationType}; -use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, VectorData}; +use graphene_std::vector::{HandleId, ManipulatorPointId, NoHashBuilder, SegmentId, StrokeId, VectorData}; #[derive(Default)] pub struct PenTool { @@ -1614,6 +1614,54 @@ impl Fsm for PenToolFsmState { overlay_context.manipulator_anchor(next_anchor, false, None); } + // Display a filled overlay of the shape if the new point closes the path + if let Some(latest_point) = tool_data.latest_point() { + let handle_start = latest_point.handle_start; + let handle_end = tool_data.handle_end.unwrap_or(tool_data.next_handle_start); + let next_point = tool_data.next_point; + let start = latest_point.id; + + if let Some(layer) = layer { + let mut vector_data = document.network_interface.compute_modified_vector(layer).unwrap(); + + let closest_point = vector_data.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| { + vector_data.point_domain.position_from_id(id).map_or(false, |pos| { + let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); + dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2) + }) + }); + + // We have the point. Join the 2 vertices and check if any path is closed. + if let Some(end) = closest_point { + let segment_id = SegmentId::generate(); + vector_data.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); + + let grouped_segments = vector_data.auto_join_paths(); + let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); + + let subpaths: Vec<_> = closed_paths + .filter_map(|path| { + let segments = path.edges.iter().filter_map(|edge| { + vector_data + .segment_domain + .iter() + .find(|(id, _, _, _)| id == &edge.id) + .map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) + }); + vector_data.subpath_from_segments_ignore_discontinuities(segments) + }) + .collect(); + + let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + .unwrap() + .with_alpha(0.05) + .to_rgba_hex_srgb(); + fill_color.insert(0, '#'); + overlay_context.fill_path(subpaths.iter(), transform, fill_color.as_str()); + } + } + } + // Draw the overlays that visualize current snapping tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 191c9c03ef..277172dfcc 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -2,8 +2,8 @@ use super::tool_prelude::*; use crate::consts::{ - COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE, - SELECTION_TOLERANCE, + COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, RESIZE_HANDLE_SIZE, ROTATE_INCREMENT, + SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE, }; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; @@ -759,7 +759,7 @@ impl Fsm for SelectToolFsmState { } // Update the selection box - let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) .unwrap() .with_alpha(0.05) .to_rgba_hex_srgb(); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index b1c5fe5a73..9e144ae6dc 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -1,7 +1,7 @@ #![allow(clippy::too_many_arguments)] use super::tool_prelude::*; -use crate::consts::{COLOR_OVERLAY_RED, DRAG_THRESHOLD}; +use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_RED, DRAG_THRESHOLD}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -456,7 +456,7 @@ impl Fsm for TextToolFsmState { font_cache, .. } = transition_data; - let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) + let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) .unwrap() .with_alpha(0.05) .to_rgba_hex_srgb(); diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index dd9a5cd679..d5e1fe0241 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -553,6 +553,94 @@ impl RegionDomain { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct HalfEdge { + pub id: SegmentId, + pub start: usize, + pub end: usize, + pub reverse: bool, +} + +impl HalfEdge { + pub fn new(id: SegmentId, start: usize, end: usize, reverse: bool) -> Self { + Self { id, start, end, reverse } + } + + pub fn reversed(&self) -> Self { + Self { + id: self.id, + start: self.start, + end: self.end, + reverse: !self.reverse, + } + } + + pub fn normalize_direction(&self) -> Self { + if self.reverse { + Self { + id: self.id, + start: self.end, + end: self.start, + reverse: false, + } + } else { + *self + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FoundSubpath { + pub edges: Vec, +} + +impl FoundSubpath { + pub fn new(segments: Vec) -> Self { + Self { edges: segments } + } + + pub fn endpoints(&self) -> Option<(&HalfEdge, &HalfEdge)> { + match (self.edges.first(), self.edges.last()) { + (Some(first), Some(last)) => Some((first, last)), + _ => None, + } + } + + pub fn push(&mut self, segment: HalfEdge) { + self.edges.push(segment); + } + + pub fn insert(&mut self, index: usize, segment: HalfEdge) { + self.edges.insert(index, segment); + } + + pub fn extend(&mut self, segments: impl IntoIterator) { + self.edges.extend(segments); + } + + pub fn splice(&mut self, range: std::ops::Range, replace_with: I) + where + I: IntoIterator, + { + self.edges.splice(range, replace_with); + } + + pub fn is_closed(&self) -> bool { + match (self.edges.first(), self.edges.last()) { + (Some(first), Some(last)) => first.start == last.end, + _ => false, + } + } + + pub fn from_segment(segment: HalfEdge) -> Self { + Self { edges: vec![segment] } + } + + pub fn contains(&self, segment_id: SegmentId) -> bool { + self.edges.iter().any(|s| s.id == segment_id) + } +} + impl VectorData { /// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: bezier_rs::BezierHandles) -> bezier_rs::Bezier { @@ -592,6 +680,114 @@ impl VectorData { .map(to_bezier) } + pub fn auto_join_paths(&self) -> Vec { + let segments = self.segment_domain.iter().map(|(id, start, end, _)| HalfEdge::new(id, start, end, false)); + + let mut paths: Vec = Vec::new(); + let mut current_path: Option<&mut FoundSubpath> = None; + let mut previous: Option<(usize, usize)> = None; + + // First pass. Generates subpaths from continuous segments. + for seg_ref in segments { + let (start, end) = (seg_ref.start, seg_ref.end); + + if previous.is_some_and(|(_, prev_end)| start == prev_end) { + if let Some(path) = current_path.as_mut() { + path.push(seg_ref); + } + } else { + paths.push(FoundSubpath::from_segment(seg_ref)); + current_path = paths.last_mut(); + } + + previous = Some((start, end)); + } + + // Second pass. Try to join paths together. + let mut joined_paths = Vec::new(); + + loop { + let mut prev_index: Option = None; + let original_len = paths.len(); + + for current in paths.into_iter() { + // If there's no previous subpath, start a new one + if prev_index.is_none() { + joined_paths.push(current); + prev_index = Some(joined_paths.len() - 1); + continue; + } + + let prev = &mut joined_paths[prev_index.unwrap()]; + + // Compare segment connections + let (prev_first, prev_last) = prev.endpoints().unwrap(); + let (cur_first, cur_last) = current.endpoints().unwrap(); + + // Join paths if the endpoints connect + if prev_last.end == cur_first.start { + prev.edges.extend(current.edges.into_iter().map(|s| s.normalize_direction())); + } else if prev_first.start == cur_last.end { + prev.edges.splice(0..0, current.edges.into_iter().rev().map(|s| s.normalize_direction())); + } else if prev_last.end == cur_last.end { + prev.edges.extend(current.edges.into_iter().rev().map(|s| s.reversed().normalize_direction())); + } else if prev_first.start == cur_first.start { + prev.edges.splice(0..0, current.edges.into_iter().map(|s| s.reversed().normalize_direction())); + } else { + // If not connected, start a new subpath + joined_paths.push(current); + prev_index = Some(joined_paths.len() - 1); + } + } + + // If no paths were joined in this pass, we're done + if joined_paths.len() == original_len { + return joined_paths; + } + + // Repeat pass with newly joined paths + paths = joined_paths; + joined_paths = Vec::new(); + } + } + + /// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities. + pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator) -> Option> { + let mut first_point = None; + let mut groups = Vec::new(); + let mut last: Option<(usize, bezier_rs::BezierHandles)> = None; + + for (handle, start, end) in segments { + first_point = Some(first_point.unwrap_or(start)); + + groups.push(bezier_rs::ManipulatorGroup { + anchor: self.point_domain.positions()[start], + in_handle: last.and_then(|(_, handle)| handle.end()), + out_handle: handle.start(), + id: self.point_domain.ids()[start], + }); + + last = Some((end, handle)); + } + + let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point; + + if let Some((end, last_handle)) = last { + if closed { + groups[0].in_handle = last_handle.end(); + } else { + groups.push(bezier_rs::ManipulatorGroup { + anchor: self.point_domain.positions()[end], + in_handle: last_handle.end(), + out_handle: None, + id: self.point_domain.ids()[end], + }); + } + } + + Some(bezier_rs::Subpath::new(groups, closed)) + } + /// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the segments are not continuous. fn subpath_from_segments(&self, segments: impl Iterator) -> Option> { let mut first_point = None; @@ -670,12 +866,12 @@ impl VectorData { /// Construct a [`bezier_rs::Bezier`] curve for stroke. pub fn stroke_bezier_paths(&self) -> impl Iterator> { - self.build_stroke_path_iter().into_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed)) + self.build_stroke_path_iter().map(|(group, closed)| bezier_rs::Subpath::new(group, closed)) } /// Construct a [`kurbo::BezPath`] curve for stroke. pub fn stroke_bezpath_iter(&self) -> impl Iterator { - self.build_stroke_path_iter().into_iter().map(|(group, closed)| { + self.build_stroke_path_iter().map(|(group, closed)| { let mut bezpath = kurbo::BezPath::new(); let mut out_handle;