mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 11:28:13 +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,
|
context: OverlayContext,
|
||||||
},
|
},
|
||||||
DuplicateSelectedLayers,
|
DuplicateSelectedLayers,
|
||||||
|
DuplicateSelectedLayersTo {
|
||||||
|
parent: LayerNodeIdentifier,
|
||||||
|
insert_index: usize,
|
||||||
|
},
|
||||||
EnterNestedNetwork {
|
EnterNestedNetwork {
|
||||||
node_id: NodeId,
|
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)
|
DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &SelectedNodes(vec![layer.to_node()]), parent)
|
||||||
});
|
});
|
||||||
|
|
||||||
for layer in layers.into_iter().rev() {
|
for original_layer in layers.into_iter().rev() {
|
||||||
let Some(parent) = layer.parent(self.metadata()) else { continue };
|
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 Some(new_layer) = self.duplicate_layer(original_layer, responses) else { continue };
|
||||||
let mut copy_ids = HashMap::new();
|
new_dragging.push(new_layer);
|
||||||
let node_id = layer.to_node();
|
responses.add(NodeGraphMessage::MoveLayerToStack {
|
||||||
copy_ids.insert(node_id, NodeId(0));
|
layer: new_layer,
|
||||||
|
parent,
|
||||||
self.network_interface
|
insert_index,
|
||||||
.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 nodes = new_dragging.iter().map(|layer| layer.to_node()).collect();
|
let nodes = new_dragging.iter().map(|layer| layer.to_node()).collect();
|
||||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
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 } => {
|
DocumentMessage::EnterNestedNetwork { node_id } => {
|
||||||
self.breadcrumb_network_path.push(node_id);
|
self.breadcrumb_network_path.push(node_id);
|
||||||
self.selection_network_path.clone_from(&self.breadcrumb_network_path);
|
self.selection_network_path.clone_from(&self.breadcrumb_network_path);
|
||||||
@@ -2218,6 +2269,30 @@ impl DocumentMessageHandler {
|
|||||||
.unwrap_or(0)
|
.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(
|
pub fn group_layers(
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
insert_index: usize,
|
insert_index: usize,
|
||||||
|
|||||||
@@ -349,6 +349,7 @@
|
|||||||
if (distance > DRAG_THRESHOLD) {
|
if (distance > DRAG_THRESHOLD) {
|
||||||
internalDragState.active = true;
|
internalDragState.active = true;
|
||||||
dragInPanel = true;
|
dragInPanel = true;
|
||||||
|
layerToClipUponClick = undefined;
|
||||||
|
|
||||||
const layer = internalDragState.listing.entry;
|
const layer = internalDragState.listing.entry;
|
||||||
if (!$nodeGraph.selected.includes(layer.id)) {
|
if (!$nodeGraph.selected.includes(layer.id)) {
|
||||||
@@ -381,7 +382,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function draggingPointerUp() {
|
function draggingPointerUp(e: PointerEvent) {
|
||||||
if (internalDragState?.active && dragDropTarget) {
|
if (internalDragState?.active && dragDropTarget) {
|
||||||
// Ensure the dragged layer is part of the selection, matching the move-in-tree behavior
|
// 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);
|
if (!$nodeGraph.selected.includes(internalDragState.layerId)) selectLayer(internalDragState.listing, false, false);
|
||||||
@@ -393,9 +394,12 @@
|
|||||||
} else if (internalDragState?.active && draggingData) {
|
} else if (internalDragState?.active && draggingData) {
|
||||||
const { select, insertParentId, insertIndex } = draggingData;
|
const { select, insertParentId, insertIndex } = draggingData;
|
||||||
|
|
||||||
// Commit the move
|
// Ensure the dragged layer is part of the selection before committing
|
||||||
select?.();
|
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
|
// Prevent the subsequent click event from processing
|
||||||
justFinishedDrag = true;
|
justFinishedDrag = true;
|
||||||
|
|||||||
@@ -756,6 +756,18 @@ impl EditorWrapper {
|
|||||||
self.dispatch(message);
|
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
|
/// Set the name for the layer
|
||||||
#[wasm_bindgen(js_name = setLayerName)]
|
#[wasm_bindgen(js_name = setLayerName)]
|
||||||
pub fn set_layer_name(&self, id: u64, name: String) {
|
pub fn set_layer_name(&self, id: u64, name: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user