mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add Alt+drag layer duplication to the Layers panel (#4216)
* Add Alt+drag layer duplication to the Layers panel * Code review
This commit is contained in:
@@ -60,6 +60,10 @@ pub enum DocumentMessage {
|
||||
context: OverlayContext,
|
||||
},
|
||||
DuplicateSelectedLayers,
|
||||
DuplicateSelectedLayersTo {
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
},
|
||||
EnterNestedNetwork {
|
||||
node_id: NodeId,
|
||||
},
|
||||
|
||||
@@ -449,37 +449,88 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &SelectedNodes(vec![layer.to_node()]), parent)
|
||||
});
|
||||
|
||||
for layer in layers.into_iter().rev() {
|
||||
let Some(parent) = layer.parent(self.metadata()) else { continue };
|
||||
for original_layer in layers.into_iter().rev() {
|
||||
let Some(parent) = original_layer.parent(self.metadata()) else { continue };
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &SelectedNodes(vec![original_layer.to_node()]), parent);
|
||||
|
||||
// Copy the layer
|
||||
let mut copy_ids = HashMap::new();
|
||||
let node_id = layer.to_node();
|
||||
copy_ids.insert(node_id, NodeId(0));
|
||||
|
||||
self.network_interface
|
||||
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::LayerChildrenUpstreamFlow)
|
||||
.enumerate()
|
||||
.for_each(|(index, node_id)| {
|
||||
copy_ids.insert(node_id, NodeId((index + 1) as u64));
|
||||
});
|
||||
|
||||
let nodes = self.network_interface.copy_nodes(©_ids, &[]).collect::<Vec<(NodeId, NodeTemplate)>>();
|
||||
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &SelectedNodes(vec![layer.to_node()]), parent);
|
||||
|
||||
let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
||||
|
||||
let layer_id = *new_ids.get(&NodeId(0)).expect("Node Id 0 should be a layer");
|
||||
let layer = LayerNodeIdentifier::new_unchecked(layer_id);
|
||||
new_dragging.push(layer);
|
||||
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids });
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
|
||||
let Some(new_layer) = self.duplicate_layer(original_layer, responses) else { continue };
|
||||
new_dragging.push(new_layer);
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack {
|
||||
layer: new_layer,
|
||||
parent,
|
||||
insert_index,
|
||||
});
|
||||
}
|
||||
let nodes = new_dragging.iter().map(|layer| layer.to_node()).collect();
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
DocumentMessage::DuplicateSelectedLayersTo { parent, insert_index } => {
|
||||
if !self.selection_network_path.is_empty() {
|
||||
log::error!("Duplicating selected layers is only supported for the document network");
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirror the placement constraints enforced when moving layers so a copy can't land somewhere a move couldn't
|
||||
let any_artboards = self
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_layers(self.metadata())
|
||||
.any(|layer| self.network_interface.is_artboard(&layer.to_node(), &self.selection_network_path));
|
||||
if any_artboards && parent != LayerNodeIdentifier::ROOT_PARENT {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_any_non_artboards = self
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_layers(self.metadata())
|
||||
.any(|layer| !self.network_interface.is_artboard(&layer.to_node(), &self.selection_network_path));
|
||||
let top_level_artboards = LayerNodeIdentifier::ROOT_PARENT
|
||||
.children(self.metadata())
|
||||
.any(|layer| self.network_interface.is_artboard(&layer.to_node(), &self.selection_network_path));
|
||||
if selected_any_non_artboards && parent == LayerNodeIdentifier::ROOT_PARENT && top_level_artboards {
|
||||
return;
|
||||
}
|
||||
|
||||
let layers_to_duplicate = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path);
|
||||
if layers_to_duplicate.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
||||
let mut new_layers = Vec::new();
|
||||
for layer in layers_to_duplicate {
|
||||
let Some(new_layer) = self.duplicate_layer(layer, responses) else { continue };
|
||||
|
||||
// Insert each copy one slot below the previous so the duplicates keep their original top-to-bottom order
|
||||
let placement_index = insert_index + new_layers.len();
|
||||
new_layers.push(new_layer);
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack {
|
||||
layer: new_layer,
|
||||
parent,
|
||||
insert_index: placement_index,
|
||||
});
|
||||
|
||||
// Compensate the local transform so a copy dropped into a differently-transformed parent stays put in world space
|
||||
if layer.parent(self.metadata()) != Some(parent) {
|
||||
let layer_world_transform = self.network_interface.document_metadata().transform_to_viewport(layer);
|
||||
let undo_parent_transform = self.network_interface.document_metadata().transform_to_viewport(parent).inverse();
|
||||
|
||||
responses.add(GraphOperationMessage::TransformSet {
|
||||
layer: new_layer,
|
||||
transform: undo_parent_transform * layer_world_transform,
|
||||
transform_in: TransformIn::Local,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let nodes = new_layers.iter().map(|layer| layer.to_node()).collect();
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
DocumentMessage::EnterNestedNetwork { node_id } => {
|
||||
self.breadcrumb_network_path.push(node_id);
|
||||
self.selection_network_path.clone_from(&self.breadcrumb_network_path);
|
||||
@@ -2218,6 +2269,30 @@ impl DocumentMessageHandler {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Copies `layer` together with its full upstream node chain, queueing the new nodes with freshly minted IDs.
|
||||
/// Returns the new layer's identifier; the caller places it into the stack with a `MoveLayerToStack` response.
|
||||
fn duplicate_layer(&mut self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) -> Option<LayerNodeIdentifier> {
|
||||
let mut copy_ids = HashMap::new();
|
||||
copy_ids.insert(layer.to_node(), NodeId(0));
|
||||
self.network_interface
|
||||
.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::LayerChildrenUpstreamFlow)
|
||||
.enumerate()
|
||||
.for_each(|(index, node_id)| {
|
||||
copy_ids.insert(node_id, NodeId((index + 1) as u64));
|
||||
});
|
||||
|
||||
let nodes = self.network_interface.copy_nodes(©_ids, &[]).collect::<Vec<(NodeId, NodeTemplate)>>();
|
||||
let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
||||
|
||||
let Some(&new_layer_id) = new_ids.get(&NodeId(0)) else {
|
||||
log::error!("Could not duplicate layer because its root node copy is missing");
|
||||
return None;
|
||||
};
|
||||
responses.add(NodeGraphMessage::AddNodes { nodes, new_ids });
|
||||
|
||||
Some(LayerNodeIdentifier::new_unchecked(new_layer_id))
|
||||
}
|
||||
|
||||
pub fn group_layers(
|
||||
responses: &mut VecDeque<Message>,
|
||||
insert_index: usize,
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
if (distance > DRAG_THRESHOLD) {
|
||||
internalDragState.active = true;
|
||||
dragInPanel = true;
|
||||
layerToClipUponClick = undefined;
|
||||
|
||||
const layer = internalDragState.listing.entry;
|
||||
if (!$nodeGraph.selected.includes(layer.id)) {
|
||||
@@ -381,7 +382,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function draggingPointerUp() {
|
||||
function draggingPointerUp(e: PointerEvent) {
|
||||
if (internalDragState?.active && dragDropTarget) {
|
||||
// Ensure the dragged layer is part of the selection, matching the move-in-tree behavior
|
||||
if (!$nodeGraph.selected.includes(internalDragState.layerId)) selectLayer(internalDragState.listing, false, false);
|
||||
@@ -393,9 +394,12 @@
|
||||
} else if (internalDragState?.active && draggingData) {
|
||||
const { select, insertParentId, insertIndex } = draggingData;
|
||||
|
||||
// Commit the move
|
||||
// Ensure the dragged layer is part of the selection before committing
|
||||
select?.();
|
||||
editor.moveLayerInTree(insertParentId, insertIndex);
|
||||
|
||||
// Holding Alt drops a duplicate of the selection at the target instead of moving the originals
|
||||
if (e.altKey) editor.duplicateLayerInTree(insertParentId, insertIndex);
|
||||
else editor.moveLayerInTree(insertParentId, insertIndex);
|
||||
|
||||
// Prevent the subsequent click event from processing
|
||||
justFinishedDrag = true;
|
||||
|
||||
@@ -756,6 +756,18 @@ impl EditorWrapper {
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Duplicate the selected layers, placing the copies within the given folder at the given index.
|
||||
/// If the folder is `None`, they are inserted into the document root.
|
||||
/// If the insert index is `None`, they are inserted at the start of the folder.
|
||||
#[wasm_bindgen(js_name = duplicateLayerInTree)]
|
||||
pub fn duplicate_layer_in_tree(&self, insert_parent_id: Option<u64>, insert_index: Option<usize>) {
|
||||
let message = DocumentMessage::DuplicateSelectedLayersTo {
|
||||
parent: insert_parent_id.map(NodeId).map(LayerNodeIdentifier::new_unchecked).unwrap_or_default(),
|
||||
insert_index: insert_index.unwrap_or_default(),
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Set the name for the layer
|
||||
#[wasm_bindgen(js_name = setLayerName)]
|
||||
pub fn set_layer_name(&self, id: u64, name: String) {
|
||||
|
||||
Reference in New Issue
Block a user