GraphiteEditor/Graphite · error
No min
Error message
No min
What it means
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.
Source
Thrown at editor/src/messages/tool/tool_messages/gradient_tool.rs:1285
// The gradient has only one point and so should become a fill
if selected_gradient.gradient.len() == 1 {
if selected_gradient.is_gradient_chain {
selected_gradient.render_gradient(responses);
} else if let Some(layer) = selected_gradient.layer {
responses.add(GraphOperationMessage::FillColorSet {
layer,
color: Some(selected_gradient.gradient.color(0).unwrap_or(Color::BLACK)),
});
}
responses.add(DocumentMessage::CommitTransaction);
responses.add(PropertiesPanelMessage::Refresh);
return ready_default;
}
// Find the minimum and maximum positions
let positions = selected_gradient.gradient.positions(selected_gradient.appearance.settings.cyclic);
let min_position = positions.iter().copied().reduce(f64::min).expect("No min");
let max_position = positions.iter().copied().reduce(f64::max).expect("No max");
let gradient_transform = selected_gradient.appearance.transform;
let (local_start, local_end) = (gradient_transform.transform_point2(DVec2::ZERO), gradient_transform.transform_point2(DVec2::X));
selected_gradient.appearance.transform = build_transform_with_y_preservation(gradient_transform, local_start.lerp(local_end, min_position), local_start.lerp(local_end, max_position));
// Remap the positions
let remapped: Vec<f64> = positions.into_iter().map(|position| (position - min_position) / (max_position - min_position)).collect();
selected_gradient.gradient.set_positions(&remapped);
// Render the new gradient
selected_gradient.render_gradient(responses);
responses.add(DocumentMessage::CommitTransaction);
responses.add(PropertiesPanelMessage::Refresh);
tool_data.selected_gradient = None;
ready_default
}View on GitHub (pinned to c507b35645)
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.
Example fix
// before
let min_position = positions.iter().copied().reduce(f64::min).expect("No min");
let max_position = positions.iter().copied().reduce(f64::max).expect("No max");
// after
if positions.len() < 2 {
log::warn!("gradient has {} stops; skipping normalization", positions.len());
return ready_default;
}
let min_position = positions.iter().copied().reduce(f64::min).expect("No min");
let max_position = positions.iter().copied().reduce(f64::max).expect("No max"); Defensive patterns
Strategy: validation
Validate before calling
let positions = selected_gradient.gradient.positions(selected_gradient.appearance.settings.cyclic);
if positions.len() < 2 {
// degenerate gradient: skip normalization, keep tool state stable
selected_gradient.render_gradient(responses);
return ready_default;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Star node can't be found
- Brush node does not exist
- Path node does not exist
- Cannot find connected anchor
- No state for selected layer
AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16).
Data as JSON: /api/errors/e4ad5244decbc25b.
Report an issue: GitHub.