diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index d8a3791515..6d3d60371c 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1202,8 +1202,8 @@ impl MessageHandler> for DocumentMessag responses.add(PortfolioMessage::UpdateDocumentWidgets); } OverlaysType::Handles => visibility_settings.handles = visible, + OverlaysType::FillableIndicator => visibility_settings.fillable_indicator = visible, } - responses.add(BroadcastEvent::ToolAbort); responses.add(OverlaysMessage::Draw); } @@ -2358,6 +2358,27 @@ impl DocumentMessageHandler { ] }, }, + LayoutGroup::Row { + widgets: vec![TextLabel::new("Fill Tool").widget_holder()], + }, + LayoutGroup::Row { + widgets: { + let mut checkbox_id = CheckboxId::default(); + vec![ + CheckboxInput::new(self.overlays_visibility_settings.fillable_indicator) + .on_update(|optional_input: &CheckboxInput| { + DocumentMessage::SetOverlaysVisibility { + visible: optional_input.checked, + overlays_type: Some(OverlaysType::FillableIndicator), + } + .into() + }) + .for_label(checkbox_id.clone()) + .widget_holder(), + TextLabel::new("Fillable Indicator".to_string()).for_checkbox(&mut checkbox_id).widget_holder(), + ] + }, + }, ]) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index e95c7e9d98..1be98e2201 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -33,6 +33,10 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, stroke: Stroke, }, + StrokeColorSet { + layer: LayerNodeIdentifier, + stroke_color: Color, + }, TransformChange { layer: LayerNodeIdentifier, transform: DAffine2, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 9f8b8586f1..deb1cf36d7 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -56,6 +56,11 @@ impl MessageHandler> for Gr modify_inputs.stroke_set(stroke); } } + GraphOperationMessage::StrokeColorSet { layer, stroke_color } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.stroke_color_set(Some(stroke_color)); + } + } GraphOperationMessage::TransformChange { layer, transform, diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index f6648c72f8..5e3e8a1d1f 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -366,6 +366,13 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.line_join_miter_limit), false), false); } + pub fn stroke_color_set(&mut self, color: Option) { + let Some(stroke_node_id) = self.existing_node_id("Stroke", false) else { return }; + + let input_connector = InputConnector::node(stroke_node_id, 1); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(color), false), false); + } + /// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform. /// A new Transform node is created if one does not exist, unless it would be given the identity transform. pub fn transform_change_with_parent(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) { 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 0db542da75..38686c2bb4 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -51,12 +51,6 @@ impl MessageHandler> for OverlaysMessag let _ = context.reset_transform(); if visibility_settings.all() { - responses.add(DocumentMessage::GridOverlays(OverlayContext { - render_context: context.clone(), - size: size.as_dvec2(), - device_pixel_ratio, - visibility_settings: visibility_settings.clone(), - })); for provider in &self.overlay_providers { responses.add(provider(OverlayContext { render_context: context.clone(), @@ -65,6 +59,12 @@ impl MessageHandler> for OverlaysMessag visibility_settings: visibility_settings.clone(), })); } + responses.add(DocumentMessage::GridOverlays(OverlayContext { + render_context: context.clone(), + size: size.as_dvec2(), + device_pixel_ratio, + visibility_settings: visibility_settings.clone(), + })); } } #[cfg(not(target_arch = "wasm32"))] diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 61543c55e6..388343ba62 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -11,6 +11,7 @@ use glam::{DAffine2, DVec2}; use graphene_core::Color; use graphene_core::renderer::Quad; use graphene_std::renderer::ClickTargetType; +use graphene_std::vector::style::Stroke; use graphene_std::vector::{PointId, SegmentId, VectorData}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; @@ -25,33 +26,48 @@ pub fn empty_provider() -> OverlayProvider { // Types of overlays used by DocumentMessage to enable/disable select group of overlays in the frontend #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum OverlaysType { + // ======= + // General + // ======= ArtboardName, - CompassRose, - QuickMeasurement, TransformMeasurement, + // =========== + // Select Tool + // =========== + QuickMeasurement, TransformCage, + CompassRose, + Pivot, HoverOutline, SelectionOutline, - Pivot, + // ================ + // Pen & Path Tools + // ================ Path, Anchors, Handles, + // ========= + // Fill Tool + // ========= + FillableIndicator, } #[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(default = "OverlaysVisibilitySettings::default")] pub struct OverlaysVisibilitySettings { pub all: bool, pub artboard_name: bool, - pub compass_rose: bool, - pub quick_measurement: bool, pub transform_measurement: bool, + pub quick_measurement: bool, pub transform_cage: bool, + pub compass_rose: bool, + pub pivot: bool, pub hover_outline: bool, pub selection_outline: bool, - pub pivot: bool, pub path: bool, pub anchors: bool, pub handles: bool, + pub fillable_indicator: bool, } impl Default for OverlaysVisibilitySettings { @@ -69,6 +85,7 @@ impl Default for OverlaysVisibilitySettings { path: true, anchors: true, handles: true, + fillable_indicator: true, } } } @@ -121,6 +138,10 @@ impl OverlaysVisibilitySettings { pub fn handles(&self) -> bool { self.all && self.anchors && self.handles } + + pub fn fillable_indicator(&self) -> bool { + self.all && self.fillable_indicator + } } #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] @@ -550,8 +571,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) { + fn draw_path_from_vector_data(&mut self, vector_data: &VectorData, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -563,10 +583,14 @@ impl OverlayContext { self.bezier_command(bezier, transform, move_to); } + 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.draw_path_from_vector_data(vector_data, transform); self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.stroke(); - - self.end_dpi_aware_transform(); } /// Used by the Pen tool in order to show how the bezier curve would look like. @@ -598,7 +622,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { + pub fn draw_path_from_subpaths(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -612,18 +636,19 @@ impl OverlayContext { self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y); for curve in curves { + let splat_value = 0.5; match curve.handles { bezier_rs::BezierHandles::Linear => { let a = transform.transform_point2(curve.end()); - let a = a.round() - DVec2::splat(0.5); + let a = a.round() - DVec2::splat(splat_value); self.render_context.line_to(a.x, a.y) } bezier_rs::BezierHandles::Quadratic { handle } => { let a = transform.transform_point2(handle); let b = transform.transform_point2(curve.end()); - let a = a.round() - DVec2::splat(0.5); - let b = b.round() - DVec2::splat(0.5); + let a = a.round() - DVec2::splat(splat_value); + let b = b.round() - DVec2::splat(splat_value); self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y) } @@ -631,9 +656,9 @@ impl OverlayContext { let a = transform.transform_point2(handle_start); let b = transform.transform_point2(handle_end); let c = transform.transform_point2(curve.end()); - let a = a.round() - DVec2::splat(0.5); - let b = b.round() - DVec2::splat(0.5); - let c = c.round() - DVec2::splat(0.5); + let a = a.round() - DVec2::splat(splat_value); + let b = b.round() - DVec2::splat(splat_value); + let c = c.round() - DVec2::splat(splat_value); self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y) } @@ -660,7 +685,7 @@ impl OverlayContext { }); if !subpaths.is_empty() { - self.push_path(subpaths.iter(), transform); + self.draw_path_from_subpaths(subpaths.iter(), transform); let color = color.unwrap_or(COLOR_OVERLAY_BLUE); self.render_context.set_stroke_style_str(color); @@ -668,18 +693,8 @@ impl OverlayContext { } } - /// 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) { + /// Default canvas pattern used for filling stroke or fill of a path. + fn fill_canvas_pattern(&self, color: &Color) -> web_sys::CanvasPattern { const PATTERN_WIDTH: usize = 4; const PATTERN_HEIGHT: usize = 4; @@ -708,12 +723,49 @@ impl OverlayContext { 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(); + return self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); + } - self.push_path(subpaths, transform); + /// Fills the area inside the path (with an optional pattern). Assumes `color` is in gamma space. + /// Used by the Pen tool to show the path being closed and by the Fill tool to show the area to be filled with a pattern. + pub fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, with_pattern: bool, clear_stroke_part: bool, stroke_width: Option) { + self.render_context.save(); + self.render_context.set_line_width(stroke_width.unwrap_or(1.)); + self.draw_path_from_subpaths(subpaths, transform); - self.render_context.set_fill_style_canvas_pattern(&pattern); + if with_pattern { + self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color)); + } else { + let color_str = format!("#{:?}", color.to_rgba_hex_srgb()); + self.render_context.set_fill_style_str(&color_str.as_str()); + } self.render_context.fill(); + + // Make the stroke transparent and erase the fill area overlapping the stroke. + if clear_stroke_part { + self.render_context.set_global_composite_operation("destination-out").expect("Failed to set global composite operation"); + self.render_context.set_stroke_style_str(&"#000000"); + self.render_context.stroke(); + } + + self.render_context.restore(); + } + + pub fn fill_stroke(&mut self, subpaths: impl Iterator>>, overlay_stroke: &Stroke) { + self.render_context.save(); + + // debug!("overlay_stroke.weight * ptz.zoom(): {:?}", overlay_stroke.weight); + self.render_context.set_line_width(overlay_stroke.weight); + self.draw_path_from_subpaths(subpaths, overlay_stroke.transform); + + self.render_context + .set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(&overlay_stroke.color.expect("Color should be set for fill_stroke()"))); + self.render_context.set_line_cap(overlay_stroke.line_cap.html_canvas_name().as_str()); + self.render_context.set_line_join(overlay_stroke.line_join.html_canvas_name().as_str()); + self.render_context.set_miter_limit(overlay_stroke.line_join_miter_limit); + self.render_context.stroke(); + + self.render_context.restore(); } pub fn get_width(&self, text: &str) -> f64 { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index a6405b952f..8b13c94173 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,7 +1,10 @@ use super::tool_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; +use graph_craft::document::value::TaggedValue; use graphene_core::vector::style::Fill; +use graphene_std::vector::PointId; +use graphene_std::vector::style::Stroke; #[derive(Default)] pub struct FillTool { @@ -72,6 +75,20 @@ impl ToolTransition for FillTool { } } +pub fn close_to_subpath(mouse_pos: DVec2, subpath: bezier_rs::Subpath, stroke_width: f64, layer_to_viewport_transform: DAffine2) -> bool { + let mouse_pos = layer_to_viewport_transform.inverse().transform_point2(mouse_pos); + let max_stroke_distance = stroke_width; + + if let Some((segment_index, t)) = subpath.project(mouse_pos) { + let nearest_point = subpath.evaluate(bezier_rs::SubpathTValue::Parametric { segment_index, t }); + // debug!("max_stroke_distance: {max_stroke_distance}"); + // debug!("mouse-stroke distance: {:?}", (mouse_pos - nearest_point).length()); + (mouse_pos - nearest_point).length_squared() <= max_stroke_distance + } else { + false + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum FillToolFsmState { #[default] @@ -96,11 +113,51 @@ impl Fsm for FillToolFsmState { 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 }; + if !overlay_context.visibility_settings.fillable_indicator() { + return self; + }; // 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); - } + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + let mut subpaths = vector_data.stroke_bezier_paths(); + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + + // Stroke + let stroke_node = graph_layer.upstream_node_id_from_name("Stroke"); + let stroke_width = get_stroke_width(layer, &document.network_interface).unwrap_or(1.0); + let zoom = document.document_ptz.zoom(); + let modified_stroke_width = stroke_width * zoom; + let stroke_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + let close_to_stroke = subpaths.any(|subpath| close_to_subpath(input.mouse.position, subpath, stroke_width, document.metadata().transform_to_viewport(layer))); + + // Fill + let fill_node = graph_layer.upstream_node_id_from_name("Fill"); + let fill_exists_and_visible = fill_node.is_some_and(|fill| document.network_interface.is_visible(&fill, &[])); + if stroke_exists_and_visible && close_to_stroke { + let overlay_stroke = || { + let mut stroke = Stroke::new(Some(preview_color), modified_stroke_width); + stroke.transform = document.metadata().transform_to_viewport(layer); + let line_cap = graph_layer.find_input("Stroke", 5).unwrap(); + stroke.line_cap = if let TaggedValue::LineCap(line_cap) = line_cap { *line_cap } else { return None }; + let line_join = graph_layer.find_input("Stroke", 6).unwrap(); + stroke.line_join = if let TaggedValue::LineJoin(line_join) = line_join { *line_join } else { return None }; + let miter_limit = graph_layer.find_input("Stroke", 7).unwrap(); + stroke.line_join_miter_limit = if let TaggedValue::F64(miter_limit) = miter_limit { *miter_limit } else { return None }; + + Some(stroke) + }; + + if let Some(stroke) = overlay_stroke() { + subpaths = vector_data.stroke_bezier_paths(); + overlay_context.fill_stroke(subpaths, &stroke); + } + } else if fill_exists_and_visible { + subpaths = vector_data.stroke_bezier_paths(); + overlay_context.fill_path(subpaths, document.metadata().transform_to_viewport(layer), &preview_color, true, stroke_exists_and_visible, Some(modified_stroke_width)); + } + } + } self } (_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => { @@ -109,11 +166,12 @@ impl Fsm for FillToolFsmState { self } (FillToolFsmState::Ready, color_event) => { - let Some(layer_identifier) = document.click(input) else { + // Get the layer the user is hovering over + let Some(layer) = document.click(input) else { return self; }; // If the layer is a raster layer, don't fill it, wait till the flood fill tool is implemented - if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) { + if NodeGraphLayer::is_raster_layer(layer, &mut document.network_interface) { return self; } let fill = match color_event { @@ -121,10 +179,33 @@ impl Fsm for FillToolFsmState { FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color.to_gamma_srgb()), _ => return self, }; + let stroke_color = match color_event { + FillToolMessage::FillPrimaryColor => global_tool_data.primary_color.to_gamma_srgb(), + FillToolMessage::FillSecondaryColor => global_tool_data.secondary_color.to_gamma_srgb(), + _ => return self, + }; responses.add(DocumentMessage::AddTransaction); - responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + let mut subpaths = vector_data.stroke_bezier_paths(); + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + + // Stroke + let stroke_node = graph_layer.upstream_node_id_from_name("Stroke"); + let stroke_width = get_stroke_width(layer, &document.network_interface).unwrap_or(1.0); + let stroke_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + let close_to_stroke = subpaths.any(|subpath| close_to_subpath(input.mouse.position, subpath, stroke_width, document.metadata().transform_to_viewport(layer))); + // Fill + let fill_node = graph_layer.upstream_node_id_from_name("Fill"); + let fill_exists_and_visible = fill_node.is_some_and(|fill| document.network_interface.is_visible(&fill, &[])); + + if stroke_exists_and_visible && close_to_stroke { + responses.add(GraphOperationMessage::StrokeColorSet { layer, stroke_color }); + } else if fill_exists_and_visible { + responses.add(GraphOperationMessage::FillSet { layer, fill }); + } + } FillToolFsmState::Filling } (FillToolFsmState::Filling, FillToolMessage::PointerUp) => FillToolFsmState::Ready, diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 79c07173a1..c76dd79a8e 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1601,6 +1601,50 @@ impl Fsm for PenToolFsmState { // The most recently placed anchor's outgoing handle (which is currently influencing the currently-being-placed segment) let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start)); + // 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(segments, true) + }) + .collect(); + + let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05); + overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, false, None); + } + } + } + if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) { let handles = BezierHandles::Cubic { handle_start, handle_end }; let end = tool_data.next_point; @@ -1740,16 +1784,12 @@ impl Fsm for PenToolFsmState { .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) + vector_data.subpath_from_segments(segments, true) }) .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()); + let fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05); + overlay_context.fill_path(subpaths.iter(), transform, &fill_color, false, false, None); } } } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 403fc37665..1a6515de26 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -79,6 +79,10 @@ impl ClickTarget { &self.target_type } + pub fn stroke_width(&self) -> f64 { + self.stroke_width + } + pub fn bounding_box(&self) -> Option<[DVec2; 2]> { self.bounding_box } diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 2f0676b461..3d084cd3ea 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -514,6 +514,14 @@ impl LineCap { LineCap::Square => "square", } } + + pub fn html_canvas_name(&self) -> String { + match self { + LineCap::Butt => String::from("butt"), + LineCap::Round => String::from("round"), + LineCap::Square => String::from("square"), + } + } } #[repr(C)] @@ -534,6 +542,14 @@ impl LineJoin { LineJoin::Round => "round", } } + + pub fn html_canvas_name(&self) -> String { + match self { + LineJoin::Bevel => String::from("bevel"), + LineJoin::Miter => String::from("miter"), + LineJoin::Round => String::from("round"), + } + } } fn daffine2_identity() -> DAffine2 { diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index 8f71996c3e..e9b7b59fc3 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -751,51 +751,15 @@ impl VectorData { } } - /// 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> { + /// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point), optionally ignoring discontinuities. + /// Returns None if any ids are invalid or if the segments are not continuous. + pub fn subpath_from_segments(&self, segments: impl Iterator, ignore_discontinuities: bool) -> 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; - let mut groups = Vec::new(); - let mut last: Option<(usize, bezier_rs::BezierHandles)> = None; - - for (handle, start, end) in segments { - if last.is_some_and(|(previous_end, _)| previous_end != start) { + if !ignore_discontinuities && last.is_some_and(|(previous_end, _)| previous_end != start) { warn!("subpath_from_segments that were not continuous"); return None; } @@ -845,7 +809,7 @@ impl VectorData { .zip(self.segment_domain.end_point.get(range)?) .map(|((&handles, &start), &end)| (handles, start, end)); - self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath)) + self.subpath_from_segments(segments_iter, false).map(|subpath| (id, subpath)) }) } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index b8ed2461ed..9bb63c6619 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -367,7 +367,7 @@ impl TaggedValue { pub fn to_u32(&self) -> u32 { match self { TaggedValue::U32(x) => *x, - _ => panic!("Passed value is not of type u32"), + _ => panic!("Cannot convert to u32"), } } }