GraphiteEditor/Graphite · error

Node Id 0 should be a layer

Error message

Node Id 0 should be a layer

What it means

Alt-drag layer duplication copies the clicked layer's upstream subgraph with remapped ids: upstream_flow_back_from_nodes numbers nodes from the clicked layer (which becomes NodeId(0)), copy_nodes produces the templates, and new_ids maps each copied id to a fresh NodeId. The expect asserts that the copy set contains the root entry NodeId(0) — i.e., that the clicked layer node itself was copied. It panics when copy_nodes skipped the layer node (filtered as non-copyable) or the upstream flow enumeration excluded it, leaving the id map without the 0 key.

Source

Thrown at editor/src/messages/tool/tool_messages/select_tool.rs:633

			let mut copy_ids = HashMap::new();
			let node_id = layer.to_node();
			copy_ids.insert(node_id, NodeId(0));

			document
				.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 = document.network_interface.copy_nodes(&copy_ids, &[]).collect::<Vec<(NodeId, NodeTemplate)>>();

			let insert_index = DocumentMessageHandler::get_calculated_insert_index(document.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();
		responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
		responses.add(NodeGraphMessage::RunDocumentGraph);
		self.layers_dragging = new_dragging;
	}

	/// Removes the duplicated layers. Called when Alt is released and the layers have previously been duplicated.
	fn stop_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
		let Some(original) = self.non_duplicated_layers.take() else {
			return;
		};

		// Delete the duplicated layers

View on GitHub (pinned to c507b35645)

Solutions

  1. Guard the lookup: use if let Some(&layer_id) = new_ids.get(&NodeId(0)) and skip the duplication (log a warning) when absent, instead of panicking mid-drag.
  2. Debug why the root was skipped: print copy_ids and the ids returned by copy_nodes for the clicked layer to find which filter drops NodeId(0).
  3. If a node class is now intentionally non-copyable, early-out in duplicate_layers... before building copy_ids when the layer's node type is not copyable.
  4. Check that the layer identifier passed in actually corresponds to a node in document.network_interface metadata (defend against desynced LayerNodeIdentifiers).

Example fix

// before
let layer_id = *new_ids.get(&NodeId(0)).expect("Node Id 0 should be a layer");

// after
let Some(&layer_id) = new_ids.get(&NodeId(0)) else {
	log::warn!("select tool: layer {:?} produced no copyable root node; skipping alt-drag duplication", layer);
	continue;
};
Defensive patterns

Strategy: validation

Validate before calling

let Some(&layer_id) = new_ids.get(&NodeId(0)) else {
	// root layer node was not copied; abort this layer's duplication safely
	continue;
};
let layer = LayerNodeIdentifier::new_unchecked(layer_id);

Prevention

When it happens

Trigger: Holding Alt and starting to drag a layer: duplicate_layers_into_current_document builds copy_ids, calls document.network_interface.copy_nodes(&copy_ids, &[]), then new_ids.get(&NodeId(0)).expect("Node Id 0 should be a layer"); the get returns None when the copied Vec<(NodeId, NodeTemplate)> has no NodeId(0) entry.

Common situations: Layer node types made non-copyable during a node-graph refactor; metadata desync where the clicked layer's node is no longer in the flow computed by upstream_flow_back_from_nodes; dragging a synthetic layer (e.g., artboard wrappers or locked/impermanent nodes) whose node is excluded by copy filters; version drift between an old document and new copy semantics.

Related errors


AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16). Data as JSON: /api/errors/e402c8cc53fb9dfe. Report an issue: GitHub.