{"record":{"id":"e4ad5244decbc25b","repo":"GraphiteEditor/Graphite","slug":"no-min","errorCode":null,"errorMessage":"No min","messagePattern":"No min","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"editor/src/messages/tool/tool_messages/gradient_tool.rs","lineNumber":1285,"sourceCode":"\n\t\t\t\t// The gradient has only one point and so should become a fill\n\t\t\t\tif selected_gradient.gradient.len() == 1 {\n\t\t\t\t\tif selected_gradient.is_gradient_chain {\n\t\t\t\t\t\tselected_gradient.render_gradient(responses);\n\t\t\t\t\t} else if let Some(layer) = selected_gradient.layer {\n\t\t\t\t\t\tresponses.add(GraphOperationMessage::FillColorSet {\n\t\t\t\t\t\t\tlayer,\n\t\t\t\t\t\t\tcolor: Some(selected_gradient.gradient.color(0).unwrap_or(Color::BLACK)),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tresponses.add(DocumentMessage::CommitTransaction);\n\t\t\t\t\tresponses.add(PropertiesPanelMessage::Refresh);\n\t\t\t\t\treturn ready_default;\n\t\t\t\t}\n\n\t\t\t\t// Find the minimum and maximum positions\n\t\t\t\tlet positions = selected_gradient.gradient.positions(selected_gradient.appearance.settings.cyclic);\n\t\t\t\tlet min_position = positions.iter().copied().reduce(f64::min).expect(\"No min\");\n\t\t\t\tlet max_position = positions.iter().copied().reduce(f64::max).expect(\"No max\");\n\n\t\t\t\tlet gradient_transform = selected_gradient.appearance.transform;\n\t\t\t\tlet (local_start, local_end) = (gradient_transform.transform_point2(DVec2::ZERO), gradient_transform.transform_point2(DVec2::X));\n\t\t\t\tselected_gradient.appearance.transform = build_transform_with_y_preservation(gradient_transform, local_start.lerp(local_end, min_position), local_start.lerp(local_end, max_position));\n\n\t\t\t\t// Remap the positions\n\t\t\t\tlet remapped: Vec<f64> = positions.into_iter().map(|position| (position - min_position) / (max_position - min_position)).collect();\n\t\t\t\tselected_gradient.gradient.set_positions(&remapped);\n\n\t\t\t\t// Render the new gradient\n\t\t\t\tselected_gradient.render_gradient(responses);\n\t\t\t\tresponses.add(DocumentMessage::CommitTransaction);\n\t\t\t\tresponses.add(PropertiesPanelMessage::Refresh);\n\t\t\t\ttool_data.selected_gradient = None;\n\n\t\t\t\tready_default\n\t\t\t}","sourceCodeStart":1267,"sourceCodeEnd":1303,"githubUrl":"https://github.com/GraphiteEditor/Graphite/blob/c507b356453361e31638b8bff8f6d46b6da2961e/editor/src/messages/tool/tool_messages/gradient_tool.rs#L1267-L1303","documentation":"While finishing a gradient edit, the tool normalizes the gradient by collecting all stop positions via gradient.positions(cyclic) and reducing them to min/max. Rust's Iterator::reduce returns None for an empty iterator, and the expect(\"No min\") turns that into a panic — so this error means the selected gradient reported zero stops. A valid gradient always has at least two stops, so an empty positions vector indicates degenerate or corrupted gradient data (bad import, malformed document, or a partial undo that left the gradient empty). It fires at the end of a gradient drag interaction when the stops are remapped to 0..1.","triggerScenarios":"Dragging a gradient's stops/endpoints in the gradient tool until the finalize path runs: selected_gradient.gradient.positions(settings.cyclic) returns an empty Vec, then positions.iter().copied().reduce(f64::min).expect(\"No min\") panics before build_transform_with_y_preservation can run.","commonSituations":"Documents from older Graphite versions or imports where the gradient stops array was never populated; a gradient whose stops were all deleted by a previous editing operation; undo/redo restoring a gradient sub-state with positions removed but the parent gradient kept; test fixtures that construct Gradient instances with no stops.","solutions":["Guard the empty case before reducing: if positions.is_empty(), skip normalization (or bail out of the interaction) instead of expecting.","Validate gradient stop count at the boundary: reject or repair gradients with fewer than two stops when loading documents or deserializing gradient inputs.","If a corrupt gradient is found in the wild, reproduce with the document file and inspect gradient.positions() output before the drag finishes to find which writer produced zero stops.","Prefer a two-stop default ([0.0, 1.0]) when repairing so downstream remapping math (division by max-min) stays well-defined."],"exampleFix":"// before\nlet min_position = positions.iter().copied().reduce(f64::min).expect(\"No min\");\nlet max_position = positions.iter().copied().reduce(f64::max).expect(\"No max\");\n\n// after\nif positions.len() < 2 {\n\tlog::warn!(\"gradient has {} stops; skipping normalization\", positions.len());\n\treturn ready_default;\n}\nlet min_position = positions.iter().copied().reduce(f64::min).expect(\"No min\");\nlet max_position = positions.iter().copied().reduce(f64::max).expect(\"No max\");","handlingStrategy":"validation","validationCode":"let positions = selected_gradient.gradient.positions(selected_gradient.appearance.settings.cyclic);\nif positions.len() < 2 {\n\t// degenerate gradient: skip normalization, keep tool state stable\n\tselected_gradient.render_gradient(responses);\n\treturn ready_default;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never call Iterator::reduce + expect without first checking the collection is non-empty.","Validate gradients on load/deserialize: reject or repair any with fewer than two stops before they reach tools.","When constructing Gradient instances in tests or importers, always seed at least two positions."],"tags":["rust","graphite-editor","gradient-tool","empty-iterator","reduce","expect-panic","data-validation"],"backgroundTag":"reduce-on-empty-iterator","analyzedSha":"c507b356453361e31638b8bff8f6d46b6da2961e","analyzedAt":"2026-08-16T21:57:18.596Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}