diff --git a/document-legacy/src/document.rs b/document-legacy/src/document.rs index e388bb70..ae970551 100644 --- a/document-legacy/src/document.rs +++ b/document-legacy/src/document.rs @@ -156,6 +156,8 @@ impl Document { pub fn click(&self, viewport_location: DVec2, network: &NodeNetwork) -> Option { self.click_xray(viewport_location).find(|&layer| !is_artboard(layer, network)) } + + /// Get the combined bounding box of the click targets of the selected visible layers in viewport space pub fn selected_visible_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> { self.selected_visible_layers() .filter_map(|layer| self.metadata.bounding_box_viewport(layer)) diff --git a/document-legacy/src/document_metadata.rs b/document-legacy/src/document_metadata.rs index dfe8a4f5..6389ec4b 100644 --- a/document-legacy/src/document_metadata.rs +++ b/document-legacy/src/document_metadata.rs @@ -222,7 +222,16 @@ fn sibling_below<'a>(graph: &'a NodeNetwork, node: &DocumentNode) -> Option<(&'a // transforms impl DocumentMetadata { /// Update the cached transforms of the layers - pub fn update_transforms(&mut self, new_transforms: HashMap, new_upstream_transforms: HashMap) { + pub fn update_transforms(&mut self, mut new_transforms: HashMap, new_upstream_transforms: HashMap) { + let mut stack = vec![(LayerNodeIdentifier::ROOT, DAffine2::IDENTITY)]; + while let Some((layer, transform)) = stack.pop() { + for child in layer.children(self) { + let Some(new_transform) = new_transforms.get_mut(&child) else { continue }; + *new_transform = transform * *new_transform; + + stack.push((child, *new_transform)); + } + } self.transforms = new_transforms; self.upstream_transforms = new_upstream_transforms; } @@ -235,6 +244,10 @@ impl DocumentMetadata { }) } + pub fn local_transform(&self, layer: LayerNodeIdentifier) -> DAffine2 { + self.transform_to_document(layer.parent(self).unwrap_or_default()).inverse() * self.transform_to_document(layer) + } + pub fn transform_to_viewport(&self, layer: LayerNodeIdentifier) -> DAffine2 { self.document_to_viewport * self.transform_to_document(layer) } @@ -299,11 +312,16 @@ impl DocumentMetadata { self.bounding_box_with_transform(layer, self.transform_to_viewport(layer)) } - /// Calculates the document bounds used for scrolling and centring (the layer bounds or the artboard (if applicable)) - pub fn document_bounds(&self) -> Option<[DVec2; 2]> { + /// Calculates the document bounds in viewport space + pub fn document_bounds_viewport_space(&self) -> Option<[DVec2; 2]> { self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds) } + /// Calculates the document bounds in document space + pub fn document_bounds_document_space(&self) -> Option<[DVec2; 2]> { + self.all_layers().filter_map(|layer| self.bounding_box_document(layer)).reduce(Quad::combine_bounds) + } + pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> graphene_core::vector::Subpath { let Some(click_targets) = self.click_targets.get(&layer) else { return graphene_core::vector::Subpath::new(); @@ -546,8 +564,8 @@ impl From for LayerNodeIdentifier { } impl From for NodeId { - fn from(identifer: LayerNodeIdentifier) -> Self { - identifer.to_node() + fn from(identifier: LayerNodeIdentifier) -> Self { + identifier.to_node() } } diff --git a/document-legacy/src/layers/layer_info.rs b/document-legacy/src/layers/layer_info.rs index 615d7c1c..7546800c 100644 --- a/document-legacy/src/layers/layer_info.rs +++ b/document-legacy/src/layers/layer_info.rs @@ -49,6 +49,7 @@ pub enum LayerDataTypeDiscriminant { Folder, Shape, Layer, + Artboard, } impl fmt::Display for LayerDataTypeDiscriminant { @@ -57,6 +58,7 @@ impl fmt::Display for LayerDataTypeDiscriminant { LayerDataTypeDiscriminant::Folder => write!(f, "Folder"), LayerDataTypeDiscriminant::Shape => write!(f, "Shape"), LayerDataTypeDiscriminant::Layer => write!(f, "Layer"), + LayerDataTypeDiscriminant::Artboard => write!(f, "Artboard"), } } } diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 23ecaf76..0cc760c2 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -1,5 +1,6 @@ use crate::consts::{DEFAULT_FONT_FAMILY, DEFAULT_FONT_STYLE}; use crate::messages::debug::utility_types::MessageLoggingVerbosity; +use crate::messages::dialog::DialogData; use crate::messages::prelude::*; use graphene_core::text::Font; @@ -112,11 +113,11 @@ impl Dispatcher { self.message_handlers.debug_message_handler.process_message(message, &mut queue, ()); } Dialog(message) => { - self.message_handlers.dialog_message_handler.process_message( - message, - &mut queue, - (&self.message_handlers.portfolio_message_handler, &self.message_handlers.preferences_message_handler), - ); + let data = DialogData { + portfolio: &self.message_handlers.portfolio_message_handler, + preferences: &self.message_handlers.preferences_message_handler, + }; + self.message_handlers.dialog_message_handler.process_message(message, &mut queue, data); } Frontend(message) => { // Handle these messages immediately by returning early diff --git a/editor/src/messages/dialog/dialog_message_handler.rs b/editor/src/messages/dialog/dialog_message_handler.rs index 5f225f61..07ecf458 100644 --- a/editor/src/messages/dialog/dialog_message_handler.rs +++ b/editor/src/messages/dialog/dialog_message_handler.rs @@ -11,9 +11,14 @@ pub struct DialogMessageHandler { preferences_dialog: PreferencesDialogMessageHandler, } -impl MessageHandler for DialogMessageHandler { +pub struct DialogData<'a> { + pub portfolio: &'a PortfolioMessageHandler, + pub preferences: &'a PreferencesMessageHandler, +} + +impl MessageHandler> for DialogMessageHandler { #[remain::check] - fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque, (portfolio, preferences): (&PortfolioMessageHandler, &PreferencesMessageHandler)) { + fn process_message(&mut self, message: DialogMessage, responses: &mut VecDeque, DialogData { portfolio, preferences }: DialogData) { #[remain::sorted] match message { #[remain::unsorted] diff --git a/editor/src/messages/dialog/mod.rs b/editor/src/messages/dialog/mod.rs index 6e7e6ee1..0607be2d 100644 --- a/editor/src/messages/dialog/mod.rs +++ b/editor/src/messages/dialog/mod.rs @@ -16,4 +16,4 @@ pub mod simple_dialogs; #[doc(inline)] pub use dialog_message::{DialogMessage, DialogMessageDiscriminant}; #[doc(inline)] -pub use dialog_message_handler::DialogMessageHandler; +pub use dialog_message_handler::*; diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 605264a3..062249b9 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -1,9 +1,10 @@ +use crate::consts::VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use graphene_core::uuid::generate_uuid; -use glam::{IVec2, UVec2}; +use glam::{DVec2, IVec2, UVec2}; /// A dialog to allow users to set some initial options about a new document. #[derive(Debug, Clone, Default)] @@ -24,13 +25,18 @@ impl MessageHandler for NewDocumentDialogMessageHa NewDocumentDialogMessage::Submit => { responses.add(PortfolioMessage::NewDocumentWithName { name: self.name.clone() }); - if !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0 { + let create_artboard = !self.infinite && self.dimensions.x > 0 && self.dimensions.y > 0; + if create_artboard { let id = generate_uuid(); responses.add(GraphOperationMessage::NewArtboard { id, artboard: graphene_core::Artboard::new(IVec2::ZERO, self.dimensions.as_ivec2()), }); - responses.add(DocumentMessage::ZoomCanvasToFitAll); + responses.add(NavigationMessage::FitViewportToBounds { + bounds: [DVec2::ZERO, self.dimensions.as_dvec2()], + padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR), + prevent_zoom_past_100: true, + }); } responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::UpdateNewNodeGraph); diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 1673b1b0..c27f63ba 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -158,7 +158,7 @@ impl MessageHandler> for DocumentMessageHand } #[remain::unsorted] Navigation(message) => { - let document_bounds = self.metadata().document_bounds(); + let document_bounds = self.metadata().document_bounds_viewport_space(); self.navigation_handler.process_message( message, responses, @@ -306,7 +306,7 @@ impl MessageHandler> for DocumentMessageHand responses.add(BroadcastEvent::DocumentIsDirty); } DeselectAllLayers => { - responses.add_front(SetSelectedLayers { replacement_selected_layers: vec![] }); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] }); self.layer_range_selection_reference = None; } DirtyRenderDocument => { @@ -623,7 +623,7 @@ impl MessageHandler> for DocumentMessageHand let viewport_size = ipp.viewport_bounds.size(); let viewport_mid = ipp.viewport_bounds.center(); - let [bounds1, bounds2] = self.metadata().document_bounds().unwrap_or([viewport_mid; 2]); + let [bounds1, bounds2] = self.metadata().document_bounds_viewport_space().unwrap_or([viewport_mid; 2]); let bounds1 = bounds1.min(viewport_mid) - viewport_size * scale; let bounds2 = bounds2.max(viewport_mid) + viewport_size * scale; let bounds_length = (bounds2 - bounds1) * (1. + SCROLLBAR_SPACING); @@ -657,20 +657,20 @@ impl MessageHandler> for DocumentMessageHand }) } SelectAllLayers => { - let all = self.all_layers().map(|path| path.to_vec()).collect(); - responses.add_front(SetSelectedLayers { replacement_selected_layers: all }); + let all = self.metadata().all_layers().map(|layer| layer.to_node()).collect(); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: all }); } SelectedLayersLower => { - responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: -1 }); + responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: 1 }); } SelectedLayersLowerToBack => { - responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MIN }); + responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MAX }); } SelectedLayersRaise => { - responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: 1 }); + responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: -1 }); } SelectedLayersRaiseToFront => { - responses.add_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MAX }); + responses.add(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MIN }); } SelectedLayersReorder { relative_index_offset } => { self.selected_layers_reorder(relative_index_offset, responses); @@ -813,7 +813,7 @@ impl MessageHandler> for DocumentMessageHand } SetViewMode { view_mode } => { self.view_mode = view_mode; - responses.add_front(DocumentMessage::DirtyRenderDocument); + responses.add_front(NodeGraphMessage::RunDocumentGraph); } StartTransaction => self.backup(responses), ToggleLayerExpansion { layer } => { @@ -876,7 +876,7 @@ impl MessageHandler> for DocumentMessageHand responses.add_front(NavigationMessage::SetCanvasZoom { zoom_factor: 2. }); } ZoomCanvasToFitAll => { - if let Some(bounds) = self.metadata().document_bounds() { + if let Some(bounds) = self.metadata().document_bounds_document_space() { responses.add(NavigationMessage::FitViewportToBounds { bounds, padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR), diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index d5fd2e30..2372d376 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs @@ -68,12 +68,10 @@ impl MessageHandler, &InputPre responses.add(SetCanvasZoom { zoom_factor: new_scale }); } FitViewportToBounds { - bounds: [bounds_corner_a, bounds_corner_b], + bounds: [pos1, pos2], padding_scale_factor, prevent_zoom_past_100, } => { - let pos1 = document.metadata.document_to_viewport.inverse().transform_point2(bounds_corner_a); - let pos2 = document.metadata.document_to_viewport.inverse().transform_point2(bounds_corner_b); let v1 = document.metadata.document_to_viewport.inverse().transform_point2(DVec2::ZERO); let v2 = document.metadata.document_to_viewport.inverse().transform_point2(ipp.viewport_bounds.size()); @@ -98,8 +96,9 @@ impl MessageHandler, &InputPre } FitViewportToSelection => { if let Some(bounds) = selection_bounds { + let transform = document.metadata.document_to_viewport.inverse(); responses.add(FitViewportToBounds { - bounds, + bounds: [transform.transform_point2(bounds[0]), transform.transform_point2(bounds[1])], padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR), prevent_zoom_past_100: false, }) @@ -382,16 +381,18 @@ impl NavigationMessageHandler { } pub fn calculate_offset_transform(&self, viewport_center: DVec2) -> DAffine2 { + let scaled_centre = viewport_center / self.snapped_scale(); + // Try to avoid fractional coordinates to reduce anti aliasing. let scale = self.snapped_scale(); - let rounded_pan = ((self.pan + viewport_center) * scale).round() / scale - viewport_center; + let rounded_pan = ((self.pan + scaled_centre) * scale).round() / scale - scaled_centre; // TODO: replace with DAffine2::from_scale_angle_translation and fix the errors - let offset_transform = DAffine2::from_translation(viewport_center); + let offset_transform = DAffine2::from_translation(scaled_centre); let scale_transform = DAffine2::from_scale(DVec2::splat(scale)); let angle_transform = DAffine2::from_angle(self.snapped_angle()); let translation_transform = DAffine2::from_translation(rounded_pan); - scale_transform * offset_transform * angle_transform * offset_transform.inverse() * translation_transform + scale_transform * offset_transform * angle_transform * translation_transform } fn create_document_transform(&self, viewport_center: DVec2, responses: &mut VecDeque) { @@ -403,7 +404,7 @@ impl NavigationMessageHandler { let new_viewport_bounds = viewport_bounds / zoom_factor; let delta_size = viewport_bounds - new_viewport_bounds; let mouse_fraction = mouse / viewport_bounds; - let delta = delta_size * (-mouse_fraction); + let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction); NavigationMessage::TranslateCanvas { delta }.into() } diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index ff96c3a4..415f9aa0 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -282,7 +282,7 @@ impl<'a> ModifyInputsContext<'a> { }; let metadata = output_node.metadata.clone(); - let new_input = output_node.inputs[0].clone(); + let new_input = output_node.inputs.first().cloned().filter(|input| input.as_node().is_some()); let node_id = generate_uuid(); output_node.metadata.position.x += 8; @@ -292,7 +292,7 @@ impl<'a> ModifyInputsContext<'a> { warn!("Node type \"{name}\" doesn't exist"); return; }; - let mut new_document_node = node_type.to_document_node_default_inputs([Some(new_input)], metadata); + let mut new_document_node = node_type.to_document_node_default_inputs([new_input], metadata); update_input(&mut new_document_node.inputs, node_id, self.document_metadata); self.network.nodes.insert(node_id, new_document_node); } @@ -575,7 +575,10 @@ impl MessageHandler { - let parent_transform = document.metadata.document_to_viewport * document.multiply_transforms(&layer[..layer.len() - 1]).unwrap_or_default(); + let layer_identifier = LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network); + let parent_transform = document + .metadata + .transform_to_viewport(layer_identifier.parent(&document.metadata).unwrap_or(LayerNodeIdentifier::ROOT)); let bounds = LayerBounds::new(document, &layer); if let Some(mut modify_inputs) = ModifyInputsContext::new_layer(&layer, document, node_graph, responses) { modify_inputs.transform_change(transform, transform_in, parent_transform, bounds, skip_rerender); @@ -597,8 +600,12 @@ impl MessageHandler { - let parent_transform = document.metadata.document_to_viewport * document.multiply_transforms(&layer[..layer.len() - 1]).unwrap_or_default(); - let current_transform = Some(document.metadata.transform_to_viewport(LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network))); + let layer_identifier = LayerNodeIdentifier::new(*layer.last().unwrap(), &document.document_network); + let parent_transform = document + .metadata + .transform_to_viewport(layer_identifier.parent(&document.metadata).unwrap_or(LayerNodeIdentifier::ROOT)); + + let current_transform = Some(document.metadata.transform_to_viewport(layer_identifier)); let bounds = LayerBounds::new(document, &layer); if let Some(mut modify_inputs) = ModifyInputsContext::new_layer(&layer, document, node_graph, responses) { modify_inputs.transform_set(transform, transform_in, parent_transform, current_transform, bounds, skip_rerender); diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index a3e4a39f..14185a58 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -34,7 +34,7 @@ impl OriginalTransforms { match self { OriginalTransforms::Layer(layer_map) => { for &layer in selected { - layer_map.entry(layer).or_insert_with(|| document.metadata.transform_to_document(layer)); + layer_map.entry(layer).or_insert_with(|| document.metadata.local_transform(layer)); } } OriginalTransforms::Path(path_map) => { @@ -349,7 +349,7 @@ impl<'a> Selected<'a> { let xy_summation = self .selected .iter() - .filter_map(|&layer| graph_modification_utils::get_viewport_pivot(layer, self.document)) + .map(|&layer| graph_modification_utils::get_viewport_pivot(layer, self.document)) .reduce(|a, b| a + b) .unwrap_or_default(); @@ -366,6 +366,40 @@ impl<'a> Selected<'a> { (min + max) / 2. } + fn transform_layer(document: &Document, layer: LayerNodeIdentifier, original_transform: Option<&DAffine2>, transformation: DAffine2, responses: &mut VecDeque) { + let Some(&original_transform) = original_transform else { return }; + let parent = layer.parent(&document.metadata); + let to = parent.map(|parent| document.metadata.transform_to_viewport(parent)).unwrap_or(document.metadata.document_to_viewport); + let new = to.inverse() * transformation * to * original_transform; + responses.add(GraphOperationMessage::TransformSet { + layer: layer.to_path(), + transform: new, + transform_in: TransformIn::Local, + skip_rerender: false, + }); + } + + fn transform_path(document: &Document, layer: LayerNodeIdentifier, initial_points: Option<&Vec<(ManipulatorPointId, DVec2)>>, transformation: DAffine2, responses: &mut VecDeque) { + let viewspace = document.metadata.transform_to_viewport(layer); + let layerspace_rotation = viewspace.inverse() * transformation; + + let Some(initial_points) = initial_points else { + return; + }; + + for (point_id, position) in initial_points { + let viewport_point = viewspace.transform_point2(*position); + let new_pos_viewport = layerspace_rotation.transform_point2(viewport_point); + let point = *point_id; + let position = new_pos_viewport; + + responses.add(GraphOperationMessage::Vector { + layer: layer.to_path(), + modification: VectorDataModification::SetManipulatorPosition { point, position }, + }); + } + } + pub fn update_transforms(&mut self, delta: DAffine2) { if !self.selected.is_empty() { let pivot = DAffine2::from_translation(*self.pivot); @@ -374,56 +408,13 @@ impl<'a> Selected<'a> { // TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481 for layer_ancestors in self.document.metadata.shallowest_unique_layers(self.selected.iter().copied()) { let layer = *layer_ancestors.last().unwrap(); - let parent = layer.parent(&self.document.metadata); - if *self.tool_type == ToolType::Select { - let original_layer_transforms = match self.original_transforms { - OriginalTransforms::Layer(layer_map) => *layer_map.get(&layer).unwrap(), - OriginalTransforms::Path(_path_map) => { - warn!("Found Path variant in original_transforms, returning identity transform for layer {layer:?}"); - DAffine2::IDENTITY - } - }; - let to = parent - .map(|parent| self.document.metadata.transform_to_viewport(parent)) - .unwrap_or(self.document.metadata.document_to_viewport); - let new = to.inverse() * transformation * to * original_layer_transforms; - self.responses.add(GraphOperationMessage::TransformSet { - layer: layer.to_path(), - transform: new, - transform_in: TransformIn::Local, - skip_rerender: false, - }); + + match &self.original_transforms { + OriginalTransforms::Layer(layer_transforms) => Self::transform_layer(self.document, layer, layer_transforms.get(&layer), transformation, self.responses), + OriginalTransforms::Path(path_transforms) => Self::transform_path(self.document, layer, path_transforms.get(&layer), transformation, self.responses), } - if *self.tool_type == ToolType::Path { - let viewspace = self.document.metadata.transform_to_viewport(layer); - let layerspace_rotation = viewspace.inverse() * transformation; - - let initial_points = match self.original_transforms { - OriginalTransforms::Layer(_layer_map) => { - warn!("Found Layer variant in original_transforms when Path wanted, returning identity transform for layer"); - None - } - OriginalTransforms::Path(path_map) => path_map.get(&layer), - }; - - let Some(original) = initial_points else { - warn!("Initial Points empty, it should not be possible to reach here without points"); - continue; - }; - for (point_id, position) in original { - let viewport_point = viewspace.transform_point2(*position); - let new_pos_viewport = layerspace_rotation.transform_point2(viewport_point); - let point = *point_id; - let position = new_pos_viewport; - - self.responses.add(GraphOperationMessage::Vector { - layer: layer.to_path(), - modification: VectorDataModification::SetManipulatorPosition { point, position }, - }); - } - } - self.responses.add(BroadcastEvent::DocumentIsDirty); } + self.responses.add(BroadcastEvent::DocumentIsDirty); } } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index ea0ce01e..5f6ea4fb 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -77,14 +77,16 @@ pub fn get_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option Option { +pub fn get_document_pivot(layer: LayerNodeIdentifier, document: &Document) -> DVec2 { let [min, max] = document.metadata.nonzero_bounding_box(layer); - get_pivot(layer, document).map(|pivot| document.metadata.transform_to_document(layer).transform_point2(min + (max - min) * pivot)) + let pivot = get_pivot(layer, document).unwrap_or(DVec2::splat(0.5)); + document.metadata.transform_to_document(layer).transform_point2(min + (max - min) * pivot) } -pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document: &Document) -> Option { +pub fn get_viewport_pivot(layer: LayerNodeIdentifier, document: &Document) -> DVec2 { let [min, max] = document.metadata.nonzero_bounding_box(layer); - get_pivot(layer, document).map(|pivot| document.metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)) + let pivot = get_pivot(layer, document).unwrap_or(DVec2::splat(0.5)); + document.metadata.transform_to_viewport(layer).transform_point2(min + (max - min) * pivot) } /// Get the currently mirrored handles for a particular layer from the shape node diff --git a/editor/src/messages/tool/common_functionality/pivot.rs b/editor/src/messages/tool/common_functionality/pivot.rs index 4e3a36d8..95c19d64 100644 --- a/editor/src/messages/tool/common_functionality/pivot.rs +++ b/editor/src/messages/tool/common_functionality/pivot.rs @@ -65,17 +65,16 @@ impl Pivot { // If just one layer is selected we can use its inner transform (as it accounts for rotation) if selected_layers_count == 1 { - if let Some(normalized_pivot) = graph_modification_utils::get_pivot(first, &document.document_legacy) { - self.normalized_pivot = normalized_pivot; - self.transform_from_normalized = Self::get_layer_pivot_transform(first, document); - self.pivot = Some(self.transform_from_normalized.transform_point2(normalized_pivot)); - } + let normalized_pivot = graph_modification_utils::get_pivot(first, &document.document_legacy).unwrap_or(DVec2::splat(0.5)); + self.normalized_pivot = normalized_pivot; + self.transform_from_normalized = Self::get_layer_pivot_transform(first, document); + self.pivot = Some(self.transform_from_normalized.transform_point2(normalized_pivot)); } else { // If more than one layer is selected we use the AABB with the mean of the pivots let xy_summation = document .document_legacy .selected_visible_layers() - .filter_map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.document_legacy)) + .map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.document_legacy)) .reduce(|a, b| a + b) .unwrap_or_default(); diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 354952d5..f728637f 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -1,13 +1,13 @@ use super::tool_prelude::*; -use crate::messages::portfolio::document::node_graph::transform_utils::get_current_transform; +use crate::messages::portfolio::document::node_graph::resolve_document_node_type; +use crate::messages::portfolio::document::node_graph::transform_utils::{get_current_normalized_pivot, get_current_transform}; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils; -use document_legacy::layers::layer_layer::CachedOutputData; -use document_legacy::LayerId; +use document_legacy::document_metadata::LayerNodeIdentifier; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{NodeInput, NodeNetwork}; -use graphene_core::raster::{BlendMode, ImageFrame}; +use graph_craft::document::{DocumentNodeMetadata, NodeInput}; +use graphene_core::raster::BlendMode; +use graphene_core::uuid::generate_uuid; use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle}; use graphene_core::Color; @@ -289,20 +289,23 @@ impl ToolTransition for BrushTool { #[derive(Clone, Debug, Default)] struct BrushToolData { strokes: Vec, - layer_path: Vec, + layer: Option, transform: DAffine2, } impl BrushToolData { - fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<&Vec> { + fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option { self.transform = DAffine2::IDENTITY; - if document.selected_layers().count() != 1 { + + if document.metadata().selected_layers().count() != 1 { return None; } - self.layer_path = document.selected_layers().next()?.to_vec(); - let layer = document.document_legacy.layer(&self.layer_path).ok().and_then(|layer| layer.as_layer().ok())?; - let network = &layer.network; - for (node, _node_id) in network.primary_flow() { + let Some(layer) = document.metadata().selected_layers().next() else { + return None; + }; + + self.layer = Some(layer); + for (node, node_id) in document.network().primary_flow_from_node(Some(layer.to_node())) { if node.name == "Brush" { let points_input = node.inputs.get(2)?; let NodeInput::Value { @@ -314,21 +317,22 @@ impl BrushToolData { }; self.strokes = strokes.clone(); - return Some(&self.layer_path); + return Some(layer); } else if node.name == "Transform" { - self.transform = get_current_transform(&node.inputs) * self.transform; + let upstream = document.metadata().upstream_transform(node_id); + let pivot = DAffine2::from_translation(upstream.transform_point2(get_current_normalized_pivot(&node.inputs))); + self.transform = pivot * get_current_transform(&node.inputs) * pivot.inverse() * self.transform; } } self.transform = DAffine2::IDENTITY; - - matches!(layer.cached_output_data, CachedOutputData::BlobURL(_) | CachedOutputData::SurfaceId(_)).then_some(&self.layer_path) + None } fn update_strokes(&self, responses: &mut VecDeque) { - let layer = self.layer_path.clone(); + let Some(layer) = self.layer else { return }; let strokes = self.strokes.clone(); - responses.add(GraphOperationMessage::Brush { layer, strokes }); + responses.add(GraphOperationMessage::Brush { layer: layer.to_path(), strokes }); } } @@ -341,27 +345,27 @@ impl Fsm for BrushToolFsmState { document, global_tool_data, input, .. } = tool_action_data; - let document_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); - let layer_position = tool_data.transform.inverse().transform_point2(document_position); - let ToolMessage::Brush(event) = event else { return self; }; match (self, event) { (BrushToolFsmState::Ready, BrushToolMessage::DragStart) => { responses.add(DocumentMessage::StartTransaction); - let layer_path = tool_data.load_existing_strokes(document); - let new_layer = layer_path.is_none(); - if new_layer { - responses.add(DocumentMessage::DeselectAllLayers); - tool_data.layer_path = document.get_path_for_new_layer(); - } - let layer_position = tool_data.transform.inverse().transform_point2(document_position); + let loaded_layer = tool_data.load_existing_strokes(document); + + let layer = loaded_layer.unwrap_or_else(|| new_brush_layer(document, responses)); + tool_data.layer = Some(layer); + + let parent = layer.parent(document.metadata()).unwrap_or_default(); + let parent_transform = document.metadata().transform_to_viewport(parent).inverse().transform_point2(input.mouse.position); + let layer_position = tool_data.transform.inverse().transform_point2(parent_transform); + + let layer_document_scale = document.metadata().transform_to_document(parent) * tool_data.transform; // TODO: Also scale it based on the input image ('Background' parameter). // TODO: Resizing the input image results in a different brush size from the chosen diameter. let layer_scale = 0.0001_f64 // Safety against division by zero - .max((tool_data.transform.matrix2 * glam::DVec2::X).length()) - .max((tool_data.transform.matrix2 * glam::DVec2::Y).length()); + .max((layer_document_scale.matrix2 * glam::DVec2::X).length()) + .max((layer_document_scale.matrix2 * glam::DVec2::Y).length()); // Start a new stroke with a single sample let blend_mode = match tool_options.draw_mode { @@ -381,17 +385,20 @@ impl Fsm for BrushToolFsmState { }, }); - if new_layer { - add_brush_render(tool_options, tool_data, responses); - } tool_data.update_strokes(responses); BrushToolFsmState::Drawing } (BrushToolFsmState::Drawing, BrushToolMessage::PointerMove) => { - if let Some(stroke) = tool_data.strokes.last_mut() { - stroke.trace.push(BrushInputSample { position: layer_position }) + if let Some(layer) = tool_data.layer { + if let Some(stroke) = tool_data.strokes.last_mut() { + let parent = layer.parent(document.metadata()).unwrap_or_default(); + let parent_position = document.metadata().transform_to_viewport(parent).inverse().transform_point2(input.mouse.position); + let layer_position = tool_data.transform.inverse().transform_point2(parent_position); + + stroke.trace.push(BrushInputSample { position: layer_position }) + } } tool_data.update_strokes(responses); @@ -438,11 +445,24 @@ impl Fsm for BrushToolFsmState { } } -fn add_brush_render(_tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque) { - let mut network = NodeNetwork::default(); - let output_node = network.push_output_node(); - if let Some(node) = network.nodes.get_mut(&output_node) { - node.inputs.push(NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true)) - } - graph_modification_utils::new_custom_layer(network, data.layer_path.clone(), responses); +fn new_brush_layer(document: &DocumentMessageHandler, responses: &mut VecDeque) -> LayerNodeIdentifier { + responses.add(DocumentMessage::DeselectAllLayers); + + let brush_node = resolve_document_node_type("Brush") + .expect("Brush node does not exist") + .to_document_node_default_inputs([], DocumentNodeMetadata::position((-8, 0))); + let cull_node = resolve_document_node_type("Cull") + .expect("Cull node does not exist") + .to_document_node_default_inputs([Some(NodeInput::node(1, 0))], DocumentNodeMetadata::default()); + + let id = generate_uuid(); + responses.add(GraphOperationMessage::NewCustomLayer { + id, + nodes: HashMap::from([(1, brush_node), (0, cull_node)]), + parent: document.new_layer_parent(), + insert_index: -1, + }); + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![id] }); + + LayerNodeIdentifier::new_unchecked(id) } diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 2f45deca..71e4fd85 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -2,6 +2,7 @@ use crate::consts::SLOWING_DIVISOR; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, Typing}; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::utility_types::{ToolData, ToolType}; @@ -23,10 +24,12 @@ pub struct TransformLayerMessageHandler { original_transforms: OriginalTransforms, pivot: DVec2, } + impl TransformLayerMessageHandler { pub fn is_transforming(&self) -> bool { self.transform_operation != TransformOperation::None } + pub fn hints(&self, responses: &mut VecDeque) { let axis_constraint = match self.transform_operation { TransformOperation::Grabbing(grabbing) => grabbing.constraint, @@ -40,18 +43,17 @@ impl TransformLayerMessageHandler { type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessageHandler, &'a ToolData, &'a mut ShapeState); impl<'a> MessageHandler> for TransformLayerMessageHandler { #[remain::check] - fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque, (document, ipp, tool_data, shape_editor): TransformData) { + fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque, (document, input, tool_data, shape_editor): TransformData) { use TransformLayerMessage::*; let using_path_tool = tool_data.active_tool_type == ToolType::Path; - let selected_layers = document.layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then_some(layer_path)).collect::>(); - let selected_layers_n = document.metadata().selected_layers().collect::>(); + let selected_layers = document.metadata().selected_layers().collect::>(); let mut selected = Selected::new( &mut self.original_transforms, &mut self.pivot, - &selected_layers_n, + &selected_layers, responses, &document.document_legacy, Some(shape_editor), @@ -65,33 +67,26 @@ impl<'a> MessageHandler> for TransformL } if using_path_tool { - if let Ok(layer) = document.document_legacy.layer(selected_layers[0]) { - if let Some(vector_data) = layer.as_vector_data() { - *selected.original_transforms = OriginalTransforms::default(); - let viewspace = &mut document.document_legacy.generate_transform_relative_to_viewport(selected_layers[0]).ok().unwrap_or_default(); + if let Some(subpaths) = selected_layers.first().and_then(|&layer| graph_modification_utils::get_subpaths(layer, &document.document_legacy)) { + *selected.original_transforms = OriginalTransforms::default(); + let viewspace = document.metadata().transform_to_viewport(selected_layers[0]); - let mut point_count: usize = 0; - let count_point = |position| { - point_count += 1; - position - }; - let get_location = |point: &ManipulatorPointId| { - vector_data - .manipulator_from_id(point.group) - .and_then(|manipulator_group| point.manipulator_type.get_position(manipulator_group)) - .map(|position| viewspace.transform_point2(position)) - }; - let points = shape_editor.selected_points(); + let mut point_count: usize = 0; + let get_location = |point: &ManipulatorPointId| { + graph_modification_utils::get_manipulator_from_id(subpaths, point.group) + .and_then(|manipulator_group| point.manipulator_type.get_position(manipulator_group)) + .map(|position| viewspace.transform_point2(position)) + }; + let points = shape_editor.selected_points(); - *selected.pivot = points.filter_map(get_location).map(count_point).sum::() / point_count as f64; - } + *selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::() / point_count as f64; } } else { *selected.pivot = selected.mean_average_of_pivots(); } - *mouse_position = ipp.mouse.position; - *start_mouse = ipp.mouse.position; + *mouse_position = input.mouse.position; + *start_mouse = input.mouse.position; selected.original_transforms.clear(); }; @@ -106,9 +101,7 @@ impl<'a> MessageHandler> for TransformL responses.add(ToolMessage::UpdateHints); responses.add(BroadcastEvent::DocumentIsDirty); - for layer_path in document.selected_layers() { - responses.add(DocumentMessage::InputFrameRasterizeRegionBelowLayer { layer_path: layer_path.to_vec() }); - } + responses.add(NodeGraphMessage::RunDocumentGraph); } BeginGrab => { if let TransformOperation::Grabbing(_) = self.transform_operation { @@ -175,9 +168,9 @@ impl<'a> MessageHandler> for TransformL ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap), ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap), PointerMove { slow_key, snap_key } => { - self.slow = ipp.keyboard.get(slow_key as usize); + self.slow = input.keyboard.get(slow_key as usize); - let new_snap = ipp.keyboard.get(snap_key as usize); + let new_snap = input.keyboard.get(snap_key as usize); if new_snap != self.snap { self.snap = new_snap; let axis_constraint = match self.transform_operation { @@ -189,7 +182,7 @@ impl<'a> MessageHandler> for TransformL } if self.typing.digits.is_empty() { - let delta_pos = ipp.mouse.position - self.mouse_position; + let delta_pos = input.mouse.position - self.mouse_position; match self.transform_operation { TransformOperation::None => unreachable!(), @@ -201,7 +194,7 @@ impl<'a> MessageHandler> for TransformL } TransformOperation::Rotating(rotation) => { let start_offset = *selected.pivot - self.mouse_position; - let end_offset = *selected.pivot - ipp.mouse.position; + let end_offset = *selected.pivot - input.mouse.position; let angle = start_offset.angle_between(end_offset); let change = if self.slow { angle / SLOWING_DIVISOR } else { angle }; @@ -212,7 +205,7 @@ impl<'a> MessageHandler> for TransformL TransformOperation::Scaling(scale) => { let change = { let previous_frame_dist = (self.mouse_position - *selected.pivot).length(); - let current_frame_dist = (ipp.mouse.position - *selected.pivot).length(); + let current_frame_dist = (input.mouse.position - *selected.pivot).length(); let start_transform_dist = (self.start_mouse - *selected.pivot).length(); (current_frame_dist - previous_frame_dist) / start_transform_dist @@ -225,7 +218,7 @@ impl<'a> MessageHandler> for TransformL } }; } - self.mouse_position = ipp.mouse.position; + self.mouse_position = input.mouse.position; } SelectionChanged => { let target_layers = document.metadata().selected_layers().collect(); diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index c81c24d5..e807fb9e 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -14,11 +14,12 @@ use graph_craft::graphene_compiler::Compiler; use graph_craft::imaginate_input::ImaginatePreferences; use graph_craft::{concrete, Type}; use graphene_core::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; -use graphene_core::raster::Image; +use graphene_core::raster::{Image, ImageFrame}; use graphene_core::renderer::{ClickTarget, GraphicElementRendered, SvgSegment, SvgSegmentList}; use graphene_core::text::FontCache; use graphene_core::transform::{Footprint, Transform}; use graphene_core::vector::style::ViewMode; +use graphene_core::vector::VectorData; use graphene_core::{Color, SurfaceFrame, SurfaceId}; use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi}; @@ -71,8 +72,7 @@ pub(crate) struct GenerationRequest { generation_id: u64, graph: NodeNetwork, path: Vec, - transform: DAffine2, - viewport_resolution: UVec2, + render_config: RenderConfig, } pub(crate) struct GenerationResponse { @@ -146,12 +146,12 @@ impl NodeRuntime { NodeRuntimeMessage::GenerationRequest(GenerationRequest { generation_id, graph, - transform, + render_config, path, - viewport_resolution, .. }) => { - let (result, monitor_nodes) = self.execute_network(&path, graph, transform, viewport_resolution).await; + let transform = render_config.viewport.transform; + let (result, monitor_nodes) = self.execute_network(&path, graph, render_config).await; let mut responses = VecDeque::new(); if let Some(ref monitor_nodes) = monitor_nodes { self.update_thumbnails(&path, monitor_nodes, &mut responses); @@ -173,7 +173,7 @@ impl NodeRuntime { } } - async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, transform: DAffine2, viewport_resolution: UVec2) -> (Result, Option) { + async fn execute_network<'a>(&'a mut self, path: &[LayerId], graph: NodeNetwork, render_config: RenderConfig) -> (Result, Option) { if self.wasm_io.is_none() { self.wasm_io = Some(WasmApplicationIo::new().await); } @@ -182,17 +182,7 @@ impl NodeRuntime { application_io: self.wasm_io.as_ref().unwrap(), node_graph_message_sender: &self.sender, imaginate_preferences: &self.imaginate_preferences, - render_config: RenderConfig { - viewport: Footprint { - transform, - resolution: viewport_resolution, - ..Default::default() - }, - #[cfg(any(feature = "resvg", feature = "vello"))] - export_format: graphene_core::application_io::ExportFormat::Canvas, - #[cfg(not(any(feature = "resvg", feature = "vello")))] - export_format: graphene_core::application_io::ExportFormat::Svg, - }, + render_config, image_frame: None, }; @@ -325,11 +315,19 @@ impl NodeRuntime { warn!("Failed to introspect monitor node for upstream transforms"); continue; }; - let Some(graphic_element_data) = value.downcast_ref::() else { - warn!("Failed to downcast transform input to vector data"); + let Some(transform) = value + .downcast_ref::>() + .map(|vector_data| vector_data.output.transform()) + .or_else(|| { + value + .downcast_ref::>>() + .map(|image| image.output.transform()) + }) + else { + warn!("Failed to downcast transform input"); continue; }; - self.upstream_transforms.insert(node_id, graphic_element_data.transform()); + self.upstream_transforms.insert(node_id, transform); } } } @@ -399,14 +397,13 @@ impl Default for NodeGraphExecutor { impl NodeGraphExecutor { /// Execute the network by flattening it and creating a borrow stack. - fn queue_execution(&self, network: NodeNetwork, layer_path: Vec, transform: DAffine2, viewport_resolution: UVec2) -> u64 { + fn queue_execution(&self, network: NodeNetwork, layer_path: Vec, render_config: RenderConfig) -> u64 { let generation_id = generate_uuid(); let request = GenerationRequest { path: layer_path, graph: network, generation_id, - transform, - viewport_resolution, + render_config, }; self.sender.send(NodeRuntimeMessage::GenerationRequest(request)).expect("Failed to send generation request"); @@ -499,11 +496,21 @@ impl NodeGraphExecutor { layer_layer.network.clone() }; - // Construct the input image frame - let document_transform = document.document_legacy.metadata.document_to_viewport; + let render_config = RenderConfig { + viewport: Footprint { + transform: document.document_legacy.metadata.document_to_viewport, + resolution: viewport_resolution, + ..Default::default() + }, + #[cfg(any(feature = "resvg", feature = "vello"))] + export_format: graphene_core::application_io::ExportFormat::Canvas, + #[cfg(not(any(feature = "resvg", feature = "vello")))] + export_format: graphene_core::application_io::ExportFormat::Svg, + view_mode: document.view_mode, + }; // Execute the node graph - let generation_id = self.queue_execution(network, layer_path.clone(), document_transform, viewport_resolution); + let generation_id = self.queue_execution(network, layer_path.clone(), render_config); self.futures.insert(generation_id, ExecutionContext { layer_path }); @@ -542,7 +549,9 @@ impl NodeGraphExecutor { .to_string(), tooltip: format!("Layer id: {node_id}"), visible: !document.document_network.disabled.contains(&layer.to_node()), - layer_type: if document.metadata.is_folder(layer) { + layer_type: if document.metadata.is_artboard(layer) { + LayerDataTypeDiscriminant::Artboard + } else if document.metadata.is_folder(layer) { LayerDataTypeDiscriminant::Folder } else { LayerDataTypeDiscriminant::Layer diff --git a/frontend/assets/icon-16px-solid/node-artboard.svg b/frontend/assets/icon-16px-solid/artboard.svg similarity index 100% rename from frontend/assets/icon-16px-solid/node-artboard.svg rename to frontend/assets/icon-16px-solid/artboard.svg diff --git a/frontend/src/components/panels/LayerTree.svelte b/frontend/src/components/panels/LayerTree.svelte index e26f0fbd..f9bcebcf 100644 --- a/frontend/src/components/panels/LayerTree.svelte +++ b/frontend/src/components/panels/LayerTree.svelte @@ -4,14 +4,8 @@ import { beginDraggingElement } from "@graphite/io-managers/drag"; import { platformIsMac } from "@graphite/utility-functions/platform"; import type { Editor } from "@graphite/wasm-communication/editor"; - import { - type LayerPanelEntry, - defaultWidgetLayout, - patchWidgetLayout, - UpdateDocumentLayerDetails, - UpdateDocumentLayerTreeStructureJs, - UpdateLayerTreeOptionsLayout, - } from "@graphite/wasm-communication/messages"; + import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructureJs, UpdateLayerTreeOptionsLayout } from "@graphite/wasm-communication/messages"; + import type { LayerType, LayerPanelEntry } from "@graphite/wasm-communication/messages"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; @@ -152,6 +146,10 @@ editor.instance.deselectAllLayers(); } + function isGroupOrArtboard(layerType: LayerType) { + return layerType === "Folder" || layerType === "Artboard"; + } + function calculateDragIndex(tree: LayoutCol, clientY: number, select?: () => void): DraggingData { const treeChildren = tree.div()?.children; const treeOffset = tree.div()?.getBoundingClientRect().top; @@ -195,9 +193,9 @@ } // Inserting below current row else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) { - insertFolder = layer.layerType === "Folder" ? layer.path : layer.path.slice(0, layer.path.length - 1); - insertIndex = layer.layerType === "Folder" ? 0 : folderIndex + 1; - highlightFolder = layer.layerType === "Folder"; + insertFolder = isGroupOrArtboard(layer.layerType) ? layer.path : layer.path.slice(0, layer.path.length - 1); + insertIndex = isGroupOrArtboard(layer.layerType) ? 0 : folderIndex + 1; + highlightFolder = isGroupOrArtboard("Folder"); closest = -distance; markerHeight = index === treeChildren.length - 1 ? rect.bottom - INSERT_MARK_OFFSET : rect.bottom; } @@ -338,7 +336,7 @@
- {#if listing.entry.layerType === "Folder"} + {#if isGroupOrArtboard(listing.entry.layerType)}