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

  1. Guard the empty case before reducing: if positions.is_empty(), skip normalization (or bail out of the interaction) instead of expecting.
  2. Validate gradient stop count at the boundary: reject or repair gradients with fewer than two stops when loading documents or deserializing gradient inputs.
  3. 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.
  4. 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

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


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