{"record":{"id":"e402c8cc53fb9dfe","repo":"GraphiteEditor/Graphite","slug":"node-id-0-should-be-a-layer","errorCode":null,"errorMessage":"Node Id 0 should be a layer","messagePattern":"Node Id 0 should be a layer","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/tool/tool_messages/select_tool.rs","lineNumber":633,"sourceCode":"\t\t\tlet mut copy_ids = HashMap::new();\n\t\t\tlet node_id = layer.to_node();\n\t\t\tcopy_ids.insert(node_id, NodeId(0));\n\n\t\t\tdocument\n\t\t\t\t.network_interface\n\t\t\t\t.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::LayerChildrenUpstreamFlow)\n\t\t\t\t.enumerate()\n\t\t\t\t.for_each(|(index, node_id)| {\n\t\t\t\t\tcopy_ids.insert(node_id, NodeId((index + 1) as u64));\n\t\t\t\t});\n\n\t\t\tlet nodes = document.network_interface.copy_nodes(&copy_ids, &[]).collect::<Vec<(NodeId, NodeTemplate)>>();\n\n\t\t\tlet insert_index = DocumentMessageHandler::get_calculated_insert_index(document.metadata(), &SelectedNodes(vec![layer.to_node()]), parent);\n\n\t\t\tlet new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();\n\n\t\t\tlet layer_id = *new_ids.get(&NodeId(0)).expect(\"Node Id 0 should be a layer\");\n\t\t\tlet layer = LayerNodeIdentifier::new_unchecked(layer_id);\n\t\t\tnew_dragging.push(layer);\n\t\t\tresponses.add(NodeGraphMessage::AddNodes { nodes, new_ids });\n\t\t\tresponses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });\n\t\t}\n\t\tlet nodes = new_dragging.iter().map(|layer| layer.to_node()).collect();\n\t\tresponses.add(NodeGraphMessage::SelectedNodesSet { nodes });\n\t\tresponses.add(NodeGraphMessage::RunDocumentGraph);\n\t\tself.layers_dragging = new_dragging;\n\t}\n\n\t/// Removes the duplicated layers. Called when Alt is released and the layers have previously been duplicated.\n\tfn stop_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {\n\t\tlet Some(original) = self.non_duplicated_layers.take() else {\n\t\t\treturn;\n\t\t};\n\n\t\t// Delete the duplicated layers","sourceCodeStart":615,"sourceCodeEnd":651,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/tool/tool_messages/select_tool.rs#L615-L651","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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).","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.","Check that the layer identifier passed in actually corresponds to a node in document.network_interface metadata (defend against desynced LayerNodeIdentifiers)."],"exampleFix":"// before\nlet layer_id = *new_ids.get(&NodeId(0)).expect(\"Node Id 0 should be a layer\");\n\n// after\nlet Some(&layer_id) = new_ids.get(&NodeId(0)) else {\n\tlog::warn!(\"select tool: layer {:?} produced no copyable root node; skipping alt-drag duplication\", layer);\n\tcontinue;\n};","handlingStrategy":"validation","validationCode":"let Some(&layer_id) = new_ids.get(&NodeId(0)) else {\n\t// root layer node was not copied; abort this layer's duplication safely\n\tcontinue;\n};\nlet layer = LayerNodeIdentifier::new_unchecked(layer_id);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate that an upstream-flow copy produced the expected root entry before indexing remap maps by fixed ids.","Test alt-drag duplication after any change to copy_nodes filters or upstream flow semantics.","Defend incoming LayerNodeIdentifiers against metadata desync by checking existence before duplicating."],"tags":["rust","graphite-editor","select-tool","missing-map-key","layer-duplication","expect-panic","node-copy"],"backgroundTag":"missing-map-key","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}