mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Robustness: VectorCraft never crashes — no panics in shipped code
Shipped code no longer unwraps, expects or panics: every one of the 124 unwrap()/expect()/panic!/unreachable! sites outside tests now handles the failure (Result/Option, ?, let-else, defaults) and reports errors to the user. - Lints: workspace clippy denies unwrap_used, expect_used, panic, todo, unimplemented and unreachable; clippy.toml allows them in tests, and integration tests, examples and testkit opt out explicitly. - Safety net (vectorcraft_engine::guard): a panic in a top-level command rolls the document back and returns EngineError::Internal; a panicking tool is reset; the MCP server answers with an internal error and keeps serving; the UI frame and each control request are guarded. - APIs that hid failures now return them: Rendered::to_png/to_jpeg/to_webp return Result (JPEG/WebP errors used to write empty files), FontDb::face returns Option, Region::top returns Option. - Fuzzing: engine/tests/import_fuzz.rs feeds garbage, hostile and mutated SVG and PDF through import → render → export (30k cases locally, no panics). - Docs: AGENTS.md/CLAUDE.md rule, docs/development.md "Robustness" section, README pointer and a PR template checklist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
0f241aac9f
commit
3e323bfa2a
@@ -241,8 +241,7 @@ impl WidthProfile {
|
||||
return (w[0].1 + (w[1].1 - w[0].1) * u, w[0].2 + (w[1].2 - w[0].2) * u);
|
||||
}
|
||||
}
|
||||
let l = p.last().unwrap();
|
||||
(l.1, l.2)
|
||||
p.last().map_or((1.0, 1.0), |l| (l.1, l.2))
|
||||
}
|
||||
/// The built-in profiles, in menu order. "uniform" is the plain stroke (no profile).
|
||||
pub const PRESETS: [ProfilePreset; 7] = [
|
||||
|
||||
@@ -615,8 +615,11 @@ impl Document {
|
||||
pub fn remove(&mut self, id: NodeId) -> Result<Arc<Node>, DocError> {
|
||||
let parent = self.parent_of(id);
|
||||
let path = self.index_path(id).ok_or(DocError::NoNode(id))?;
|
||||
let idx = *path.last().unwrap();
|
||||
let idx = *path.last().ok_or(DocError::NoNode(id))?;
|
||||
let ch = self.children_mut(parent)?;
|
||||
if idx >= ch.len() {
|
||||
return Err(DocError::NoNode(id));
|
||||
}
|
||||
Ok(ch.remove(idx))
|
||||
}
|
||||
|
||||
@@ -719,8 +722,7 @@ impl Document {
|
||||
n.id = self.alloc_id();
|
||||
if let Some(ch) = n.children_mut() {
|
||||
let old: Vec<Arc<Node>> = std::mem::take(ch);
|
||||
let fresh: Vec<Arc<Node>> = old.iter().map(|c| Arc::new(self.reid(c))).collect();
|
||||
*n.children_mut().unwrap() = fresh;
|
||||
*ch = old.iter().map(|c| Arc::new(self.reid(c))).collect();
|
||||
}
|
||||
n
|
||||
}
|
||||
@@ -781,7 +783,7 @@ impl Document {
|
||||
|
||||
fn default_graphic_styles() -> Vec<GraphicStyle> {
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
let solid = |hex| Paint::solid(Color::from_hex(hex).unwrap());
|
||||
let solid = |hex| Paint::solid(Color::from_hex(hex).unwrap_or(Color::BLACK));
|
||||
[
|
||||
GraphicStyle::new(DEFAULT_GRAPHIC_STYLE, Appearance::default_art()),
|
||||
GraphicStyle::new("Black Outline", Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0)),
|
||||
|
||||
+24
-13
@@ -466,11 +466,7 @@ impl Spine {
|
||||
if pts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut cum = vec![0.0];
|
||||
for w in pts.windows(2) {
|
||||
cum.push(cum.last().unwrap() + w[0].distance(w[1]));
|
||||
}
|
||||
Some(Self { pts, cum })
|
||||
Some(Self { cum: cumulative_lengths(&pts), pts })
|
||||
}
|
||||
pub fn length(&self) -> f64 {
|
||||
*self.cum.last().unwrap_or(&0.0)
|
||||
@@ -639,6 +635,18 @@ fn poly_len(c: &CubicBez) -> f64 {
|
||||
c.p0.distance(c.p1) + c.p1.distance(c.p2) + c.p2.distance(c.p3)
|
||||
}
|
||||
|
||||
/// Running length along a polyline: `0, |p0p1|, |p0p1| + |p1p2|, …` (one entry per point).
|
||||
fn cumulative_lengths(pts: &[Point]) -> Vec<f64> {
|
||||
let mut total = 0.0;
|
||||
let mut cum = Vec::with_capacity(pts.len());
|
||||
cum.push(0.0);
|
||||
for w in pts.windows(2) {
|
||||
total += w[0].distance(w[1]);
|
||||
cum.push(total);
|
||||
}
|
||||
cum
|
||||
}
|
||||
|
||||
/// Map every point of `path` through the non-linear function `f`. Segments are split into pieces
|
||||
/// no longer than about `max_piece` (up to 64 per segment) and their control points mapped, which
|
||||
/// approximates the image curve to O(h²). Closedness and subpath structure are preserved.
|
||||
@@ -662,7 +670,9 @@ pub fn map_nonlinear(path: &PathData, max_piece: f64, f: impl Fn(Point) -> Point
|
||||
let k = ((poly_len(&c) / max_piece).ceil() as usize).clamp(1, 64);
|
||||
for j in 0..k {
|
||||
let sub = c.subsegment((j as f64 / k as f64)..((j + 1) as f64 / k as f64));
|
||||
res.last_mut().unwrap().h_out = f(sub.p1);
|
||||
if let Some(last) = res.last_mut() {
|
||||
last.h_out = f(sub.p1);
|
||||
}
|
||||
if j + 1 < k {
|
||||
res.push(Anchor { p: f(sub.p3), h_in: f(sub.p2), h_out: f(sub.p3), kind: Default::default() });
|
||||
} else {
|
||||
@@ -673,8 +683,9 @@ pub fn map_nonlinear(path: &PathData, max_piece: f64, f: impl Fn(Point) -> Point
|
||||
}
|
||||
if let Some(h) = pending_in
|
||||
&& sp.closed
|
||||
&& let Some(first) = res.first_mut()
|
||||
{
|
||||
res[0].h_in = h;
|
||||
first.h_in = h;
|
||||
}
|
||||
let anchors = res.into_iter().map(|a| Anchor::with_handles(a.p, a.h_in, a.h_out)).collect();
|
||||
subs.push(SubPath::new(anchors, sp.closed));
|
||||
@@ -704,11 +715,8 @@ fn resample(poly: &[Point], m: usize) -> Vec<Point> {
|
||||
if poly.len() == 1 {
|
||||
return vec![poly[0]; m];
|
||||
}
|
||||
let mut cum = vec![0.0];
|
||||
for w in poly.windows(2) {
|
||||
cum.push(cum.last().unwrap() + w[0].distance(w[1]));
|
||||
}
|
||||
let total = *cum.last().unwrap();
|
||||
let cum = cumulative_lengths(poly);
|
||||
let total = cum.last().copied().unwrap_or(0.0);
|
||||
let mut out = Vec::with_capacity(m);
|
||||
let mut i = 0;
|
||||
for k in 0..m {
|
||||
@@ -749,7 +757,10 @@ impl Coons {
|
||||
let r = path.bounds()?;
|
||||
let tol = (r.width() + r.height()).max(1e-6) / 2000.0;
|
||||
let mut poly = flatten_subpath(sp, tol);
|
||||
if poly.len() > 1 && poly[0].distance(*poly.last().unwrap()) < 1e-9 {
|
||||
if poly.len() > 1
|
||||
&& let (Some(a), Some(b)) = (poly.first(), poly.last())
|
||||
&& a.distance(*b) < 1e-9
|
||||
{
|
||||
poly.pop();
|
||||
}
|
||||
if poly.len() < 3 {
|
||||
|
||||
Reference in New Issue
Block a user