diff --git a/editor/src/document/artboard_message_handler.rs b/editor/src/document/artboard_message_handler.rs index f7fa0177..678f5dc9 100644 --- a/editor/src/document/artboard_message_handler.rs +++ b/editor/src/document/artboard_message_handler.rs @@ -30,7 +30,7 @@ impl MessageHandler for ArtboardMessageHandler { match message { // Sub-messages #[remain::unsorted] - DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(&operation) { + DispatchOperation(operation) => match self.artboards_graphene_document.handle_operation(*operation) { Ok(_) => (), Err(e) => log::error!("Artboard Error: {:?}", e), }, @@ -46,7 +46,7 @@ impl MessageHandler for ArtboardMessageHandler { path: vec![artboard_id], insert_index: -1, transform: DAffine2::from_scale_angle_translation(size.into(), 0., position.into()).to_cols_array(), - style: style::PathStyle::new(None, Some(Fill::new(Color::WHITE))), + style: style::PathStyle::new(None, Fill::solid(Color::WHITE)), } .into(), ) diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 0912b66e..05678ceb 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -628,7 +628,7 @@ impl MessageHandler for Docum match message { // Sub-messages #[remain::unsorted] - DispatchOperation(op) => match self.graphene_document.handle_operation(&op) { + DispatchOperation(op) => match self.graphene_document.handle_operation(*op) { Ok(Some(document_responses)) => { for response in document_responses { match &response { diff --git a/editor/src/document/layer_panel.rs b/editor/src/document/layer_panel.rs index 5c899f78..b196dd68 100644 --- a/editor/src/document/layer_panel.rs +++ b/editor/src/document/layer_panel.rs @@ -26,15 +26,17 @@ pub fn layer_panel_entry(layer_metadata: &LayerMetadata, transform: DAffine2, la let arr = arr.iter().map(|x| (*x).into()).collect::>(); let mut thumbnail = String::new(); - layer.data.clone().render(&mut thumbnail, &mut vec![transform], ViewMode::Normal); + let mut svg_defs = String::new(); + layer.data.clone().render(&mut thumbnail, &mut svg_defs, &mut vec![transform], ViewMode::Normal); let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::>().join(","); let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() { format!( - r#"{}"#, + r#"{}{}"#, x_min, y_min, x_max - x_min, y_max - y_min, + svg_defs, transform, thumbnail, ) diff --git a/editor/src/document/overlays_message_handler.rs b/editor/src/document/overlays_message_handler.rs index c26d0e0f..47080ad1 100644 --- a/editor/src/document/overlays_message_handler.rs +++ b/editor/src/document/overlays_message_handler.rs @@ -17,7 +17,7 @@ impl MessageHandler for OverlaysMessageHandler { match message { // Sub-messages #[remain::unsorted] - DispatchOperation(operation) => match self.overlays_graphene_document.handle_operation(&operation) { + DispatchOperation(operation) => match self.overlays_graphene_document.handle_operation(*operation) { Ok(_) => responses.push_back(OverlaysMessage::Rerender.into()), Err(e) => log::error!("OverlaysError: {:?}", e), }, diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 48edf3aa..57126d77 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -72,6 +72,10 @@ impl Default for Mapping { entry! {action=TextMessage::Interact, key_up=Lmb}, entry! {action=TextMessage::Abort, key_down=KeyEscape}, entry! {action=TextMessage::CommitText, key_down=KeyEnter, modifiers=[KeyControl]}, + // Gradient + entry! {action=GradientToolMessage::PointerDown, key_down=Lmb}, + entry! {action=GradientToolMessage::PointerMove { constrain_axis: KeyShift }, message=InputMapperMessage::PointerMove}, + entry! {action=GradientToolMessage::PointerUp, key_up=Lmb}, // Rectangle entry! {action=RectangleToolMessage::DragStart, key_down=Lmb}, entry! {action=RectangleToolMessage::DragStop, key_up=Lmb}, @@ -127,6 +131,7 @@ impl Default for Mapping { entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }, key_down=KeyI}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Text }, key_down=KeyT}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Fill }, key_down=KeyF}, + entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Gradient }, key_down=KeyH}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Path }, key_down=KeyA}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Pen }, key_down=KeyP}, entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Freehand }, key_down=KeyN}, diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 90085ca4..98102019 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -77,6 +77,7 @@ pub mod message_prelude { pub use crate::viewport_tools::tools::eyedropper_tool::{EyedropperToolMessage, EyedropperToolMessageDiscriminant}; pub use crate::viewport_tools::tools::fill_tool::{FillToolMessage, FillToolMessageDiscriminant}; pub use crate::viewport_tools::tools::freehand_tool::{FreehandToolMessage, FreehandToolMessageDiscriminant}; + pub use crate::viewport_tools::tools::gradient_tool::{GradientToolMessage, GradientToolMessageDiscriminant}; pub use crate::viewport_tools::tools::line_tool::{LineToolMessage, LineToolMessageDiscriminant}; pub use crate::viewport_tools::tools::navigate_tool::{NavigateToolMessage, NavigateToolMessageDiscriminant}; pub use crate::viewport_tools::tools::path_tool::{PathToolMessage, PathToolMessageDiscriminant}; diff --git a/editor/src/viewport_tools/snapping.rs b/editor/src/viewport_tools/snapping.rs index 807f19f0..bf3d5fcc 100644 --- a/editor/src/viewport_tools/snapping.rs +++ b/editor/src/viewport_tools/snapping.rs @@ -34,7 +34,7 @@ impl SnapHandler { Operation::AddOverlayLine { path: layer_path.clone(), transform, - style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), style::Fill::None), } .into(), ) diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index f34b539a..a7e98151 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -83,13 +83,13 @@ impl Default for ToolFsmState { Eyedropper => eyedropper_tool::EyedropperTool, Text => text_tool::TextTool, Fill => fill_tool::FillTool, - // Gradient => gradient::Gradient, - // Brush => brush::Brush, - // Heal => heal::Heal, - // Clone => clone::Clone, - // Patch => patch::Patch, - // BlurSharpen => blursharpen::BlurSharpen, - // Relight => relight::Relight, + Gradient => gradient_tool::GradientTool, + // Brush => brush_tool::BrushTool, + // Heal => heal_tool::HealTool, + // Clone => clone_tool:::CloneTool, + // Patch => patch_tool:::PatchTool, + // BlurSharpen => blursharpen_tool:::BlurSharpenTool, + // Relight => relight_tool:::RelightTool, Path => path_tool::PathTool, Pen => pen_tool::PenTool, Freehand => freehand_tool::FreehandTool, @@ -191,8 +191,8 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy ToolType::Navigate => None, // Some(NavigateToolMessage::DocumentIsDirty.into()), ToolType::Eyedropper => None, // Some(EyedropperToolMessage::DocumentIsDirty.into()), ToolType::Text => Some(TextMessage::DocumentIsDirty.into()), - ToolType::Fill => None, // Some(FillToolMessage::DocumentIsDirty.into()), - ToolType::Gradient => None, // Some(GradientMessage::DocumentIsDirty.into()), + ToolType::Fill => None, // Some(FillToolMessage::DocumentIsDirty.into()), + ToolType::Gradient => Some(GradientToolMessage::DocumentIsDirty.into()), ToolType::Brush => None, // Some(BrushMessage::DocumentIsDirty.into()), ToolType::Heal => None, // Some(HealMessage::DocumentIsDirty.into()), ToolType::Clone => None, // Some(CloneMessage::DocumentIsDirty.into()), @@ -215,7 +215,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy ToolType::Eyedropper => Some(EyedropperToolMessage::Abort.into()), ToolType::Text => Some(TextMessage::Abort.into()), ToolType::Fill => Some(FillToolMessage::Abort.into()), - // ToolType::Gradient => Some(GradientMessage::Abort.into()), + ToolType::Gradient => Some(GradientToolMessage::Abort.into()), // ToolType::Brush => Some(BrushMessage::Abort.into()), // ToolType::Heal => Some(HealMessage::Abort.into()), // ToolType::Clone => Some(CloneMessage::Abort.into()), @@ -249,7 +249,7 @@ pub fn message_to_tool_type(message: &ToolMessage) -> ToolType { Eyedropper(_) => ToolType::Eyedropper, Text(_) => ToolType::Text, Fill(_) => ToolType::Fill, - // Gradient(_) => ToolType::Gradient, + Gradient(_) => ToolType::Gradient, // Brush(_) => ToolType::Brush, // Heal(_) => ToolType::Heal, // Clone(_) => ToolType::Clone, diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index aeb02cd4..87e93144 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -31,27 +31,27 @@ pub enum ToolMessage { #[remain::unsorted] #[child] Fill(FillToolMessage), + #[remain::unsorted] + #[child] + Gradient(GradientToolMessage), // #[remain::unsorted] // #[child] - // Gradient(GradientMessage), + // Brush(BrushToolMessage), // #[remain::unsorted] // #[child] - // Brush(BrushMessage), + // Heal(HealToolMessage), // #[remain::unsorted] // #[child] - // Heal(HealMessage), + // Clone(CloneToolMessage), // #[remain::unsorted] // #[child] - // Clone(CloneMessage), + // Patch(PatchToolMessage), // #[remain::unsorted] // #[child] - // Patch(PatchMessage), + // Detail(DetailToolMessage), // #[remain::unsorted] // #[child] - // Detail(DetailMessage), - // #[remain::unsorted] - // #[child] - // Relight(RelightMessage), + // Relight(RelightToolMessage), #[remain::unsorted] #[child] Path(PathToolMessage), diff --git a/editor/src/viewport_tools/tools/ellipse_tool.rs b/editor/src/viewport_tools/tools/ellipse_tool.rs index 0d92bcb6..f351f649 100644 --- a/editor/src/viewport_tools/tools/ellipse_tool.rs +++ b/editor/src/viewport_tools/tools/ellipse_tool.rs @@ -120,7 +120,7 @@ impl Fsm for EllipseToolFsmState { path: shape_data.path.clone().unwrap(), insert_index: -1, transform: DAffine2::ZERO.to_cols_array(), - style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), + style: style::PathStyle::new(None, style::Fill::solid(tool_data.primary_color)), } .into(), ); diff --git a/editor/src/viewport_tools/tools/eyedropper_tool.rs b/editor/src/viewport_tools/tools/eyedropper_tool.rs index 1a93a9a2..9b7a5aab 100644 --- a/editor/src/viewport_tools/tools/eyedropper_tool.rs +++ b/editor/src/viewport_tools/tools/eyedropper_tool.rs @@ -101,10 +101,10 @@ impl Fsm for EyedropperToolFsmState { if let Some(path) = document.graphene_document.intersects_quad_root(quad).last() { if let Ok(layer) = document.graphene_document.layer(path) { if let LayerDataType::Shape(shape) = &layer.data { - if let Some(fill) = shape.style.fill() { + if shape.style.fill().is_some() { match lmb_or_rmb { - EyedropperToolMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor { color: fill.color() }.into()), - EyedropperToolMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor { color: fill.color() }.into()), + EyedropperToolMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor { color: shape.style.fill().color() }.into()), + EyedropperToolMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor { color: shape.style.fill().color() }.into()), _ => {} } } diff --git a/editor/src/viewport_tools/tools/fill_tool.rs b/editor/src/viewport_tools/tools/fill_tool.rs index a1b873e2..5452b0a4 100644 --- a/editor/src/viewport_tools/tools/fill_tool.rs +++ b/editor/src/viewport_tools/tools/fill_tool.rs @@ -12,6 +12,7 @@ use graphene::intersection::Quad; use graphene::Operation; use glam::DVec2; +use graphene::layers::style::Fill; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -103,8 +104,10 @@ impl Fsm for FillToolFsmState { RightMouseDown => tool_data.secondary_color, Abort => unreachable!(), }; + let fill = Fill::Solid(color); + responses.push_back(DocumentMessage::StartTransaction.into()); - responses.push_back(Operation::SetLayerFill { path: path.to_vec(), color }.into()); + responses.push_back(Operation::SetLayerFill { path: path.to_vec(), fill }.into()); responses.push_back(DocumentMessage::CommitTransaction.into()); } diff --git a/editor/src/viewport_tools/tools/freehand_tool.rs b/editor/src/viewport_tools/tools/freehand_tool.rs index 6b933f2e..17db8878 100644 --- a/editor/src/viewport_tools/tools/freehand_tool.rs +++ b/editor/src/viewport_tools/tools/freehand_tool.rs @@ -225,7 +225,7 @@ fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData) -> Messag insert_index: -1, transform: DAffine2::IDENTITY.to_cols_array(), points, - style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None), + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), style::Fill::None), } .into() } diff --git a/editor/src/viewport_tools/tools/gradient_tool.rs b/editor/src/viewport_tools/tools/gradient_tool.rs new file mode 100644 index 00000000..9d06ebfb --- /dev/null +++ b/editor/src/viewport_tools/tools/gradient_tool.rs @@ -0,0 +1,416 @@ +use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE}; +use crate::document::DocumentMessageHandler; +use crate::frontend::utility_types::MouseCursorIcon; +use crate::input::keyboard::{Key, MouseMotion}; +use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; +use crate::message_prelude::*; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; +use crate::viewport_tools::snapping::SnapHandler; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; + +use graphene::color::Color; +use graphene::intersection::Quad; +use graphene::layers::layer_info::{Layer, LayerDataType}; +use graphene::layers::style::{Fill, Gradient, PathStyle, Stroke}; +use graphene::Operation; + +use glam::{DAffine2, DVec2}; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct GradientTool { + fsm_state: GradientToolFsmState, + data: GradientToolData, +} + +#[remain::sorted] +#[impl_message(Message, ToolMessage, Gradient)] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum GradientToolMessage { + // Standard messages + #[remain::unsorted] + Abort, + #[remain::unsorted] + DocumentIsDirty, + + // Tool-specific messages + PointerDown, + PointerMove { + constrain_axis: Key, + }, + PointerUp, +} + +impl<'a> MessageHandler> for GradientTool { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + if action == ToolMessage::UpdateCursor { + self.fsm_state.update_cursor(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } + } + + advertise_actions!(GradientToolMessageDiscriminant; PointerDown, PointerUp, PointerMove, Abort); +} + +impl PropertyHolder for GradientTool {} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum GradientToolFsmState { + Ready, + Drawing, +} + +impl Default for GradientToolFsmState { + fn default() -> Self { + GradientToolFsmState::Ready + } +} + +/// Computes the transform from gradient space to layer space (where gradient space is 0..1 in layer space) +fn gradient_space_transform(path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler) -> DAffine2 { + let bounds = layer.current_bounding_box().unwrap(); + let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); + + document.graphene_document.multiply_transforms(&path[..path.len() - 1]).unwrap() * bound_transform +} + +/// Contains info on the overlays for a single gradient +#[derive(Clone, Debug, Default)] +pub struct GradientOverlay { + pub handles: [Vec; 2], + pub line: Vec, + path: Vec, + transform: DAffine2, + gradient: Gradient, +} + +impl GradientOverlay { + fn generate_overlay_handle(translation: DVec2, responses: &mut VecDeque, selected: bool) -> Vec { + let path = vec![generate_uuid()]; + + let size = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); + + let fill = if selected { Fill::solid(COLOR_ACCENT) } else { Fill::solid(Color::WHITE) }; + + let operation = Operation::AddOverlayEllipse { + path: path.clone(), + transform: DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array(), + style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), fill), + }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + + path + } + fn generate_overlay_line(start: DVec2, end: DVec2, responses: &mut VecDeque) -> Vec { + let path = vec![generate_uuid()]; + + let line_vector = end - start; + let scale = DVec2::splat(line_vector.length()); + let angle = -line_vector.angle_between(DVec2::X); + let translation = start; + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + + let operation = Operation::AddOverlayLine { + path: path.clone(), + transform, + style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None), + }; + responses.push_back(DocumentMessage::Overlays(operation.into()).into()); + + path + } + + pub fn new(fill: &Gradient, dragging_start: Option, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler, responses: &mut VecDeque) -> Self { + let transform = gradient_space_transform(path, layer, document); + let Gradient { start, end, .. } = fill; + let [start, end] = [transform.transform_point2(*start), transform.transform_point2(*end)]; + + let line = Self::generate_overlay_line(start, end, responses); + let handles = [ + Self::generate_overlay_handle(start, responses, dragging_start == Some(true)), + Self::generate_overlay_handle(end, responses, dragging_start == Some(false)), + ]; + + let path = path.to_vec(); + let gradient = fill.clone(); + + Self { + handles, + line, + path, + transform, + gradient, + } + } + + pub fn delete_overlays(self, responses: &mut VecDeque) { + responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: self.line }.into()).into()); + let [start, end] = self.handles; + responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: start }.into()).into()); + responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: end }.into()).into()); + } + + pub fn evaluate_gradient_start(&self) -> DVec2 { + self.transform.transform_point2(self.gradient.start) + } + + pub fn evaluate_gradient_end(&self) -> DVec2 { + self.transform.transform_point2(self.gradient.end) + } +} + +/// Contains information about the selected gradient handle +#[derive(Clone, Debug, Default)] +struct SelectedGradient { + path: Vec, + transform: DAffine2, + gradient: Gradient, + dragging_start: bool, +} + +impl SelectedGradient { + pub fn new(gradient: Gradient, path: &[LayerId], layer: &Layer, document: &DocumentMessageHandler) -> Self { + let transform = gradient_space_transform(path, layer, document); + Self { + path: path.to_vec(), + transform, + gradient, + dragging_start: false, + } + } + + pub fn with_gradient_start(mut self, start: DVec2) -> Self { + self.gradient.start = self.transform.inverse().transform_point2(start); + self + } + + pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque, snap_rotate: bool) { + if snap_rotate { + let point = if self.dragging_start { + self.transform.transform_point2(self.gradient.end) + } else { + self.transform.transform_point2(self.gradient.start) + }; + + let delta = point - mouse; + + let length = delta.length(); + let mut angle = -delta.angle_between(DVec2::X); + + let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians(); + angle = (angle / snap_resolution).round() * snap_resolution; + + let rotated = DVec2::new(length * angle.cos(), length * angle.sin()); + mouse = point - rotated; + } + + mouse = self.transform.inverse().transform_point2(mouse); + + if self.dragging_start { + self.gradient.start = mouse; + } else { + self.gradient.end = mouse; + } + + self.gradient.transform = self.transform.inverse(); + let fill = Fill::LinearGradient(self.gradient.clone()); + let path = self.path.clone(); + responses.push_back(Operation::SetLayerFill { path, fill }.into()); + } +} + +#[derive(Clone, Debug, Default)] +struct GradientToolData { + gradient_overlays: Vec, + selected_gradient: Option, + snap_handler: SnapHandler, +} + +pub fn start_snap(snap_handler: &mut SnapHandler, document: &DocumentMessageHandler, layer: &Layer, path: &[LayerId]) { + snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); + if let LayerDataType::Shape(s) = &layer.data { + let transform = document.graphene_document.multiply_transforms(path).unwrap(); + let snap_points = s + .path + .iter() + .filter_map(|shape| match shape { + kurbo::PathEl::MoveTo(point) => Some(point), + kurbo::PathEl::LineTo(point) => Some(point), + kurbo::PathEl::QuadTo(_, point) => Some(point), + kurbo::PathEl::CurveTo(_, _, point) => Some(point), + kurbo::PathEl::ClosePath => None, + }) + .map(|point| DVec2::new(point.x, point.y)) + .map(|pos| transform.transform_point2(pos)) + .collect(); + snap_handler.add_snap_points(document, snap_points); + } +} + +impl Fsm for GradientToolFsmState { + type ToolData = GradientToolData; + type ToolOptions = (); + + fn transition( + self, + event: ToolMessage, + document: &DocumentMessageHandler, + tool_data: &DocumentToolData, + data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) -> Self { + if let ToolMessage::Gradient(event) = event { + match (self, event) { + (_, GradientToolMessage::DocumentIsDirty) => { + while let Some(overlay) = data.gradient_overlays.pop() { + overlay.delete_overlays(responses); + } + + for path in document.selected_visible_layers() { + let layer = document.graphene_document.layer(path).unwrap(); + + if let Ok(Fill::LinearGradient(gradient)) = layer.style().map(|style| style.fill()) { + let dragging_start = data + .selected_gradient + .as_ref() + .map_or(None, |selected| if selected.path == path { Some(selected.dragging_start) } else { None }); + data.gradient_overlays.push(GradientOverlay::new(gradient, dragging_start, path, layer, document, responses)) + } + } + + self + } + (GradientToolFsmState::Ready, GradientToolMessage::PointerDown) => { + responses.push_back(ToolMessage::DocumentIsDirty.into()); + + let mouse = input.mouse.position; + let tolerance = VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE.powi(2); + + let mut dragging = false; + for overlay in &data.gradient_overlays { + if overlay.evaluate_gradient_start().distance_squared(mouse) < tolerance { + dragging = true; + start_snap(&mut data.snap_handler, document, document.graphene_document.layer(&overlay.path).unwrap(), &overlay.path); + data.selected_gradient = Some(SelectedGradient { + path: overlay.path.clone(), + transform: overlay.transform.clone(), + gradient: overlay.gradient.clone(), + dragging_start: true, + }) + } + if overlay.evaluate_gradient_end().distance_squared(mouse) < tolerance { + dragging = true; + start_snap(&mut data.snap_handler, document, document.graphene_document.layer(&overlay.path).unwrap(), &overlay.path); + data.selected_gradient = Some(SelectedGradient { + path: overlay.path.clone(), + transform: overlay.transform.clone(), + gradient: overlay.gradient.clone(), + dragging_start: false, + }) + } + } + if dragging { + GradientToolFsmState::Drawing + } else { + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]); + let intersection = document.graphene_document.intersects_quad_root(quad).pop(); + + if let Some(intersection) = intersection { + if !document.selected_layers_contains(&intersection) { + let replacement_selected_layers = vec![intersection.clone()]; + + responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into()); + } + + let layer = document.graphene_document.layer(&intersection).unwrap(); + + let gradient = Gradient::new(DVec2::ZERO, tool_data.secondary_color, DVec2::ONE, tool_data.primary_color, DAffine2::IDENTITY, generate_uuid()); + let mut selected_gradient = SelectedGradient::new(gradient, &intersection, layer, document).with_gradient_start(input.mouse.position); + selected_gradient.update_gradient(input.mouse.position, responses, false); + + data.selected_gradient = Some(selected_gradient); + + start_snap(&mut data.snap_handler, document, layer, &intersection); + + GradientToolFsmState::Drawing + } else { + GradientToolFsmState::Ready + } + } + } + (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { + if let Some(selected_gradient) = &mut data.selected_gradient { + let mouse = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position); + selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize)); + } + GradientToolFsmState::Drawing + } + + (GradientToolFsmState::Drawing, GradientToolMessage::PointerUp) => { + data.snap_handler.cleanup(responses); + + GradientToolFsmState::Ready + } + + (_, GradientToolMessage::Abort) => { + data.snap_handler.cleanup(responses); + + while let Some(overlay) = data.gradient_overlays.pop() { + overlay.delete_overlays(responses); + } + GradientToolFsmState::Ready + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Draw Gradient"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Snap 15°"), + plus: true, + }, + ])]), + GradientToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Snap 15°"), + plus: false, + }])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } + + fn update_cursor(&self, responses: &mut VecDeque) { + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into()); + } +} diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index 4292efbe..9873f95a 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -169,7 +169,7 @@ impl Fsm for LineToolFsmState { path: data.path.clone().unwrap(), insert_index: -1, transform: DAffine2::ZERO.to_cols_array(), - style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None), + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), style::Fill::None), } .into(), ); diff --git a/editor/src/viewport_tools/tools/mod.rs b/editor/src/viewport_tools/tools/mod.rs index d1ed0f70..51cbd5c3 100644 --- a/editor/src/viewport_tools/tools/mod.rs +++ b/editor/src/viewport_tools/tools/mod.rs @@ -3,6 +3,7 @@ pub mod ellipse_tool; pub mod eyedropper_tool; pub mod fill_tool; pub mod freehand_tool; +pub mod gradient_tool; pub mod line_tool; pub mod navigate_tool; pub mod path_tool; diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index d4edb479..f5cbece2 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -182,7 +182,7 @@ impl Fsm for PenToolFsmState { transform: transform.to_cols_array(), insert_index: -1, bez_path: data.bez_path.clone().into_iter().collect(), - style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None), + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), style::Fill::None), closed: false, } .into(), diff --git a/editor/src/viewport_tools/tools/rectangle_tool.rs b/editor/src/viewport_tools/tools/rectangle_tool.rs index 57e62b18..8e9df192 100644 --- a/editor/src/viewport_tools/tools/rectangle_tool.rs +++ b/editor/src/viewport_tools/tools/rectangle_tool.rs @@ -119,7 +119,7 @@ impl Fsm for RectangleToolFsmState { path: shape_data.path.clone().unwrap(), insert_index: -1, transform: DAffine2::ZERO.to_cols_array(), - style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), + style: style::PathStyle::new(None, style::Fill::solid(tool_data.primary_color)), } .into(), ); diff --git a/editor/src/viewport_tools/tools/shape_tool.rs b/editor/src/viewport_tools/tools/shape_tool.rs index c24c9745..99b3c916 100644 --- a/editor/src/viewport_tools/tools/shape_tool.rs +++ b/editor/src/viewport_tools/tools/shape_tool.rs @@ -162,7 +162,7 @@ impl Fsm for ShapeToolFsmState { insert_index: -1, transform: DAffine2::ZERO.to_cols_array(), sides: data.sides, - style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))), + style: style::PathStyle::new(None, style::Fill::solid(tool_data.primary_color)), } .into(), ); diff --git a/editor/src/viewport_tools/tools/shared/transformation_cage.rs b/editor/src/viewport_tools/tools/shared/transformation_cage.rs index e63196c5..490757a2 100644 --- a/editor/src/viewport_tools/tools/shared/transformation_cage.rs +++ b/editor/src/viewport_tools/tools/shared/transformation_cage.rs @@ -127,7 +127,7 @@ pub fn add_bounding_box(responses: &mut Vec) -> Vec { let operation = Operation::AddOverlayRect { path: path.clone(), transform: DAffine2::ZERO.to_cols_array(), - style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None), }; responses.push(DocumentMessage::Overlays(operation.into()).into()); @@ -145,7 +145,7 @@ fn add_transform_handles(responses: &mut Vec) -> [Vec; 8] { let operation = Operation::AddOverlayRect { path: current_path.clone(), transform: DAffine2::ZERO.to_cols_array(), - style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))), + style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)), }; responses.push(DocumentMessage::Overlays(operation.into()).into()); diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index fdb1e834..715955f6 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -266,7 +266,7 @@ fn add_spline(data: &SplineToolData, tool_data: &DocumentToolData, show_preview: insert_index: -1, transform: DAffine2::IDENTITY.to_cols_array(), points, - style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None), + style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), style::Fill::None), } .into() } diff --git a/editor/src/viewport_tools/tools/text_tool.rs b/editor/src/viewport_tools/tools/text_tool.rs index 9ebde140..679b8b81 100644 --- a/editor/src/viewport_tools/tools/text_tool.rs +++ b/editor/src/viewport_tools/tools/text_tool.rs @@ -150,7 +150,7 @@ fn resize_overlays(overlays: &mut Vec>, responses: &mut VecDeque - + diff --git a/graphene/src/boolean_ops.rs b/graphene/src/boolean_ops.rs index ad037243..bcbfd0f6 100644 --- a/graphene/src/boolean_ops.rs +++ b/graphene/src/boolean_ops.rs @@ -385,7 +385,7 @@ impl PathGraph { concat_paths(&mut curve, &self.edge(vertices[index - 1].0, vertices[index].0, vertices[index].1).unwrap().curve); } curve.push(PathEl::ClosePath); - ShapeLayer::from_bez_path(BezPath::from_vec(curve), *style, false) + ShapeLayer::from_bez_path(BezPath::from_vec(curve), style.clone(), false) } } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 88dcc5b2..41c7462a 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -38,8 +38,14 @@ impl Default for Document { impl Document { /// Wrapper around render, that returns the whole document as a Response. pub fn render_root(&mut self, mode: ViewMode) -> String { - self.root.render(&mut vec![], mode); - self.root.cache.clone() + let mut svg_defs = String::from(""); + + self.root.render(&mut vec![], mode, &mut svg_defs); + + svg_defs.push_str(""); + + svg_defs.push_str(&self.root.cache); + svg_defs } pub fn current_state_identifier(&self) -> u64 { @@ -48,7 +54,7 @@ impl Document { /// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`. pub fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>) { - self.layer(path).unwrap().intersects_quad(quad, path, intersections); + self.layer(&path).unwrap().intersects_quad(quad, path, intersections); } /// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers. @@ -85,7 +91,7 @@ impl Document { return Ok(&self.root); } let (path, id) = split_path(path)?; - self.folder(path)?.layer(id).ok_or_else(|| DocumentError::LayerNotFound(path.into())) + self.folder(&path)?.layer(id).ok_or_else(|| DocumentError::LayerNotFound(path.into())) } /// Returns a mutable reference to the layer or folder at the path. @@ -172,7 +178,7 @@ impl Document { } pub fn folder_children_paths(&self, path: &[LayerId]) -> Vec> { - if let Ok(folder) = self.folder(path) { + if let Ok(folder) = self.folder(&path) { folder.list_layers().iter().map(|f| [path, &[*f]].concat()).collect() } else { vec![] @@ -251,7 +257,7 @@ impl Document { let mut layer_id = None; if let Ok((path, id)) = split_path(path) { layer_id = Some(id); - self.mark_as_dirty(path)?; + self.mark_as_dirty(&path)?; folder = self.folder_mut(path)?; if let Some(folder_layer) = folder.layer_mut(id) { *folder_layer = layer; @@ -294,12 +300,12 @@ impl Document { /// Deletes the layer specified by `path`. pub fn delete(&mut self, path: &[LayerId]) -> Result<(), DocumentError> { let (path, id) = split_path(path)?; - self.mark_as_dirty(path)?; + self.mark_as_dirty(&path)?; self.folder_mut(path)?.remove_layer(id) } pub fn visible_layers(&self, path: &mut Vec, paths: &mut Vec>) -> Result<(), DocumentError> { - if !self.layer(path)?.visible { + if !self.layer(&path)?.visible { return Ok(()); } if let Ok(folder) = self.folder(&path) { @@ -315,13 +321,13 @@ impl Document { } pub fn viewport_bounding_box(&self, path: &[LayerId]) -> Result, DocumentError> { - let layer = self.layer(path)?; + let layer = self.layer(&path)?; let transform = self.multiply_transforms(path)?; Ok(layer.data.bounding_box(transform)) } pub fn bounding_box_and_transform(&self, path: &[LayerId]) -> Result, DocumentError> { - let layer = self.layer(path)?; + let layer = self.layer(&path)?; let transform = self.multiply_transforms(&path[..path.len() - 1])?; Ok(layer.data.bounding_box(layer.transform).map(|bounds| (bounds, transform))) } @@ -348,7 +354,7 @@ impl Document { } pub fn mark_downstream_as_dirty(&mut self, path: &[LayerId]) -> Result<(), DocumentError> { - let mut layer = self.layer_mut(path)?; + let mut layer = self.layer_mut(&path)?; layer.cache_dirty = true; let mut path = path.to_vec(); @@ -427,59 +433,59 @@ impl Document { /// Mutate the document by applying the `operation` to it. If the operation necessitates a /// reaction from the frontend, responses may be returned. - pub fn handle_operation(&mut self, operation: &Operation) -> Result>, DocumentError> { + pub fn handle_operation(&mut self, operation: Operation) -> Result>, DocumentError> { use DocumentResponse::*; operation.pseudo_hash().hash(&mut self.state_identifier); - let responses = match &operation { + let responses = match operation { Operation::AddEllipse { path, insert_index, transform, style } => { - let layer = Layer::new(LayerDataType::Shape(ShapeLayer::ellipse(*style)), *transform); + let layer = Layer::new(LayerDataType::Shape(ShapeLayer::ellipse(style)), transform); - self.set_layer(path, layer, *insert_index)?; + self.set_layer(&path, layer, insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::AddOverlayEllipse { path, transform, style } => { - let mut ellipse = ShapeLayer::ellipse(*style); + let mut ellipse = ShapeLayer::ellipse(style); ellipse.render_index = -1; - let layer = Layer::new(LayerDataType::Shape(ellipse), *transform); - self.set_layer(path, layer, -1)?; + let layer = Layer::new(LayerDataType::Shape(ellipse), transform); + self.set_layer(&path, layer, -1)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat()) + Some([vec![DocumentChanged, CreatedLayer { path }]].concat()) } Operation::AddRect { path, insert_index, transform, style } => { - let layer = Layer::new(LayerDataType::Shape(ShapeLayer::rectangle(*style)), *transform); + let layer = Layer::new(LayerDataType::Shape(ShapeLayer::rectangle(style)), transform); - self.set_layer(path, layer, *insert_index)?; + self.set_layer(&path, layer, insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::AddOverlayRect { path, transform, style } => { - let mut rect = ShapeLayer::rectangle(*style); + let mut rect = ShapeLayer::rectangle(style); rect.render_index = -1; - let layer = Layer::new(LayerDataType::Shape(rect), *transform); - self.set_layer(path, layer, -1)?; + let layer = Layer::new(LayerDataType::Shape(rect), transform); + self.set_layer(&path, layer, -1)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat()) + Some([vec![DocumentChanged, CreatedLayer { path }]].concat()) } Operation::AddLine { path, insert_index, transform, style } => { - let layer = Layer::new(LayerDataType::Shape(ShapeLayer::line(*style)), *transform); + let layer = Layer::new(LayerDataType::Shape(ShapeLayer::line(style)), transform); - self.set_layer(path, layer, *insert_index)?; + self.set_layer(&path, layer, insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::AddOverlayLine { path, transform, style } => { - let mut line = ShapeLayer::line(*style); + let mut line = ShapeLayer::line(style); line.render_index = -1; - let layer = Layer::new(LayerDataType::Shape(line), *transform); - self.set_layer(path, layer, -1)?; + let layer = Layer::new(LayerDataType::Shape(line), transform); + self.set_layer(&path, layer, -1)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat()) + Some([vec![DocumentChanged, CreatedLayer { path }]].concat()) } Operation::AddText { path, @@ -490,22 +496,22 @@ impl Document { style, size, } => { - let layer = Layer::new(LayerDataType::Text(TextLayer::new(text.clone(), *style, *size)), *transform); + let layer = Layer::new(LayerDataType::Text(TextLayer::new(text.clone(), style, size)), transform); - self.set_layer(path, layer, *insert_index)?; + self.set_layer(&path, layer, insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::SetTextEditability { path, editable } => { - self.layer_mut(path)?.as_text_mut()?.editable = *editable; - self.mark_as_dirty(path)?; + self.layer_mut(&path)?.as_text_mut()?.editable = editable; + self.mark_as_dirty(&path)?; Some(vec![DocumentChanged]) } Operation::SetTextContent { path, new_text } => { - self.layer_mut(path)?.as_text_mut()?.update_text(new_text.clone()); - self.mark_as_dirty(path)?; + self.layer_mut(&path)?.as_text_mut()?.update_text(new_text.clone()); + self.mark_as_dirty(&path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::AddNgon { path, @@ -514,20 +520,20 @@ impl Document { style, sides, } => { - let layer = Layer::new(LayerDataType::Shape(ShapeLayer::ngon(*sides, *style)), *transform); + let layer = Layer::new(LayerDataType::Shape(ShapeLayer::ngon(sides, style)), transform); - self.set_layer(path, layer, *insert_index)?; + self.set_layer(&path, layer, insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::AddOverlayShape { path, style, bez_path, closed } => { - let mut shape = ShapeLayer::from_bez_path(bez_path.clone(), *style, *closed); + let mut shape = ShapeLayer::from_bez_path(bez_path.clone(), style, closed); shape.render_index = -1; let layer = Layer::new(LayerDataType::Shape(shape), DAffine2::IDENTITY.to_cols_array()); - self.set_layer(path, layer, -1)?; + self.set_layer(&path, layer, -1)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat()) + Some([vec![DocumentChanged, CreatedLayer { path }]].concat()) } Operation::AddShape { path, @@ -537,9 +543,9 @@ impl Document { bez_path, closed, } => { - let shape = ShapeLayer::from_bez_path(bez_path.clone(), *style, *closed); - self.set_layer(path, Layer::new(LayerDataType::Shape(shape), *transform), *insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat()) + let shape = ShapeLayer::from_bez_path(bez_path.clone(), style, closed); + self.set_layer(&path, Layer::new(LayerDataType::Shape(shape), transform), insert_index)?; + Some([vec![DocumentChanged, CreatedLayer { path }]].concat()) } Operation::AddPolyline { path, @@ -549,8 +555,8 @@ impl Document { style, } => { let points: Vec = points.iter().map(|&it| it.into()).collect(); - self.set_layer(path, Layer::new(LayerDataType::Shape(ShapeLayer::poly_line(points, *style)), *transform), *insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + self.set_layer(&path, Layer::new(LayerDataType::Shape(ShapeLayer::poly_line(points, style)), transform), insert_index)?; + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::BooleanOperation { operation, selected } => { // TODO: proper difference @@ -565,13 +571,13 @@ impl Document { let mut responses = Vec::new(); if selected.len() > 1 && selected.len() < 3 { // ? apparently `selected` should be reversed - let mut shapes = self.transformed_shapes(selected)?; + let mut shapes = self.transformed_shapes(&selected)?; let mut shape_drain = shapes.drain(..).rev(); - let new_shapes = boolean_operation(*operation, shape_drain.next().unwrap(), shape_drain.next().unwrap())?; + let new_shapes = boolean_operation(operation, shape_drain.next().unwrap(), shape_drain.next().unwrap())?; for path in selected { - self.delete(path)?; - responses.push(DocumentResponse::DeletedLayer { path: path.clone() }) + self.delete(&path)?; + responses.push(DocumentResponse::DeletedLayer { path }) } for new_shape in new_shapes { let new_id = self.add_layer(&[], Layer::new(LayerDataType::Shape(new_shape), DAffine2::IDENTITY.to_cols_array()), -1)?; @@ -588,8 +594,8 @@ impl Document { style, } => { let points: Vec = points.iter().map(|&it| it.into()).collect(); - self.set_layer(path, Layer::new(LayerDataType::Shape(ShapeLayer::spline(points, *style)), *transform), *insert_index)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + self.set_layer(&path, Layer::new(LayerDataType::Shape(ShapeLayer::spline(points, style)), transform), insert_index)?; + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::DeleteLayer { path } => { fn aggregate_deletions(folder: &FolderLayer, path: &mut Vec, responses: &mut Vec) { @@ -603,10 +609,10 @@ impl Document { } } let mut responses = Vec::new(); - if let Ok(folder) = self.folder(path) { + if let Ok(folder) = self.folder(&path) { aggregate_deletions(folder, &mut path.clone(), &mut responses) }; - self.delete(path)?; + self.delete(&path)?; let (folder, _) = split_path(path.as_slice()).unwrap_or((&[], 0)); responses.extend([DocumentChanged, DeletedLayer { path: path.clone() }, FolderChanged { path: folder.to_vec() }]); @@ -618,10 +624,10 @@ impl Document { layer, insert_index, } => { - let (folder_path, layer_id) = split_path(destination_path)?; + let (folder_path, layer_id) = split_path(&destination_path)?; let folder = self.folder_mut(folder_path)?; - folder.add_layer(layer.clone(), Some(layer_id), *insert_index).ok_or(DocumentError::IndexOutOfBounds)?; - self.mark_as_dirty(destination_path)?; + folder.add_layer(layer.clone(), Some(layer_id), insert_index).ok_or(DocumentError::IndexOutOfBounds)?; + self.mark_as_dirty(&destination_path)?; fn aggregate_insertions(folder: &FolderLayer, path: &mut Vec, responses: &mut Vec) { for (id, layer) in folder.layer_ids.iter().zip(folder.layers()) { @@ -635,16 +641,16 @@ impl Document { } let mut responses = Vec::new(); - if let Ok(folder) = self.folder(destination_path) { - aggregate_insertions(folder, &mut destination_path.clone(), &mut responses) + if let Ok(folder) = self.folder(&destination_path) { + aggregate_insertions(folder, &mut destination_path.as_slice().to_vec(), &mut responses) }; responses.extend([DocumentChanged, CreatedLayer { path: destination_path.clone() }, FolderChanged { path: folder_path.to_vec() }]); - responses.extend(update_thumbnails_upstream(destination_path)); + responses.extend(update_thumbnails_upstream(&destination_path)); Some(responses) } Operation::DuplicateLayer { path } => { - let layer = self.layer(path)?.clone(); + let layer = self.layer(&path)?.clone(); let (folder_path, _) = split_path(path.as_slice()).unwrap_or((&[], 0)); let folder = self.folder_mut(folder_path)?; if let Some(new_layer_id) = folder.add_layer(layer, None, -1) { @@ -662,126 +668,123 @@ impl Document { } } Operation::RenameLayer { layer_path: path, new_name: name } => { - self.layer_mut(path)?.name = Some(name.clone()); - Some(vec![LayerChanged { path: path.clone() }]) + self.layer_mut(&path)?.name = Some(name.clone()); + Some(vec![LayerChanged { path }]) } Operation::CreateFolder { path } => { - self.set_layer(path, Layer::new(LayerDataType::Folder(FolderLayer::default()), DAffine2::IDENTITY.to_cols_array()), -1)?; - self.mark_as_dirty(path)?; + self.set_layer(&path, Layer::new(LayerDataType::Folder(FolderLayer::default()), DAffine2::IDENTITY.to_cols_array()), -1)?; + self.mark_as_dirty(&path)?; - Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::TransformLayer { path, transform } => { - let layer = self.layer_mut(path).unwrap(); - let transform = DAffine2::from_cols_array(transform) * layer.transform; + let layer = self.layer_mut(&path).unwrap(); + let transform = DAffine2::from_cols_array(&transform) * layer.transform; layer.transform = transform; - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::TransformLayerInViewport { path, transform } => { - let transform = DAffine2::from_cols_array(transform); - self.apply_transform_relative_to_viewport(path, transform)?; - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + let transform = DAffine2::from_cols_array(&transform); + self.apply_transform_relative_to_viewport(&path, transform)?; + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetLayerTransformInViewport { path, transform } => { - let transform = DAffine2::from_cols_array(transform); - self.set_transform_relative_to_viewport(path, transform)?; - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + let transform = DAffine2::from_cols_array(&transform); + self.set_transform_relative_to_viewport(&path, transform)?; + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetShapePath { path, bez_path } => { - self.mark_as_dirty(path)?; + self.mark_as_dirty(&path)?; - if let LayerDataType::Shape(shape) = &mut self.layer_mut(path)?.data { + if let LayerDataType::Shape(shape) = &mut self.layer_mut(&path)?.data { shape.path = bez_path.clone(); } - Some(vec![DocumentChanged, LayerChanged { path: path.clone() }]) + Some(vec![DocumentChanged, LayerChanged { path }]) } Operation::SetShapePathInViewport { path, bez_path, transform } => { - let transform = DAffine2::from_cols_array(transform); - self.set_transform_relative_to_viewport(path, transform)?; - self.mark_as_dirty(path)?; + let transform = DAffine2::from_cols_array(&transform); + self.set_transform_relative_to_viewport(&path, transform)?; + self.mark_as_dirty(&path)?; - if let LayerDataType::Text(t) = &mut self.layer_mut(path)?.data { + if let LayerDataType::Text(t) = &mut self.layer_mut(&path)?.data { let bezpath = t.to_bez_path(); - self.layer_mut(path)?.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.style, true)); + self.layer_mut(&path)?.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.style.clone(), true)); } - if let LayerDataType::Shape(shape) = &mut self.layer_mut(path)?.data { + if let LayerDataType::Shape(shape) = &mut self.layer_mut(&path)?.data { shape.path = bez_path.clone(); } - Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } Operation::TransformLayerInScope { path, transform, scope } => { - let transform = DAffine2::from_cols_array(transform); - let scope = DAffine2::from_cols_array(scope); - self.transform_relative_to_scope(path, Some(scope), transform)?; - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + let transform = DAffine2::from_cols_array(&transform); + let scope = DAffine2::from_cols_array(&scope); + self.transform_relative_to_scope(&path, Some(scope), transform)?; + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetLayerTransformInScope { path, transform, scope } => { - let transform = DAffine2::from_cols_array(transform); - let scope = DAffine2::from_cols_array(scope); - self.set_transform_relative_to_scope(path, Some(scope), transform)?; - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + let transform = DAffine2::from_cols_array(&transform); + let scope = DAffine2::from_cols_array(&scope); + self.set_transform_relative_to_scope(&path, Some(scope), transform)?; + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetLayerTransform { path, transform } => { - let transform = DAffine2::from_cols_array(transform); - let layer = self.layer_mut(path)?; + let transform = DAffine2::from_cols_array(&transform); + let layer = self.layer_mut(&path)?; layer.transform = transform; - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::ToggleLayerVisibility { path } => { - self.mark_as_dirty(path)?; - let layer = self.layer_mut(path)?; + self.mark_as_dirty(&path)?; + let layer = self.layer_mut(&path)?; layer.visible = !layer.visible; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetLayerVisibility { path, visible } => { - self.mark_as_dirty(path)?; - let layer = self.layer_mut(path)?; - layer.visible = *visible; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + self.mark_as_dirty(&path)?; + let layer = self.layer_mut(&path)?; + layer.visible = visible; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetLayerName { path, name } => { - self.mark_as_dirty(path)?; - let mut layer = self.layer_mut(path)?; + self.mark_as_dirty(&path)?; + let mut layer = self.layer_mut(&path)?; layer.name = if name.as_str() == "" { None } else { Some(name.clone()) }; - Some(vec![LayerChanged { path: path.clone() }]) + Some(vec![LayerChanged { path }]) } Operation::SetLayerBlendMode { path, blend_mode } => { - self.mark_as_dirty(path)?; - self.layer_mut(path)?.blend_mode = *blend_mode; + self.mark_as_dirty(&path)?; + self.layer_mut(&path)?.blend_mode = blend_mode; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetLayerOpacity { path, opacity } => { - self.mark_as_dirty(path)?; - self.layer_mut(path)?.opacity = *opacity; + self.mark_as_dirty(&path)?; + self.layer_mut(&path)?.opacity = opacity; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } Operation::SetLayerStyle { path, style } => { - let layer = self.layer_mut(path)?; + let layer = self.layer_mut(&path)?; match &mut layer.data { - LayerDataType::Shape(s) => s.style = *style, + LayerDataType::Shape(s) => s.style = style, _ => return Err(DocumentError::NotAShape), } - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(path)].concat()) + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat()) } - Operation::SetLayerFill { path, color } => { - let layer = self.layer_mut(path)?; - match &mut layer.data { - LayerDataType::Shape(s) => s.style.set_fill(layers::style::Fill::new(*color)), - _ => return Err(DocumentError::NotAShape), - } - self.mark_as_dirty(path)?; - Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) + Operation::SetLayerFill { path, fill } => { + let layer = self.layer_mut(&path)?; + layer.style_mut()?.set_fill(fill); + self.mark_as_dirty(&path)?; + Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat()) } }; Ok(responses) diff --git a/graphene/src/layers/folder_layer.rs b/graphene/src/layers/folder_layer.rs index b5ab80af..2424dbc0 100644 --- a/graphene/src/layers/folder_layer.rs +++ b/graphene/src/layers/folder_layer.rs @@ -15,9 +15,9 @@ pub struct FolderLayer { } impl LayerData for FolderLayer { - fn render(&mut self, svg: &mut String, transforms: &mut Vec, view_mode: ViewMode) { + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode) { for layer in &mut self.layers { - let _ = writeln!(svg, "{}", layer.render(transforms, view_mode)); + let _ = writeln!(svg, "{}", layer.render(transforms, view_mode, svg_defs)); } } diff --git a/graphene/src/layers/layer_info.rs b/graphene/src/layers/layer_info.rs index 5693a50c..0571e862 100644 --- a/graphene/src/layers/layer_info.rs +++ b/graphene/src/layers/layer_info.rs @@ -1,7 +1,7 @@ use super::blend_mode::BlendMode; use super::folder_layer::FolderLayer; use super::shape_layer::ShapeLayer; -use super::style::ViewMode; +use super::style::{PathStyle, ViewMode}; use super::text_layer::TextLayer; use crate::intersection::Quad; use crate::DocumentError; @@ -37,14 +37,14 @@ impl LayerDataType { } pub trait LayerData { - fn render(&mut self, svg: &mut String, transforms: &mut Vec, view_mode: ViewMode); + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode); fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>); fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]>; } impl LayerData for LayerDataType { - fn render(&mut self, svg: &mut String, transforms: &mut Vec, view_mode: ViewMode) { - self.inner_mut().render(svg, transforms, view_mode) + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode) { + self.inner_mut().render(svg, svg_defs, transforms, view_mode) } fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>) { @@ -78,6 +78,8 @@ pub struct Layer { pub cache: String, #[serde(skip)] pub thumbnail_cache: String, + #[serde(skip)] + pub svg_defs_cache: String, #[serde(skip, default = "return_true")] pub cache_dirty: bool, pub blend_mode: BlendMode, @@ -93,6 +95,7 @@ impl Layer { transform: glam::DAffine2::from_cols_array(&transform), cache: String::new(), thumbnail_cache: String::new(), + svg_defs_cache: String::new(), cache_dirty: true, blend_mode: BlendMode::Normal, opacity: 1., @@ -103,7 +106,7 @@ impl Layer { LayerIter { stack: vec![self] } } - pub fn render(&mut self, transforms: &mut Vec, view_mode: ViewMode) -> &str { + pub fn render(&mut self, transforms: &mut Vec, view_mode: ViewMode, svg_defs: &mut String) -> &str { if !self.visible { return ""; } @@ -111,7 +114,8 @@ impl Layer { if self.cache_dirty { transforms.push(self.transform); self.thumbnail_cache.clear(); - self.data.render(&mut self.thumbnail_cache, transforms, view_mode); + self.svg_defs_cache.clear(); + self.data.render(&mut self.thumbnail_cache, &mut self.svg_defs_cache, transforms, view_mode); self.cache.clear(); let _ = writeln!(self.cache, r#" Err(DocumentError::NotText), } } + + pub fn style(&self) -> Result<&PathStyle, DocumentError> { + match &self.data { + LayerDataType::Shape(s) => Ok(&s.style), + LayerDataType::Text(t) => Ok(&t.style), + _ => return Err(DocumentError::NotAShape), + } + } + + pub fn style_mut(&mut self) -> Result<&mut PathStyle, DocumentError> { + match &mut self.data { + LayerDataType::Shape(s) => Ok(&mut s.style), + LayerDataType::Text(t) => Ok(&mut t.style), + _ => return Err(DocumentError::NotAShape), + } + } } impl Clone for Layer { @@ -187,6 +208,7 @@ impl Clone for Layer { transform: self.transform, cache: String::new(), thumbnail_cache: String::new(), + svg_defs_cache: String::new(), cache_dirty: true, blend_mode: self.blend_mode, opacity: self.opacity, diff --git a/graphene/src/layers/shape_layer.rs b/graphene/src/layers/shape_layer.rs index c9db7a6d..c5a70d09 100644 --- a/graphene/src/layers/shape_layer.rs +++ b/graphene/src/layers/shape_layer.rs @@ -21,7 +21,7 @@ pub struct ShapeLayer { } impl LayerData for ShapeLayer { - fn render(&mut self, svg: &mut String, transforms: &mut Vec, view_mode: ViewMode) { + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode) { let mut path = self.path.clone(); let transform = self.transform(transforms, view_mode); let inverse = transform.inverse(); @@ -36,7 +36,7 @@ impl LayerData for ShapeLayer { let _ = svg.write_str(&(entry.to_string() + if i == 5 { "" } else { "," })); }); let _ = svg.write_str(r#")">"#); - let _ = write!(svg, r#""#, path.to_svg(), self.style.render(view_mode)); + let _ = write!(svg, r#""#, path.to_svg(), self.style.render(view_mode, svg_defs)); let _ = svg.write_str(""); } diff --git a/graphene/src/layers/style/mod.rs b/graphene/src/layers/style/mod.rs index 7a9d4a9c..4388b669 100644 --- a/graphene/src/layers/style/mod.rs +++ b/graphene/src/layers/style/mod.rs @@ -1,8 +1,12 @@ +use std::fmt::Write; + use crate::color::Color; use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WIDTH}; use serde::{Deserialize, Serialize}; +use glam::{DAffine2, DVec2}; + const OPACITY_PRECISION: usize = 3; fn format_opacity(name: &str, opacity: f32) -> String { @@ -26,27 +30,106 @@ impl Default for ViewMode { } } +/// A gradient fill. +/// +/// Contains the start and end points, along with the colors at varying points along the length. #[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] -pub struct Fill { - color: Color, +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct Gradient { + pub start: DVec2, + pub end: DVec2, + pub transform: DAffine2, + pub positions: Vec<(f64, Color)>, + uuid: u64, +} +impl Gradient { + /// Constructs a new gradient with the colors at 0 and 1 specified. + pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, uuid: u64) -> Self { + Gradient { + start, + end, + positions: vec![(0., start_color), (1., end_color)], + transform, + uuid, + } + } + + /// Adds the gradient def with the uuid specified + fn render_defs(&self, svg_defs: &mut String) { + let positions = self + .positions + .iter() + .map(|(position, color)| format!(r##""##, position, color.rgba_hex())) + .collect::(); + + let start = self.transform.inverse().transform_point2(self.start); + let end = self.transform.inverse().transform_point2(self.end); + + let transform = self + .transform + .to_cols_array() + .iter() + .enumerate() + .map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," }) + .collect::(); + + let _ = write!( + svg_defs, + r#"{}"#, + self.uuid, start.x, end.x, start.y, end.y, transform, positions + ); + } +} + +/// Describes the fill of a layer. +/// +/// Can be None, solid or potentially some sort of image or pattern +#[repr(C)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Fill { + None, + Solid(Color), + LinearGradient(Gradient), +} + +impl Default for Fill { + fn default() -> Self { + Self::None + } } impl Fill { - pub fn new(color: Color) -> Self { - Self { color } + /// Construct a new solid fill + pub fn solid(color: Color) -> Self { + Self::Solid(color) } + /// Evaluate the color at some point on the fill pub fn color(&self) -> Color { - self.color + match self { + Self::None => Color::BLACK, + Self::Solid(color) => *color, + // ToDo: Should correctly sample the gradient + Self::LinearGradient(Gradient { positions, .. }) => positions[0].1, + } } - pub fn render(fill: Option) -> String { - match fill { - Some(c) => format!(r##" fill="#{}"{}"##, c.color.rgb_hex(), format_opacity("fill", c.color.a())), - None => r#" fill="none""#.to_string(), + /// Renders the fill, adding necessary defs. + pub fn render(&self, svg_defs: &mut String) -> String { + match self { + Self::None => r#" fill="none""#.to_string(), + Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a())), + Self::LinearGradient(gradient) => { + gradient.render_defs(svg_defs); + format!(r##" fill="url('#{}')""##, gradient.uuid) + } } } + + /// Check if the fill is not none + pub fn is_some(&self) -> bool { + *self != Self::None + } } #[repr(C)] @@ -75,19 +158,19 @@ impl Stroke { } #[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct PathStyle { stroke: Option, - fill: Option, + fill: Fill, } impl PathStyle { - pub fn new(stroke: Option, fill: Option) -> Self { + pub fn new(stroke: Option, fill: Fill) -> Self { Self { stroke, fill } } - pub fn fill(&self) -> Option { - self.fill + pub fn fill(&self) -> &Fill { + &self.fill } pub fn stroke(&self) -> Option { @@ -95,7 +178,7 @@ impl PathStyle { } pub fn set_fill(&mut self, fill: Fill) { - self.fill = Some(fill); + self.fill = fill; } pub fn set_stroke(&mut self, stroke: Stroke) { @@ -103,17 +186,17 @@ impl PathStyle { } pub fn clear_fill(&mut self) { - self.fill = None; + self.fill = Fill::None; } pub fn clear_stroke(&mut self) { self.stroke = None; } - pub fn render(&self, view_mode: ViewMode) -> String { - let fill_attribute = match (view_mode, self.fill) { - (ViewMode::Outline, _) => Fill::render(None), - (_, fill) => Fill::render(fill), + pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String) -> String { + let fill_attribute = match (view_mode, &self.fill) { + (ViewMode::Outline, _) => Fill::None.render(svg_defs), + (_, fill) => fill.render(svg_defs), }; let stroke_attribute = match (view_mode, self.stroke) { (ViewMode::Outline, _) => Stroke::new(LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WIDTH).render(), diff --git a/graphene/src/layers/text_layer/mod.rs b/graphene/src/layers/text_layer/mod.rs index e9bdac3f..9139ccc8 100644 --- a/graphene/src/layers/text_layer/mod.rs +++ b/graphene/src/layers/text_layer/mod.rs @@ -27,7 +27,7 @@ pub struct TextLayer { } impl LayerData for TextLayer { - fn render(&mut self, svg: &mut String, transforms: &mut Vec, view_mode: ViewMode) { + fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec, view_mode: ViewMode) { let transform = self.transform(transforms, view_mode); let inverse = transform.inverse(); if !inverse.is_finite() { @@ -43,24 +43,20 @@ impl LayerData for TextLayer { if self.editable { let _ = write!( svg, - r#""#, + r#""#, transform .to_cols_array() .iter() .enumerate() .map(|(i, entry)| { entry.to_string() + if i == 5 { "" } else { "," } }) .collect::(), - match self.style.fill() { - Some(fill) => format!("#{}", fill.color().rgba_hex()), - None => "gray".to_string(), - } ); } else { let mut path = self.to_bez_path(); path.apply_affine(glam_to_kurbo(transform)); - let _ = write!(svg, r#""#, path.to_svg(), self.style.render(view_mode)); + let _ = write!(svg, r#""#, path.to_svg(), self.style.render(view_mode, svg_defs)); } let _ = svg.write_str(""); } diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index e441bf88..f6eebe0e 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -1,5 +1,4 @@ use crate::boolean_ops::BooleanOperation as BooleanOperationType; -use crate::color::Color; use crate::layers::blend_mode::BlendMode; use crate::layers::layer_info::Layer; use crate::layers::style; @@ -179,7 +178,7 @@ pub enum Operation { }, SetLayerFill { path: Vec, - color: Color, + fill: style::Fill, }, }