diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e908259586..f8cb0a27f4 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -6,7 +6,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::utility_types::{ BoxSelection, ContextMenuInformation, FrontendClickTargets, FrontendGraphInput, FrontendGraphOutput, FrontendNode, FrontendNodeType, NodeGraphErrorDiagnostic, Transform, }; -use crate::messages::portfolio::document::utility_types::nodes::{JsRawBuffer, LayerPanelEntry, RawBuffer}; +use crate::messages::portfolio::document::utility_types::nodes::{LayerPanelEntry, LayerStructureEntry}; use crate::messages::portfolio::document::utility_types::wires::{WirePath, WirePathUpdate}; use crate::messages::prelude::*; use glam::IVec2; @@ -236,11 +236,7 @@ pub enum FrontendMessage { }, UpdateDocumentLayerStructure { #[serde(rename = "dataBuffer")] - data_buffer: RawBuffer, - }, - UpdateDocumentLayerStructureJs { - #[serde(rename = "dataBuffer")] - data_buffer: JsRawBuffer, + data_buffer: Vec, }, UpdateDocumentRulers { origin: (f64, f64), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index c807b60a36..dfa5d2af49 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -19,7 +19,7 @@ use crate::messages::portfolio::document::properties_panel::properties_panel_mes use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate}; -use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; +use crate::messages::portfolio::document::utility_types::nodes::LayerStructureEntry; use crate::messages::portfolio::utility_types::{PanelType, PersistentData}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; @@ -317,7 +317,7 @@ impl MessageHandler> for DocumentMes DocumentMessage::ClearLayersPanel => { // Send an empty layer list if layers_panel_open { - let data_buffer: RawBuffer = Self::default().serialize_root(); + let data_buffer: Vec = Self::default().serialize_root(); responses.add(FrontendMessage::UpdateDocumentLayerStructure { data_buffer }); } @@ -380,7 +380,7 @@ impl MessageHandler> for DocumentMes DocumentMessage::DocumentStructureChanged => { if layers_panel_open { self.network_interface.load_structure(); - let data_buffer: RawBuffer = self.serialize_root(); + let data_buffer: Vec = self.serialize_root(); self.update_layers_panel_control_bar_widgets(layers_panel_open, responses); self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses); @@ -1892,69 +1892,26 @@ impl DocumentMessageHandler { } /// Called recursively by the entry function [`serialize_root`]. - fn serialize_structure(&self, folder: LayerNodeIdentifier, structure_section: &mut Vec, data_section: &mut Vec, path: &mut Vec) { - let mut space = 0; - for layer_node in folder.children(self.metadata()) { - data_section.push(layer_node.to_node().0); - space += 1; + fn serialize_structure_flat(&self, folder: LayerNodeIdentifier, depth: u32, entries: &mut Vec) { + let mut children = folder.children(self.metadata()).peekable(); + while let Some(layer_node) = children.next() { + let is_last_in_parent = children.peek().is_none(); + entries.push(LayerStructureEntry { + id: layer_node.to_node(), + depth, + is_last_in_parent, + }); if layer_node.has_children(self.metadata()) && !self.collapsed.0.contains(&layer_node) { - path.push(layer_node); - - // TODO: Skip if folder is not expanded. - structure_section.push(space); - self.serialize_structure(layer_node, structure_section, data_section, path); - space = 0; - - path.pop(); + self.serialize_structure_flat(layer_node, depth + 1, entries); } } - structure_section.push(space | (1 << 63)); } - /// Serializes the layer structure into a condensed 1D structure. - /// - /// # Format - /// It is a string of numbers broken into three sections: - /// - /// | Data | Description | Length | - /// |------------------------------------------------------------------------------------------------------------------------------ |---------------------------------------------------------------|------------------| - /// | `4,` `2, 1, -2, -0,` `16533113728871998040,3427872634365736244,18115028555707261608,15878401910454357952,449479075714955186` | Encoded example data | | - /// | _____________________________________________________________________________________________________________________________ | _____________________________________________________________ | ________________ | - /// | **Length** section: `4` | Length of the **Structure** section (`L` = `structure.len()`) | First value | - /// | **Structure** section: `2, 1, -2, -0` | The **Structure** section | Next `L` values | - /// | **Data** section: `16533113728871998040, 3427872634365736244, 18115028555707261608, 15878401910454357952, 449479075714955186` | The **Data** section (layer IDs) | Remaining values | - /// - /// The data section lists the layer IDs for all folders/layers in the tree as read from top to bottom. - /// The structure section lists signed numbers. The sign indicates a folder indentation change (`+` is down a level, `-` is up a level). - /// The numbers in the structure block encode the indentation. For example: - /// - `2` means read two elements from the data section, then place a `[`. - /// - `-x` means read `x` elements from the data section and then insert a `]`. - /// - /// ```text - /// 2 V 1 V -2 A -0 A - /// 16533113728871998040,3427872634365736244, 18115028555707261608, 15878401910454357952,449479075714955186 - /// 16533113728871998040,3427872634365736244,[ 18115028555707261608,[15878401910454357952,449479075714955186] ] - /// ``` - /// - /// Resulting layer panel: - /// ```text - /// 16533113728871998040 - /// 3427872634365736244 - /// [3427872634365736244,18115028555707261608] - /// [3427872634365736244,18115028555707261608,15878401910454357952] - /// [3427872634365736244,18115028555707261608,449479075714955186] - /// ``` - pub fn serialize_root(&self) -> RawBuffer { - let mut structure_section = vec![NodeId(0).0]; - let mut data_section = Vec::new(); - self.serialize_structure(LayerNodeIdentifier::ROOT_PARENT, &mut structure_section, &mut data_section, &mut vec![]); - - // Remove the ROOT element. Prepend `L`, the length (excluding the ROOT) of the structure section (which happens to be where the ROOT element was). - structure_section[0] = structure_section.len() as u64 - 1; - // Append the data section to the end. - structure_section.extend(data_section); - - structure_section.as_slice().into() + /// Serializes the layer structure into a flat list with depth information. + pub fn serialize_root(&self) -> Vec { + let mut entries = Vec::new(); + self.serialize_structure_flat(LayerNodeIdentifier::ROOT_PARENT, 0, &mut entries); + entries } pub fn undo_with_history(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque) { @@ -3229,6 +3186,83 @@ impl Iterator for ClickXRayIter<'_> { mod document_message_handler_tests { use super::*; use crate::test_utils::test_prelude::*; + use std::collections::HashSet; + + #[tokio::test] + async fn test_serialize_root_empty_document() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + let entries = editor.active_document().serialize_root(); + assert!(entries.is_empty()); + } + + #[tokio::test] + async fn test_serialize_root_flat_structure() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + editor.handle_message(DocumentMessage::CreateEmptyFolder).await; + let folder = editor.active_document().metadata().all_layers().next().unwrap(); + + let mut excluded = HashSet::from([folder.to_node()]); + + editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await; + let rect1 = editor.active_document().metadata().all_layers().find(|layer| !excluded.contains(&layer.to_node())).unwrap(); + excluded.insert(rect1.to_node()); + + editor.drag_tool(ToolType::Rectangle, 100., 100., 200., 200., ModifierKeys::empty()).await; + let rect2 = editor.active_document().metadata().all_layers().find(|layer| !excluded.contains(&layer.to_node())).unwrap(); + + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rect1.to_node()] }).await; + editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder, insert_index: 0 }).await; + + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rect2.to_node()] }).await; + editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder, insert_index: 1 }).await; + + let entries = editor.active_document().serialize_root(); + assert_eq!(entries.len(), 3); + + assert_eq!(entries[0].id, folder.to_node()); + assert_eq!(entries[0].depth, 0); + assert!(entries[0].is_last_in_parent); + + assert_eq!(entries[1].id, rect1.to_node()); + assert_eq!(entries[1].depth, 1); + assert!(!entries[1].is_last_in_parent); + + assert_eq!(entries[2].id, rect2.to_node()); + assert_eq!(entries[2].depth, 1); + assert!(entries[2].is_last_in_parent); + } + + #[tokio::test] + async fn test_serialize_root_excludes_collapsed_children() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + + editor.handle_message(DocumentMessage::CreateEmptyFolder).await; + let folder = editor.active_document().metadata().all_layers().next().unwrap(); + + editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await; + let rect = editor.active_document().metadata().all_layers().find(|layer| layer.to_node() != folder.to_node()).unwrap(); + + editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![rect.to_node()] }).await; + editor.handle_message(DocumentMessage::MoveSelectedLayersTo { parent: folder, insert_index: 0 }).await; + + editor + .handle_message(DocumentMessage::ToggleLayerExpansion { + id: folder.to_node(), + recursive: false, + }) + .await; + + let entries = editor.active_document().serialize_root(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].id, folder.to_node()); + assert_eq!(entries[0].depth, 0); + assert!(entries[0].is_last_in_parent); + } #[tokio::test] async fn test_layer_selection_with_shift_and_ctrl() { diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index 07f262b382..4fdcb9a9ce 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -3,32 +3,14 @@ use super::network_interface::NodeNetworkInterface; use crate::messages::tool::common_functionality::graph_modification_utils; use glam::DVec2; use graph_craft::document::{NodeId, NodeNetwork}; -use serde::ser::SerializeStruct; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] -pub struct RawBuffer(Vec); - -impl From<&[u64]> for RawBuffer { - fn from(iter: &[u64]) -> Self { - let v_from_raw: Vec = iter.iter().flat_map(|x| x.to_ne_bytes()).collect(); - Self(v_from_raw) - } -} -#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq, specta::Type)] -pub struct JsRawBuffer(Vec); - -impl From for JsRawBuffer { - fn from(buffer: RawBuffer) -> Self { - Self(buffer.0) - } -} -impl serde::Serialize for JsRawBuffer { - fn serialize(&self, serializer: S) -> Result { - let mut buffer = serializer.serialize_struct("Buffer", 2)?; - buffer.serialize_field("pointer", &(self.0.as_ptr() as usize))?; - buffer.serialize_field("length", &(self.0.len()))?; - buffer.end() - } +pub struct LayerStructureEntry { + #[serde(rename = "layerId")] + pub id: NodeId, + pub depth: u32, + #[serde(rename = "isLastInParent")] + pub is_last_in_parent: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index aba662ef31..958bb37cae 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -6,12 +6,12 @@ import { patchLayout, UpdateDocumentLayerDetails, - UpdateDocumentLayerStructureJs, + UpdateDocumentLayerStructure, UpdateLayersPanelControlBarLeftLayout, UpdateLayersPanelControlBarRightLayout, UpdateLayersPanelBottomBarLayout, } from "@graphite/messages"; - import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages"; + import type { LayerPanelEntry, Layout, LayerStructureEntry } from "@graphite/messages"; import type { NodeGraphState } from "@graphite/state-providers/node-graph"; import type { TooltipState } from "@graphite/state-providers/tooltip"; import { pasteFile } from "@graphite/utility-functions/files"; @@ -91,9 +91,8 @@ layersPanelBottomBarLayout = layersPanelBottomBarLayout; }); - editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerStructureJs, (data) => { - const structure = newUpdateDocumentLayerStructure(data.dataBuffer); - rebuildLayerHierarchy(structure); + editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerStructure, (data) => { + rebuildLayerHierarchy(data.dataBuffer); }); editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (data) => { @@ -117,7 +116,7 @@ editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarLeftLayout); editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelControlBarRightLayout); editor.subscriptions.unsubscribeJsMessage(UpdateLayersPanelBottomBarLayout); - editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructureJs); + editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerStructure); editor.subscriptions.unsubscribeJsMessage(UpdateDocumentLayerDetails); removeEventListener("pointerup", draggingPointerUp); @@ -130,65 +129,6 @@ removeEventListener("keyup", clippingKeyPress); }); - type DocumentLayerStructure = { - layerId: bigint; - children: DocumentLayerStructure[]; - }; - - function newUpdateDocumentLayerStructure(dataBuffer: DataBuffer): DocumentLayerStructure { - const pointerNum = Number(dataBuffer.pointer); - const lengthNum = Number(dataBuffer.length); - - const wasmMemoryBuffer = editor.raw.buffer; - - // Decode the folder structure encoding - const encoding = new DataView(wasmMemoryBuffer, pointerNum, lengthNum); - - // The structure section indicates how to read through the upcoming layer list and assign depths to each layer - const structureSectionLength = Number(encoding.getBigUint64(0, true)); - const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, pointerNum + 8, structureSectionLength * 8); - - // The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel - const layerIdsSection = new DataView(wasmMemoryBuffer, pointerNum + 8 + structureSectionLength * 8); - - let layersEncountered = 0; - let currentFolder: DocumentLayerStructure = { layerId: BigInt(-1), children: [] }; - const currentFolderStack = [currentFolder]; - - for (let i = 0; i < structureSectionLength; i += 1) { - const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true); - const msbMask = BigInt(1) << BigInt(64 - 1); - - // Set the MSB to 0 to clear the sign and then read the number as usual - const numberOfLayersAtThisDepth = msbSigned & ~msbMask; - - // Store child folders in the current folder (until we are interrupted by an indent) - for (let j = 0; j < numberOfLayersAtThisDepth; j += 1) { - const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true); - layersEncountered += 1; - - const childLayer: DocumentLayerStructure = { layerId, children: [] }; - currentFolder.children.push(childLayer); - } - - // Check the sign of the MSB, where a 1 is a negative (outward) indent - const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0); - // Inward - if (subsequentDirectionOfDepthChange) { - currentFolderStack.push(currentFolder); - currentFolder = currentFolder.children[currentFolder.children.length - 1]; - } - // Outward - else { - const popped = currentFolderStack.pop(); - if (!popped) throw Error("Too many negative indents in the folder structure"); - if (popped) currentFolder = popped; - } - } - - return currentFolder; - } - function toggleNodeVisibilityLayerPanel(id: bigint) { editor.handle.toggleNodeVisibilityLayerPanel(id); } @@ -515,32 +455,40 @@ dragInPanel = false; } - function rebuildLayerHierarchy(updateDocumentLayerStructure: DocumentLayerStructure) { + function rebuildLayerHierarchy(layerStructure: LayerStructureEntry[]) { const layerWithNameBeingEdited = layers.find((layer: LayerListingInfo) => layer.editingName); const layerIdWithNameBeingEdited = layerWithNameBeingEdited?.entry.id; // Clear the layer hierarchy before rebuilding it layers = []; - // Build the new layer hierarchy - const recurse = (folder: DocumentLayerStructure) => { - folder.children.forEach((item, index) => { - const mapping = layerCache.get(String(item.layerId)); - if (mapping) { - mapping.id = item.layerId; - layers.push({ - folderIndex: index, - bottomLayer: index === folder.children.length - 1, - entry: mapping, - editingName: layerIdWithNameBeingEdited === item.layerId, - }); + const indexByDepth: number[] = []; + + layerStructure.forEach((item) => { + const depth = item.depth; + + if (indexByDepth.length <= depth) { + for (let i = indexByDepth.length; i <= depth; i += 1) { + indexByDepth[i] = 0; } + } else if (indexByDepth.length > depth + 1) { + indexByDepth.length = depth + 1; + } - // Call self recursively if there are any children - if (item.children.length >= 1) recurse(item); - }); - }; - recurse(updateDocumentLayerStructure); + const folderIndex = indexByDepth[depth] ?? 0; + indexByDepth[depth] = folderIndex + 1; + + const mapping = layerCache.get(String(item.layerId)); + if (mapping) { + mapping.id = item.layerId; + layers.push({ + folderIndex, + bottomLayer: item.isLastInParent, + entry: mapping, + editingName: layerIdWithNameBeingEdited === item.layerId, + }); + } + }); layers = layers; } diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 89add1e27c..4477e95b33 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -796,13 +796,15 @@ export class TriggerSaveActiveDocument extends JsMessage { export class DocumentChanged extends JsMessage {} -export type DataBuffer = { - pointer: bigint; - length: bigint; -}; +export class LayerStructureEntry { + readonly layerId!: bigint; + readonly depth!: number; + readonly isLastInParent!: boolean; +} -export class UpdateDocumentLayerStructureJs extends JsMessage { - readonly dataBuffer!: DataBuffer; +export class UpdateDocumentLayerStructure extends JsMessage { + @Type(() => LayerStructureEntry) + readonly dataBuffer!: LayerStructureEntry[]; } export type TextAlign = "Left" | "Center" | "Right" | "JustifyLeft"; @@ -1717,7 +1719,7 @@ export const messageMakers: Record = { UpdateDocumentArtwork, UpdateDocumentBarLayout, UpdateDocumentLayerDetails, - UpdateDocumentLayerStructureJs, + UpdateDocumentLayerStructure, UpdateDocumentRulers, UpdateDocumentScrollbars, UpdateExportReorderIndex, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 6877874007..aaa17a1d22 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -154,7 +154,7 @@ impl EditorHandle { } // Sends a FrontendMessage to JavaScript - fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { + fn send_frontend_message_to_js(&self, message: FrontendMessage) { if let FrontendMessage::UpdateImageData { ref image_data } = message { let new_hash = calculate_hash(image_data); let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); @@ -166,10 +166,6 @@ impl EditorHandle { return; } - if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message { - message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() }; - } - let message_type = message.to_discriminant().local_name(); let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true);