M14.4: no panics in the shipped code main gained since the no-panics branch

The M3/M4 work added ten panic sites the new workspace lints deny:
- doc: the lens/taper width profiles fall back to a uniform profile
  instead of `expect` (the presets are a constant table, covered by tests).
- engine: `FlattenOptions::default` builds the medium preset directly
  (`builtin`), `apply` skips an empty group instead of `expect`, the
  writable-format fallback reports an error instead of `expect`, and the
  new-art template's stroke comes back as an `Option` (it always has one)
  instead of `expect`.
- tools: the freeform gradient drag returns no actions for a grab it never
  starts (was `unreachable!`), and the gradient tool's release takes the
  gesture with `if let` instead of `expect`.

Behaviour on the success paths is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-05 06:39:18 +07:00
1 parent 3e323bfa2a
commit 1a5f7f6441
8 files changed
+42 -25

No files matched your search

+4 -4
View File
@@ -270,13 +270,13 @@ impl WidthProfile {
}
/// The lens profile (thin ends, full width in the middle).
pub fn lens() -> Self {
Self::preset("lens").expect("built-in")
Self::preset("lens").unwrap_or_default()
}
pub fn taper_end() -> Self {
Self::preset("taperEnd").expect("built-in")
Self::preset("taperEnd").unwrap_or_default()
}
pub fn taper_start() -> Self {
Self::preset("taperStart").expect("built-in")
Self::preset("taperStart").unwrap_or_default()
}
/// The (left, right) factors just before and just after `t`: they differ only at a
/// discontinuous point (two points at `t`, within `1e-7`).
@@ -1023,7 +1023,7 @@ mod tests {
a.rebase_gradients(Rect::new(0.0, 0.0, 100.0, 100.0), Rect::new(100.0, 0.0, 300.0, 100.0));
let start = |p: Paint| match p {
Paint::Gradient(g) => g.geom.unwrap().start,
_ => unreachable!(),
_ => panic!("a gradient"),
};
// The fill's start stays on the left edge; the stroke's on its inflated box's (−5 → 95).
assert_eq!((start(a.fill_paint()), start(a.stroke_paint())), (Point::new(100.0, 0.0), Point::new(95.0, 0.0)));
+1 -1
View File
@@ -272,7 +272,7 @@ pub fn writable_format(format_param: Option<&str>, path: Option<&str>) -> std::r
let f = match (format_param, path.map(extension).filter(|e| !e.is_empty())) {
(Some(f), _) => format(f).ok_or_else(|| format!("unknown format `{f}` (see document.formats)"))?,
(None, Some(e)) => format(&e).ok_or_else(|| format!("unknown extension `.{e}`: pass `format` (see document.formats)"))?,
(None, None) => format("png").expect("png is in FORMATS"),
(None, None) => format("png").ok_or("no PNG encoder")?,
};
if f.write { Ok(f) } else { Err(format!("{} files can be opened but not written (see document.formats)", f.label)) }
}
+10 -4
View File
@@ -133,7 +133,7 @@ pub struct FlattenOptions {
impl Default for FlattenOptions {
fn default() -> Self {
Self::preset("medium").expect("built-in preset")
Self::builtin("medium")
}
}
@@ -155,12 +155,17 @@ impl FlattenOptions {
pub fn preset(name: &str) -> Option<Self> {
let key = name.trim().to_ascii_lowercase();
let id = Self::PRESETS.into_iter().find(|id| key == *id || Self::preset_label(id).is_some_and(|l| l.eq_ignore_ascii_case(&key)))?;
Some(Self::builtin(id))
}
/// The built-in preset `id` (one of [`Self::PRESETS`]; anything else is low).
fn builtin(id: &str) -> Self {
let (balance, line_art_ppi, gradient_ppi, clip_complex_regions, anti_alias) = match id {
"high" => (100.0, 1200.0, 300.0, true, false),
"medium" => (75.0, 300.0, 300.0, false, true),
_ => (75.0, 300.0, 150.0, false, true),
};
Some(Self {
Self {
balance,
line_art_ppi,
gradient_ppi,
@@ -170,7 +175,7 @@ impl FlattenOptions {
anti_alias,
preserve_alpha: false,
preserve_overprints: true,
})
}
}
/// The built-in presets, finest first.
@@ -984,6 +989,8 @@ fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan: Plan, o:
let (mut vector, mut rasterized) = (0, 0);
let mut ids = vec![];
for f in plan.flat {
// A group always has objects.
let Some(&top) = f.roots.last() else { continue };
let mut children = vec![];
// The image goes under the regions, reaching a pixel past its own: their edges then fall on
// matching pixels instead of on what is behind the group.
@@ -1001,7 +1008,6 @@ fn apply(d: &mut Document, sel: &mut vectorcraft_doc::Selection, plan: Plan, o:
children.push(Arc::new(n));
vector += 1;
}
let top = *f.roots.last().expect("a group has objects");
let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?;
let g = Node::group(d.alloc_id(), children);
ids.push(d.insert(par, idx + 1, g)?);
+2 -1
View File
@@ -381,8 +381,9 @@ fn edit_gradient(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(m) = mode
&& ids.is_empty()
&& p.get("ids").is_none()
&& let Some(st) = s.new_art_stroke_mut()
{
s.new_art_stroke_mut().gradient_mode = m;
st.gradient_mode = m;
}
// The last gradient is the one the first object now shows (the defaults' without objects).
let shown = match ids.first() {
+11 -6
View File
@@ -155,9 +155,9 @@ impl Session {
st
}
/// The template's top stroke for an edit (`stroke.set`): the template becomes the user's own
/// (an inherited one as new art takes it now), with a stroke to edit.
pub(crate) fn new_art_stroke_mut(&mut self) -> &mut StrokeLayer {
/// The template for an edit (`stroke.set`): it becomes the user's own (an inherited one as new
/// art takes it now), with a stroke to edit.
fn new_art_template_mut(&mut self) -> &mut Appearance {
let own = self.template().map(Cow::into_owned);
if self.basic_only() {
(self.paint.opacity, self.paint.blend, self.paint.style) = (1.0, BlendMode::Normal, None);
@@ -167,7 +167,13 @@ impl Session {
if t.stroke().is_none() {
t.set_stroke(Paint::None);
}
t.stroke_mut().expect("a stroke was just made")
t
}
/// The template's top stroke for an edit (see [`Session::new_art_template_mut`], which always
/// gives it one).
pub(crate) fn new_art_stroke_mut(&mut self) -> Option<&mut StrokeLayer> {
self.new_art_template_mut().stroke_mut()
}
/// A graphic style clicked with nothing selected: new art takes it (`add`: its fills, strokes
@@ -175,8 +181,7 @@ impl Session {
pub(crate) fn new_art_style(&mut self, g: &GraphicStyle, add: bool) {
let ap = if g.unit_box { g.appearance.clone() } else { unplace_all(g.appearance.clone()) };
if add {
let _ = self.new_art_stroke_mut();
let t = self.paint.appearance.as_mut().expect("made by new_art_stroke_mut");
let t = self.new_art_template_mut();
t.items.extend(ap.items);
t.effects.extend(ap.effects);
self.paint.style = None;
+7 -4
View File
@@ -217,8 +217,8 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
}
};
// With nothing selected the Stroke panel sets up the next object drawn.
if new_art {
set(s.new_art_stroke_mut());
if new_art && let Some(st) = s.new_art_stroke_mut() {
set(st);
}
edit_items(s, &ids, item, C, "Stroke", false, |n, index| {
if index.is_none()
@@ -360,8 +360,11 @@ fn stroke_advanced(s: &mut Session, p: &Value) -> Result<Value> {
}
};
// As stroke.set: with nothing selected, for the next object drawn.
if no_targets(&ids, p) && (scale.is_some() || swap || flip.is_some()) {
arrows_and_profile(s.new_art_stroke_mut());
if no_targets(&ids, p)
&& (scale.is_some() || swap || flip.is_some())
&& let Some(st) = s.new_art_stroke_mut()
{
arrows_and_profile(st);
}
edit_items(s, &ids, item, C, "Stroke", false, |n, index| {
let Some(st) = n.appearance.stroke_at_mut(index) else { return Ok(()) };
+3 -2
View File
@@ -230,7 +230,8 @@ pub(super) fn drag(cx: &ToolContext, g: &mut Gesture, p: Point) -> Vec<Action> {
let spread = if scale > 0.0 { ((r + at.distance(p) - d) / scale).clamp(0.0, 1.0) } else { 0.0 };
("paint.freeform.setPoint", json!({ "index": index, "spread": spread }))
}
_ => unreachable!("checked above"),
// Checked above.
_ => return out,
};
out.push(Action::Preview(cmd.into(), on_proxy(cx, params)));
out
@@ -501,7 +502,7 @@ mod tests {
// The handle of a point without spread drags one out.
let mut d0 = d.clone();
let n = d0.node_mut(s.objects[0]).unwrap();
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { unreachable!() };
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { panic!("a gradient fill") };
g.freeform.as_mut().unwrap().points[2].spread = 0.0;
n.appearance.set_fill(Paint::Gradient(g));
let mut cx = crate::testutil::cx(&d0, &s, &p);
+4 -3
View File
@@ -475,8 +475,9 @@ impl Tool for GradientTool {
}
(PointerKind::Drag, Some(g)) => return freeform::drag(cx, g, ev.pos),
(PointerKind::Up, Some(_)) => {
let g = self.free.take().expect("checked");
return freeform::release(cx, FreeformAnnotator::of(cx).as_ref(), g, &mut self.lines);
if let Some(g) = self.free.take() {
return freeform::release(cx, FreeformAnnotator::of(cx).as_ref(), g, &mut self.lines);
}
}
(PointerKind::Move, None) => freeform::hover(cx, &mut self.lines, ev.pos),
(PointerKind::DoubleClick, _) => {
@@ -903,7 +904,7 @@ mod tests {
fn a_radial_shows_its_extent_ellipse_aspect_handle_and_focal_dot() {
let (mut d, s) = graded(two());
let n = d.node_mut(s.objects[0]).unwrap();
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { unreachable!() };
let Paint::Gradient(mut g) = n.appearance.fill_paint() else { panic!("a gradient fill") };
g.gradient.kind = GradientKind::Radial;
let geom = g.geom.as_mut().unwrap();
geom.aspect = 0.5;