Add Layers panel support for displaying multiple groups with instances of the same children layers (#3982)
* Add Layers panel support for displaying multiple groups with instances of the same children layers * Fix insert folder box drawing
This commit is contained in:
parent
86e41a110a
commit
d41883a942
|
|
@ -197,7 +197,7 @@ pub enum DocumentMessage {
|
||||||
undo_count: usize,
|
undo_count: usize,
|
||||||
},
|
},
|
||||||
ToggleLayerExpansion {
|
ToggleLayerExpansion {
|
||||||
id: NodeId,
|
instance_path: Vec<NodeId>,
|
||||||
recursive: bool,
|
recursive: bool,
|
||||||
},
|
},
|
||||||
ToggleSelectedVisibility,
|
ToggleSelectedVisibility,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
||||||
use graphene_std::vector::misc::dvec2_to_point;
|
use graphene_std::vector::misc::dvec2_to_point;
|
||||||
use graphene_std::vector::style::RenderMode;
|
use graphene_std::vector::style::RenderMode;
|
||||||
use kurbo::{Affine, BezPath, Line, PathSeg};
|
use kurbo::{Affine, BezPath, Line, PathSeg};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -84,8 +85,8 @@ pub struct DocumentMessageHandler {
|
||||||
//
|
//
|
||||||
// Contains the NodeNetwork and acts an an interface to manipulate the NodeNetwork with custom setters in order to keep NetworkMetadata in sync
|
// Contains the NodeNetwork and acts an an interface to manipulate the NodeNetwork with custom setters in order to keep NetworkMetadata in sync
|
||||||
pub network_interface: NodeNetworkInterface,
|
pub network_interface: NodeNetworkInterface,
|
||||||
/// List of the [`LayerNodeIdentifier`]s that are currently collapsed by the user in the Layers panel.
|
/// Tracks which layer instances are collapsed in the Layers panel, keyed by instance path.
|
||||||
/// Collapsed means that the expansion arrow isn't set to show the children of these layers.
|
#[serde(deserialize_with = "deserialize_collapsed_layers", default)]
|
||||||
pub collapsed: CollapsedLayers,
|
pub collapsed: CollapsedLayers,
|
||||||
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
/// The full Git commit hash of the Graphite repository that was used to build the editor.
|
||||||
/// We save this to provide a hint about which version of the editor was used to create the document.
|
/// We save this to provide a hint about which version of the editor was used to create the document.
|
||||||
|
|
@ -317,7 +318,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||||
DocumentMessage::ClearLayersPanel => {
|
DocumentMessage::ClearLayersPanel => {
|
||||||
// Send an empty layer list
|
// Send an empty layer list
|
||||||
if layers_panel_open {
|
if layers_panel_open {
|
||||||
let layer_structure = Self::default().build_layer_structure(LayerNodeIdentifier::ROOT_PARENT);
|
let layer_structure = Self::default().build_layer_structure();
|
||||||
responses.add(FrontendMessage::UpdateDocumentLayerStructure { layer_structure });
|
responses.add(FrontendMessage::UpdateDocumentLayerStructure { layer_structure });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,7 +381,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||||
DocumentMessage::DocumentStructureChanged => {
|
DocumentMessage::DocumentStructureChanged => {
|
||||||
if layers_panel_open {
|
if layers_panel_open {
|
||||||
self.network_interface.load_structure();
|
self.network_interface.load_structure();
|
||||||
let layer_structure = self.build_layer_structure(LayerNodeIdentifier::ROOT_PARENT);
|
let layer_structure = self.build_layer_structure();
|
||||||
|
|
||||||
self.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
|
self.update_layers_panel_control_bar_widgets(layers_panel_open, responses);
|
||||||
self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
|
self.update_layers_panel_bottom_bar_widgets(layers_panel_open, responses);
|
||||||
|
|
@ -1167,25 +1168,27 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||||
responses.add(OverlaysMessage::Draw);
|
responses.add(OverlaysMessage::Draw);
|
||||||
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
responses.add(PortfolioMessage::UpdateOpenDocumentsList);
|
||||||
}
|
}
|
||||||
DocumentMessage::ToggleLayerExpansion { id, recursive } => {
|
DocumentMessage::ToggleLayerExpansion { instance_path, recursive } => {
|
||||||
let layer = LayerNodeIdentifier::new(id, &self.network_interface);
|
let is_collapsed = self.collapsed.0.contains(&instance_path);
|
||||||
let metadata = self.metadata();
|
|
||||||
|
|
||||||
let is_collapsed = self.collapsed.0.contains(&layer);
|
|
||||||
|
|
||||||
if is_collapsed {
|
if is_collapsed {
|
||||||
if recursive {
|
if recursive {
|
||||||
let children: HashSet<_> = layer.descendants(metadata).collect();
|
// Remove this path and all descendant paths (paths that start with this one)
|
||||||
self.collapsed.0.retain(|collapsed_layer| !children.contains(collapsed_layer) && collapsed_layer != &layer);
|
self.collapsed.0.retain(|path| !path.starts_with(&instance_path));
|
||||||
} else {
|
} else {
|
||||||
self.collapsed.0.retain(|collapsed_layer| collapsed_layer != &layer);
|
self.collapsed.0.retain(|path| *path != instance_path);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if recursive {
|
if recursive {
|
||||||
let children_to_add: Vec<_> = layer.descendants(metadata).filter(|child| !self.collapsed.0.contains(child)).collect();
|
// Collapse all expanded descendant instances by collecting their paths from the structure tree
|
||||||
self.collapsed.0.extend(children_to_add);
|
let descendant_paths = self.collect_descendant_instance_paths(&instance_path);
|
||||||
|
for path in descendant_paths {
|
||||||
|
if !self.collapsed.0.contains(&path) {
|
||||||
|
self.collapsed.0.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.collapsed.0.push(layer);
|
self.collapsed.0.push(instance_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::SendGraph);
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
|
|
@ -1740,22 +1743,218 @@ impl DocumentMessageHandler {
|
||||||
Ok(document_message_handler)
|
Ok(document_message_handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively builds the layer structure tree for a folder.
|
/// Builds the layer structure tree by traversing the node graph directly.
|
||||||
fn build_layer_structure(&self, folder: LayerNodeIdentifier) -> Vec<LayerStructureEntry> {
|
/// Unlike the canonical `structure` field of [`DocumentMetadata`] (which stores single-parent relationships), this allows
|
||||||
folder
|
/// the same layer to appear under multiple parents when the graph feeds the same child content into separate parent layers.
|
||||||
.children(self.metadata())
|
fn build_layer_structure(&self) -> Vec<LayerStructureEntry> {
|
||||||
.map(|layer_node| {
|
let network = &self.network_interface;
|
||||||
let children = if layer_node.has_children(self.metadata()) && !self.collapsed.0.contains(&layer_node) {
|
|
||||||
self.build_layer_structure(layer_node)
|
let Some(root_node) = network.root_node(&[]) else { return Vec::new() };
|
||||||
} else {
|
let Some(first_root_layer_id) = network
|
||||||
Vec::new()
|
.upstream_flow_back_from_nodes(vec![root_node.node_id], &[], FlowType::PrimaryFlow)
|
||||||
};
|
.find(|node_id| network.is_layer(node_id, &[]))
|
||||||
LayerStructureEntry {
|
else {
|
||||||
layer_id: layer_node.to_node(),
|
return Vec::new();
|
||||||
children,
|
};
|
||||||
|
|
||||||
|
let selected_layers: HashSet<NodeId> = network.selected_nodes().selected_layers(self.metadata()).map(LayerNodeIdentifier::to_node).collect();
|
||||||
|
|
||||||
|
let ancestors = HashSet::new();
|
||||||
|
let instance_path = Vec::new();
|
||||||
|
let mut root_entries = Vec::new();
|
||||||
|
|
||||||
|
// The first root layer is the topmost entry
|
||||||
|
root_entries.push(self.build_layer_entry(first_root_layer_id, &ancestors, &selected_layers, &instance_path));
|
||||||
|
|
||||||
|
// Layers in the primary flow (input[0] chain) from the first root layer are root-level siblings
|
||||||
|
let mut root_ancestors = HashSet::new();
|
||||||
|
root_ancestors.insert(first_root_layer_id);
|
||||||
|
|
||||||
|
for sibling_id in network.upstream_flow_back_from_nodes(vec![first_root_layer_id], &[], FlowType::PrimaryFlow).skip(1) {
|
||||||
|
if network.is_layer(&sibling_id, &[]) && !root_ancestors.contains(&sibling_id) {
|
||||||
|
root_entries.push(self.build_layer_entry(sibling_id, &root_ancestors, &selected_layers, &instance_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a single `LayerStructureEntry` for the given layer, including its `children_present` flag,
|
||||||
|
/// `descendant_selected` flag, and (if expanded) its children collected from the graph.
|
||||||
|
fn build_layer_entry(&self, layer_id: NodeId, ancestors: &HashSet<NodeId>, selected_layers: &HashSet<NodeId>, parent_instance_path: &[NodeId]) -> LayerStructureEntry {
|
||||||
|
let mut instance_path = parent_instance_path.to_vec();
|
||||||
|
instance_path.push(layer_id);
|
||||||
|
|
||||||
|
let mut child_ancestors = ancestors.clone();
|
||||||
|
child_ancestors.insert(layer_id);
|
||||||
|
|
||||||
|
let children_present = self.has_layer_children_in_graph(layer_id, &child_ancestors);
|
||||||
|
|
||||||
|
let collapsed = self.collapsed.0.contains(&instance_path);
|
||||||
|
|
||||||
|
let children = if children_present && !collapsed {
|
||||||
|
self.collect_layer_children(layer_id, &child_ancestors, selected_layers, &instance_path)
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute whether any descendant is selected (checking expanded children and, if collapsed, via graph traversal)
|
||||||
|
let descendant_selected = if !children.is_empty() {
|
||||||
|
children.iter().any(|child| child.descendant_selected || selected_layers.contains(&child.layer_id))
|
||||||
|
} else if children_present {
|
||||||
|
// Layer is collapsed but has children, so check via graph traversal
|
||||||
|
self.has_selected_descendant_in_graph(layer_id, &child_ancestors, selected_layers)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
LayerStructureEntry {
|
||||||
|
layer_id,
|
||||||
|
children,
|
||||||
|
children_present,
|
||||||
|
descendant_selected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether a layer has any child layers reachable via horizontal flow in the graph.
|
||||||
|
fn has_layer_children_in_graph(&self, layer_id: NodeId, child_ancestors: &HashSet<NodeId>) -> bool {
|
||||||
|
let network = &self.network_interface;
|
||||||
|
|
||||||
|
network
|
||||||
|
.upstream_flow_back_from_nodes(vec![layer_id], &[], FlowType::HorizontalFlow)
|
||||||
|
.skip(1)
|
||||||
|
.any(|id| network.is_layer(&id, &[]) && !child_ancestors.contains(&id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether any descendant layer in the graph (via horizontal + primary flow) is selected.
|
||||||
|
/// Used when a layer is collapsed to determine if the ancestor-of-selected indicator should show.
|
||||||
|
fn has_selected_descendant_in_graph(&self, layer_id: NodeId, ancestors: &HashSet<NodeId>, selected_layers: &HashSet<NodeId>) -> bool {
|
||||||
|
let network = &self.network_interface;
|
||||||
|
|
||||||
|
// Find child layers via horizontal flow
|
||||||
|
let mut stack: Vec<NodeId> = network
|
||||||
|
.upstream_flow_back_from_nodes(vec![layer_id], &[], FlowType::HorizontalFlow)
|
||||||
|
.skip(1)
|
||||||
|
.filter(|node_id| network.is_layer(node_id, &[]) && !ancestors.contains(node_id))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut visited = ancestors.clone();
|
||||||
|
|
||||||
|
// Iteratively explore all descendant layers via a depth-first traversal
|
||||||
|
while let Some(current_id) = stack.pop() {
|
||||||
|
// Skip already-visited layers to avoid infinite loops from graph cycles
|
||||||
|
if !visited.insert(current_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found a selected descendant, the ancestor indicator should be shown
|
||||||
|
if selected_layers.contains(¤t_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check this layer's children via horizontal flow
|
||||||
|
for node_id in network.upstream_flow_back_from_nodes(vec![current_id], &[], FlowType::HorizontalFlow).skip(1) {
|
||||||
|
if network.is_layer(&node_id, &[]) && !visited.contains(&node_id) {
|
||||||
|
stack.push(node_id);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.collect()
|
|
||||||
|
// Check stacked siblings via primary flow
|
||||||
|
for node_id in network.upstream_flow_back_from_nodes(vec![current_id], &[], FlowType::PrimaryFlow).skip(1) {
|
||||||
|
if network.is_layer(&node_id, &[]) && !visited.contains(&node_id) {
|
||||||
|
stack.push(node_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects the child entries for a given layer by traversing its horizontal and primary flows.
|
||||||
|
/// The horizontal flow (a layer's secondary input chain) finds nested content layers, and the
|
||||||
|
/// primary flow from those (their stack's top output) finds stacked siblings at the same depth.
|
||||||
|
/// `ancestors` contains layer IDs in the current path from root, used for cycle prevention.
|
||||||
|
fn collect_layer_children(&self, layer_id: NodeId, ancestors: &HashSet<NodeId>, selected_layers: &HashSet<NodeId>, instance_path: &[NodeId]) -> Vec<LayerStructureEntry> {
|
||||||
|
let network = &self.network_interface;
|
||||||
|
|
||||||
|
// Find the first nested layer via horizontal flow (content inside this layer)
|
||||||
|
let Some(nested_id) = network
|
||||||
|
.upstream_flow_back_from_nodes(vec![layer_id], &[], FlowType::HorizontalFlow)
|
||||||
|
.skip(1)
|
||||||
|
.find(|id| network.is_layer(id, &[]))
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cycle detected, this layer is already an ancestor in the current branch
|
||||||
|
if ancestors.contains(&nested_id) {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The nested layer is the first child at this depth level
|
||||||
|
let mut children = vec![self.build_layer_entry(nested_id, ancestors, selected_layers, instance_path)];
|
||||||
|
|
||||||
|
// Primary flow from the nested layer finds stacked siblings (more children of this layer)
|
||||||
|
for sibling_id in network.upstream_flow_back_from_nodes(vec![nested_id], &[], FlowType::PrimaryFlow).skip(1) {
|
||||||
|
if network.is_layer(&sibling_id, &[]) && !ancestors.contains(&sibling_id) {
|
||||||
|
children.push(self.build_layer_entry(sibling_id, ancestors, selected_layers, instance_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
children
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects instance paths for all descendant layers of the given instance path by traversing the graph.
|
||||||
|
/// Used for recursive collapse to find all expandable descendants.
|
||||||
|
fn collect_descendant_instance_paths(&self, instance_path: &[NodeId]) -> Vec<Vec<NodeId>> {
|
||||||
|
let Some(&layer_id) = instance_path.last() else { return Vec::new() };
|
||||||
|
let network = &self.network_interface;
|
||||||
|
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
let mut stack: Vec<(NodeId, Vec<NodeId>)> = Vec::new();
|
||||||
|
|
||||||
|
// Seed with child layers via horizontal flow
|
||||||
|
for node_id in network.upstream_flow_back_from_nodes(vec![layer_id], &[], FlowType::HorizontalFlow).skip(1) {
|
||||||
|
if network.is_layer(&node_id, &[]) {
|
||||||
|
let mut child_path = instance_path.to_vec();
|
||||||
|
child_path.push(node_id);
|
||||||
|
stack.push((node_id, child_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
|
||||||
|
// Depth-first traversal collecting all unique descendant instance paths
|
||||||
|
while let Some((current_id, current_path)) = stack.pop() {
|
||||||
|
// Skip paths we've already visited to prevent cycles
|
||||||
|
if !visited.insert(current_path.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record this descendant's instance path for collapsing
|
||||||
|
paths.push(current_path.clone());
|
||||||
|
|
||||||
|
// Add nested content layers found via horizontal flow
|
||||||
|
for node_id in network.upstream_flow_back_from_nodes(vec![current_id], &[], FlowType::HorizontalFlow).skip(1) {
|
||||||
|
if network.is_layer(&node_id, &[]) {
|
||||||
|
let mut child_path = current_path.clone();
|
||||||
|
child_path.push(node_id);
|
||||||
|
stack.push((node_id, child_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stacked sibling layers found via primary flow
|
||||||
|
for node_id in network.upstream_flow_back_from_nodes(vec![current_id], &[], FlowType::PrimaryFlow).skip(1) {
|
||||||
|
if network.is_layer(&node_id, &[]) {
|
||||||
|
// Siblings share the same parent path (everything up to the last element of current_path)
|
||||||
|
let mut sibling_path = current_path[..current_path.len() - 1].to_vec();
|
||||||
|
sibling_path.push(node_id);
|
||||||
|
stack.push((node_id, sibling_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo_with_history(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) {
|
pub fn undo_with_history(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
|
@ -3221,6 +3420,16 @@ impl Iterator for ClickXRayIter<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deserializes `CollapsedLayers` with backwards compatibility for the old format
|
||||||
|
/// (flat list of layer node IDs) by consuming the entire value first, then attempting
|
||||||
|
/// to interpret it as the new format. Falls back to an empty default for old documents.
|
||||||
|
fn deserialize_collapsed_layers<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<CollapsedLayers, D::Error> {
|
||||||
|
use serde::Deserialize;
|
||||||
|
// Buffer the entire value to avoid leaving the deserializer in a bad state on type mismatch
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
Ok(serde_json::from_value(value).unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod document_message_handler_tests {
|
mod document_message_handler_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||||
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
|
send: Box::new(NodeGraphMessage::SelectedNodesUpdated.into()),
|
||||||
});
|
});
|
||||||
network_interface.load_structure();
|
network_interface.load_structure();
|
||||||
collapsed.0.retain(|&layer| network_interface.document_metadata().layer_exists(layer));
|
collapsed.0.retain(|path| path.iter().all(|&node_id| network_interface.document_network().nodes.contains_key(&node_id)));
|
||||||
}
|
}
|
||||||
NodeGraphMessage::SelectedNodesUpdated => {
|
NodeGraphMessage::SelectedNodesUpdated => {
|
||||||
let selected_layers = network_interface.selected_nodes().selected_layers(network_interface.document_metadata()).collect::<Vec<_>>();
|
let selected_layers = network_interface.selected_nodes().selected_layers(network_interface.document_metadata()).collect::<Vec<_>>();
|
||||||
|
|
@ -2047,7 +2047,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeGraphMessage::UpdateLayerPanel => {
|
NodeGraphMessage::UpdateLayerPanel => {
|
||||||
Self::update_layer_panel(network_interface, selection_network_path, collapsed, layers_panel_open, responses);
|
Self::update_layer_panel(network_interface, selection_network_path, layers_panel_open, responses);
|
||||||
}
|
}
|
||||||
NodeGraphMessage::UpdateEdges => {
|
NodeGraphMessage::UpdateEdges => {
|
||||||
// Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes
|
// Update the import/export UI edges whenever the PTZ changes or the bounding box of all nodes changes
|
||||||
|
|
@ -2684,7 +2684,7 @@ impl NodeGraphMessageHandler {
|
||||||
Some(NodeGraphErrorDiagnostic { position, error })
|
Some(NodeGraphErrorDiagnostic { position, error })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], collapsed: &CollapsedLayers, layers_panel_open: bool, responses: &mut VecDeque<Message>) {
|
fn update_layer_panel(network_interface: &NodeNetworkInterface, selection_network_path: &[NodeId], layers_panel_open: bool, responses: &mut VecDeque<Message>) {
|
||||||
if !layers_panel_open {
|
if !layers_panel_open {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2695,14 +2695,8 @@ impl NodeGraphMessageHandler {
|
||||||
.map(|layer| layer.to_node())
|
.map(|layer| layer.to_node())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let mut ancestors_of_selected = HashSet::new();
|
|
||||||
let mut descendants_of_selected = HashSet::new();
|
let mut descendants_of_selected = HashSet::new();
|
||||||
for selected_layer in &selected_layers {
|
for selected_layer in &selected_layers {
|
||||||
for ancestor in LayerNodeIdentifier::new(*selected_layer, network_interface).ancestors(network_interface.document_metadata()) {
|
|
||||||
if ancestor != LayerNodeIdentifier::ROOT_PARENT && ancestor.to_node() != *selected_layer {
|
|
||||||
ancestors_of_selected.insert(ancestor.to_node());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for descendant in LayerNodeIdentifier::new(*selected_layer, network_interface).descendants(network_interface.document_metadata()) {
|
for descendant in LayerNodeIdentifier::new(*selected_layer, network_interface).descendants(network_interface.document_metadata()) {
|
||||||
descendants_of_selected.insert(descendant.to_node());
|
descendants_of_selected.insert(descendant.to_node());
|
||||||
}
|
}
|
||||||
|
|
@ -2727,22 +2721,6 @@ impl NodeGraphMessageHandler {
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
let parents_visible = layer.ancestors(network_interface.document_metadata()).filter(|&ancestor| ancestor != layer).all(|layer| {
|
|
||||||
if layer != LayerNodeIdentifier::ROOT_PARENT {
|
|
||||||
network_interface.document_node(&layer.to_node(), &[]).map(|node| node.visible).unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let parents_unlocked: bool = layer.ancestors(network_interface.document_metadata()).filter(|&ancestor| ancestor != layer).all(|layer| {
|
|
||||||
if layer != LayerNodeIdentifier::ROOT_PARENT {
|
|
||||||
!network_interface.is_locked(&layer.to_node(), &[])
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let clippable = layer.can_be_clipped(network_interface.document_metadata());
|
let clippable = layer.can_be_clipped(network_interface.document_metadata());
|
||||||
|
|
||||||
let data = LayerPanelEntry {
|
let data = LayerPanelEntry {
|
||||||
|
|
@ -2752,18 +2730,9 @@ impl NodeGraphMessageHandler {
|
||||||
alias: network_interface.display_name(&node_id, &[]),
|
alias: network_interface.display_name(&node_id, &[]),
|
||||||
in_selected_network: selection_network_path.is_empty(),
|
in_selected_network: selection_network_path.is_empty(),
|
||||||
children_allowed,
|
children_allowed,
|
||||||
children_present: layer.has_children(network_interface.document_metadata()),
|
|
||||||
expanded: layer.has_children(network_interface.document_metadata()) && !collapsed.0.contains(&layer),
|
|
||||||
depth: layer.ancestors(network_interface.document_metadata()).count() as u32 - 1,
|
|
||||||
visible: network_interface.is_visible(&node_id, &[]),
|
visible: network_interface.is_visible(&node_id, &[]),
|
||||||
parents_visible,
|
|
||||||
unlocked: !network_interface.is_locked(&node_id, &[]),
|
unlocked: !network_interface.is_locked(&node_id, &[]),
|
||||||
parents_unlocked,
|
|
||||||
parent_id: layer
|
|
||||||
.parent(network_interface.document_metadata())
|
|
||||||
.and_then(|parent| if parent != LayerNodeIdentifier::ROOT_PARENT { Some(parent.to_node()) } else { None }),
|
|
||||||
selected: selected_layers.contains(&node_id),
|
selected: selected_layers.contains(&node_id),
|
||||||
ancestor_of_selected: ancestors_of_selected.contains(&node_id),
|
|
||||||
descendant_of_selected: descendants_of_selected.contains(&node_id),
|
descendant_of_selected: descendants_of_selected.contains(&node_id),
|
||||||
clipped: get_clip_mode(layer, network_interface).unwrap_or(false) && clippable,
|
clipped: get_clip_mode(layer, network_interface).unwrap_or(false) && clippable,
|
||||||
clippable,
|
clippable,
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,17 @@ use graph_craft::document::{NodeId, NodeNetwork};
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||||
pub struct LayerStructureEntry {
|
pub struct LayerStructureEntry {
|
||||||
|
/// The node ID of the layer this entry represents.
|
||||||
#[serde(rename = "layerId")]
|
#[serde(rename = "layerId")]
|
||||||
pub layer_id: NodeId,
|
pub layer_id: NodeId,
|
||||||
|
/// The expanded child entries nested within this layer. Empty when the layer is collapsed or has no children.
|
||||||
pub children: Vec<LayerStructureEntry>,
|
pub children: Vec<LayerStructureEntry>,
|
||||||
|
/// Whether this layer has children reachable in the graph, even when they are omitted from `children` because the layer is collapsed.
|
||||||
|
#[serde(rename = "childrenPresent")]
|
||||||
|
pub children_present: bool,
|
||||||
|
/// Whether any descendant layer in the graph is selected, including through collapsed subtrees not listed in `children`.
|
||||||
|
#[serde(rename = "descendantSelected")]
|
||||||
|
pub descendant_selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
|
|
@ -28,21 +36,9 @@ pub struct LayerPanelEntry {
|
||||||
pub in_selected_network: bool,
|
pub in_selected_network: bool,
|
||||||
#[serde(rename = "childrenAllowed")]
|
#[serde(rename = "childrenAllowed")]
|
||||||
pub children_allowed: bool,
|
pub children_allowed: bool,
|
||||||
#[serde(rename = "childrenPresent")]
|
|
||||||
pub children_present: bool,
|
|
||||||
pub expanded: bool,
|
|
||||||
pub depth: u32,
|
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
#[serde(rename = "parentsVisible")]
|
|
||||||
pub parents_visible: bool,
|
|
||||||
pub unlocked: bool,
|
pub unlocked: bool,
|
||||||
#[serde(rename = "parentsUnlocked")]
|
|
||||||
pub parents_unlocked: bool,
|
|
||||||
#[serde(rename = "parentId")]
|
|
||||||
pub parent_id: Option<NodeId>,
|
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
#[serde(rename = "ancestorOfSelected")]
|
|
||||||
pub ancestor_of_selected: bool,
|
|
||||||
#[serde(rename = "descendantOfSelected")]
|
#[serde(rename = "descendantOfSelected")]
|
||||||
pub descendant_of_selected: bool,
|
pub descendant_of_selected: bool,
|
||||||
pub clipped: bool,
|
pub clipped: bool,
|
||||||
|
|
@ -163,4 +159,7 @@ impl SelectedNodes {
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||||
pub struct CollapsedLayers(pub Vec<LayerNodeIdentifier>);
|
/// Tracks which layer instances are collapsed in the Layers panel. Each entry is an "instance path":
|
||||||
|
/// the sequence of ancestor node IDs from the root down to the collapsed layer. This allows the same
|
||||||
|
/// layer appearing under multiple parents to have independent expand/collapse state per instance.
|
||||||
|
pub struct CollapsedLayers(pub Vec<Vec<NodeId>>);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,14 @@
|
||||||
bottomLayer: boolean;
|
bottomLayer: boolean;
|
||||||
editingName: boolean;
|
editingName: boolean;
|
||||||
entry: LayerPanelEntry;
|
entry: LayerPanelEntry;
|
||||||
|
depth: number;
|
||||||
|
parentId: bigint | undefined;
|
||||||
|
childrenPresent: boolean;
|
||||||
|
expanded: boolean;
|
||||||
|
ancestorOfSelected: boolean;
|
||||||
|
parentsVisible: boolean;
|
||||||
|
parentsUnlocked: boolean;
|
||||||
|
instancePath: bigint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type DraggingData = {
|
type DraggingData = {
|
||||||
|
|
@ -28,6 +36,7 @@
|
||||||
insertDepth: number;
|
insertDepth: number;
|
||||||
insertIndex: number | undefined;
|
insertIndex: number | undefined;
|
||||||
highlightFolder: boolean;
|
highlightFolder: boolean;
|
||||||
|
highlightFolderIndex: number | undefined;
|
||||||
markerHeight: number;
|
markerHeight: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -131,10 +140,10 @@
|
||||||
editor.toggleLayerLock(id);
|
editor.toggleLayerLock(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExpandArrowClickWithModifiers(e: MouseEvent, id: bigint) {
|
function handleExpandArrowClickWithModifiers(e: MouseEvent, instancePath: bigint[]) {
|
||||||
const accel = operatingSystem() === "Mac" ? e.metaKey : e.ctrlKey;
|
const accel = operatingSystem() === "Mac" ? e.metaKey : e.ctrlKey;
|
||||||
const collapseRecursive = e.altKey || accel;
|
const collapseRecursive = e.altKey || accel;
|
||||||
editor.toggleLayerExpansion(id, collapseRecursive);
|
editor.toggleLayerExpansion(BigUint64Array.from(instancePath), collapseRecursive);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,6 +280,7 @@
|
||||||
|
|
||||||
// Whether you are inserting into a folder and should show the folder outline
|
// Whether you are inserting into a folder and should show the folder outline
|
||||||
let highlightFolder = false;
|
let highlightFolder = false;
|
||||||
|
let highlightFolderIndex: number | undefined = undefined;
|
||||||
|
|
||||||
let markerHeight = 0;
|
let markerHeight = 0;
|
||||||
const layerPanel = document.querySelector("[data-layer-panel]"); // Selects the element with the data-layer-panel attribute
|
const layerPanel = document.querySelector("[data-layer-panel]"); // Selects the element with the data-layer-panel attribute
|
||||||
|
|
@ -279,40 +289,41 @@
|
||||||
Array.from(treeChildren).forEach((treeChild) => {
|
Array.from(treeChildren).forEach((treeChild) => {
|
||||||
const indexAttribute = treeChild.getAttribute("data-index");
|
const indexAttribute = treeChild.getAttribute("data-index");
|
||||||
if (!indexAttribute) return;
|
if (!indexAttribute) return;
|
||||||
const { folderIndex, entry: layer } = layers[parseInt(indexAttribute, 10)];
|
const listing = layers[parseInt(indexAttribute, 10)];
|
||||||
|
|
||||||
const rect = treeChild.getBoundingClientRect();
|
const rect = treeChild.getBoundingClientRect();
|
||||||
if (rect.top > clientY || rect.bottom < clientY) {
|
if (rect.top > clientY || rect.bottom < clientY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pointerPercentage = (clientY - rect.top) / rect.height;
|
const pointerPercentage = (clientY - rect.top) / rect.height;
|
||||||
if (layer.childrenAllowed) {
|
if (listing.entry.childrenAllowed || listing.childrenPresent) {
|
||||||
if (pointerPercentage < 0.25) {
|
if (pointerPercentage < 0.25) {
|
||||||
insertParentId = layer.parentId;
|
insertParentId = listing.parentId;
|
||||||
insertDepth = layer.depth - 1;
|
insertDepth = listing.depth - 1;
|
||||||
insertIndex = folderIndex;
|
insertIndex = listing.folderIndex;
|
||||||
markerHeight = rect.top - layerPanelTop;
|
markerHeight = rect.top - layerPanelTop;
|
||||||
} else if (pointerPercentage < 0.75 || (layer.childrenPresent && layer.expanded)) {
|
} else if (pointerPercentage < 0.75 || (listing.childrenPresent && listing.expanded)) {
|
||||||
insertParentId = layer.id;
|
insertParentId = listing.entry.id;
|
||||||
insertDepth = layer.depth;
|
insertDepth = listing.depth;
|
||||||
insertIndex = 0;
|
insertIndex = 0;
|
||||||
highlightFolder = true;
|
highlightFolder = true;
|
||||||
|
highlightFolderIndex = parseInt(indexAttribute, 10);
|
||||||
} else {
|
} else {
|
||||||
insertParentId = layer.parentId;
|
insertParentId = listing.parentId;
|
||||||
insertDepth = layer.depth - 1;
|
insertDepth = listing.depth - 1;
|
||||||
insertIndex = folderIndex + 1;
|
insertIndex = listing.folderIndex + 1;
|
||||||
markerHeight = rect.bottom - layerPanelTop;
|
markerHeight = rect.bottom - layerPanelTop;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (pointerPercentage < 0.5) {
|
if (pointerPercentage < 0.5) {
|
||||||
insertParentId = layer.parentId;
|
insertParentId = listing.parentId;
|
||||||
insertDepth = layer.depth - 1;
|
insertDepth = listing.depth - 1;
|
||||||
insertIndex = folderIndex;
|
insertIndex = listing.folderIndex;
|
||||||
markerHeight = rect.top - layerPanelTop;
|
markerHeight = rect.top - layerPanelTop;
|
||||||
} else {
|
} else {
|
||||||
insertParentId = layer.parentId;
|
insertParentId = listing.parentId;
|
||||||
insertDepth = layer.depth - 1;
|
insertDepth = listing.depth - 1;
|
||||||
insertIndex = folderIndex + 1;
|
insertIndex = listing.folderIndex + 1;
|
||||||
markerHeight = rect.bottom - layerPanelTop;
|
markerHeight = rect.bottom - layerPanelTop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -320,7 +331,7 @@
|
||||||
// Dragging to the empty space below all layers
|
// Dragging to the empty space below all layers
|
||||||
let lastLayer = treeChildren[treeChildren.length - 1];
|
let lastLayer = treeChildren[treeChildren.length - 1];
|
||||||
if (lastLayer.getBoundingClientRect().bottom < clientY) {
|
if (lastLayer.getBoundingClientRect().bottom < clientY) {
|
||||||
const numberRootLayers = layers.filter((layer) => layer.entry.depth === 1).length;
|
const numberRootLayers = layers.filter((listing) => listing.depth === 1).length;
|
||||||
insertParentId = undefined;
|
insertParentId = undefined;
|
||||||
insertDepth = 0;
|
insertDepth = 0;
|
||||||
insertIndex = numberRootLayers;
|
insertIndex = numberRootLayers;
|
||||||
|
|
@ -334,6 +345,7 @@
|
||||||
insertDepth,
|
insertDepth,
|
||||||
insertIndex,
|
insertIndex,
|
||||||
highlightFolder,
|
highlightFolder,
|
||||||
|
highlightFolderIndex,
|
||||||
markerHeight,
|
markerHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -493,42 +505,57 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildLayerHierarchy(layerStructure: LayerStructureEntry[]) {
|
function rebuildLayerHierarchy(layerStructure: LayerStructureEntry[]) {
|
||||||
const layerWithNameBeingEdited = layers.find((layer: LayerListingInfo) => layer.editingName);
|
// Track the editing state by flat list index, not layer ID, since a layer can appear at multiple positions
|
||||||
const layerIdWithNameBeingEdited = layerWithNameBeingEdited?.entry.id;
|
const editingIndex = layers.findIndex((layer: LayerListingInfo) => layer.editingName);
|
||||||
|
|
||||||
// Clear the layer hierarchy before rebuilding it
|
// Clear the layer hierarchy before rebuilding it
|
||||||
layers = [];
|
layers = [];
|
||||||
|
|
||||||
// Build the new layer hierarchy
|
// Build the new layer hierarchy
|
||||||
const recurse = (children: LayerStructureEntry[]) => {
|
const recurse = (children: LayerStructureEntry[], depth: number, parentId: bigint | undefined, parentPath: bigint[], parentsVisible: boolean, parentsUnlocked: boolean) => {
|
||||||
children.forEach((item, index) => {
|
children.forEach((item, index) => {
|
||||||
|
const instancePath = [...parentPath, item.layerId];
|
||||||
const mapping = layerCache.get(String(item.layerId));
|
const mapping = layerCache.get(String(item.layerId));
|
||||||
|
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
mapping.id = item.layerId;
|
mapping.id = item.layerId;
|
||||||
layers.push({
|
layers.push({
|
||||||
folderIndex: index,
|
folderIndex: index,
|
||||||
bottomLayer: index === children.length - 1,
|
bottomLayer: index === children.length - 1,
|
||||||
entry: mapping,
|
entry: mapping,
|
||||||
editingName: layerIdWithNameBeingEdited === item.layerId,
|
editingName: editingIndex === layers.length,
|
||||||
|
depth,
|
||||||
|
parentId,
|
||||||
|
childrenPresent: item.childrenPresent,
|
||||||
|
expanded: item.childrenPresent && item.children.length > 0,
|
||||||
|
ancestorOfSelected: item.descendantSelected,
|
||||||
|
parentsVisible,
|
||||||
|
parentsUnlocked,
|
||||||
|
instancePath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call self recursively if there are any children
|
// Call self recursively, propagating this layer's visibility/lock state to its children
|
||||||
if (item.children.length >= 1) recurse(item.children);
|
const childParentsVisible = parentsVisible && (mapping?.visible ?? true);
|
||||||
|
const childParentsUnlocked = parentsUnlocked && (mapping?.unlocked ?? true);
|
||||||
|
if (item.children.length >= 1) recurse(item.children, depth + 1, item.layerId, instancePath, childParentsVisible, childParentsUnlocked);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
recurse(layerStructure);
|
recurse(layerStructure, 1, undefined, [], true, true);
|
||||||
layers = layers;
|
layers = layers;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLayerInTree(targetId: bigint, targetLayer: LayerPanelEntry) {
|
function updateLayerInTree(targetId: bigint, targetLayer: LayerPanelEntry) {
|
||||||
layerCache.set(String(targetId), targetLayer);
|
layerCache.set(String(targetId), targetLayer);
|
||||||
|
|
||||||
const layer = layers.find((layer: LayerListingInfo) => layer.entry.id === targetId);
|
let changed = false;
|
||||||
if (layer) {
|
layers.forEach((layer) => {
|
||||||
layer.entry = targetLayer;
|
if (layer.entry.id === targetId) {
|
||||||
layers = layers;
|
layer.entry = targetLayer;
|
||||||
}
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) layers = layers;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -556,29 +583,29 @@
|
||||||
class="layer"
|
class="layer"
|
||||||
classes={{
|
classes={{
|
||||||
selected,
|
selected,
|
||||||
"ancestor-of-selected": listing.entry.ancestorOfSelected,
|
"ancestor-of-selected": listing.ancestorOfSelected,
|
||||||
"descendant-of-selected": listing.entry.descendantOfSelected,
|
"descendant-of-selected": listing.entry.descendantOfSelected,
|
||||||
"selected-but-not-in-selected-network": selected && !listing.entry.inSelectedNetwork,
|
"selected-but-not-in-selected-network": selected && !listing.entry.inSelectedNetwork,
|
||||||
"insert-folder": (draggingData?.highlightFolder || false) && draggingData?.insertParentId === listing.entry.id,
|
"insert-folder": (draggingData?.highlightFolder || false) && draggingData?.highlightFolderIndex === index,
|
||||||
}}
|
}}
|
||||||
styles={{ "--layer-indent-levels": `${listing.entry.depth - 1}` }}
|
styles={{ "--layer-indent-levels": `${listing.depth - 1}` }}
|
||||||
data-layer
|
data-layer
|
||||||
data-index={index}
|
data-index={index}
|
||||||
on:pointerdown={(e) => layerPointerDown(e, listing)}
|
on:pointerdown={(e) => layerPointerDown(e, listing)}
|
||||||
on:click={(e) => selectLayerWithModifiers(e, listing)}
|
on:click={(e) => selectLayerWithModifiers(e, listing)}
|
||||||
>
|
>
|
||||||
{#if listing.entry.childrenAllowed}
|
{#if listing.entry.childrenAllowed || listing.childrenPresent}
|
||||||
<button
|
<button
|
||||||
class="expand-arrow"
|
class="expand-arrow"
|
||||||
class:expanded={listing.entry.expanded}
|
class:expanded={listing.expanded}
|
||||||
disabled={!listing.entry.childrenPresent}
|
disabled={!listing.childrenPresent}
|
||||||
data-tooltip-label={listing.entry.expanded ? "Collapse (All)" : "Expand (All)"}
|
data-tooltip-label={listing.expanded ? "Collapse (All)" : "Expand (All)"}
|
||||||
data-tooltip-description={(listing.entry.expanded
|
data-tooltip-description={(listing.expanded
|
||||||
? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)"
|
? "Hide this layer's children. (To recursively collapse all descendants, perform the shortcut shown.)"
|
||||||
: "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") +
|
: "Show this layer's children. (To recursively expand all descendants, perform the shortcut shown.)") +
|
||||||
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")}
|
(listing.ancestorOfSelected && !listing.expanded ? "\n\nA selected layer is currently contained within.\n" : "")}
|
||||||
data-tooltip-shortcut={$tooltip.altClickShortcut?.shortcut ? JSON.stringify($tooltip.altClickShortcut.shortcut) : undefined}
|
data-tooltip-shortcut={$tooltip.altClickShortcut?.shortcut ? JSON.stringify($tooltip.altClickShortcut.shortcut) : undefined}
|
||||||
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
|
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.instancePath)}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
></button>
|
></button>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -628,27 +655,27 @@
|
||||||
on:change={(e) => onEditLayerNameChange(listing, e)}
|
on:change={(e) => onEditLayerNameChange(listing, e)}
|
||||||
/>
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
{#if !listing.entry.unlocked || !listing.entry.parentsUnlocked}
|
{#if !listing.entry.unlocked || !listing.parentsUnlocked}
|
||||||
<IconButton
|
<IconButton
|
||||||
class="status-toggle"
|
class="status-toggle"
|
||||||
classes={{ inherited: !listing.entry.parentsUnlocked }}
|
classes={{ inherited: !listing.parentsUnlocked }}
|
||||||
action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
|
action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
|
||||||
size={24}
|
size={24}
|
||||||
icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"}
|
icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"}
|
||||||
hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"}
|
hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"}
|
||||||
tooltipLabel={listing.entry.unlocked ? "Lock" : "Unlock"}
|
tooltipLabel={listing.entry.unlocked ? "Lock" : "Unlock"}
|
||||||
tooltipDescription={!listing.entry.parentsUnlocked ? "A parent of this layer is locked and that status is being inherited." : ""}
|
tooltipDescription={!listing.parentsUnlocked ? "A parent of this layer is locked and that status is being inherited." : ""}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<IconButton
|
<IconButton
|
||||||
class="status-toggle"
|
class="status-toggle"
|
||||||
classes={{ inherited: !listing.entry.parentsVisible }}
|
classes={{ inherited: !listing.parentsVisible }}
|
||||||
action={(e) => (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())}
|
action={(e) => (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())}
|
||||||
size={24}
|
size={24}
|
||||||
icon={listing.entry.visible ? "EyeVisible" : "EyeHidden"}
|
icon={listing.entry.visible ? "EyeVisible" : "EyeHidden"}
|
||||||
hoverIcon={listing.entry.visible ? "EyeHide" : "EyeShow"}
|
hoverIcon={listing.entry.visible ? "EyeHide" : "EyeShow"}
|
||||||
tooltipLabel={listing.entry.visible ? "Hide" : "Show"}
|
tooltipLabel={listing.entry.visible ? "Hide" : "Show"}
|
||||||
tooltipDescription={!listing.entry.parentsVisible ? "A parent of this layer is hidden and that status is being inherited." : ""}
|
tooltipDescription={!listing.parentsVisible ? "A parent of this layer is hidden and that status is being inherited." : ""}
|
||||||
/>
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -737,9 +764,13 @@
|
||||||
background: rgba(var(--color-4-dimgray-rgb), 0.5);
|
background: rgba(var(--color-4-dimgray-rgb), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.insert-folder {
|
&.insert-folder::after {
|
||||||
outline: 3px solid var(--color-e-nearwhite);
|
content: "";
|
||||||
outline-offset: -3px;
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 3px solid var(--color-e-nearwhite);
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-arrow {
|
.expand-arrow {
|
||||||
|
|
|
||||||
|
|
@ -858,9 +858,9 @@ impl EditorWrapper {
|
||||||
|
|
||||||
/// Toggle expansions state of a layer from the layer list
|
/// Toggle expansions state of a layer from the layer list
|
||||||
#[wasm_bindgen(js_name = toggleLayerExpansion)]
|
#[wasm_bindgen(js_name = toggleLayerExpansion)]
|
||||||
pub fn toggle_layer_expansion(&self, id: u64, recursive: bool) {
|
pub fn toggle_layer_expansion(&self, instance_path: &[u64], recursive: bool) {
|
||||||
let id = NodeId(id);
|
let instance_path = instance_path.iter().map(|&id| NodeId(id)).collect();
|
||||||
let message = DocumentMessage::ToggleLayerExpansion { id, recursive };
|
let message = DocumentMessage::ToggleLayerExpansion { instance_path, recursive };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue