From 0d703e857b83df1ba1894bbd96144f51cabcb74d Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Thu, 29 Dec 2022 20:00:58 +0000 Subject: [PATCH] Gradient tool improvements (#927) * Reuse existing gradient * Double click to insert gradient stop * Add history states to the gradient tool * Do trig in viewport space so it is actually perpendicular * Sync tool options with active gradient * Deleting points with delete key * More tolerance on inserting points --- document-legacy/src/color.rs | 13 ++ document-legacy/src/document.rs | 6 + document-legacy/src/layers/style/mod.rs | 48 +++++ .../messages/input_mapper/default_mapping.rs | 3 + .../portfolio/document/document_message.rs | 5 + .../document/document_message_handler.rs | 22 +- editor/src/messages/tool/tool_message.rs | 1 + .../src/messages/tool/tool_message_handler.rs | 4 + .../tool/tool_messages/gradient_tool.rs | 203 ++++++++++++++++-- 9 files changed, 281 insertions(+), 24 deletions(-) diff --git a/document-legacy/src/color.rs b/document-legacy/src/color.rs index fc302829..46c3c2bb 100644 --- a/document-legacy/src/color.rs +++ b/document-legacy/src/color.rs @@ -205,4 +205,17 @@ impl Color { Some(Color::from_rgb8(r, g, b)) } + + /// Linearly interpolates between two colors based on t. + /// + /// T must be between 0 and 1. + pub fn lerp(self, other: Color, t: f32) -> Option { + assert!((0. ..=1.).contains(&t)); + Color::from_rgbaf32( + self.red + ((other.red - self.red) * t), + self.green + ((other.green - self.green) * t), + self.blue + ((other.blue - self.blue) * t), + self.alpha + ((other.alpha - self.alpha) * t), + ) + } } diff --git a/document-legacy/src/document.rs b/document-legacy/src/document.rs index 7749ccd7..2f276f0f 100644 --- a/document-legacy/src/document.rs +++ b/document-legacy/src/document.rs @@ -32,6 +32,12 @@ pub struct Document { pub state_identifier: DefaultHasher, } +impl PartialEq for Document { + fn eq(&self, other: &Self) -> bool { + self.state_identifier.finish() == other.state_identifier.finish() + } +} + impl Default for Document { fn default() -> Self { Self { diff --git a/document-legacy/src/layers/style/mod.rs b/document-legacy/src/layers/style/mod.rs index bb8f4daf..57556ca8 100644 --- a/document-legacy/src/layers/style/mod.rs +++ b/document-legacy/src/layers/style/mod.rs @@ -138,6 +138,45 @@ impl Gradient { } } } + + /// Insert a stop into the gradient, the index if successful + pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option { + // Transform the start and end positions to the same coordinate space as the mouse. + let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end)); + + // Calculate the new position by finding the closest point on the line + let new_position = ((end - start).angle_between(mouse - start)).cos() * start.distance(mouse) / start.distance(end); + + // Don't insert point past end of line + if !(0. ..=1.).contains(&new_position) { + return None; + } + + // Compute the color of the inserted stop + let get_color = |index: usize, time: f64| match (self.positions[index].1, self.positions.get(index + 1).and_then(|x| x.1)) { + // Lerp between the nearest colours if applicable + (Some(a), Some(b)) => a.lerp( + b, + ((time - self.positions[index].0) / self.positions.get(index + 1).map(|end| end.0 - self.positions[index].0).unwrap_or_default()) as f32, + ), + // Use the start or the end colour if applicable + (Some(v), _) | (_, Some(v)) => Some(v), + _ => Some(Color::WHITE), + }; + + // Compute the correct index to keep the positions in order + let mut index = 0; + while self.positions.len() > index && self.positions[index].0 <= new_position { + index += 1; + } + + let new_color = get_color(index - 1, new_position); + + // Insert the new stop + self.positions.insert(index, (new_position, new_color)); + + Some(index) + } } /// Describes the fill of a layer. @@ -189,6 +228,15 @@ impl Fill { pub fn is_some(&self) -> bool { *self != Self::None } + + /// Extract a gradient from the fill + pub fn as_gradient(&self) -> Option<&Gradient> { + if let Self::Gradient(gradient) = self { + Some(gradient) + } else { + None + } + } } /// The stroke (outline) style of an SVG element. diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index 7df5e40a..0e6be3b4 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -116,6 +116,9 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(Lmb); action_dispatch=GradientToolMessage::PointerDown), entry!(PointerMove; refresh_keys=[Shift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: Shift }), entry!(KeyUp(Lmb); action_dispatch=GradientToolMessage::PointerUp), + entry!(DoubleClick; action_dispatch=GradientToolMessage::InsertStop), + entry!(KeyDown(Delete); action_dispatch=GradientToolMessage::DeleteStop), + entry!(KeyDown(Backspace); action_dispatch=GradientToolMessage::DeleteStop), // // RectangleToolMessage entry!(KeyDown(Lmb); action_dispatch=RectangleToolMessage::DragStart), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 67ee5aa7..ca2d3072 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, use crate::messages::prelude::*; use document_legacy::boolean_ops::BooleanOperation as BooleanOperationType; +use document_legacy::document::Document as DocumentLegacy; use document_legacy::layers::blend_mode::BlendMode; use document_legacy::layers::style::ViewMode; use document_legacy::LayerId; @@ -46,6 +47,10 @@ pub enum DocumentMessage { axis: AlignAxis, aggregate: AlignAggregate, }, + BackupDocument { + document: DocumentLegacy, + layer_metadata: HashMap, LayerMetadata>, + }, BooleanOperation(BooleanOperationType), ClearLayerTree, CommitTransaction, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index cd446e58..bcff9405 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -232,6 +232,7 @@ impl MessageHandler self.backup_with_document(document, layer_metadata, responses), BooleanOperation(op) => { // Convert Vec<&[LayerId]> to Vec> because Vec<&[LayerId]> does not implement several traits (Debug, Serialize, Deserialize, ...) required by DocumentOperation enum responses.push_back(StartTransaction.into()); @@ -1276,9 +1277,10 @@ impl DocumentMessageHandler { .unwrap_or_else(|| panic!("Layer data cannot be found because the path {:?} does not exist", path)) } - pub fn backup(&mut self, responses: &mut VecDeque) { + /// Places a document into the history system + fn backup_with_document(&mut self, document: DocumentLegacy, layer_metadata: HashMap, LayerMetadata>, responses: &mut VecDeque) { self.document_redo_history.clear(); - self.document_undo_history.push_back((self.document_legacy.clone(), self.layer_metadata.clone())); + self.document_undo_history.push_back((document, layer_metadata)); if self.document_undo_history.len() > crate::consts::MAX_UNDO_HISTORY_LEN { self.document_undo_history.pop_front(); } @@ -1287,6 +1289,22 @@ impl DocumentMessageHandler { responses.push_back(PortfolioMessage::UpdateOpenDocumentsList.into()); } + /// Copies the entire document into the history system + pub fn backup(&mut self, responses: &mut VecDeque) { + self.backup_with_document(self.document_legacy.clone(), self.layer_metadata.clone(), responses); + } + + /// Push a message backing up the document in its current state + pub fn backup_nonmut(&self, responses: &mut VecDeque) { + responses.push_back( + DocumentMessage::BackupDocument { + document: self.document_legacy.clone(), + layer_metadata: self.layer_metadata.clone(), + } + .into(), + ); + } + pub fn rollback(&mut self, responses: &mut VecDeque) -> Result<(), EditorError> { self.backup(responses); self.undo(responses) diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index 78d14d63..ddfa8726 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -125,6 +125,7 @@ pub enum ToolMessage { }, DeactivateTools, InitTools, + RefreshToolOptions, ResetColors, SelectPrimaryColor { color: Color, diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index cede64b4..9e4af670 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -139,6 +139,10 @@ impl MessageHandler { + let tool_data = &mut self.tool_state.tool_data; + tool_data.tools.get(&tool_data.active_tool_type).unwrap().register_properties(responses, LayoutTarget::ToolOptions); + } ToolMessage::ResetColors => { let document_data = &mut self.tool_state.document_tool_data; diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 5cf06e39..6634579d 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -1,5 +1,5 @@ use crate::application::generate_uuid; -use crate::consts::{COLOR_ACCENT, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_TOLERANCE}; +use crate::consts::{COLOR_ACCENT, DRAG_THRESHOLD, LINE_ROTATE_SNAP_ANGLE, MANIPULATOR_GROUP_MARKER_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion}; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; @@ -48,6 +48,8 @@ pub enum GradientToolMessage { DocumentIsDirty, // Tool-specific messages + DeleteStop, + InsertStop, PointerDown, PointerMove { constrain_axis: Key, @@ -87,7 +89,13 @@ impl<'a> MessageHandler> for GradientTool } if let ToolMessage::Gradient(GradientToolMessage::UpdateOptions(action)) = message { match action { - GradientOptionsUpdate::Type(gradient_type) => self.options.gradient_type = gradient_type, + GradientOptionsUpdate::Type(gradient_type) => { + self.options.gradient_type = gradient_type; + if let Some(selected_gradient) = &mut self.data.selected_gradient { + selected_gradient.gradient.gradient_type = gradient_type; + selected_gradient.render_gradient(responses); + } + } } return; } @@ -105,6 +113,8 @@ impl<'a> MessageHandler> for GradientTool PointerUp, PointerMove, Abort, + InsertStop, + DeleteStop, ); } @@ -112,7 +122,11 @@ impl PropertyHolder for GradientTool { fn properties(&self) -> Layout { Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![WidgetHolder::new(Widget::RadioInput(RadioInput { - selected_index: if self.options.gradient_type == GradientType::Radial { 1 } else { 0 }, + selected_index: if self.selected_gradient().unwrap_or(self.options.gradient_type) == GradientType::Radial { + 1 + } else { + 0 + }, entries: vec![ RadioEntryData { value: "linear".into(), @@ -289,6 +303,35 @@ impl SelectedGradient { } } + /// Update the selected gradient, checking for removal or change of gradient. + pub fn update(gradient: &mut Option, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque) { + let Some(inner_gradient) = gradient else { + return; + }; + + // Clear the gradient if layer deleted + let Ok(layer) = document.document_legacy.layer(&inner_gradient.path) else{ + responses.push_back(ToolMessage::RefreshToolOptions.into()); + *gradient = None; + return; + }; + + // Update transform + inner_gradient.transform = gradient_space_transform(&inner_gradient.path, layer, document, font_cache); + + // Clear if no longer a gradient + let Some(gradient) = layer.style().ok().and_then(|style|style.fill().as_gradient()) else{ + responses.push_back(ToolMessage::RefreshToolOptions.into()); + *gradient = None; + return; + }; + + if gradient.gradient_type != inner_gradient.gradient.gradient_type { + responses.push_back(ToolMessage::RefreshToolOptions.into()); + } + inner_gradient.gradient = gradient.clone(); + } + pub fn with_gradient_start(mut self, start: DVec2) -> Self { self.gradient.start = self.transform.inverse().transform_point2(start); self @@ -316,17 +359,18 @@ impl SelectedGradient { mouse = point - rotated; } - mouse = self.transform.inverse().transform_point2(mouse); + let transformed_mouse = self.transform.inverse().transform_point2(mouse); match self.dragging { - GradientDragTarget::Start => self.gradient.start = mouse, - GradientDragTarget::End => self.gradient.end = mouse, + GradientDragTarget::Start => self.gradient.start = transformed_mouse, + GradientDragTarget::End => self.gradient.end = transformed_mouse, GradientDragTarget::Step(s) => { - // Calculate the new position by finding the closest point on the line - let new_pos = ((self.gradient.end - self.gradient.start).angle_between(mouse - self.gradient.start)).cos() * self.gradient.start.distance(mouse) - / self.gradient.start.distance(self.gradient.end); + let (start, end) = (self.transform.transform_point2(self.gradient.start), self.transform.transform_point2(self.gradient.end)); - // Should not go off end but can swap (like inscape) + // Calculate the new position by finding the closest point on the line + let new_pos = ((end - start).angle_between(mouse - start)).cos() * start.distance(mouse) / start.distance(end); + + // Should not go off end but can swap let clamped = new_pos.clamp(0., 1.); self.gradient.positions[s].0 = clamped; let new_pos = self.gradient.positions[s]; @@ -335,7 +379,11 @@ impl SelectedGradient { self.dragging = GradientDragTarget::Step(self.gradient.positions.iter().position(|x| *x == new_pos).unwrap()); } } + self.render_gradient(responses); + } + /// Update the layer fill to the current gradient + pub fn render_gradient(&mut self, responses: &mut VecDeque) { self.gradient.transform = self.transform; let fill = Fill::Gradient(self.gradient.clone()); let path = self.path.clone(); @@ -343,12 +391,19 @@ impl SelectedGradient { } } +impl GradientTool { + /// Get the gradient type of the selected gradient (if it exists) + pub fn selected_gradient(&self) -> Option { + self.data.selected_gradient.as_ref().map(|selected| selected.gradient.gradient_type) + } +} + impl ToolTransition for GradientTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { document_dirty: Some(GradientToolMessage::DocumentIsDirty.into()), tool_abort: Some(GradientToolMessage::Abort.into()), - selection_changed: None, + selection_changed: Some(GradientToolMessage::DocumentIsDirty.into()), } } } @@ -358,6 +413,7 @@ struct GradientToolData { gradient_overlays: Vec, selected_gradient: Option, snap_manager: SnapManager, + drag_start: DVec2, } pub fn start_snap(snap_manager: &mut SnapManager, document: &DocumentMessageHandler, font_cache: &FontCache) { @@ -384,6 +440,10 @@ impl Fsm for GradientToolFsmState { overlay.delete_overlays(responses); } + if self != GradientToolFsmState::Drawing { + SelectedGradient::update(&mut tool_data.selected_gradient, document, font_cache, responses); + } + for path in document.selected_visible_layers() { if !document.document_legacy.multiply_transforms(path).unwrap().inverse().is_finite() { continue; @@ -401,10 +461,93 @@ impl Fsm for GradientToolFsmState { self } + (GradientToolFsmState::Ready, GradientToolMessage::DeleteStop) => { + let Some(selected_gradient) = &mut tool_data.selected_gradient else{ + return self; + }; + + // Skip if invalid gradient + if selected_gradient.gradient.positions.len() < 2 { + return self; + } + + // Remove the selected point + match selected_gradient.dragging { + GradientDragTarget::Start => selected_gradient.gradient.positions.remove(0), + GradientDragTarget::End => selected_gradient.gradient.positions.pop().unwrap(), + GradientDragTarget::Step(index) => selected_gradient.gradient.positions.remove(index), + }; + + // The gradient has only one point and so should become a fill + if selected_gradient.gradient.positions.len() == 1 { + let fill = Fill::Solid(selected_gradient.gradient.positions[0].1.unwrap_or(Color::BLACK)); + let path = selected_gradient.path.clone(); + responses.push_back(Operation::SetLayerFill { path, fill }.into()); + return self; + } + + // Find the minimum and maximum positions + let min_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min"); + let max_position = selected_gradient.gradient.positions.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max"); + + // Recompute the start and end posiiton of the gradient (in viewport transform) + let transform = selected_gradient.transform; + let (start, end) = (transform.transform_point2(selected_gradient.gradient.start), transform.transform_point2(selected_gradient.gradient.end)); + let (new_start, new_end) = (start.lerp(end, min_position), start.lerp(end, max_position)); + selected_gradient.gradient.start = transform.inverse().transform_point2(new_start); + selected_gradient.gradient.end = transform.inverse().transform_point2(new_end); + + // Remap the positions + for (position, _) in selected_gradient.gradient.positions.iter_mut() { + *position = (*position - min_position) / (max_position - min_position); + } + + // Render the new gradient + selected_gradient.render_gradient(responses); + + self + } + (_, GradientToolMessage::InsertStop) => { + for overlay in &tool_data.gradient_overlays { + let mouse = input.mouse.position; + let (start, end) = (overlay.evaluate_gradient_start(), overlay.evaluate_gradient_end()); + + // Compute the distance from the mouse to the gradient line in viewport space + let distance = (end - start).angle_between(mouse - start).sin() * (mouse - start).length(); + + // If click is on the line then insert point + if distance < SELECTION_THRESHOLD { + let mut gradient = overlay.gradient.clone(); + + // Try and insert the new stop + if let Some(index) = gradient.insert_stop(mouse, overlay.transform) { + document.backup_nonmut(responses); + + let layer = document.document_legacy.layer(&overlay.path); + if let Ok(layer) = layer { + let mut selected_gradient = SelectedGradient::new(gradient, &overlay.path, layer, document, font_cache); + + // Select the new point + selected_gradient.dragging = GradientDragTarget::Step(index); + + // Update the layer fill + selected_gradient.render_gradient(responses); + + tool_data.selected_gradient = Some(selected_gradient); + } + + break; + } + } + } + + self + } (GradientToolFsmState::Ready, GradientToolMessage::PointerDown) => { responses.push_back(BroadcastEvent::DocumentIsDirty.into()); let mouse = input.mouse.position; + tool_data.drag_start = mouse; let tolerance = MANIPULATOR_GROUP_MARKER_SIZE.powi(2); let mut dragging = false; @@ -441,6 +584,7 @@ impl Fsm for GradientToolFsmState { } } if dragging { + document.backup_nonmut(responses); GradientToolFsmState::Drawing } else { let tolerance = DVec2::splat(SELECTION_TOLERANCE); @@ -456,17 +600,24 @@ impl Fsm for GradientToolFsmState { let layer = document.document_legacy.layer(&intersection).unwrap(); - let gradient = Gradient::new( - DVec2::ZERO, - global_tool_data.secondary_color, - DVec2::ONE, - global_tool_data.primary_color, - DAffine2::IDENTITY, - generate_uuid(), - tool_options.gradient_type, - ); - let mut selected_gradient = SelectedGradient::new(gradient, &intersection, layer, document, font_cache).with_gradient_start(input.mouse.position); - selected_gradient.update_gradient(input.mouse.position, responses, false, tool_options.gradient_type); + responses.push_back(DocumentMessage::StartTransaction.into()); + + // Use the already existing gradient if it exists + let gradient = if let Some(gradient) = layer.style().ok().map(|style| style.fill()).and_then(|fill| fill.as_gradient()) { + gradient.clone() + } else { + // Generate a new gradient + Gradient::new( + DVec2::ZERO, + global_tool_data.secondary_color, + DVec2::ONE, + global_tool_data.primary_color, + DAffine2::IDENTITY, + generate_uuid(), + tool_options.gradient_type, + ) + }; + let selected_gradient = SelectedGradient::new(gradient, &intersection, layer, document, font_cache).with_gradient_start(input.mouse.position); tool_data.selected_gradient = Some(selected_gradient); @@ -487,6 +638,14 @@ impl Fsm for GradientToolFsmState { } (GradientToolFsmState::Drawing, GradientToolMessage::PointerUp) => { + match tool_data.drag_start.distance(input.mouse.position) <= DRAG_THRESHOLD { + true => { + responses.push_back(DocumentMessage::AbortTransaction.into()); + responses.push_back(GradientToolMessage::DocumentIsDirty.into()); + } + false => responses.push_back(DocumentMessage::CommitTransaction.into()), + } + tool_data.snap_manager.cleanup(responses); GradientToolFsmState::Ready