Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ pub enum DocumentMessage {
context: OverlayContext,
},
DuplicateSelectedLayers,
DuplicateSelectedLayersTo {
parent: LayerNodeIdentifier,
insert_index: usize,
},
EnterNestedNetwork {
node_id: NodeId,
},
Expand Down
117 changes: 96 additions & 21 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,34 +449,85 @@ 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);

let Some(new_layer) = self.duplicate_layer(original_layer, responses) else { continue };
new_dragging.push(new_layer);
responses.add(NodeGraphMessage::MoveLayerToStack {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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;
}

// Copy the layer
let mut copy_ids = HashMap::new();
let node_id = layer.to_node();
copy_ids.insert(node_id, NodeId(0));
// 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;
}

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 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 nodes = self.network_interface.copy_nodes(&copy_ids, &[]).collect::<Vec<(NodeId, NodeTemplate)>>();
let layers_to_duplicate = self.network_interface.shallowest_unique_layers_sorted(&self.selection_network_path);
if layers_to_duplicate.is_empty() {
return;
}

let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &SelectedNodes(vec![layer.to_node()]), parent);
responses.add(DocumentMessage::AddTransaction);

let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
let mut new_layers = Vec::new();
for layer in layers_to_duplicate {
let Some(new_layer) = self.duplicate_layer(layer, responses) else { continue };

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 });
// 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_dragging.iter().map(|layer| layer.to_node()).collect();

let nodes = new_layers.iter().map(|layer| layer.to_node()).collect();
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
responses.add(NodeGraphMessage::RunDocumentGraph);
}
Expand Down Expand Up @@ -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(&copy_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,
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/components/panels/Layers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions frontend/wrapper/src/editor_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading