From 8e548f14b4130dfc4dbe6ae644515403b0b692ee Mon Sep 17 00:00:00 2001 From: 0hypercube <0hypercube@gmail.com> Date: Sat, 8 Jan 2022 18:45:08 +0000 Subject: [PATCH 1/8] migrate to using MoveSelectedLayersTo --- editor/src/document/document_file.rs | 42 -------- frontend/src/components/panels/LayerTree.vue | 101 ++++++++++--------- frontend/wasm/src/api.rs | 5 +- 3 files changed, 55 insertions(+), 93 deletions(-) diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index 429852e9e6..faf3e9454e 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -176,11 +176,6 @@ pub enum DocumentMessage { insert_index: isize, }, ReorderSelectedLayers(i32), // relative_position, - MoveLayerInTree { - layer: Vec, - insert_above: bool, - neighbor: Vec, - }, SetSnapping(bool), } @@ -1024,42 +1019,6 @@ impl MessageHandler for DocumentMessageHand } } RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()), - MoveLayerInTree { - layer: target_layer, - insert_above, - neighbor, - } => { - let neighbor_id = neighbor.last().expect("Tried to move next to root"); - let neighbor_path = &neighbor[..neighbor.len() - 1]; - - if !neighbor.starts_with(&target_layer) { - let containing_folder = self.graphene_document.folder(neighbor_path).expect("Neighbor does not exist"); - let neighbor_index = containing_folder.position_of_layer(*neighbor_id).expect("Neighbor layer does not exist"); - - let layer = self.graphene_document.layer(&target_layer).expect("Layer moving does not exist.").to_owned(); - let destination_path = [neighbor_path.to_vec(), vec![generate_uuid()]].concat(); - let insert_index = if insert_above { neighbor_index } else { neighbor_index + 1 } as isize; - - responses.push_back(DocumentMessage::StartTransaction.into()); - responses.push_back( - DocumentOperation::InsertLayer { - layer, - destination_path: destination_path.clone(), - insert_index, - } - .into(), - ); - responses.push_back( - DocumentMessage::UpdateLayerMetadata { - layer_path: destination_path, - layer_metadata: *self.layer_metadata(&target_layer), - } - .into(), - ); - responses.push_back(DocumentOperation::DeleteLayer { path: target_layer }.into()); - responses.push_back(DocumentMessage::CommitTransaction.into()); - } - } SetSnapping(new_status) => { self.snapping_enabled = new_status; } @@ -1077,7 +1036,6 @@ impl MessageHandler for DocumentMessageHand SaveDocument, SetSnapping, DebugPrintDocument, - MoveLayerInTree, ); if self.layer_metadata.values().any(|data| data.selected) { diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index c9aa7ca1a1..741fef5733 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -29,8 +29,8 @@ - -
+ +
, - layers: [] as LayerPanelEntry[], + layers: [] as { folderIndex: number; entry: LayerPanelEntry }[], layerDepths: [] as number[], selectionRangeStartLayer: undefined as undefined | LayerPanelEntry, selectionRangeEndLayer: undefined as undefined | LayerPanelEntry, opacity: 100, - draggingData: undefined as undefined | { path: BigUint64Array; above: boolean; nearestPath: BigUint64Array; insertLine: HTMLDivElement }, + draggingData: undefined as undefined | { insertFolder: BigUint64Array; insertIndex: number; insertLine: HTMLDivElement }, }; }, methods: { @@ -343,32 +343,32 @@ export default defineComponent({ }, async clearSelection() { this.layers.forEach((layer) => { - layer.layer_metadata.selected = false; + layer.entry.layer_metadata.selected = false; }); }, - closest(tree: HTMLElement, clientY: number): [BigUint64Array, boolean, Node] { + closest(tree: HTMLElement, clientY: number): { insertFolder: BigUint64Array; insertIndex: number; insertAboveNode: Node } { const treeChildren = tree.children; // Closest distance to the middle of the row along the Y axis let closest = Infinity; // The nearest row parent (element of the tree) - let nearestElement = tree.lastChild as Node; + let insertAboveNode = tree.lastChild as Node; - // The nearest element in the path to the mouse - let nearestPath = new BigUint64Array(); + // Folder to insert into + let insertFolder = new BigUint64Array(); - // Item goes above or below the mouse - let above = false; + // Insert index + let insertIndex = -1; Array.from(treeChildren).forEach((treeChild) => { - if (treeChild.childElementCount <= 2) return; - - const child = treeChild.children[2] as HTMLElement; + const layerComponents = treeChild.getElementsByClassName("layer"); + if (layerComponents.length !== 1) return; + const child = layerComponents[0]; const indexAttribute = child.getAttribute("data-index"); if (!indexAttribute) return; - const layer = this.layers[parseInt(indexAttribute, 10)]; + const { folderIndex, entry: layer } = this.layers[parseInt(indexAttribute, 10)]; const rect = child.getBoundingClientRect(); const position = rect.top + rect.height / 2; @@ -376,30 +376,31 @@ export default defineComponent({ // Inserting above current row if (distance > 0 && distance < closest) { + insertAboveNode = treeChild; + insertFolder = layer.path.slice(0, layer.path.length - 1); + insertIndex = folderIndex; closest = distance; - nearestPath = layer.path; - above = true; - if (child.parentNode) { - nearestElement = child.parentNode; - } } // Inserting below current row - else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0 && layer.layer_type !== "Folder") { - closest = -distance; - nearestPath = layer.path; + else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0) { if (child.parentNode && child.parentNode.nextSibling) { - nearestElement = child.parentNode.nextSibling; + insertAboveNode = child.parentNode.nextSibling; } + insertFolder = layer.layer_type === "Folder" ? layer.path : layer.path.slice(0, layer.path.length - 1); + insertIndex = layer.layer_type === "Folder" ? 0 : folderIndex + 1; + closest = -distance; } // Inserting with no nesting at the end of the panel else if (closest === Infinity) { - nearestPath = layer.path.slice(0, 1); + insertIndex = folderIndex; } }); - return [nearestPath, above, nearestElement]; + return { insertFolder, insertIndex, insertAboveNode }; }, async dragStart(event: DragEvent, layer: LayerPanelEntry) { + this.selectLayer(layer, event.ctrlKey, event.shiftKey); + // Set style of cursor for drag if (event.dataTransfer) { event.dataTransfer.dropEffect = "move"; @@ -413,31 +414,30 @@ export default defineComponent({ insertLine.classList.add("insert-mark"); tree.appendChild(insertLine); - const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY); + const { insertFolder, insertIndex, insertAboveNode } = this.closest(tree, event.clientY); // Set the initial state of the insert line - if (nearestElement.parentNode) { - insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`; // TODO: use layerIndent function to calculate this - tree.insertBefore(insertLine, nearestElement); + if (insertAboveNode.parentNode) { + insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * (insertFolder.length + 1)}px`; // TODO: use layerIndent function to calculate this + tree.insertBefore(insertLine, insertAboveNode); } - this.draggingData = { path: layer.path, above, nearestPath, insertLine }; + this.draggingData = { insertFolder, insertIndex, insertLine }; }, updateInsertLine(event: DragEvent) { // Stop the drag from being shown as cancelled event.preventDefault(); const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement; - - const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY); + const { insertFolder, insertIndex, insertAboveNode } = this.closest(tree, event.clientY); if (this.draggingData) { - this.draggingData.nearestPath = nearestPath; - this.draggingData.above = above; + this.draggingData.insertFolder = insertFolder; + this.draggingData.insertIndex = insertIndex; - if (nearestElement.parentNode) { - this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`; - tree.insertBefore(this.draggingData.insertLine, nearestElement); + if (insertAboveNode.parentNode) { + this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * (insertFolder.length + 1)}px`; + tree.insertBefore(this.draggingData.insertLine, insertAboveNode); } } }, @@ -448,12 +448,15 @@ export default defineComponent({ }, async drop() { this.removeLine(); + if (this.draggingData) { - this.editor.instance.move_layer_in_tree(this.draggingData.path, this.draggingData.above, this.draggingData.nearestPath); + const { insertFolder, insertIndex } = this.draggingData; + + this.editor.instance.move_layer_in_tree(insertFolder, insertIndex); } }, setBlendModeForSelectedLayers() { - const selected = this.layers.filter((layer) => layer.layer_metadata.selected); + const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected); if (selected.length < 1) { this.blendModeSelectedIndex = 0; @@ -462,8 +465,8 @@ export default defineComponent({ } this.blendModeDropdownDisabled = false; - const firstEncounteredBlendMode = selected[0].blend_mode; - const allBlendModesAlike = !selected.find((layer) => layer.blend_mode !== firstEncounteredBlendMode); + const firstEncounteredBlendMode = selected[0].entry.blend_mode; + const allBlendModesAlike = !selected.find((layer) => layer.entry.blend_mode !== firstEncounteredBlendMode); if (allBlendModesAlike) { this.blendModeSelectedIndex = this.blendModeEntries.flat().findIndex((entry) => entry.value === firstEncounteredBlendMode); @@ -474,7 +477,7 @@ export default defineComponent({ }, setOpacityForSelectedLayers() { // todo figure out why this is here - const selected = this.layers.filter((layer) => layer.layer_metadata.selected); + const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected); if (selected.length < 1) { this.opacity = 100; @@ -483,8 +486,8 @@ export default defineComponent({ } this.opacityNumberInputDisabled = false; - const firstEncounteredOpacity = selected[0].opacity; - const allOpacitiesAlike = !selected.find((layer) => layer.opacity !== firstEncounteredOpacity); + const firstEncounteredOpacity = selected[0].entry.opacity; + const allOpacitiesAlike = !selected.find((layer) => layer.entry.opacity !== firstEncounteredOpacity); if (allOpacitiesAlike) { this.opacity = firstEncounteredOpacity; @@ -497,14 +500,14 @@ export default defineComponent({ mounted() { this.editor.dispatcher.subscribeJsMessage(DisplayFolderTreeStructure, (displayFolderTreeStructure) => { const path = [] as bigint[]; - this.layers = [] as LayerPanelEntry[]; + this.layers = [] as { folderIndex: number; entry: LayerPanelEntry }[]; - const recurse = (folder: DisplayFolderTreeStructure, layers: LayerPanelEntry[], cache: Map): void => { - folder.children.forEach((item) => { + const recurse = (folder: DisplayFolderTreeStructure, layers: { folderIndex: number; entry: LayerPanelEntry }[], cache: Map): void => { + folder.children.forEach((item, index) => { // TODO: fix toString path.push(BigInt(item.layerId.toString())); const mapping = cache.get(path.toString()); - if (mapping) layers.push(mapping); + if (mapping) layers.push({ folderIndex: index, entry: mapping }); if (item.children.length >= 1) recurse(item, layers, cache); path.pop(); }); diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index bdfbbed484..04c455e599 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -383,8 +383,9 @@ impl JsEditorHandle { } /// Move a layer to be next to the specified neighbor - pub fn move_layer_in_tree(&self, layer: Vec, insert_above: bool, neighbor: Vec) { - let message = DocumentMessage::MoveLayerInTree { layer, insert_above, neighbor }; + pub fn move_layer_in_tree(&self, path: Vec, insert_index: isize) { + log::info!("Move layers in tree, Path {:?}, insert index {}", &path, insert_index); + let message = DocumentMessage::MoveSelectedLayersTo { path, insert_index }; self.dispatch(message); } From 90d71bb0783a445564ab9f60b298d975bfe6a21f Mon Sep 17 00:00:00 2001 From: 0hypercube <0hypercube@gmail.com> Date: Mon, 10 Jan 2022 18:56:41 +0000 Subject: [PATCH 2/8] Fix dragging a selected layer with multiple selected layers --- frontend/src/components/panels/LayerTree.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index 741fef5733..9ed57b25bd 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -399,7 +399,7 @@ export default defineComponent({ return { insertFolder, insertIndex, insertAboveNode }; }, async dragStart(event: DragEvent, layer: LayerPanelEntry) { - this.selectLayer(layer, event.ctrlKey, event.shiftKey); + if (!layer.layer_metadata.selected) this.selectLayer(layer, event.ctrlKey, event.shiftKey); // Set style of cursor for drag if (event.dataTransfer) { From d4c24d38415b73249473cfdc742abc47e6b212e4 Mon Sep 17 00:00:00 2001 From: 0hypercube <0hypercube@gmail.com> Date: Mon, 10 Jan 2022 20:35:03 +0000 Subject: [PATCH 3/8] Fix CreatedLayer overriding selection --- editor/src/document/document_file.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index faf3e9454e..6c65ee1311 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -832,10 +832,12 @@ impl MessageHandler for DocumentMessageHand } DocumentResponse::LayerChanged { path } => responses.push_back(LayerChanged(path.clone()).into()), DocumentResponse::CreatedLayer { path } => { - self.layer_metadata.insert(path.clone(), LayerMetadata::new(false)); + if !self.layer_metadata.contains_key(path) { + self.layer_metadata.insert(path.clone(), LayerMetadata::new(false)); + } responses.push_back(LayerChanged(path.clone()).into()); self.layer_range_selection_reference = path.clone(); - responses.push_back(SetSelectedLayers(vec![path.clone()]).into()); + responses.push_back(AddSelectedLayers(vec![path.clone()]).into()); } DocumentResponse::DocumentChanged => responses.push_back(RenderDocument.into()), }; From 4cc1ca8aed4d502f35bbf604d2686c2d988365e9 Mon Sep 17 00:00:00 2001 From: 0hypercube <0hypercube@gmail.com> Date: Tue, 11 Jan 2022 16:49:45 +0000 Subject: [PATCH 4/8] Fix MoveSelectedLayersTo behaviour --- editor/src/document/document_file.rs | 31 ++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index 6c65ee1311..dc8d203551 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -535,6 +535,16 @@ impl DocumentMessageHandler { Some(layer_panel_entry(layer_metadata, transform, layer, path.to_vec())) } + + /// When working with an insert index, deleting the layers may cause the insert index to point to a different location (if the layer being deleted was located before the insert index). + /// + /// This function updates the insert index so that it points to the same place after the specified `layers` are deleted. + fn update_insert_index<'a>(&self, layers: &[&'a [LayerId]], path: &[LayerId], insert_index: isize) -> Result { + let folder = self.graphene_document.folder(path)?; + let layer_ids_above = if insert_index < 0 { &folder.layer_ids } else { &folder.layer_ids[..(insert_index as usize)] }; + + Ok(insert_index - layer_ids_above.iter().filter(|layer_id| layers.iter().any(|x| *x == [path, &[**layer_id]].concat())).count() as isize) + } } impl MessageHandler for DocumentMessageHandler { @@ -915,6 +925,13 @@ impl MessageHandler for DocumentMessageHand responses.push_back(ToolMessage::DocumentIsDirty.into()); } MoveSelectedLayersTo { path, insert_index } => { + let layers = self.selected_layers().collect::>(); + + // Trying to insert into self. + if layers.iter().any(|layer| path.starts_with(layer)) { + return; + } + let insert_index = self.update_insert_index(&layers, &path, insert_index).unwrap(); responses.push_back(DocumentsMessage::Copy(Clipboard::System).into()); responses.push_back(DocumentMessage::DeleteSelectedLayers.into()); responses.push_back( @@ -946,15 +963,11 @@ impl MessageHandler for DocumentMessageHand if let Some(insert_path) = insert { let (id, path) = insert_path.split_last().expect("Can't move the root folder"); if let Some(folder) = self.graphene_document.layer(path).ok().and_then(|layer| layer.as_folder().ok()) { - let selected: Vec<_> = selected_layers - .iter() - .filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1) - .map(|x| x.last().unwrap()) - .collect(); - let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect(); - let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 }; - let fallback = offset * (non_selected.len()); - let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize; + let layer_index = folder.layer_ids.iter().position(|comparison_id| comparison_id == id).unwrap() as isize; + + // If moving down, insert below this layer, if moving up, insert above this layer + let insert_index = if relative_position < 0 { layer_index } else { layer_index + 1 }; + responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into()); } } From d8a2441d3077b3fb7afd1503eda823e56e6fb946 Mon Sep 17 00:00:00 2001 From: 0hypercube <0hypercube@gmail.com> Date: Tue, 11 Jan 2022 16:57:05 +0000 Subject: [PATCH 5/8] Squashed commit of the following: commit 095d577a49f6b904ba3038327109860e5519f981 Author: Keavon Chambers Date: Mon Jan 10 18:06:12 2022 -0800 Fix NumberInput clamping regression with undefined bounds commit 9f54a376c4ad20a60731bb4a4eca5a25001fcfd1 Author: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Sun Jan 9 15:52:55 2022 -0800 Fix bounds with artboards for zoom-to-fit and scrollbar scaling (#473) * - document load keeps postition - zoom to fit - scrollbars use artboard dimensions * - review comments - svg export uses all artboard bounds Co-authored-by: Keavon Chambers commit 61432de4801d63d62faa18aaa624b11a122a97b1 Author: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sat Jan 8 21:06:15 2022 +0000 Fix rotation input (#472) --- editor/src/consts.rs | 3 +- .../src/document/artboard_message_handler.rs | 44 +++++++++------- editor/src/document/document_file.rs | 40 ++++++++++---- .../src/document/document_message_handler.rs | 2 +- editor/src/document/movement_handler.rs | 52 ++++++++++++------- editor/src/input/input_mapper.rs | 2 +- .../widgets/inputs/MenuBarInput.vue | 2 +- .../components/widgets/inputs/NumberInput.vue | 14 ++--- frontend/wasm/src/api.rs | 6 ++- 9 files changed, 104 insertions(+), 61 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index dd414e6e72..9fe8c1e574 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -45,4 +45,5 @@ pub const FILE_EXPORT_SUFFIX: &str = ".svg"; pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.); // Document -pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.1"; +pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.2"; +pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05; diff --git a/editor/src/document/artboard_message_handler.rs b/editor/src/document/artboard_message_handler.rs index 4046a33c04..a113b7e009 100644 --- a/editor/src/document/artboard_message_handler.rs +++ b/editor/src/document/artboard_message_handler.rs @@ -30,6 +30,12 @@ pub struct ArtboardMessageHandler { pub artboard_ids: Vec, } +impl ArtboardMessageHandler { + pub fn is_infinite_canvas(&self) -> bool { + self.artboard_ids.is_empty() + } +} + impl MessageHandler for ArtboardMessageHandler { fn process_action(&mut self, message: ArtboardMessage, _data: (&mut LayerMetadata, &GrapheneDocument, &InputPreprocessor), responses: &mut VecDeque) { // let (layer_metadata, document, ipp) = data; @@ -55,29 +61,31 @@ impl MessageHandler {} - } - // Render an infinite canvas if there are no artboards - if self.artboard_ids.is_empty() { - responses.push_back( - FrontendMessage::UpdateArtboards { - svg: r##""##.to_string(), - } - .into(), - ) - } else { - responses.push_back( - FrontendMessage::UpdateArtboards { - svg: self.artboards_graphene_document.render_root(ViewMode::Normal), + responses.push_back(DocumentMessage::RenderDocument.into()); + } + RenderArtboards => { + // Render an infinite canvas if there are no artboards + if self.artboard_ids.is_empty() { + responses.push_back( + FrontendMessage::UpdateArtboards { + svg: r##""##.to_string(), + } + .into(), + ) + } else { + responses.push_back( + FrontendMessage::UpdateArtboards { + svg: self.artboards_graphene_document.render_root(ViewMode::Normal), + } + .into(), + ); } - .into(), - ); + } } } fn actions(&self) -> ActionList { - actions!(ArtBoardMessageDiscriminant;) + actions!(ArtboardMessageDiscriminant;) } } diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index dc8d203551..1192567944 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -8,10 +8,9 @@ use super::movement_handler::{MovementMessage, MovementMessageHandler}; use super::overlay_message_handler::OverlayMessageHandler; use super::transform_layer_handler::{TransformLayerMessage, TransformLayerMessageHandler}; use super::vectorize_layer_metadata; - -use crate::consts::DEFAULT_DOCUMENT_NAME; -use crate::consts::GRAPHITE_DOCUMENT_VERSION; -use crate::consts::{ASYMPTOTIC_EFFECT, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING}; +use crate::consts::{ + ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR, +}; use crate::document::Clipboard; use crate::input::InputPreprocessor; use crate::message_prelude::*; @@ -82,7 +81,6 @@ pub struct DocumentMessageHandler { #[serde(with = "vectorize_layer_metadata")] pub layer_metadata: HashMap, LayerMetadata>, layer_range_selection_reference: Vec, - #[serde(skip)] movement_handler: MovementMessageHandler, #[serde(skip)] overlay_message_handler: OverlayMessageHandler, @@ -177,6 +175,7 @@ pub enum DocumentMessage { }, ReorderSelectedLayers(i32), // relative_position, SetSnapping(bool), + ZoomCanvasToFitAll, } impl From for DocumentMessage { @@ -220,13 +219,10 @@ impl DocumentMessageHandler { document } - pub fn with_name_and_content(name: String, serialized_content: String, ipp: &InputPreprocessor) -> Result { + pub fn with_name_and_content(name: String, serialized_content: String) -> Result { match Self::deserialize_document(&serialized_content) { Ok(mut document) => { document.name = name; - let starting_root_transform = document.movement_handler.calculate_offset_transform(ipp.viewport_bounds.size() / 2.); - document.graphene_document.root.transform = starting_root_transform; - document.artboard_message_handler.artboards_graphene_document.root.transform = starting_root_transform; Ok(document) } Err(DocumentError::InvalidFile(msg)) => Err(EditorError::Document(msg)), @@ -545,6 +541,14 @@ impl DocumentMessageHandler { Ok(insert_index - layer_ids_above.iter().filter(|layer_id| layers.iter().any(|x| *x == [path, &[**layer_id]].concat())).count() as isize) } + + pub fn document_bounds(&self) -> Option<[DVec2; 2]> { + if self.artboard_message_handler.is_infinite_canvas() { + self.graphene_document.viewport_bounding_box(&[]).ok().flatten() + } else { + self.artboard_message_handler.artboards_graphene_document.viewport_bounding_box(&[]).ok().flatten() + } + } } impl MessageHandler for DocumentMessageHandler { @@ -582,7 +586,8 @@ impl MessageHandler for DocumentMessageHand ); } ExportDocument => { - let bbox = self.graphene_document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]); + // TODO(MFISH33): Add Dialog to select artboards + let bbox = self.document_bounds().unwrap_or([DVec2::ZERO, ipp.viewport_bounds.size()]); let size = bbox[1] - bbox[0]; let name = match self.name.ends_with(FILE_SAVE_SUFFIX) { true => self.name.clone().replace(FILE_SAVE_SUFFIX, FILE_EXPORT_SUFFIX), @@ -870,7 +875,7 @@ impl MessageHandler for DocumentMessageHand let scale = 0.5 + ASYMPTOTIC_EFFECT + document_transform_scale * SCALE_EFFECT; let viewport_size = ipp.viewport_bounds.size(); let viewport_mid = ipp.viewport_bounds.center(); - let [bounds1, bounds2] = self.graphene_document.visible_layers_bounding_box().unwrap_or([viewport_mid; 2]); + let [bounds1, bounds2] = self.document_bounds().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); @@ -1037,6 +1042,18 @@ impl MessageHandler for DocumentMessageHand SetSnapping(new_status) => { self.snapping_enabled = new_status; } + ZoomCanvasToFitAll => { + if let Some(bounds) = self.document_bounds() { + responses.push_back( + MovementMessage::FitViewportToBounds { + bounds, + padding_scale_factor: Some(VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR), + prevent_zoom_past_100: true, + } + .into(), + ) + } + } } } @@ -1051,6 +1068,7 @@ impl MessageHandler for DocumentMessageHand SaveDocument, SetSnapping, DebugPrintDocument, + ZoomCanvasToFitAll, ); if self.layer_metadata.values().any(|data| data.selected) { diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index ee000cfdfd..4b409d525f 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -294,7 +294,7 @@ impl MessageHandler for DocumentsMessageHa document, document_is_saved, } => { - let document = DocumentMessageHandler::with_name_and_content(document_name, document, ipp); + let document = DocumentMessageHandler::with_name_and_content(document_name, document); match document { Ok(mut document) => { document.set_save_state(document_is_saved); diff --git a/editor/src/document/movement_handler.rs b/editor/src/document/movement_handler.rs index 071e0cdda4..aebf53014a 100644 --- a/editor/src/document/movement_handler.rs +++ b/editor/src/document/movement_handler.rs @@ -39,7 +39,11 @@ pub enum MovementMessage { center_on_mouse: bool, }, WheelCanvasZoom, - ZoomCanvasToFitAll, + FitViewportToBounds { + bounds: [DVec2; 2], + padding_scale_factor: Option, + prevent_zoom_past_100: bool, + }, TranslateCanvas(DVec2), TranslateCanvasByViewportFraction(DVec2), } @@ -273,25 +277,34 @@ impl MessageHandler for Moveme responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: self.snapped_angle() }.into()); } - ZoomCanvasToFitAll => { - if let Some([pos1, pos2]) = document.visible_layers_bounding_box() { - let pos1 = document.root.transform.inverse().transform_point2(pos1); - let pos2 = document.root.transform.inverse().transform_point2(pos2); - let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO); - let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_bounds.size()); - - let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5); - let size = (pos2 - pos1) / (v2 - v1); - let size = 1. / size; - let new_scale = size.min_element(); - - self.pan += center; - self.zoom *= new_scale; - responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.zoom }.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + FitViewportToBounds { + bounds: [bounds_corner_a, bounds_corner_b], + padding_scale_factor, + prevent_zoom_past_100, + } => { + let pos1 = document.root.transform.inverse().transform_point2(bounds_corner_a); + let pos2 = document.root.transform.inverse().transform_point2(bounds_corner_b); + let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO); + let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_bounds.size()); + + let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5); + let size = (pos2 - pos1) / (v2 - v1); + let size = 1. / size; + let new_scale = size.min_element(); + + self.pan += center; + self.zoom *= new_scale; + + self.zoom /= padding_scale_factor.unwrap_or(1.) as f64; + + if self.zoom > 1. && prevent_zoom_past_100 { + self.zoom = 1. } + + responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.zoom }.into()); + responses.push_back(ToolMessage::DocumentIsDirty.into()); + responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); + self.create_document_transform(&ipp.viewport_bounds, responses); } TranslateCanvas(delta) => { let transformed_delta = document.root.transform.inverse().transform_vector2(delta); @@ -321,7 +334,6 @@ impl MessageHandler for Moveme IncreaseCanvasZoom, DecreaseCanvasZoom, WheelCanvasTranslate, - ZoomCanvasToFitAll, TranslateCanvas, TranslateCanvasByViewportFraction, ); diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 5e1b7d6649..f15425ea57 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -234,6 +234,7 @@ impl Default for Mapping { entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl]}, entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]}, entry! {action=DocumentMessage::DebugPrintDocument, key_down=Key9}, + entry! {action=DocumentMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]}, // Initiate Transform Layers entry! {action=TransformLayerMessage::BeginGrab, key_down=KeyG}, entry! {action=TransformLayerMessage::BeginRotate, key_down=KeyR}, @@ -251,7 +252,6 @@ impl Default for Mapping { entry! {action=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }, key_down=KeyMinus, modifiers=[KeyControl]}, entry! {action=MovementMessage::SetCanvasZoom(1.), key_down=Key1, modifiers=[KeyControl]}, entry! {action=MovementMessage::SetCanvasZoom(2.), key_down=Key2, modifiers=[KeyControl]}, - entry! {action=MovementMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]}, entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]}, entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]}, entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }, message=InputMapperMessage::MouseScroll}, diff --git a/frontend/src/components/widgets/inputs/MenuBarInput.vue b/frontend/src/components/widgets/inputs/MenuBarInput.vue index 4c4737a63f..caceb16146 100644 --- a/frontend/src/components/widgets/inputs/MenuBarInput.vue +++ b/frontend/src/components/widgets/inputs/MenuBarInput.vue @@ -71,7 +71,7 @@ function makeMenuEntries(editor: EditorState): MenuListEntries { icon: "File", action: (): void => { editor.instance.new_document(); - editor.instance.create_artboard(0, 0, 1920, 1080); + editor.instance.create_artboard_and_fit_to_viewport(0, 0, 1920, 1080); }, }, { label: "Open…", shortcut: ["KeyControl", "KeyO"], action: (): void => editor.instance.open_document() }, diff --git a/frontend/src/components/widgets/inputs/NumberInput.vue b/frontend/src/components/widgets/inputs/NumberInput.vue index 6f8c1ca75c..9c485ad262 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.vue +++ b/frontend/src/components/widgets/inputs/NumberInput.vue @@ -152,8 +152,6 @@