diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 91135299ad..cd80eea7bb 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1227,6 +1227,7 @@ impl MessageHandler> for DocumentMes responses.add(PortfolioMessage::UpdateDocumentWidgets); } OverlaysType::Handles => visibility_settings.handles = visible, + OverlaysType::FillableIndicator => visibility_settings.fillable_indicator = visible, } responses.add(EventMessage::ToolAbort); @@ -2300,7 +2301,7 @@ impl DocumentMessageHandler { widgets: { let checkbox_id = CheckboxId::new(); vec![ - CheckboxInput::new(self.overlays_visibility_settings.pivot) + CheckboxInput::new(self.overlays_visibility_settings.origin) .on_update(|optional_input: &CheckboxInput| { DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked, @@ -2411,6 +2412,27 @@ impl DocumentMessageHandler { ] }, }, + LayoutGroup::Row { + widgets: vec![TextLabel::new("Fill Tool").widget_holder()], + }, + LayoutGroup::Row { + widgets: { + let checkbox_id = CheckboxId::new(); + 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(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 8ebde695b5..0419845dad 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 @@ -4,7 +4,6 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node use crate::messages::prelude::*; use glam::{DAffine2, IVec2}; use graph_craft::document::NodeId; -use graphene_std::Artboard; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::raster::BlendMode; use graphene_std::raster_types::{CPU, Raster}; @@ -14,6 +13,7 @@ use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::PointId; use graphene_std::vector::VectorModificationType; use graphene_std::vector::style::{Fill, Stroke}; +use graphene_std::{Artboard, Color}; #[impl_message(Message, DocumentMessage, GraphOperation)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -41,6 +41,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 5bf6cce032..719e6d813c 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 @@ -63,6 +63,11 @@ impl MessageHandler> for 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 4079fefd32..162db9c2fb 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -7,7 +7,6 @@ use glam::{DAffine2, IVec2}; use graph_craft::concrete; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; -use graphene_std::Artboard; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::raster::BlendMode; use graphene_std::raster_types::{CPU, Raster}; @@ -17,7 +16,7 @@ use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::Vector; use graphene_std::vector::style::{Fill, Stroke}; use graphene_std::vector::{PointId, VectorModificationType}; -use graphene_std::{Graphic, NodeInputDecleration}; +use graphene_std::{Artboard, Color, Graphic, NodeInputDecleration}; #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] pub enum TransformIn { @@ -394,6 +393,14 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true); } + pub fn stroke_color_set(&mut self, color: Option) { + let Some(stroke_node_id) = self.existing_node_id("Stroke", false) else { return }; + + let stroke_color = if let Some(color) = color { Table::new_from_element(color) } else { Table::new() }; + let input_connector = InputConnector::node(stroke_node_id, 1); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(stroke_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 e6da0a9bcb..59c5f7e0d8 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -56,14 +56,6 @@ impl MessageHandler> for OverlaysMes let _ = canvas_context.reset_transform(); if visibility_settings.all() { - responses.add(DocumentMessage::GridOverlays { - context: OverlayContext { - render_context: canvas_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: canvas_context.clone(), @@ -72,6 +64,14 @@ impl MessageHandler> for OverlaysMes visibility_settings: visibility_settings.clone(), })); } + responses.add(DocumentMessage::GridOverlays { + context: OverlayContext { + render_context: canvas_context.clone(), + size: size.as_dvec2(), + device_pixel_ratio, + visibility_settings: visibility_settings.clone(), + }, + }); } } #[cfg(all(not(target_family = "wasm"), not(test)))] diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 31e87ade01..10d2ca47c5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -13,6 +13,7 @@ use graphene_std::math::quad::Quad; use graphene_std::subpath::Subpath; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2}; +use graphene_std::vector::style::Stroke; use graphene_std::vector::{PointId, SegmentId, Vector}; use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg}; use std::collections::HashMap; @@ -28,36 +29,50 @@ pub fn empty_provider() -> OverlayProvider { /// Types of overlays used by DocumentMessage to enable/disable the selected set of viewport overlays. #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum OverlaysType { + // ======= + // General + // ======= ArtboardName, - CompassRose, - QuickMeasurement, TransformMeasurement, + // =========== + // Select Tool + // =========== + QuickMeasurement, TransformCage, - HoverOutline, - SelectionOutline, + CompassRose, Pivot, Origin, + HoverOutline, + SelectionOutline, + // ================ + // Pen & Path Tools + // ================ Path, Anchors, Handles, + // ========= + // Fill Tool + // ========= + FillableIndicator, } #[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -#[serde(default)] +#[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 hover_outline: bool, - pub selection_outline: bool, + pub compass_rose: bool, pub pivot: bool, pub origin: bool, + pub hover_outline: bool, + pub selection_outline: bool, pub path: bool, pub anchors: bool, pub handles: bool, + pub fillable_indicator: bool, } impl Default for OverlaysVisibilitySettings { @@ -76,6 +91,7 @@ impl Default for OverlaysVisibilitySettings { path: true, anchors: true, handles: true, + fillable_indicator: true, } } } @@ -132,6 +148,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)] @@ -754,8 +774,7 @@ impl OverlayContext { self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); } - /// Used by the Pen and Path tools to outline the path of the shape. - pub fn outline_vector(&mut self, vector: &Vector, transform: DAffine2) { + pub fn draw_path_from_vector_data(&mut self, vector: &Vector, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -767,10 +786,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: &Vector, transform: DAffine2) { + self.draw_path_from_vector_data(vector, 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. @@ -831,7 +854,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(); @@ -892,7 +915,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); @@ -901,18 +924,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; @@ -941,12 +954,57 @@ impl OverlayContext { let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&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); + return self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap(); + } - self.render_context.set_fill_style_canvas_pattern(&pattern); + /// 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); + + 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.cap.html_canvas_name().as_str()); + self.render_context.set_line_join(overlay_stroke.join.html_canvas_name().as_str()); + self.render_context.set_miter_limit(overlay_stroke.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/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 13915b692d..dcdc8ca88d 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -9,6 +9,7 @@ use graphene_std::math::quad::Quad; use graphene_std::subpath; use graphene_std::transform::Footprint; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; +use graphene_std::vector::style::Stroke; use graphene_std::vector::{PointId, Vector}; use std::collections::{HashMap, HashSet}; use std::num::NonZeroU64; @@ -81,6 +82,24 @@ impl DocumentMetadata { footprint * local_transform } + pub fn transform_to_viewport_with_stroke_transform(&self, layer: LayerNodeIdentifier, _stroke: Stroke) -> DAffine2 { + // We're not allowed to convert the root parent to a node id + if layer == LayerNodeIdentifier::ROOT_PARENT { + return self.document_to_viewport; + } + + let footprint = self.upstream_footprints.get(&layer.to_node()).map(|footprint| footprint.transform).unwrap_or(self.document_to_viewport); + let local_transform = self.local_transforms.get(&layer.to_node()).copied().unwrap_or_default(); + + // let has_real_stroke = vector_data.style.stroke().filter(|stroke| stroke.weight() > 0.); + // let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); + // let applied_stroke_transform = set_stroke_transform.unwrap_or(*instance.transform); + // let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform); + // let stroke_transform = self.upstream_transform(stroke_node); + + footprint * local_transform + } + pub fn transform_to_viewport_if_feeds(&self, layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> DAffine2 { // We're not allowed to convert the root parent to a node id if layer == LayerNodeIdentifier::ROOT_PARENT { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 228ab5a5f3..35d14da394 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,7 +1,13 @@ 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_std::vector::style::Fill; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_stroke_width}; +use graph_craft::document::value::TaggedValue; +use graphene_std::NodeInputDecleration; +use graphene_std::subpath::Subpath; +use graphene_std::vector::PointId; +use graphene_std::vector::stroke::{CapInput, JoinInput, MiterLimitInput}; +use graphene_std::vector::style::{Fill, Stroke, StrokeCap, StrokeJoin}; +use kurbo::Shape; #[derive(Default, ExtractField)] pub struct FillTool { @@ -73,6 +79,30 @@ impl ToolTransition for FillTool { } } +pub fn close_to_subpath(mouse_pos: DVec2, subpath: Subpath, stroke_width: f64, _zoom: 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; + + let subpath_bezpath = subpath.to_bezpath(); + let mouse_point = kurbo::Point::new(mouse_pos.x, mouse_pos.y); + let mut is_close = false; + for seg in subpath_bezpath.segments() { + if seg.contains(mouse_point) { + is_close = true; + } + } + return is_close; + + // if let Some((segment_index, t)) = subpath.project(mouse_pos) { + // let nearest_point = subpath.evaluate(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] @@ -104,11 +134,57 @@ 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_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + + let stroke = vector_data.style.stroke().unwrap(); + 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 close_to_stroke = subpaths.any(|subpath| close_to_subpath(input.mouse.position, subpath, stroke_width, zoom, 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, &[])); + + subpaths = vector_data.stroke_bezier_paths(); + if stroke_exists_and_visible && close_to_stroke { + let overlay_stroke = || { + let mut overlay_stroke = Stroke::new(Some(preview_color), modified_stroke_width); + overlay_stroke.transform = document.metadata().transform_to_viewport_with_stroke_transform(layer, stroke); + let line_cap = graph_layer.find_input("Stroke", CapInput::INDEX).unwrap(); + overlay_stroke.cap = if let TaggedValue::StrokeCap(line_cap) = line_cap { *line_cap } else { StrokeCap::default() }; + let line_join = graph_layer.find_input("Stroke", JoinInput::INDEX).unwrap(); + overlay_stroke.join = if let TaggedValue::StrokeJoin(line_join) = line_join { *line_join } else { StrokeJoin::default() }; + let miter_limit = graph_layer.find_input("Stroke", MiterLimitInput::INDEX).unwrap(); + overlay_stroke.join_miter_limit = if let TaggedValue::F64(miter_limit) = miter_limit { *miter_limit } else { f64::default() }; + + overlay_stroke + }; + + overlay_context.fill_stroke(subpaths, &overlay_stroke()); + } else if fill_exists_and_visible { + overlay_context.fill_path( + subpaths, + document.metadata().transform_to_viewport_with_stroke_transform(layer, stroke), + &preview_color, + true, + stroke_exists_and_visible, + Some(modified_stroke_width), + ); + } + } + } self } (_, FillToolMessage::PointerMove | FillToolMessage::WorkingColorChanged) => { @@ -117,11 +193,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 { @@ -129,10 +206,34 @@ 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_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + let stroke_width = get_stroke_width(layer, &document.network_interface).unwrap_or(1.0); + let zoom = document.document_ptz.zoom(); + let close_to_stroke = subpaths.any(|subpath| close_to_subpath(input.mouse.position, subpath, stroke_width, zoom, 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 547f638fa0..4c4515ff16 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1648,6 +1648,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, (Some(handle_start), Some(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 end = tool_data.next_point; let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end))); @@ -1752,54 +1796,6 @@ impl Fsm for PenToolFsmState { } } - // 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 Some(mut vector) = document.network_interface.compute_modified_vector(layer) - { - let closest_point = vector.extendable_points(preferences.vector_meshes).filter(|&id| id != start).find(|&id| { - vector.point_domain.position_from_id(id).is_some_and(|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.push(segment_id, start, end, (Some(handle_start), Some(handle_end)), StrokeId::ZERO); - - let grouped_segments = vector.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 - .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.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/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 2850401270..39a66c1a22 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -223,6 +223,14 @@ impl StrokeCap { StrokeCap::Square => "square", } } + + pub fn html_canvas_name(&self) -> String { + match self { + StrokeCap::Butt => String::from("butt"), + StrokeCap::Round => String::from("round"), + StrokeCap::Square => String::from("square"), + } + } } #[repr(C)] @@ -243,6 +251,14 @@ impl StrokeJoin { StrokeJoin::Round => "round", } } + + pub fn html_canvas_name(&self) -> String { + match self { + StrokeJoin::Bevel => String::from("bevel"), + StrokeJoin::Miter => String::from("miter"), + StrokeJoin::Round => String::from("round"), + } + } } #[repr(C)] diff --git a/node-graph/gcore/src/vector/vector_attributes.rs b/node-graph/gcore/src/vector/vector_attributes.rs index 461e3a1abf..33422e8df5 100644 --- a/node-graph/gcore/src/vector/vector_attributes.rs +++ b/node-graph/gcore/src/vector/vector_attributes.rs @@ -804,13 +804,18 @@ impl Vector { } } - /// Construct a [`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`] 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 manipulators_list = Vec::new(); let mut last: Option<(usize, BezierHandles)> = None; for (handle, start, end) in segments { + if !ignore_discontinuities && last.is_some_and(|(previous_end, _)| previous_end != start) { + warn!("subpath_from_segments that were not continuous"); + return None; + } first_point = Some(first_point.unwrap_or(start)); manipulators_list.push(ManipulatorGroup { diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 3bd200e922..5a31c8b646 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"), } } } diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index f470e628e2..671ab8ab74 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -670,6 +670,7 @@ impl Render for Table { let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); let applied_stroke_transform = set_stroke_transform.unwrap_or(*row.transform); let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform); + let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse()); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); let layer_bounds = vector.bounding_box().unwrap_or_default();