diff --git a/Cargo.toml b/Cargo.toml index b97a288f75..a69d965409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,6 +132,8 @@ web-sys = { version = "=0.3.77", features = [ "HtmlCanvasElement", "CanvasRenderingContext2d", "CanvasPattern", + "DomMatrix", + "SvgMatrix", "OffscreenCanvas", "OffscreenCanvasRenderingContext2d", "TextMetrics", diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 7c83ae6c9e..886a4a669f 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1091,6 +1091,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); @@ -2733,6 +2734,23 @@ impl DocumentMessageHandler { .widget_instance(), ] }), + LayoutGroup::row(vec![TextLabel::new("Fill Tool").widget_instance()]), + LayoutGroup::row({ + 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) + .widget_instance(), + TextLabel::new("Fillable Indicator".to_string()).for_checkbox(checkbox_id).widget_instance(), + ] + }), ])) .widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(), 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 7c0d8bb8e1..72e6da9247 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 @@ -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 db6148934c..8fe4aa6014 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 @@ -66,6 +66,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 26903bd89e..a04d9bc127 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -495,6 +495,16 @@ 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_proto_node_id(graphene_std::vector::stroke::IDENTIFIER, 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 3bf436251c..ba639b0b06 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -50,13 +50,6 @@ impl MessageHandler> for OverlaysMes canvas_context.clear_rect(0., 0., width, height); if visibility_settings.all() { - responses.add(DocumentMessage::GridOverlays { - context: OverlayContext { - render_context: canvas_context.clone(), - visibility_settings: visibility_settings.clone(), - viewport: *viewport, - }, - }); for provider in &self.overlay_providers { responses.add(provider(OverlayContext { render_context: canvas_context.clone(), @@ -64,6 +57,13 @@ impl MessageHandler> for OverlaysMes viewport: *viewport, })); } + responses.add(DocumentMessage::GridOverlays { + context: OverlayContext { + render_context: canvas_context.clone(), + visibility_settings: visibility_settings.clone(), + viewport: *viewport, + }, + }); } } #[cfg(all(not(target_family = "wasm"), not(test)))] diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 9991d39c57..2d055ea142 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -10,12 +10,14 @@ use crate::messages::prelude::ViewportMessageHandler; use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; +use graphene_std::Color; use graphene_std::math::quad::Quad; use graphene_std::subpath::{self, Subpath}; use graphene_std::table::Table; use graphene_std::text::{Font, TextAlign, TypesettingConfig}; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::point_to_dvec2; +use graphene_std::vector::style::Stroke; use graphene_std::vector::{PointId, SegmentId, Vector}; use kurbo::{self, BezPath, ParamCurve}; use kurbo::{Affine, PathSeg}; @@ -45,19 +47,32 @@ pub enum GizmoEmphasis { #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum OverlaysType { + // ======= + // General + // ======= ArtboardName, - CompassRose, - QuickMeasurement, TransformMeasurement, + // =========== + // Select Tool + // =========== + QuickMeasurement, TransformCage, + CompassRose, + Pivot, + Origin, HoverOutline, SelectionOutline, LayerOriginCross, - Pivot, - Origin, + // ================ + // Pen & Path Tools + // ================ Path, Anchors, Handles, + // ========= + // Fill Tool + // ========= + FillableIndicator, } // TODO Remove duplicated definition of this in `utility_types_web.rs` @@ -67,18 +82,19 @@ pub enum OverlaysType { 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 origin: bool, pub hover_outline: bool, pub selection_outline: bool, pub layer_origin_cross: bool, - pub pivot: bool, - pub origin: bool, pub path: bool, pub anchors: bool, pub handles: bool, + pub fillable_indicator: bool, } // TODO Remove duplicated definition of this in `utility_types_web.rs` @@ -87,18 +103,19 @@ impl Default for OverlaysVisibilitySettings { Self { all: true, artboard_name: true, - compass_rose: true, - quick_measurement: true, transform_measurement: true, + quick_measurement: true, transform_cage: true, + compass_rose: true, + pivot: true, + origin: true, hover_outline: true, selection_outline: true, layer_origin_cross: true, - pivot: true, - origin: true, path: true, anchors: true, handles: true, + fillable_indicator: true, } } } @@ -160,6 +177,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 + } } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -405,10 +426,16 @@ impl OverlayContext { self.internal().fill_path(subpaths, transform, color); } - /// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string. + /// Fills the shape's fill region with a pattern of the given color. 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: &str) { - self.internal().fill_path_pattern(subpaths, transform, color); + pub fn fill_overlay(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, stroke: Option) { + self.internal().fill_overlay(subpaths, transform, color, stroke); + } + + /// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space. + /// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp + pub fn stroke_overlay(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, stroke: Option) { + self.internal().stroke_overlay(subpaths, transform, color, stroke); } pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { @@ -975,7 +1002,7 @@ impl OverlayContextInternal { path.push(bezier.as_path_el()); } - fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) -> BezPath { + fn path_from_subpaths(&mut self, subpaths: impl Iterator>>, transform: DAffine2) -> BezPath { let mut path = BezPath::new(); for subpath in subpaths { @@ -1038,31 +1065,21 @@ impl OverlayContextInternal { } if !subpaths.is_empty() { - let path = self.push_path(subpaths.iter(), transform); + let path = self.path_from_subpaths(subpaths.iter(), transform); let color = color.unwrap_or(COLOR_OVERLAY_BLUE); self.scene.stroke(&kurbo::Stroke::new(1.), self.get_transform(), Self::parse_color(color), None, &path); } } - /// Fills the area inside the path. Assumes `color` is in gamma space. - /// Used by the Pen tool to show the path being closed. - fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { - let path = self.push_path(subpaths, transform); - - self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path); - } - - /// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string. - /// Used by the fill tool to show the area to be filled. - fn fill_path_pattern(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { + pub fn fill_canvas_pattern_image(&self, color: &Color) -> peniko::ImageBrush { const PATTERN_WIDTH: u32 = 4; const PATTERN_HEIGHT: u32 = 4; // Create a 4x4 pixel pattern with colored pixels at (0,0) and (2,2) // This matches the Canvas2D checkerboard pattern let mut data = vec![0u8; (PATTERN_WIDTH * PATTERN_HEIGHT * 4) as usize]; - let rgba = hex_to_rgba_u8(color); + let rgba = color.to_rgba8_srgb(); // ┌▄▄┬──┬──┬──┐ // ├▀▀┼──┼──┼──┤ @@ -1092,12 +1109,37 @@ impl OverlayContextInternal { }, }; - let path = self.push_path(subpaths, transform); - let brush = peniko::Brush::Image(image); + image + } + + /// Fills the area inside the path. Assumes `color` is in gamma space. + /// Used by the Pen tool to show the path being closed. + fn fill_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &str) { + let path = self.path_from_subpaths(subpaths, transform); + + self.scene.fill(peniko::Fill::NonZero, self.get_transform(), Self::parse_color(color), None, &path); + } + + /// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space. + /// Used by the fill tool to show the area to be filled. + fn fill_overlay(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, stroke: Option) { + let path = self.path_from_subpaths(subpaths, transform); + let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(color)); self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path); } + /// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space. + /// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp + pub fn stroke_overlay(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, stroke: Option) { + let path = self.path_from_subpaths(subpaths, transform); + + if let Some(stroke) = stroke { + let brush = peniko::Brush::Image(self.fill_canvas_pattern_image(&color)); + self.scene.stroke(&kurbo::Stroke::new(stroke.weight), self.get_transform(), &brush, None, &path); + } + } + fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { // Use the proper text-to-path system for accurate text rendering const FONT_SIZE: f64 = 12.; diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index c03ba387d3..ae64a9be0b 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -11,15 +11,18 @@ use crate::messages::viewport::ViewportMessageHandler; use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; +use graphene_std::Color; 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::{PaintOrder, Stroke, StrokeAlign}; use graphene_std::vector::{PointId, SegmentId, Vector}; +use js_sys::{Array, Reflect}; use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; -use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; +use web_sys::{CanvasPattern, DomMatrix, OffscreenCanvas, OffscreenCanvasRenderingContext2d}; pub type OverlayProvider = fn(OverlayContext) -> Message; @@ -38,19 +41,32 @@ pub enum GizmoEmphasis { #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum OverlaysType { + // ======= + // General + // ======= ArtboardName, - CompassRose, - QuickMeasurement, TransformMeasurement, + // =========== + // Select Tool + // =========== + QuickMeasurement, TransformCage, + CompassRose, + Pivot, + Origin, HoverOutline, SelectionOutline, LayerOriginCross, - Pivot, - Origin, + // ================ + // Pen & Path Tools + // ================ Path, Anchors, Handles, + // ========= + // Fill Tool + // ========= + FillableIndicator, } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -59,18 +75,19 @@ pub enum OverlaysType { 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 origin: bool, pub hover_outline: bool, pub selection_outline: bool, pub layer_origin_cross: bool, - pub pivot: bool, - pub origin: bool, pub path: bool, pub anchors: bool, pub handles: bool, + pub fillable_indicator: bool, } impl Default for OverlaysVisibilitySettings { @@ -78,18 +95,19 @@ impl Default for OverlaysVisibilitySettings { Self { all: true, artboard_name: true, - compass_rose: true, - quick_measurement: true, transform_measurement: true, + quick_measurement: true, transform_cage: true, + compass_rose: true, + pivot: true, + origin: true, hover_outline: true, selection_outline: true, layer_origin_cross: true, - pivot: true, - origin: true, path: true, anchors: true, handles: true, + fillable_indicator: true, } } } @@ -150,6 +168,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 + } } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -851,8 +873,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(); @@ -864,6 +885,12 @@ 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(); @@ -928,39 +955,41 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) { - self.start_dpi_aware_transform(); - + pub fn draw_path_from_subpaths(&mut self, subpaths: impl Iterator>>, stroke_transform: DAffine2) -> bool { + // Subpaths on a layer is considered "closed" only if all subpaths are closed. + let mut is_closed_on_all = true; self.render_context.begin_path(); for subpath in subpaths { - let subpath = subpath.borrow(); - let mut curves = subpath.iter().peekable(); + let subpath = subpath.borrow().clone(); + let mut bezpath = subpath.to_bezpath(); + bezpath.apply_affine(Affine::new((stroke_transform).to_cols_array())); + let mut curves = bezpath.segments().peekable(); let Some(&first) = curves.peek() else { continue; }; - let start_point = transform.transform_point2(point_to_dvec2(first.start())); + let start_point = point_to_dvec2(first.start()); self.render_context.move_to(start_point.x, start_point.y); for curve in curves { match curve { PathSeg::Line(line) => { - let a = transform.transform_point2(point_to_dvec2(line.p1)); + let a = point_to_dvec2(line.p1); let a = a.round() - DVec2::splat(0.5); self.render_context.line_to(a.x, a.y); } PathSeg::Quad(quad_bez) => { - let a = transform.transform_point2(point_to_dvec2(quad_bez.p1)); - let b = transform.transform_point2(point_to_dvec2(quad_bez.p2)); + let a = point_to_dvec2(quad_bez.p1); + let b = point_to_dvec2(quad_bez.p2); let a = a.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5); self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y); } PathSeg::Cubic(cubic_bez) => { - let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1)); - let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2)); - let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3)); + let a = point_to_dvec2(cubic_bez.p1); + let b = point_to_dvec2(cubic_bez.p2); + let c = point_to_dvec2(cubic_bez.p3); let a = a.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5); let c = c.round() - DVec2::splat(0.5); @@ -971,14 +1000,17 @@ impl OverlayContext { if subpath.closed() { self.render_context.close_path(); + } else { + is_closed_on_all = false; } } - - self.end_dpi_aware_transform(); + return is_closed_on_all; } /// Used by the Select tool to outline a path or a free point when selected or hovered. pub fn outline(&mut self, target_types: impl Iterator>, transform: DAffine2, color: Option<&str>) { + self.render_context.save(); + self.start_dpi_aware_transform(); let mut subpaths: Vec> = vec![]; target_types.for_each(|target_type| match target_type.borrow() { @@ -989,27 +1021,20 @@ impl OverlayContext { }); if !subpaths.is_empty() { - self.push_path(subpaths.iter(), transform); + // TODO: Modify stroke_transform to take note of this + self.draw_path_from_subpaths(subpaths.iter(), transform); let color = color.unwrap_or(COLOR_OVERLAY_BLUE); self.render_context.set_stroke_style_str(color); self.render_context.set_line_width(1.); self.render_context.stroke(); } + self.end_dpi_aware_transform(); + self.render_context.restore(); } - /// 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 an sRGB hex string. - /// 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: &str) { + /// 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; @@ -1025,7 +1050,7 @@ impl OverlayContext { // 4x4 pixels, 4 components (RGBA) per pixel let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT]; - let rgba = hex_to_rgba_u8(color); + let rgba = hex_to_rgba_u8(color.to_rgba_hex_srgb().as_str()); // ┌▄▄┬──┬──┬──┐ // ├▀▀┼──┼──┼──┤ @@ -1040,14 +1065,172 @@ 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(); + let dom_matrix = self.render_context.get_transform().unwrap().inverse(); + let set_pattern_transform = |pattern: &CanvasPattern, matrix: &DomMatrix| { + // Get the JS function: pattern.setTransform + let func = Reflect::get(pattern, &JsValue::from_str("setTransform"))?; + // Pass it the matrix + Reflect::apply(&func.into(), pattern, &Array::of1(matrix))?; + Ok::<(), JsValue>(()) + }; + set_pattern_transform(&pattern, &dom_matrix); + + return pattern; + } - self.push_path(subpaths, transform); + /// 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.draw_path_from_subpaths(subpaths, transform); - self.render_context.set_fill_style_canvas_pattern(&pattern); + self.render_context.set_fill_style_str(color); self.render_context.fill(); } + /// Fills the shape's fill region with a pattern of the given color. Assumes `color` is in gamma space. + /// https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp + pub fn fill_overlay(&mut self, subpaths: impl Iterator>>, transform: DAffine2, color: &Color, stroke: Option) { + // Render for elements with fill + // Render for elements with fill only + // Render for elements with fill and stroke + //----PaintOrder + //----StrokeAlign + + self.render_context.save(); + self.start_dpi_aware_transform(); + + if let Some(stroke) = stroke { + let has_real_stroke = stroke.weight() > 0. && stroke.transform.matrix2.determinant() != 0.; + let applied_stroke_transform = if has_real_stroke { stroke.transform } else { transform }; + let element_transform = if has_real_stroke { transform * stroke.transform.inverse() } else { DAffine2::IDENTITY }; + + let [a, b, c, d, e, f] = element_transform.to_cols_array(); + self.render_context.transform(a, b, c, d, e, f).expect("element_transform should be set to render stroke properly"); + + // For layers with open subpaths, stroke align is ignored and set to default + let is_closed_on_all = self.draw_path_from_subpaths(subpaths, applied_stroke_transform); + let stroke_align = if is_closed_on_all { stroke.align } else { StrokeAlign::Center }; + + let do_fill = || { + self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color)); + self.render_context.fill(); + }; + let do_stroke = |stroke_weight: f64| { + self.render_context.set_line_width(stroke_weight); + self.render_context.set_stroke_style_str(&"#000000"); + self.render_context.stroke(); + }; + let composite_mode = |composite_operation: &str| { + self.render_context + .set_global_composite_operation(composite_operation) + .expect("Failed to set global composite operation"); + }; + + match (stroke_align, stroke.paint_order) { + (StrokeAlign::Inside, PaintOrder::StrokeAbove) => { + do_fill(); + composite_mode("destination-out"); + do_stroke(stroke.weight() * 2.); + } + (StrokeAlign::Inside, PaintOrder::StrokeBelow) => do_fill(), + (StrokeAlign::Center, PaintOrder::StrokeAbove) => { + do_fill(); + composite_mode("destination-out"); + do_stroke(stroke.weight()); + } + (StrokeAlign::Center, PaintOrder::StrokeBelow) => { + do_fill(); + } + // Paint order does not affect this + (StrokeAlign::Outside, _) => { + do_fill(); + } + } + } else { + self.render_context.set_fill_style_canvas_pattern(&self.fill_canvas_pattern(color)); + self.render_context.fill(); + } + + self.end_dpi_aware_transform(); + self.render_context.restore(); + } + + /// Fills the shape's stroke region with a pattern of the given color. Assumes `color` is in gamma space. + /// WARN: Don't use source-in, destination-atop, destination-in, copy + /// on the main canvas as it will erase the existing overlays + pub fn stroke_overlay(&mut self, subpaths: impl Iterator>>, layer_to_viewport: DAffine2, color: &Color, stroke: Option) { + // Render for elements with stroke + //----StrokeAlign + // Render for elements with stroke only + // Render for elements with stroke and fill + //----PaintOrder + + self.render_context.save(); + self.start_dpi_aware_transform(); + + if let Some(stroke) = stroke { + let has_real_stroke = stroke.weight() > 0. && stroke.transform.matrix2.determinant() != 0.; + let applied_stroke_transform = if has_real_stroke { stroke.transform } else { layer_to_viewport }; + let element_transform = if has_real_stroke { layer_to_viewport * stroke.transform.inverse() } else { DAffine2::IDENTITY }; + + let [a, b, c, d, e, f] = element_transform.to_cols_array(); + self.render_context.transform(a, b, c, d, e, f).expect("element_transform should be set to render stroke properly"); + + // For layers with open subpaths, stroke align is ignored and set to default + let is_closed_on_all = self.draw_path_from_subpaths(subpaths, applied_stroke_transform); + let stroke_align = if is_closed_on_all { stroke.align } else { StrokeAlign::Center }; + + let do_stroke = |stroke_weight: f64| { + self.render_context.set_stroke_style_canvas_pattern(&self.fill_canvas_pattern(color)); + self.render_context.set_line_width(stroke_weight); + self.render_context.set_line_cap(stroke.cap.html_canvas_name().as_str()); + self.render_context.set_line_join(stroke.join.html_canvas_name().as_str()); + self.render_context.set_miter_limit(stroke.join_miter_limit); + self.render_context.stroke(); + }; + let do_fill = || { + self.render_context.set_fill_style_str(&"#000000"); + self.render_context.fill(); + }; + let composite_mode = |composite_operation: &str| { + self.render_context + .set_global_composite_operation(composite_operation) + .expect("Failed to set global composite operation"); + }; + + match (stroke_align, stroke.paint_order) { + (StrokeAlign::Inside, PaintOrder::StrokeAbove) => { + // Clips away the stroke lying outside the path drawn from the subpaths + self.render_context.clip(); + do_stroke(stroke.weight() * 2.); + } + (StrokeAlign::Inside, PaintOrder::StrokeBelow) => {} + (StrokeAlign::Center, PaintOrder::StrokeAbove) => { + do_stroke(stroke.weight()); + } + (StrokeAlign::Center, PaintOrder::StrokeBelow) => { + do_stroke(stroke.weight()); + composite_mode("destination-out"); + do_fill(); + } + // Paint order does not affect this + (StrokeAlign::Outside, _) => { + do_stroke(stroke.weight() * 2.); + composite_mode("destination-out"); + do_fill(); + } + } + } + + self.end_dpi_aware_transform(); + self.render_context.restore(); + } + + pub fn get_width(&self, text: &str) -> f64 { + self.render_context.measure_text(text).expect("Failed to measure text dimensions").width() + } + pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) { let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions"); let x = match pivot[0] { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 7fc992b459..dc5792d729 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -3135,6 +3135,10 @@ impl NodeNetworkInterface { self.document_metadata.layer_vector_data.get(&layer).map(|arc| arc.as_ref().clone()) } + pub fn vector_data_from_layer(&self, layer: LayerNodeIdentifier) -> Option { + self.document_metadata.layer_vector_data.get(&layer).map(|arc| arc.as_ref().clone()) + } + /// Loads the structure of layer nodes from a node graph. pub fn load_structure(&mut self) { self.document_metadata.structure = HashMap::from_iter([(LayerNodeIdentifier::ROOT_PARENT, NodeRelations::default())]); diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index c1a2a8fe75..1bd301d95b 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,7 +1,12 @@ use super::tool_prelude::*; +use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; 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}; +use graphene_std::subpath::Subpath; +use graphene_std::vector::PointId; +use graphene_std::vector::misc::Tangent; +use graphene_std::vector::style::{Fill, PaintOrder, Stroke, StrokeAlign}; +use kurbo::{ParamCurve, ParamCurveNearest, Shape}; #[derive(Default, ExtractField)] pub struct FillTool { @@ -74,6 +79,69 @@ impl ToolTransition for FillTool { } } +pub fn near_to_subpath(mouse_pos: DVec2, subpath: Subpath, is_closed_on_all: bool, stroke: Option, layer_to_viewport_transform: DAffine2) -> bool { + let mut is_near = false; + + if let Some(stroke) = stroke { + let mouse_pos = layer_to_viewport_transform.inverse().transform_point2(mouse_pos); + // WARN: Zoom multiplied into stroke width gives false positive when zoom in 1000x + + let subpath_bezpath = subpath.to_bezpath(); + let mouse_point = kurbo::Point::new(mouse_pos.x, mouse_pos.y); + for seg in subpath_bezpath.segments() { + let nearest = seg.nearest(mouse_point, 0.01); + let is_inside_seg = if is_closed_on_all { + // Inside/outside detection for closed bezpaths + subpath_bezpath.contains(mouse_point) + } else { + // Inside/outside detection for open bezpaths + let tangent = seg.tangent_at(nearest.t); + let normal = kurbo::Vec2::new(-tangent.y, tangent.x); + let dir = (seg.eval(nearest.t) - mouse_point).normalize(); + + normal.dot(dir) >= 0.0 + }; + let stroke_align = if is_closed_on_all { stroke.align } else { StrokeAlign::Center }; + let mut max_stroke_distance = stroke.weight(); + match (stroke_align, stroke.paint_order) { + (StrokeAlign::Inside, PaintOrder::StrokeAbove) => { + if is_inside_seg { + max_stroke_distance *= 2.0; + } else { + max_stroke_distance = -1.0; + } + } + (StrokeAlign::Inside, PaintOrder::StrokeBelow) => { + max_stroke_distance = -1.0; + } + (StrokeAlign::Center, PaintOrder::StrokeAbove) => {} + (StrokeAlign::Center, PaintOrder::StrokeBelow) => { + if is_inside_seg { + max_stroke_distance = -1.0; + } + } + // Paint order does not affect this + (StrokeAlign::Outside, _) => { + if is_inside_seg { + max_stroke_distance = -1.0; + } else { + max_stroke_distance *= 2.0; + } + } + } + + if seg.nearest(mouse_point, 0.01).distance_sq <= max_stroke_distance { + is_near = true; + } + } + } + + is_near +} + +const STROKE_ID: DefinitionIdentifier = DefinitionIdentifier::ProtoNode(graphene_std::vector::stroke::IDENTIFIER); +const FILL_ID: DefinitionIdentifier = DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER); + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum FillToolFsmState { #[default] @@ -109,10 +177,36 @@ 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 }; - // Get the layer the user is hovering over - if let Some(layer) = document.click(input, viewport) { - let color_hex = format!("#{}", preview_color.to_rgba_hex_srgb()); - overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &color_hex); + if !overlay_context.visibility_settings.fillable_indicator() { + return self; + } + // Get the layer the user is hovering + if let Some(layer) = document.click(input, viewport) + && let Some(vector_data) = document.network_interface.vector_data_from_layer(layer) + { + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + + // Stroke + let stroke_node = graph_layer.upstream_node_id_from_name(&STROKE_ID); + let stroke_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + let stroke = vector_data.style.stroke(); + + let mut subpaths = vector_data.stroke_bezier_paths(); + let is_closed_on_all = subpaths.all(|subpath| subpath.closed); + subpaths = vector_data.stroke_bezier_paths(); + let near_to_stroke = subpaths.any(|subpath| near_to_subpath(input.mouse.position, subpath, is_closed_on_all, stroke.clone(), document.metadata().transform_to_viewport(layer))); + + // Fill + let fill_node = graph_layer.upstream_node_id_from_name(&FILL_ID); + let fill_exists_and_visible = fill_node.is_some_and(|fill| document.network_interface.is_visible(&fill, &[])); + + subpaths = vector_data.stroke_bezier_paths(); + let layer_to_viewport = document.metadata().transform_to_viewport(layer); + if stroke_exists_and_visible && near_to_stroke { + overlay_context.stroke_overlay(subpaths, layer_to_viewport, &preview_color, stroke); + } else if fill_exists_and_visible { + overlay_context.fill_overlay(subpaths, layer_to_viewport, &preview_color, stroke); + } } self @@ -123,11 +217,11 @@ impl Fsm for FillToolFsmState { self } (FillToolFsmState::Ready, color_event) => { - let Some(layer_identifier) = document.click(input, viewport) else { + let Some(layer) = document.click(input, viewport) 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 { @@ -135,9 +229,37 @@ 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.vector_data_from_layer(layer) { + let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface); + + // Stroke + let stroke_node = graph_layer.upstream_node_id_from_name(&STROKE_ID); + let stroke_exists_and_visible = stroke_node.is_some_and(|stroke| document.network_interface.is_visible(&stroke, &[])); + let stroke = vector_data.style.stroke(); + + let mut subpaths = vector_data.stroke_bezier_paths(); + let is_closed_on_all = subpaths.all(|subpath| subpath.closed); + subpaths = vector_data.stroke_bezier_paths(); + let near_to_stroke = subpaths.any(|subpath| near_to_subpath(input.mouse.position, subpath, is_closed_on_all, stroke.clone(), document.metadata().transform_to_viewport(layer))); + + // Fill + let fill_node = graph_layer.upstream_node_id_from_name(&FILL_ID); + let fill_exists_and_visible = fill_node.is_some_and(|fill| document.network_interface.is_visible(&fill, &[])); + + if stroke_exists_and_visible && near_to_stroke { + responses.add(GraphOperationMessage::StrokeColorSet { layer, stroke_color }); + } else if fill_exists_and_visible { + responses.add(GraphOperationMessage::FillSet { layer, fill }); + } + } FillToolFsmState::Filling } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 954272c29a..983926e962 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1670,6 +1670,49 @@ 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 Some(mut vector) = document.network_interface.compute_modified_vector(layer) + { + let closest_point = vector.anchor_points().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(segments, true) + }) + .collect(); + + overlay_context.fill_path(subpaths.iter(), transform, COLOR_OVERLAY_BLUE_05); + } + } + } + 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))); @@ -1774,50 +1817,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.anchor_points().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 fill_color = COLOR_OVERLAY_BLUE_05; - overlay_context.fill_path(subpaths.iter(), transform, fill_color); - } - } - } - // Draw the overlays that visualize current snapping tool_data.snap_manager.draw_overlays(SnapData::new(document, input, viewport), &mut overlay_context); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 565409937a..33a3d0c67b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -415,7 +415,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 type u32"), } } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6f1690ad37..68ba0280b0 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -867,14 +867,14 @@ impl Render for Table { impl Render for Table { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for row in self.iter() { - let multiplied_transform = *row.transform; let vector = &row.element; // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform let has_real_stroke = vector.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(*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 = set_stroke_transform.map(|stroke_transform| *row.transform * stroke_transform.inverse()); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); let layer_bounds = vector.bounding_box().unwrap_or_default(); let transformed_bounds = vector.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default(); @@ -885,6 +885,7 @@ impl Render for Table { let mut path = String::new(); for mut bezpath in row.element.stroke_bezpath_iter() { + // Only seems to affect upstream-transformed (from stroke node) layers ~~with row.transform~~ bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); path.push_str(bezpath.to_svg().as_str()); } @@ -903,6 +904,7 @@ impl Render for Table { let wants_stroke_below = vector.style.stroke().map(|s| s.paint_order) == Some(PaintOrder::StrokeBelow); if needs_separate_alignment_fill && !wants_stroke_below { + log::info!("Entering needs_separate_alignment_fill && !wants_stroke_below"); render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); @@ -942,6 +944,7 @@ impl Render for Table { let use_face_fill = vector.use_face_fill(); if use_face_fill { + log::info!("Entering vector.is_branching()"); for mut face_path in vector.construct_faces().filter(|face| face.area() >= 0.) { face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); @@ -969,6 +972,8 @@ impl Render for Table { render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); + // Only seem to affect layers with downstream-transformed layers (from stroke node) with ~~row.transform*stroke_transform.inverse()~~ + // and affect layers with upstream-transformed (from stroke node) layers ~~with IDENTITY~~ let matrix = format_transform_matrix(element_transform); if !matrix.is_empty() { attributes.push("transform", matrix); @@ -976,6 +981,7 @@ impl Render for Table { let defs = &mut attributes.0.svg_defs; if let Some((ref id, mask_type, ref vector_row)) = push_id { + log::info!("Entering Some(p) = push_id"); let mut svg = SvgRender::new(); vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform)); let stroke = row.element.style.stroke().unwrap(); @@ -1009,11 +1015,13 @@ impl Render for Table { } let fill_and_stroke = style.render(defs, element_transform, applied_stroke_transform, bounds_matrix, transformed_bounds_matrix, &render_params); + // log::info!("file_and_stroke: {:?}", fill_and_stroke); if let Some((id, mask_type, _)) = push_id { let selector = format!("url(#{id})"); attributes.push(mask_type.to_attribute(), selector); } + // Look here to see how to adjust the stroke attributes.push_val(fill_and_stroke); if vector.is_branching() && !use_face_fill { @@ -1032,6 +1040,7 @@ impl Render for Table { // When splitting passes and stroke is below, draw the fill after the stroke. if needs_separate_alignment_fill && wants_stroke_below { + log::info!("Entering needs_separate_alignment_fill && wants_stroke_below"); render.leaf_tag("path", |attributes| { attributes.push("d", path); let matrix = format_transform_matrix(element_transform); diff --git a/node-graph/libraries/vector-types/src/vector/style.rs b/node-graph/libraries/vector-types/src/vector/style.rs index 0828c4e6f2..526082d309 100644 --- a/node-graph/libraries/vector-types/src/vector/style.rs +++ b/node-graph/libraries/vector-types/src/vector/style.rs @@ -237,6 +237,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)] @@ -258,6 +266,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/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index a2f8edd188..70b8475017 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -1055,13 +1055,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/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 734420732c..6f2e26cf64 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -59,6 +59,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + let footprint = Footprint::default(); let mut metadata = RenderMetadata::default(); + // All the metadata of the upstream Graphic(s) is collected here data.collect_metadata(&mut metadata, footprint, None); let contains_artboard = data.contains_artboard();