From c9f0ed14f00f2d4af42aa16e67dd8182a90811bd Mon Sep 17 00:00:00 2001 From: andeecollard Date: Sat, 17 Oct 2026 06:15:52 +0100 Subject: [PATCH] M12: add centre-line scenes and baseline quality measurements Measure Create Strokes on exact code-drawn scenes in aa and hard variants, including centre/width error, topology, ends, spurs and painted coverage. Keep the existing allocator report in one ignored test and document the current baseline without changing tracing behaviour or dependencies. Co-Authored-By: Codex Co-Authored-By: Claude Opus 5.5 --- crates/trace/tests/quality/centre_geom.rs | 69 +++ crates/trace/tests/quality/centre_lines.rs | 550 ++++++++++++++++++++ crates/trace/tests/quality/centre_scenes.rs | 446 ++++++++++++++++ crates/trace/tests/quality/main.rs | 15 + crates/trace/tests/quality/measure.rs | 2 +- docs/image-trace-quality.md | 120 +++++ docs/roadmap.md | 3 +- docs/target-app-parity.md | 4 +- 8 files changed, 1206 insertions(+), 3 deletions(-) create mode 100644 crates/trace/tests/quality/centre_geom.rs create mode 100644 crates/trace/tests/quality/centre_lines.rs create mode 100644 crates/trace/tests/quality/centre_scenes.rs diff --git a/crates/trace/tests/quality/centre_geom.rs b/crates/trace/tests/quality/centre_geom.rs new file mode 100644 index 000000000..3e78d734b --- /dev/null +++ b/crates/trace/tests/quality/centre_geom.rs @@ -0,0 +1,69 @@ +//! Point arithmetic for the code-drawn centre-line scenes. + +use std::ops::{Add, Mul, Neg, Sub}; + +/// A point or vector in pixel coordinates (pixel `(x, y)` covers `[x, x+1] × [y, y+1]`, y down). +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct P { + pub x: f64, + pub y: f64, +} + +impl P { + pub const fn new(x: f64, y: f64) -> Self { + Self { x, y } + } + pub fn dot(self, o: P) -> f64 { + self.x * o.x + self.y * o.y + } + pub fn len(self) -> f64 { + self.x.hypot(self.y) + } + pub fn dist(self, o: P) -> f64 { + (self - o).len() + } + /// Unit vector, or `None` for a (near) zero vector. + pub fn unit(self) -> Option

{ + let l = self.len(); + (l > 1e-12 && l.is_finite()).then(|| P::new(self.x / l, self.y / l)) + } + pub fn lerp(self, o: P, t: f64) -> P { + self + (o - self) * t + } +} + +impl Add for P { + type Output = P; + fn add(self, o: P) -> P { + P::new(self.x + o.x, self.y + o.y) + } +} +impl Sub for P { + type Output = P; + fn sub(self, o: P) -> P { + P::new(self.x - o.x, self.y - o.y) + } +} +impl Mul for P { + type Output = P; + fn mul(self, k: f64) -> P { + P::new(self.x * k, self.y * k) + } +} +impl Neg for P { + type Output = P; + fn neg(self) -> P { + P::new(-self.x, -self.y) + } +} + +/// Distance from `q` to segment `ab`, and the parameter of the nearest point. +pub fn seg_dist(q: P, a: P, b: P) -> (f64, f64) { + let ab = b - a; + let l2 = ab.dot(ab); + if l2 < 1e-18 { + return (q.dist(a), 0.0); + } + let t = ((q - a).dot(ab) / l2).clamp(0.0, 1.0); + (q.dist(a + ab * t), t) +} diff --git a/crates/trace/tests/quality/centre_lines.rs b/crates/trace/tests/quality/centre_lines.rs new file mode 100644 index 000000000..a54190cc5 --- /dev/null +++ b/crates/trace/tests/quality/centre_lines.rs @@ -0,0 +1,550 @@ +//! Centre-line measurements on code-drawn scenes. No external images or tracer required. +use super::centre_geom::{P, seg_dist}; +use super::centre_scenes::{self as scenes, Scene}; +use kurbo::{BezPath, ParamCurve, PathSeg}; +use std::fmt::Write as _; +use std::time::Instant; +use vectorcraft_trace::{Raster, TraceParams, TraceResult, trace_within}; + +/// Nearest-point queries over a point set (uniform grid buckets). +struct Near { + cell: f64, + gw: usize, + gh: usize, + buckets: Vec>, + pts: Vec

, +} + +impl Near { + fn new(pts: Vec

, w: usize, h: usize, cell: f64) -> Self { + let gw = (w as f64 / cell).ceil() as usize + 2; + let gh = (h as f64 / cell).ceil() as usize + 2; + let mut buckets = vec![vec![]; gw * gh]; + for (i, p) in pts.iter().enumerate() { + let (cx, cy) = (((p.x / cell).floor().max(0.0) as usize).min(gw - 1), ((p.y / cell).floor().max(0.0) as usize).min(gh - 1)); + buckets[cy * gw + cx].push(i); + } + Self { cell, gw, gh, buckets, pts } + } + /// Nearest point within `max` (index, distance). + fn nearest(&self, q: P, max: f64) -> Option<(usize, f64)> { + let r = (max / self.cell).ceil() as i64; + let (cx, cy) = ((q.x / self.cell).floor() as i64, (q.y / self.cell).floor() as i64); + let mut best: Option<(usize, f64)> = None; + for y in cy - r..=cy + r { + for x in cx - r..=cx + r { + if x < 0 || y < 0 || x >= self.gw as i64 || y >= self.gh as i64 { + continue; + } + for &i in &self.buckets[y as usize * self.gw + x as usize] { + let d = self.pts[i].dist(q); + if d <= max && best.is_none_or(|b| d < b.1) { + best = Some((i, d)); + } + } + } + } + best + } +} + +fn mean(v: &[f64]) -> f64 { + if v.is_empty() { f64::NAN } else { v.iter().sum::() / v.len() as f64 } +} +fn p95(v: &[f64]) -> f64 { + if v.is_empty() { + return f64::NAN; + } + let mut s = v.to_vec(); + s.sort_by(f64::total_cmp); + s[((s.len() - 1) as f64 * 0.95).round() as usize] +} + +#[derive(Default, Clone, Debug)] +struct Metrics { + name: String, + input: &'static str, + strokes: usize, + fills: usize, + anchors: usize, + ms: f64, + c_mean: f64, + c_p95: f64, + g_mean: f64, + g_p95: f64, + complete: f64, + w_uni: f64, + w_uni_p95: f64, + junc_total: usize, + junc_ok: usize, + junc_wrong: usize, + junc_extra: usize, + junc_pos: f64, + ends_gt: usize, + ends_found: usize, + end_err: f64, + end_signed: f64, + spurs: usize, + iou_uni: f64, + area_uni: f64, + fill_as_fill: f64, +} + +fn iou(a: &[bool], m: &[bool]) -> (f64, f64) { + let (mut i, mut u, mut ra, mut rm) = (0usize, 0usize, 0usize, 0usize); + for (x, y) in a.iter().zip(m) { + i += usize::from(*x && *y); + u += usize::from(*x || *y); + ra += usize::from(*x); + rm += usize::from(*y); + } + (i as f64 / u.max(1) as f64, (ra as f64 - rm as f64) / rm.max(1) as f64) +} + +fn evaluate(sc: &Scene, input: &'static str, cov: &[f32]) -> Metrics { + let rgba = cov + .iter() + .flat_map(|&c| { + let v = ((1.0 - c) * 255.0).round() as u8; + [v, v, v, 255] + }) + .collect(); + let img = Raster::new(sc.w as u32, sc.h as u32, rgba); + let params = TraceParams { strokes: true, fills: true, stroke_width: sc.wmax, ignore_white: true, noise: 4, ..TraceParams::default() }; + let t0 = Instant::now(); + let res = trace_within(&img, ¶ms, super::MAX_ANCHORS).unwrap(); + let ms = t0.elapsed().as_secs_f64() * 1e3; + measure(sc, input, cov, &res, ms) +} + +fn measure(sc: &Scene, input: &'static str, cov: &[f32], res: &TraceResult, ms: f64) -> Metrics { + let t = geometry(res); + let in_fill = |q: P, grow: f64| sc.fills.iter().any(|f| f.dist(q) <= grow); + // Ground-truth centre samples (outside fills) and output samples. + let mut gt_pts = vec![]; + let mut gt_w = vec![]; + for s in &sc.strokes { + for (p, w) in s.pts.iter().zip(&s.w) { + if !in_fill(*p, 0.0) { + gt_pts.push(*p); + gt_w.push(*w); + } + } + } + let gt = Near::new(gt_pts.clone(), sc.w, sc.h, 4.0); + let mut out_pts = vec![]; + let (mut cd, mut wu) = (vec![], vec![]); + for s in &t.strokes { + for (p, _) in &s.samples { + if in_fill(*p, 0.0) { + continue; + } + out_pts.push(*p); + if let Some((i, d)) = gt.nearest(*p, 30.0) { + cd.push(d); + // Width: only where the sample is on a stroke (not a stray piece). + if d < 2.0 { + wu.push((s.width - gt_w[i]).abs()); + } + } else { + cd.push(30.0); + } + } + } + let out = Near::new(out_pts, sc.w, sc.h, 4.0); + let gd: Vec = gt_pts.iter().map(|&p| out.nearest(p, 30.0).map_or(30.0, |x| x.1)).collect(); + let complete = gd.iter().filter(|&&d| d <= 1.5).count() as f64 / gd.len().max(1) as f64; + // Junctions (away from fills). + let outj: Vec<_> = t.junctions.iter().filter(|j| !in_fill(j.pos, 2.0)).collect(); + let mut used = vec![false; outj.len()]; + let (mut ok, mut wrong, mut pos) = (0, 0, vec![]); + for &(p, deg) in &sc.junctions { + let wl = gt.nearest(p, 5.0).map_or(4.0, |(i, _)| gt_w[i]); + let tol = wl + 3.0; + let best = + outj.iter().enumerate().filter(|(k, j)| !used[*k] && j.pos.dist(p) <= tol).min_by(|a, b| a.1.pos.dist(p).total_cmp(&b.1.pos.dist(p))); + if let Some((k, j)) = best { + used[k] = true; + pos.push(j.pos.dist(p)); + if j.degree == deg { + ok += 1; + } else { + wrong += 1; + } + } + } + let extra = used.iter().filter(|u| !**u).count(); + // Free ends: ground truth = open stroke ends not at a junction, corner or fill. + let mut gt_ends = vec![]; + for s in sc.strokes.iter().filter(|s| !s.closed) { + for (p, w) in [(s.pts[0], s.w[0]), (*s.pts.last().unwrap_or(&s.pts[0]), *s.w.last().unwrap_or(&s.w[0]))] { + let near_j = sc.junctions.iter().any(|(q, _)| q.dist(p) < 1.0) || sc.corners.iter().any(|q| q.dist(p) < 1.0); + let on_other = sc.strokes.iter().filter(|o| !std::ptr::eq(*o, s)).any(|o| o.pts.iter().any(|q| q.dist(p) < 0.5)); + if !near_j && !on_other && !in_fill(p, w) { + // Its direction (outwards), for the signed error. + let k = + if p == s.pts[0] { (s.pts[0] - s.pts[s.pts.len().min(9) - 1]).unit() } else { (p - s.pts[s.pts.len().saturating_sub(9)]).unit() }; + gt_ends.push((p, w, k.unwrap_or(P::new(1.0, 0.0)))); + } + } + } + let out_ends: Vec

= t.ends.iter().copied().filter(|p| !in_fill(*p, 2.0)).collect(); + let mut used_e = vec![false; out_ends.len()]; + let (mut found, mut err, mut signed) = (0, vec![], vec![]); + for &(p, w, dir) in >_ends { + let tol = 0.5 * w + 4.0; + let best = out_ends.iter().enumerate().filter(|(k, q)| !used_e[*k] && q.dist(p) <= tol).min_by(|a, b| a.1.dist(p).total_cmp(&b.1.dist(p))); + if let Some((k, q)) = best { + used_e[k] = true; + found += 1; + err.push(q.dist(p)); + signed.push((*q - p).dot(dir)); + } + } + let spurs = used_e.iter().filter(|u| !**u).count(); + let r_uni = render(res, sc.w, sc.h, false); + // Coverage against the exact shape (pixel centres inside it); the thresholded mask + // over-counts pixels exactly half covered. + let exact = sc.exact(); + let (iou_uni, area_uni) = iou(&r_uni, &exact); + // Fill pixels traced as fills. + let rf = render(res, sc.w, sc.h, true); + let (mut fp, mut ff) = (0usize, 0usize); + for y in 0..sc.h { + for x in 0..sc.w { + let q = P::new(x as f64 + 0.5, y as f64 + 0.5); + if cov[y * sc.w + x] >= 0.5 && sc.fills.iter().any(|f| f.inside(q)) { + fp += 1; + ff += usize::from(rf[y * sc.w + x]); + } + } + } + Metrics { + name: sc.name.clone(), + input, + strokes: res.paths.iter().filter(|p| p.stroke.is_some()).count(), + fills: t.fills.len(), + anchors: res.paths.iter().filter(|p| p.stroke.is_some()).map(|p| p.path.anchor_count()).sum(), + ms, + c_mean: mean(&cd), + c_p95: p95(&cd), + g_mean: mean(&gd), + g_p95: p95(&gd), + complete, + w_uni: mean(&wu), + w_uni_p95: p95(&wu), + junc_total: sc.junctions.len(), + junc_ok: ok, + junc_wrong: wrong, + junc_extra: extra, + junc_pos: mean(&pos), + ends_gt: gt_ends.len(), + ends_found: found, + end_err: mean(&err), + end_signed: mean(&signed), + spurs, + iou_uni, + area_uni, + fill_as_fill: if fp > 0 { ff as f64 / fp as f64 } else { f64::NAN }, + } +} + +struct StrokeSamples { + samples: Vec<(P, f64)>, + closed: bool, + width: f64, +} +struct Junction { + pos: P, + degree: usize, +} +struct Geometry { + strokes: Vec, + fills: Vec<()>, + junctions: Vec, + ends: Vec

, +} + +fn point(p: kurbo::Point) -> P { + P::new(p.x, p.y) +} + +/// Evaluate the emitted curves at <=0.5 px control-polygon steps (as for the scene +/// yardstick). Keep subpaths separate: today's compound strokes share one width. +fn samples(path: &BezPath) -> Vec

{ + let mut pts = vec![]; + for seg in path.segments() { + let poly = match seg { + PathSeg::Line(l) => l.p0.distance(l.p1), + PathSeg::Quad(q) => q.p0.distance(q.p1) + q.p1.distance(q.p2), + PathSeg::Cubic(c) => c.p0.distance(c.p1) + c.p1.distance(c.p2) + c.p2.distance(c.p3), + }; + let n = (poly / 0.5).ceil().max(1.0) as usize; + if pts.is_empty() { + pts.push(point(seg.eval(0.0))); + } + for i in 1..=n { + pts.push(point(seg.eval(i as f64 / n as f64))); + } + } + pts +} + +/// Recover topology from emitted geometry, without consulting scene truth. Candidates +/// are open endpoints and intersections. Coincident points merge within 0.05 px; +/// each touching endpoint contributes one arm, each interior contributes two. +/// This also measures a bar chained through a T, or two chained strokes crossing. +fn geometry(res: &TraceResult) -> Geometry { + let mut strokes = vec![]; + for path in &res.paths { + if let Some(width) = path.stroke { + for sp in &path.path.subpaths { + let mut bez = BezPath::new(); + sp.to_bezpath_into(&mut bez); + strokes.push(StrokeSamples { samples: samples(&bez).into_iter().map(|p| (p, width)).collect(), closed: sp.closed, width }); + } + } + } + let mut candidates = vec![]; + for s in &strokes { + if !s.closed && !s.samples.is_empty() { + candidates.extend([s.samples[0].0, s.samples.last().unwrap().0]); + } + } + for (i, a) in strokes.iter().enumerate() { + for b in &strokes[i + 1..] { + for aa in a.samples.windows(2) { + let (p, r) = (aa[0].0, aa[1].0 - aa[0].0); + for bb in b.samples.windows(2) { + let (q, s) = (bb[0].0, bb[1].0 - bb[0].0); + if aa[0].0.x.max(aa[1].0.x) < bb[0].0.x.min(bb[1].0.x) + || bb[0].0.x.max(bb[1].0.x) < aa[0].0.x.min(aa[1].0.x) + || aa[0].0.y.max(aa[1].0.y) < bb[0].0.y.min(bb[1].0.y) + || bb[0].0.y.max(bb[1].0.y) < aa[0].0.y.min(aa[1].0.y) + { + continue; + } + let cross = |v: P, w: P| v.x * w.y - v.y * w.x; + let d = cross(r, s); + if d.abs() < 1e-12 { + continue; + } + let t = cross(q - p, s) / d; + let u = cross(q - p, r) / d; + if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) { + candidates.push(p + r * t); + } + } + } + } + } + let mut nodes: Vec

= vec![]; + for p in candidates { + if !nodes.iter().any(|q| q.dist(p) <= 0.05) { + nodes.push(p); + } + } + let (mut junctions, mut ends) = (vec![], vec![]); + for pos in nodes { + let degree = strokes + .iter() + .map(|s| { + if s.samples.is_empty() { + return 0; + } + if !s.closed { + // A run returning to its junction contributes both endpoint arms. + let arms = usize::from(pos.dist(s.samples[0].0) <= 0.05) + usize::from(pos.dist(s.samples.last().unwrap().0) <= 0.05); + if arms > 0 { + return arms; + } + } + if s.samples.windows(2).any(|p| seg_dist(pos, p[0].0, p[1].0).0 <= 0.05) { 2 } else { 0 } + }) + .sum(); + if degree >= 3 { + junctions.push(Junction { pos, degree }); + } + if degree == 1 { + ends.push(pos); + } + } + Geometry { strokes, fills: vec![(); res.paths.iter().filter(|p| p.stroke.is_none()).count()], junctions, ends } +} + +/// Pixel-centre nonzero-winding fill of the actual painted paths. Strokes retain +/// their emitted square/round caps. Overlapping paths are unioned, never cancelled. +fn render(res: &TraceResult, w: usize, h: usize, fills_only: bool) -> Vec { + let mut cov = vec![false; w * h]; + for path in &res.paths { + if fills_only && path.stroke.is_some() { + continue; + } + let bez = super::measure::painted(path); + let polys = super::raster::polylines(&bez, 0.02); + let mut xs = vec![]; + for y in 0..h { + let yc = y as f64 + 0.5; + xs.clear(); + for poly in &polys { + for i in 0..poly.len() { + let (a, b) = (poly[i], poly[(i + 1) % poly.len()]); + if (a.y <= yc) != (b.y <= yc) { + xs.push((a.x + (b.x - a.x) * (yc - a.y) / (b.y - a.y), if b.y > a.y { 1 } else { -1 })); + } + } + } + xs.sort_by(|a, b| a.0.total_cmp(&b.0)); + let mut wind = 0; + for pair in xs.windows(2) { + wind += pair[0].1; + if wind != 0 { + let xa = (pair[0].0 - 0.5).ceil().max(0.0) as usize; + let xb = ((pair[1].0 - 0.5).floor() + 1.0).max(0.0) as usize; + for x in xa.min(w)..xb.min(w) { + cov[y * w + x] = true; + } + } + } + } + } + cov +} + +fn f(v: f64, d: usize) -> String { + if v.is_nan() { "–".into() } else { format!("{v:.d$}") } +} + +pub fn report() -> String { + let mut rows = vec![]; + for sc in scenes::all() { + let cov = sc.rasterise(); + let hard: Vec = cov.iter().map(|&c| if c >= 0.5 { 1.0 } else { 0.0 }).collect(); + for (input, c) in [("aa", &cov), ("hard", &hard)] { + rows.push(evaluate(&sc, input, c)); + } + } + let mut md = String::from( + "## Centre lines\n\nCode-drawn scenes only: 23 scenes, each anti-aliased (8×8 coverage samples) and hard (that coverage thresholded at 0.5). Coordinates, widths and scene Stroke values are fixed; junction/letter/crossing coordinates include the 0.27/0.41 px offset. Create Fills and Strokes are on, Stroke is the scene's limit (13 px, mix 6 px, varying 12 px), Black and White threshold 128, Noise 4, Ignore White on, other settings at their defaults. Today's tracer has no minimum-length setting (the dashed scenes specify 0 for future use).\n\nDistances use dense exact centre samples (≤0.25 px), emitted curves sampled at ≤0.5 px control-polygon steps, and nearest-point searches capped at 30 px. Found is the fraction of true samples within 1.5 px; width errors use output samples within 2 px of a true stroke. Samples inside exact fills are excluded. Topology is recovered from emitted subpath endpoints and intersections, merging coincident positions within 0.05 px; an endpoint contributes one arm, an interior two. Junction matching radius is local true width + 3 px; ends use half-width + 4 px. Each match is used once. Free ends exclude true junctions, corners, other strokes and fills; unmatched emitted ends are spurs. Junctions/ends within 2 px of fills are excluded. End error is distance, with signed error along the outward tangent in parentheses. Coverage is a union of painted output at pixel centres (0.02 px flattening tolerance, actual emitted caps), compared with the exact swept shape at pixel centres, for both input variants. Area error is signed relative to exact area. Anchors counts stroke anchors, including all compound subpaths; paths counts stroked + filled paths. Time covers tracing only, one scene at a time. Profile-dependent columns are unavailable (–).\n\n### Centre line and width (px)\n\n| scene | input | paths | out→true mean / p95 | true→out mean / p95 | found % | width err uniform mean / p95 | variable mean / p95 | profile mean |\n|---|---|---|---|---|---|---|---|---|\n", + ); + for m in &rows { + writeln!( + md, + "| {} | {} | {}+{} | {} / {} | {} / {} | {} | {} / {} | – | – |", + m.name, + m.input, + m.strokes, + m.fills, + f(m.c_mean, 2), + f(m.c_p95, 2), + f(m.g_mean, 2), + f(m.g_p95, 2), + f(100.0 * m.complete, 1), + f(m.w_uni, 2), + f(m.w_uni_p95, 2) + ) + .unwrap(); + } + md += "\n### Topology, ends, coverage\n\n| scene | input | junctions ok / wrong deg / missed / extra | junction pos err | ends found | end err (signed) | spurs | IoU uniform / profile | area err uniform / profile | fill→fill % | anchors | width pts | ms |\n|---|---|---|---|---|---|---|---|---|---|---|---|---|\n"; + for m in &rows { + writeln!( + md, + "| {} | {} | {} / {} / {} / {} | {} | {}/{} | {} ({}) | {} | {} / – | {}% / – | {} | {} | – | {} |", + m.name, + m.input, + m.junc_ok, + m.junc_wrong, + m.junc_total - m.junc_ok - m.junc_wrong, + m.junc_extra, + f(m.junc_pos, 2), + m.ends_found, + m.ends_gt, + f(m.end_err, 2), + f(m.end_signed, 2), + m.spurs, + f(m.iou_uni, 3), + f(100.0 * m.area_uni, 1), + f(100.0 * m.fill_as_fill, 1), + m.anchors, + f(m.ms, 1) + ) + .unwrap(); + } + let sum = |g: &dyn Fn(&Metrics) -> usize, inp: &str| rows.iter().filter(|m| m.input == inp).map(g).sum::(); + for inp in ["aa", "hard"] { + writeln!( + md, + "\n**{inp} totals**: junctions {} correct, {} wrong degree, {} missed, {} extra (of {}); ends {} of {}; spurs {}.", + sum(&|m| m.junc_ok, inp), + sum(&|m| m.junc_wrong, inp), + sum(&|m| m.junc_total - m.junc_ok - m.junc_wrong, inp), + sum(&|m| m.junc_extra, inp), + sum(&|m| m.junc_total, inp), + sum(&|m| m.ends_found, inp), + sum(&|m| m.ends_gt, inp), + sum(&|m| m.spurs, inp) + ) + .unwrap(); + } + md +} + +#[test] +fn known_geometry_measures_junctions_ends_missing_ink_and_caps() { + use vectorcraft_geom::PathData; + use vectorcraft_trace::TracedPath; + let make = |points: &[(f64, f64)], square_caps: bool| { + let mut path = BezPath::new(); + path.move_to(points[0]); + for &p in &points[1..] { + path.line_to(p); + } + TracedPath { path: PathData::from_bezpath(&path), color: [0; 3], pixels: 0, stroke: Some(2.0), shape: None, gradient: None, square_caps } + }; + // An interior meeting must count as two arms, irrespective of path chaining. + let bar = make(&[(10.27, 20.41), (50.27, 20.41)], false); + for (stem, degree, ends) in [(make(&[(30.27, 20.41), (30.27, 40.41)], false), 3, 3), (make(&[(30.27, 10.41), (30.27, 40.41)], false), 4, 4)] { + let res = TraceResult { paths: vec![bar.clone(), stem], ..Default::default() }; + let g = geometry(&res); + assert_eq!(g.junctions.len(), 1); + assert_eq!(g.junctions[0].degree, degree); + assert!(g.junctions[0].pos.dist(P::new(30.27, 20.41)) < 1e-9); + assert_eq!(g.ends.len(), ends); + } + let returning = make(&[(30.27, 20.41), (45.27, 35.41), (15.27, 35.41), (30.27, 20.41)], false); + let stem = make(&[(30.27, 20.41), (30.27, 10.41)], false); + let g = geometry(&TraceResult { paths: vec![returning, stem], ..Default::default() }); + assert_eq!(g.junctions.len(), 1); + assert_eq!(g.junctions[0].degree, 3); + assert_eq!(g.ends.len(), 1); + // Closed subpaths have no free ends, even if their first sample is repeated. + let mut ring = BezPath::new(); + ring.move_to((10.0, 10.0)); + ring.line_to((20.0, 10.0)); + ring.line_to((20.0, 20.0)); + ring.close_path(); + let res = TraceResult { paths: vec![TracedPath { path: PathData::from_bezpath(&ring), ..bar.clone() }], ..Default::default() }; + let g = geometry(&res); + assert!(g.ends.is_empty() && g.junctions.is_empty()); + + let mut sc = scenes::lines(2.0); + sc.strokes.truncate(1); + let st = &sc.strokes[0]; + let (a, b) = (st.pts[0], *st.pts.last().unwrap()); + let res = TraceResult { paths: vec![make(&[(a.x, a.y), (b.x, b.y)], false)], ..Default::default() }; + let cov = sc.rasterise(); + let m = measure(&sc, "aa", &cov, &res, 0.0); + assert!(m.c_mean < 0.01 && m.g_p95 <= 0.251 && m.w_uni < 1e-9, "{m:?}"); + assert_eq!((m.ends_found, m.ends_gt, m.spurs), (2, 2, 0)); + assert!(m.end_err < 1e-9 && m.iou_uni > 0.999 && m.area_uni.abs() < 1e-9, "{m:?}"); + let missing = measure(&sc, "aa", &cov, &TraceResult::default(), 0.0); + assert_eq!((missing.g_mean, missing.complete, missing.ends_found, missing.iou_uni, missing.area_uni), (30.0, 0.0, 0, 0.0, -1.0)); + let diagonal = make(&[(10.27, 10.41), (30.27, 30.41)], false); + let round = render(&TraceResult { paths: vec![diagonal.clone()], ..Default::default() }, 50, 50, false); + let square = render(&TraceResult { paths: vec![TracedPath { square_caps: true, ..diagonal }], ..Default::default() }, 50, 50, false); + assert!(square.iter().filter(|p| **p).count() > round.iter().filter(|p| **p).count()); + // Preserve the exact catalogue and special scene limits. + assert_eq!(scenes::all().len(), 23); + assert_eq!((scenes::mix().wmax, scenes::varying().wmax, scenes::dashed(2.0).min_len), (6.0, 12.0, 0.0)); + assert_eq!(scenes::junctions(4.0).junctions[0].0, P::new(60.27, 40.41)); +} diff --git a/crates/trace/tests/quality/centre_scenes.rs b/crates/trace/tests/quality/centre_scenes.rs new file mode 100644 index 000000000..38e4eb3f7 --- /dev/null +++ b/crates/trace/tests/quality/centre_scenes.rs @@ -0,0 +1,446 @@ +//! Code-drawn test scenes with exact answers: centre lines with widths (round caps), filled +//! areas, junctions with their degrees, and corners (stroke ends that meet but are no junction). + +use super::centre_geom::{P, seg_dist}; + +#[derive(Clone, Debug)] +pub struct GtStroke { + /// Dense centre line (≤ 0.25 px apart). + pub pts: Vec

, + pub w: Vec, + pub closed: bool, +} + +#[derive(Clone, Debug)] +pub enum GtFill { + Disc(P, f64), + Poly(Vec

), +} + +impl GtFill { + pub fn inside(&self, q: P) -> bool { + match self { + GtFill::Disc(c, r) => q.dist(*c) <= *r, + GtFill::Poly(p) => { + let n = p.len(); + let mut inside = false; + for i in 0..n { + let (a, b) = (p[i], p[(i + 1) % n]); + if (a.y > q.y) != (b.y > q.y) && q.x < a.x + (q.y - a.y) / (b.y - a.y) * (b.x - a.x) { + inside = !inside; + } + } + inside + } + } + } + /// Distance outside (0 inside), approximate for polygons (to the edges). + pub fn dist(&self, q: P) -> f64 { + if self.inside(q) { + return 0.0; + } + match self { + GtFill::Disc(c, r) => q.dist(*c) - r, + GtFill::Poly(p) => { + let n = p.len(); + (0..n).map(|i| seg_dist(q, p[i], p[(i + 1) % n]).0).fold(f64::MAX, f64::min) + } + } + } +} + +#[derive(Clone, Debug)] +pub struct Scene { + pub name: String, + pub w: usize, + pub h: usize, + pub strokes: Vec, + pub fills: Vec, + pub junctions: Vec<(P, usize)>, + pub corners: Vec

, + pub wmax: f64, + pub min_len: f64, + /// Added to every coordinate (odd widths at integer positions put pixel centres exactly on + /// the edge: ties that make coverage numbers meaningless). + pub offset: P, +} + +impl Scene { + fn new(name: &str, w: usize, h: usize, wmax: f64) -> Self { + Self { + name: name.into(), + w, + h, + strokes: vec![], + fills: vec![], + junctions: vec![], + corners: vec![], + wmax, + min_len: 20.0, + offset: P::new(0.0, 0.0), + } + } + /// A polyline stroke of constant width. + fn line(&mut self, pts: &[P], w: f64) { + self.var(pts, &vec![w; pts.len()], false); + } + /// A polyline stroke with a width at each vertex (linear between), densified. + fn var(&mut self, pts: &[P], ws: &[f64], closed: bool) { + let pts: Vec

= pts.iter().map(|&p| p + self.offset).collect(); + let pts = &pts[..]; + let mut d = vec![]; + let mut dw = vec![]; + let n = pts.len(); + let segs = if closed { n } else { n.saturating_sub(1) }; + for i in 0..segs { + let (a, b) = (pts[i], pts[(i + 1) % n]); + let (wa, wb) = (ws[i], ws[(i + 1) % n]); + let k = ((a.dist(b) / 0.25).ceil() as usize).max(1); + for j in 0..k { + let t = j as f64 / k as f64; + d.push(a.lerp(b, t)); + dw.push(wa + (wb - wa) * t); + } + } + if !closed && let (Some(&l), Some(&lw)) = (pts.last(), ws.last()) { + d.push(l); + dw.push(lw); + } + self.strokes.push(GtStroke { pts: d, w: dw, closed }); + } + fn curve(&mut self, f: impl Fn(f64) -> P, wf: impl Fn(f64) -> f64, n: usize, closed: bool) { + let m = if closed { n } else { n + 1 }; + let pts: Vec

= (0..m).map(|k| f(k as f64 / n as f64)).collect(); + let ws: Vec = (0..m).map(|k| wf(k as f64 / n as f64)).collect(); + self.var(&pts, &ws, closed); + } + fn arc(&mut self, c: P, r: f64, a0: f64, a1: f64, w: f64) { + let n = ((r * (a1 - a0).abs() / 0.5).ceil() as usize).max(8); + self.curve( + |t| { + let a = a0 + (a1 - a0) * t; + c + P::new(r * a.cos(), r * a.sin()) + }, + |_| w, + n, + false, + ); + } + fn ring(&mut self, c: P, r: f64, w: f64) { + let n = ((r * std::f64::consts::TAU / 0.5).ceil() as usize).max(16); + self.curve( + |t| { + let a = std::f64::consts::TAU * t; + c + P::new(r * a.cos(), r * a.sin()) + }, + |_| w, + n, + true, + ); + } + + /// Ink coverage (8 × 8 samples per pixel). + pub fn rasterise(&self) -> Vec { + self.rasterise_at(8) + } + /// Exact shape point-sampled at pixel centres (the reference for coverage). + pub fn exact(&self) -> Vec { + self.rasterise_at(1).into_iter().map(|c| c >= 1.0).collect() + } + fn rasterise_at(&self, ss: usize) -> Vec { + let (sw, sh) = (self.w * ss, self.h * ss); + let mut sub = vec![false; sw * sh]; + let at = |x: usize, y: usize| P::new((x as f64 + 0.5) / ss as f64, (y as f64 + 0.5) / ss as f64); + for s in &self.strokes { + let n = s.pts.len(); + let segs = if s.closed { n } else { n.saturating_sub(1) }; + let mut stamp = |a: P, b: P, ra: f64, rb: f64| { + let r = ra.max(rb); + let x0 = (((a.x.min(b.x) - r) * ss as f64).floor().max(0.0)) as usize; + let y0 = (((a.y.min(b.y) - r) * ss as f64).floor().max(0.0)) as usize; + let x1 = ((((a.x.max(b.x) + r) * ss as f64).ceil().max(0.0)) as usize).min(sw); + let y1 = ((((a.y.max(b.y) + r) * ss as f64).ceil().max(0.0)) as usize).min(sh); + for y in y0..y1 { + for x in x0..x1 { + let q = at(x, y); + let (d, t) = seg_dist(q, a, b); + if d <= ra + (rb - ra) * t { + sub[y * sw + x] = true; + } + } + } + }; + if n == 1 { + stamp(s.pts[0], s.pts[0], s.w[0] / 2.0, s.w[0] / 2.0); + } + for i in 0..segs { + let j = (i + 1) % n; + stamp(s.pts[i], s.pts[j], s.w[i] / 2.0, s.w[j] / 2.0); + } + } + for f in &self.fills { + for y in 0..sh { + for x in 0..sw { + if !sub[y * sw + x] && f.inside(at(x, y)) { + sub[y * sw + x] = true; + } + } + } + } + let mut out = vec![0.0f32; self.w * self.h]; + for y in 0..self.h { + for x in 0..self.w { + let mut c = 0; + for yy in 0..ss { + for xx in 0..ss { + c += usize::from(sub[(y * ss + yy) * sw + x * ss + xx]); + } + } + out[y * self.w + x] = c as f32 / (ss * ss) as f32; + } + } + out + } +} + +fn pol(c: P, r: f64, deg: f64) -> P { + let a = deg.to_radians(); + c + P::new(r * a.cos(), r * a.sin()) +} + +/// 24 straight lines of width `w`, at 0°–172.5° in 7.5° steps, centres at varied sub-pixel phases. +pub fn lines(w: f64) -> Scene { + let mut s = Scene::new(&format!("lines w{w}"), 480, 320, 13.0); + for k in 0..24 { + let (cx, cy) = ((k % 6) as f64 * 80.0 + 40.0 + 0.13 * k as f64 % 1.0, (k / 6) as f64 * 80.0 + 40.0 + 0.29 * k as f64 % 1.0); + let c = P::new(cx, cy); + let a = 7.5 * k as f64; + s.line(&[pol(c, 25.0, a + 180.0), pol(c, 25.0, a)], w); + } + s +} + +pub fn curves(w: f64) -> Scene { + let mut s = Scene::new(&format!("curves w{w}"), 400, 300, 13.0); + // Sine waves. + s.curve(|t| P::new(30.0 + 340.0 * t, 50.0 + 20.0 * (t * 12.0).sin()), |_| w, 400, false); + s.curve(|t| P::new(30.0 + 340.0 * t, 110.0 + 10.0 * (t * 25.0).sin()), |_| w, 600, false); + // Cubic S-curve. + let cubic = |t: f64| { + let (p0, p1, p2, p3) = (P::new(40.0, 280.0), P::new(60.0, 140.0), P::new(200.0, 300.0), P::new(190.0, 160.0)); + let u = 1.0 - t; + p0 * (u * u * u) + p1 * (3.0 * u * u * t) + p2 * (3.0 * u * t * t) + p3 * (t * t * t) + }; + s.curve(cubic, |_| w, 300, false); + // Spiral. + s.curve( + |t| { + let a = t * 4.0 * std::f64::consts::PI; + P::new(300.0, 225.0) + P::new(a.cos(), a.sin()) * (12.0 + 50.0 * t) + }, + |_| w, + 800, + false, + ); + s +} + +pub fn circles() -> Scene { + let mut s = Scene::new("circles", 520, 260, 13.0); + let mut x = 50.0; + for (r, w) in [(8.0, 1.0), (12.0, 2.0), (18.0, 3.0), (25.0, 4.0), (35.0, 6.0), (45.0, 8.0)] { + s.ring(P::new(x, 70.0 + 0.3 * r % 1.0), r, w); + s.ring(P::new(x + 10.0, 190.0), r * 1.2, (w * 0.5_f64).max(1.0)); + x += 2.0 * r + 35.0; + } + s +} + +pub fn junctions(w: f64) -> Scene { + let mut s = Scene::new(&format!("junctions w{w}"), 520, 220, 13.0); + s.offset = P::new(0.27, 0.41); + // T. + s.line(&[P::new(20.0, 40.0), P::new(100.0, 40.0)], w); + s.line(&[P::new(60.0, 40.0), P::new(60.0, 100.0)], w); + s.junctions.push((P::new(60.0, 40.0), 3)); + // Y (120°). + let c = P::new(180.0, 70.0); + for a in [-90.0, 30.0, 150.0] { + s.line(&[c, pol(c, 40.0, a)], w); + } + s.junctions.push((c, 3)); + // X (90°). + let c = P::new(300.0, 70.0); + s.line(&[pol(c, 40.0, 45.0), pol(c, 40.0, 225.0)], w); + s.line(&[pol(c, 40.0, 135.0), pol(c, 40.0, 315.0)], w); + s.junctions.push((c, 4)); + // Oblique T (stem at 50°). + s.line(&[P::new(380.0, 40.0), P::new(500.0, 40.0)], w); + s.line(&[P::new(430.0, 40.0), pol(P::new(430.0, 40.0), 70.0, 50.0)], w); + s.junctions.push((P::new(430.0, 40.0), 3)); + // Thin stem on a thick bar. + let bar = (w * 2.0).min(12.0); + s.line(&[P::new(20.0, 160.0), P::new(140.0, 160.0)], bar); + s.line(&[P::new(80.0, 160.0), P::new(80.0, 215.0)], (w * 0.75).max(1.0)); + s.junctions.push((P::new(80.0, 160.0), 3)); + // Narrow Y (arms 40° apart). + let c = P::new(230.0, 150.0); + s.line(&[c, pol(c, 60.0, 180.0)], w); + s.line(&[c, pol(c, 50.0, -20.0)], w); + s.line(&[c, pol(c, 50.0, 20.0)], w); + s.junctions.push((c, 3)); + // Asterisk-free "K": arms meeting the stem at one point (degree 4). + let c = P::new(400.0, 165.0); + s.line(&[P::new(400.0, 120.0), P::new(400.0, 210.0)], w); + s.line(&[c, pol(c, 50.0, -40.0)], w); + s.line(&[c, pol(c, 50.0, 40.0)], w); + s.junctions.push((c, 4)); + shifted(s) +} + +pub fn crossings(w: f64) -> Scene { + let mut s = Scene::new(&format!("crossings w{w}"), 560, 140, 13.0); + s.offset = P::new(0.27, 0.41); + for (k, ang) in [20.0, 30.0, 45.0, 60.0, 90.0].into_iter().enumerate() { + let c = P::new(60.0 + 110.0 * k as f64, 70.0); + s.line(&[pol(c, 50.0, 180.0 - ang / 2.0), pol(c, 50.0, -ang / 2.0)], w); + s.line(&[pol(c, 50.0, 180.0 + ang / 2.0), pol(c, 50.0, ang / 2.0)], w); + s.junctions.push((c, 4)); + } + shifted(s) +} + +pub fn dashed(w: f64) -> Scene { + let mut s = Scene::new(&format!("dashed w{w}"), 400, 260, 13.0); + s.min_len = 0.0; + for (k, (dash, gap, ang)) in [(12.0, 4.0, 0.0), (6.0, 3.0, 15.0), (10.0, 4.0, 37.0), (16.0, 6.0, 90.0), (8.0, 5.0, 135.0)].into_iter().enumerate() + { + let c = P::new(80.0 + 120.0 * (k % 3) as f64, 70.0 + 120.0 * (k / 3) as f64); + let a: f64 = f64::to_radians(ang); + let dir = P::new(a.cos(), a.sin()); + let mut t = -50.0; + while t + dash <= 50.0 { + s.line(&[c + dir * t, c + dir * (t + dash)], w); + // The visible gap is `gap`: the round caps reach w/2 past each dash's ends. + t += dash + gap + w; + } + } + s +} + +/// Letter-like strokes drawn from lines and arcs (no fonts). +pub fn letters(w: f64) -> Scene { + let mut s = Scene::new(&format!("letters w{w}"), 520, 120, 13.0); + s.offset = P::new(0.27, 0.41); + // A: two legs meeting at the apex (a corner), crossbar (two T's). + let (l, apex, r) = (P::new(20.0, 100.0), P::new(50.0, 20.0), P::new(80.0, 100.0)); + s.line(&[l, apex, r], w); + let (bl, br) = (l.lerp(apex, 0.4), r.lerp(apex, 0.4)); + s.line(&[bl, br], w); + s.junctions.push((bl, 3)); + s.junctions.push((br, 3)); + s.corners.push(apex); + // H. + s.line(&[P::new(110.0, 20.0), P::new(110.0, 100.0)], w); + s.line(&[P::new(160.0, 20.0), P::new(160.0, 100.0)], w); + s.line(&[P::new(110.0, 60.0), P::new(160.0, 60.0)], w); + s.junctions.push((P::new(110.0, 60.0), 3)); + s.junctions.push((P::new(160.0, 60.0), 3)); + // e: bar across a circle that opens at the lower right. + let c = P::new(215.0, 65.0); + s.arc(c, 25.0, 0.0, -std::f64::consts::PI * 1.75, w); + s.line(&[pol(c, 25.0, 180.0), pol(c, 25.0, 0.0)], w); + s.junctions.push((pol(c, 25.0, 180.0), 3)); + s.corners.push(pol(c, 25.0, 0.0)); + // k. + let c = P::new(275.0, 75.0); + s.line(&[P::new(275.0, 15.0), P::new(275.0, 105.0)], w); + s.line(&[c, P::new(310.0, 45.0)], w); + s.line(&[c, P::new(310.0, 105.0)], w); + s.junctions.push((c, 4)); + // x. + s.line(&[P::new(330.0, 40.0), P::new(370.0, 100.0)], w); + s.line(&[P::new(370.0, 40.0), P::new(330.0, 100.0)], w); + s.junctions.push((P::new(350.0, 70.0), 4)); + // s: two arcs. + s.arc(P::new(405.0, 50.0), 15.0, -0.2, -std::f64::consts::PI * 1.5, w); + s.arc(P::new(405.0, 80.0), 15.0, -std::f64::consts::PI * 0.5, std::f64::consts::PI * 0.8, w); + s.corners.push(P::new(405.0, 65.0)); + // o. + s.ring(P::new(475.0, 65.0), 25.0, w); + shifted(s) +} + +/// Thin strokes and wide fills, with strokes running into fills; Stroke width 6. +pub fn mix() -> Scene { + let mut s = Scene::new("mix", 480, 300, 6.0); + s.fills.push(GtFill::Disc(P::new(100.0, 100.0), 30.0)); + s.fills.push(GtFill::Poly(vec![P::new(260.0, 60.0), P::new(340.0, 60.0), P::new(340.0, 110.0), P::new(260.0, 110.0)])); + s.fills.push(GtFill::Disc(P::new(400.0, 220.0), 22.0)); + // Lines into the disc and the rectangle. + s.line(&[P::new(20.0, 200.0), P::new(80.0, 120.0)], 3.0); + s.line(&[P::new(130.0, 100.0), P::new(260.0, 85.0)], 2.0); + s.line(&[P::new(300.0, 110.0), P::new(300.0, 200.0), P::new(380.0, 210.0)], 4.0); + // Free strokes. + s.line(&[P::new(40.0, 260.0), P::new(250.0, 280.0)], 3.0); + s.curve(|t| P::new(150.0 + 120.0 * t, 200.0 + 25.0 * (t * 7.0).sin()), |_| 2.0, 200, false); + s.line(&[P::new(420.0, 30.0), P::new(460.0, 150.0)], 5.0); + s +} + +/// Strokes whose width varies: tapers, a lens, a swelling brush curve; Stroke width 12. +pub fn varying() -> Scene { + let mut s = Scene::new("varying", 480, 300, 12.0); + s.curve(|t| P::new(30.0 + 200.0 * t, 40.0), |t| 2.0 + 8.0 * t, 200, false); + s.curve(|t| P::new(260.0 + 200.0 * t, 40.0 + 30.0 * t), |t| 2.0 + 8.0 * (std::f64::consts::PI * t).sin(), 200, false); + s.curve(|t| P::new(30.0 + 420.0 * t, 150.0 + 40.0 * (t * 6.0).sin()), |t| 5.5 + 3.0 * (t * 9.0).sin(), 600, false); + s.curve( + |t| { + let a = t * 1.6 * std::f64::consts::PI; + P::new(120.0, 250.0) + P::new(a.cos(), a.sin()) * 35.0 + }, + |t| 2.0 + 9.0 * t, + 300, + false, + ); + s.curve(|t| P::new(250.0 + 200.0 * t, 260.0 - 50.0 * t * t), |t| 9.0 - 6.0 * t, 200, false); + s +} + +/// Shifted copies of the integer-placed scenes' junctions and corners (strokes shift in `var`). +fn shifted(mut s: Scene) -> Scene { + let o = s.offset; + for j in &mut s.junctions { + j.0 = j.0 + o; + } + for c in &mut s.corners { + *c = *c + o; + } + s +} + +pub fn all() -> Vec { + let mut v = vec![]; + for w in [1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0] { + v.push(lines(w)); + } + for w in [2.0, 4.0, 8.0] { + v.push(curves(w)); + } + v.push(circles()); + for w in [2.0, 4.0, 8.0] { + v.push(junctions(w)); + } + for w in [3.0, 6.0] { + v.push(crossings(w)); + } + for w in [2.0, 4.0] { + v.push(dashed(w)); + } + for w in [3.0, 5.0] { + v.push(letters(w)); + } + v.push(mix()); + v.push(varying()); + v +} diff --git a/crates/trace/tests/quality/main.rs b/crates/trace/tests/quality/main.rs index debb3ea76..2b83c668a 100644 --- a/crates/trace/tests/quality/main.rs +++ b/crates/trace/tests/quality/main.rs @@ -6,6 +6,7 @@ //! ``` //! //! `TRACE_QUALITY_MP` (default `1,4,16`) sets the megapixel sizes of the cost table. +//! `TRACE_QUALITY_CLASS=centre-lines` selects only the code-drawn centre-line report. //! //! A yardstick: `TRACE_QUALITY_IMAGES=dir` also writes the drawings there as PNG files, and //! `TRACE_QUALITY_YARDSTICK=dir` measures the SVG files of the same names found there (what another @@ -15,6 +16,9 @@ // The peak-memory measure needs a counting global allocator. #![allow(unsafe_code)] +mod centre_geom; +mod centre_lines; +mod centre_scenes; mod inputs; mod measure; mod raster; @@ -347,12 +351,23 @@ fn report() -> String { } } } + md += ¢re_lines::report(); md } #[test] #[ignore = "slow: measures every preset and writes the quality report (run with --release)"] fn quality_report() { + // One ignored test owns the allocator's cost measurements. Selecting this class + // here avoids a second ignored test allocating concurrently with those measurements. + if std::env::var("TRACE_QUALITY_CLASS").as_deref() == Ok("centre-lines") { + let md = centre_lines::report(); + println!("{md}"); + if let Ok(path) = std::env::var("TRACE_QUALITY_REPORT") { + std::fs::write(path, md).unwrap(); + } + return; + } let md = report(); println!("{md}"); // Gradients finds none in art that is flat colour, however it is degraded. diff --git a/crates/trace/tests/quality/measure.rs b/crates/trace/tests/quality/measure.rs index 10c3e32c1..b1af2d1fc 100644 --- a/crates/trace/tests/quality/measure.rs +++ b/crates/trace/tests/quality/measure.rs @@ -56,7 +56,7 @@ pub fn fidelity(src: &Raster, out: &Raster) -> (f64, f64) { } /// A traced path as the shape it paints: stroked paths become their stroke's outline. -fn painted(tp: &TracedPath) -> BezPath { +pub(super) fn painted(tp: &TracedPath) -> BezPath { let bez = tp.path.to_bezpath(); match tp.stroke { Some(width) => { diff --git a/docs/image-trace-quality.md b/docs/image-trace-quality.md index ad5438c0f..1d76b4eca 100644 --- a/docs/image-trace-quality.md +++ b/docs/image-trace-quality.md @@ -1,5 +1,6 @@ # Image Trace quality +> **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (centre-lines baseline; no tracing behaviour change) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: a higher Paths never gives fewer segments on collapsed loops) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: the notch rule reads only its own piece) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: an edge's sub-pixel shift is the same from either side) @@ -1070,10 +1071,129 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab | Outline, enhanced | photo-like | 16 | 1.64 s, 632 paths, 34884 anchors, 256 MB | | Outline, enhanced | four-colour badge | 16 | 0.73 s, 1 paths, 49 anchors, 176 MB | +## Centre lines + +Baseline measured on upstream `8bc65329` (2026-10-17). Earlier preset and cost tables retain their previous measurement. To regenerate only this class: + +```text +TRACE_QUALITY_CLASS=centre-lines TRACE_QUALITY_REPORT=centre-lines.md cargo test --release -p vectorcraft-trace --test quality quality_report -- --ignored --nocapture +``` + +Code-drawn scenes only: 23 scenes, each anti-aliased (8×8 coverage samples) and hard (that coverage thresholded at 0.5). Coordinates, widths and scene Stroke values are fixed; junction/letter/crossing coordinates include the 0.27/0.41 px offset. Create Fills and Strokes are on, Stroke is the scene's limit (13 px, mix 6 px, varying 12 px), Black and White threshold 128, Noise 4, Ignore White on, other settings at their defaults. Today's tracer has no minimum-length setting (the dashed scenes specify 0 for future use). + +Distances use dense exact centre samples (≤0.25 px), emitted curves sampled at ≤0.5 px control-polygon steps, and nearest-point searches capped at 30 px. Found is the fraction of true samples within 1.5 px; width errors use output samples within 2 px of a true stroke. Samples inside exact fills are excluded. Topology is recovered from emitted subpath endpoints and intersections, merging coincident positions within 0.05 px; an endpoint contributes one arm, an interior two. Junction matching radius is local true width + 3 px; ends use half-width + 4 px. Each match is used once. Free ends exclude true junctions, corners, other strokes and fills; unmatched emitted ends are spurs. Junctions/ends within 2 px of fills are excluded. End error is distance, with signed error along the outward tangent in parentheses. Coverage is a union of painted output at pixel centres (0.02 px flattening tolerance, actual emitted caps), compared with the exact swept shape at pixel centres, for both input variants. Area error is signed relative to exact area. Anchors counts stroke anchors, including all compound subpaths; paths counts stroked + filled paths. Time covers tracing only, one scene at a time. Profile-dependent columns are unavailable (–). + +### Centre line and width (px) + +| scene | input | paths | out→true mean / p95 | true→out mean / p95 | found % | width err uniform mean / p95 | variable mean / p95 | profile mean | +|---|---|---|---|---|---|---|---|---| +| lines w1 | aa | 93+0 | 0.08 / 0.13 | 2.89 / 30.00 | 88.0 | 0.41 / 1.00 | – | – | +| lines w1 | hard | 92+0 | 0.17 / 0.36 | 2.19 / 9.26 | 75.9 | 0.30 / 1.00 | – | – | +| lines w2 | aa | 24+0 | 0.08 / 0.17 | 0.50 / 0.38 | 95.6 | 0.05 / 0.06 | – | – | +| lines w2 | hard | 24+0 | 0.19 / 0.41 | 0.58 / 0.66 | 95.6 | 0.14 / 1.00 | – | – | +| lines w3 | aa | 24+0 | 0.08 / 0.15 | 0.53 / 0.48 | 95.3 | 0.08 / 0.09 | – | – | +| lines w3 | hard | 24+0 | 0.18 / 0.41 | 0.62 / 0.85 | 95.3 | 0.19 / 1.00 | – | – | +| lines w4 | aa | 24+0 | 0.08 / 0.16 | 0.57 / 1.18 | 95.1 | 0.11 / 0.12 | – | – | +| lines w4 | hard | 24+0 | 0.17 / 0.44 | 0.63 / 1.06 | 95.1 | 0.14 / 1.00 | – | – | +| lines w6 | aa | 24+0 | 0.09 / 0.18 | 1.14 / 6.75 | 93.6 | 0.13 / 0.19 | – | – | +| lines w6 | hard | 24+0 | 0.18 / 0.51 | 1.18 / 6.20 | 93.7 | 0.26 / 1.00 | – | – | +| lines w8 | aa | 24+0 | 0.10 / 0.22 | 1.17 / 7.37 | 93.4 | 0.16 / 0.26 | – | – | +| lines w8 | hard | 24+0 | 0.23 / 0.64 | 1.23 / 6.95 | 93.4 | 0.17 / 1.00 | – | – | +| lines w10 | aa | 24+0 | 0.11 / 0.26 | 1.20 / 7.96 | 93.3 | 0.18 / 0.32 | – | – | +| lines w10 | hard | 24+0 | 0.27 / 0.68 | 1.29 / 7.79 | 93.4 | 0.35 / 1.00 | – | – | +| lines w12 | aa | 24+0 | 0.15 / 0.42 | 1.59 / 13.81 | 92.6 | 0.21 / 0.39 | – | – | +| lines w12 | hard | 24+0 | 0.31 / 0.78 | 1.31 / 8.06 | 93.3 | 0.32 / 1.00 | – | – | +| curves w2 | aa | 4+0 | 0.11 / 0.22 | 0.15 / 0.26 | 100.0 | 0.00 / 0.01 | – | – | +| curves w2 | hard | 4+0 | 0.24 / 0.57 | 0.26 / 0.57 | 100.0 | 0.05 / 0.06 | – | – | +| curves w4 | aa | 4+0 | 0.13 / 0.27 | 0.18 / 0.30 | 99.6 | 0.01 / 0.03 | – | – | +| curves w4 | hard | 4+0 | 0.27 / 0.64 | 0.28 / 0.63 | 99.9 | 0.00 / 0.00 | – | – | +| curves w8 | aa | 4+0 | 0.16 / 0.31 | 0.24 / 0.36 | 98.1 | 0.01 / 0.02 | – | – | +| curves w8 | hard | 4+0 | 0.28 / 0.63 | 0.35 / 0.65 | 98.1 | 0.00 / 0.00 | – | – | +| circles | aa | 25+0 | 0.09 / 0.25 | 0.26 / 0.37 | 97.2 | 0.01 / 0.03 | – | – | +| circles | hard | 23+0 | 0.28 / 0.66 | 0.46 / 0.78 | 96.1 | 0.02 / 0.09 | – | – | +| junctions w2 | aa | 7+0 | 0.12 / 0.31 | 3.81 / 30.00 | 86.6 | 0.15 / 2.50 | – | – | +| junctions w2 | hard | 7+0 | 0.38 / 0.87 | 4.09 / 30.00 | 86.3 | 0.16 / 2.50 | – | – | +| junctions w4 | aa | 7+0 | 0.18 / 0.86 | 1.87 / 17.27 | 90.6 | 0.29 / 5.00 | – | – | +| junctions w4 | hard | 7+0 | 0.42 / 1.15 | 2.10 / 17.87 | 90.3 | 0.31 / 5.00 | – | – | +| junctions w8 | aa | 7+0 | 0.28 / 1.63 | 2.11 / 19.60 | 86.9 | 0.34 / 6.00 | – | – | +| junctions w8 | hard | 7+0 | 0.43 / 1.64 | 2.24 / 19.46 | 86.5 | 0.33 / 6.00 | – | – | +| crossings w3 | aa | 5+0 | 0.18 / 0.81 | 4.35 / 30.00 | 80.6 | 0.06 / 0.09 | – | – | +| crossings w3 | hard | 5+0 | 0.39 / 1.13 | 4.48 / 30.00 | 80.4 | 0.16 / 0.32 | – | – | +| crossings w6 | aa | 5+0 | 0.43 / 2.30 | 5.75 / 30.00 | 73.9 | 0.12 / 0.19 | – | – | +| crossings w6 | hard | 5+0 | 0.58 / 2.40 | 5.89 / 30.00 | 72.4 | 0.19 / 0.26 | – | – | +| dashed w2 | aa | 31+0 | 0.08 / 0.15 | 0.14 / 0.27 | 100.0 | 0.18 / 0.87 | – | – | +| dashed w2 | hard | 31+0 | 0.09 / 0.25 | 0.16 / 0.35 | 100.0 | 0.22 / 1.00 | – | – | +| dashed w4 | aa | 29+0 | 0.15 / 0.76 | 0.34 / 1.78 | 94.0 | 0.19 / 1.00 | – | – | +| dashed w4 | hard | 29+0 | 0.19 / 0.70 | 0.40 / 1.93 | 93.7 | 0.15 / 1.00 | – | – | +| letters w3 | aa | 7+0 | 0.16 / 0.61 | 0.19 / 0.40 | 99.4 | 0.02 / 0.13 | – | – | +| letters w3 | hard | 7+0 | 0.32 / 0.79 | 0.33 / 0.73 | 99.6 | 0.06 / 0.20 | – | – | +| letters w5 | aa | 7+0 | 0.29 / 1.46 | 0.30 / 1.14 | 96.8 | 0.02 / 0.20 | – | – | +| letters w5 | hard | 7+0 | 0.40 / 1.57 | 0.40 / 1.18 | 96.8 | 0.08 / 0.20 | – | – | +| mix | aa | 3+1 | 0.10 / 0.26 | 12.89 / 30.00 | 57.3 | 0.03 / 0.12 | – | – | +| mix | hard | 3+1 | 0.21 / 0.47 | 12.94 / 30.00 | 57.3 | 0.08 / 0.15 | – | – | +| varying | aa | 5+0 | 0.13 / 0.29 | 0.19 / 0.33 | 99.6 | 1.90 / 4.21 | – | – | +| varying | hard | 5+0 | 0.24 / 0.61 | 0.28 / 0.63 | 99.4 | 1.91 / 4.36 | – | – | + +### Topology, ends, coverage + +| scene | input | junctions ok / wrong deg / missed / extra | junction pos err | ends found | end err (signed) | spurs | IoU uniform / profile | area err uniform / profile | fill→fill % | anchors | width pts | ms | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| lines w1 | aa | 0 / 0 / 0 / 81 | – | 41/48 | 0.96 (-0.95) | 90 | 0.633 / – | 10.9% / – | – | 186 | – | 5.0 | +| lines w1 | hard | 0 / 0 / 0 / 0 | – | 35/48 | 1.24 (-1.19) | 149 | 0.632 / – | -25.8% / – | – | 184 | – | 3.7 | +| lines w2 | aa | 0 / 0 / 0 / 0 | – | 44/48 | 0.22 (-0.18) | 4 | 0.894 / – | -3.1% / – | – | 52 | – | 4.1 | +| lines w2 | hard | 0 / 0 / 0 / 0 | – | 44/48 | 0.41 (-0.24) | 4 | 0.786 / – | -3.4% / – | – | 52 | – | 4.2 | +| lines w3 | aa | 0 / 0 / 0 / 0 | – | 44/48 | 0.20 (-0.17) | 4 | 0.889 / – | -6.6% / – | – | 52 | – | 4.1 | +| lines w3 | hard | 0 / 0 / 0 / 0 | – | 44/48 | 0.48 (-0.31) | 4 | 0.835 / – | -4.5% / – | – | 49 | – | 4.2 | +| lines w4 | aa | 0 / 0 / 0 / 0 | – | 44/48 | 0.22 (-0.18) | 4 | 0.916 / – | -3.5% / – | – | 58 | – | 4.3 | +| lines w4 | hard | 0 / 0 / 0 / 0 | – | 44/48 | 0.37 (-0.15) | 4 | 0.868 / – | -3.6% / – | – | 56 | – | 4.3 | +| lines w6 | aa | 0 / 0 / 0 / 0 | – | 45/48 | 0.23 (-0.15) | 3 | 0.911 / – | -5.3% / – | – | 64 | – | 4.7 | +| lines w6 | hard | 0 / 0 / 0 / 0 | – | 45/48 | 0.39 (-0.10) | 3 | 0.874 / – | -2.7% / – | – | 57 | – | 4.6 | +| lines w8 | aa | 0 / 0 / 0 / 0 | – | 45/48 | 0.27 (-0.20) | 3 | 0.910 / – | -5.1% / – | – | 62 | – | 5.0 | +| lines w8 | hard | 0 / 0 / 0 / 0 | – | 45/48 | 0.48 (-0.18) | 3 | 0.878 / – | -4.8% / – | – | 62 | – | 5.0 | +| lines w10 | aa | 0 / 0 / 0 / 0 | – | 45/48 | 0.33 (-0.19) | 3 | 0.908 / – | -4.6% / – | – | 73 | – | 5.3 | +| lines w10 | hard | 0 / 0 / 0 / 0 | – | 45/48 | 0.50 (-0.18) | 3 | 0.876 / – | -2.7% / – | – | 67 | – | 5.4 | +| lines w12 | aa | 0 / 0 / 0 / 0 | – | 46/48 | 0.46 (-0.21) | 2 | 0.900 / – | -4.9% / – | – | 72 | – | 5.7 | +| lines w12 | hard | 0 / 0 / 0 / 0 | – | 45/48 | 0.55 (-0.15) | 3 | 0.880 / – | -3.6% / – | – | 64 | – | 6.0 | +| curves w2 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.19 (-0.15) | 0 | 0.922 / – | -0.1% / – | – | 35 | – | 4.2 | +| curves w2 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.21 (-0.10) | 0 | 0.814 / – | -1.8% / – | – | 27 | – | 4.3 | +| curves w4 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.99 (-0.89) | 0 | 0.939 / – | -1.0% / – | – | 34 | – | 4.6 | +| curves w4 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.63 (-0.47) | 0 | 0.881 / – | -0.1% / – | – | 25 | – | 4.6 | +| curves w8 | aa | 0 / 0 / 0 / 0 | – | 6/8 | 1.46 (-1.34) | 2 | 0.953 / – | -1.1% / – | – | 41 | – | 5.2 | +| curves w8 | hard | 0 / 0 / 0 / 0 | – | 6/8 | 1.21 (-0.91) | 2 | 0.924 / – | -1.1% / – | – | 30 | – | 5.2 | +| circles | aa | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 32 | 0.962 / – | -1.2% / – | – | 70 | – | 7.0 | +| circles | hard | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 28 | 0.887 / – | -1.2% / – | – | 52 | – | 7.6 | +| junctions w2 | aa | 6 / 0 / 1 / 1 | 1.22 | 20/23 | 0.28 (-0.19) | 1 | 0.777 / – | -6.6% / – | – | 67 | – | 3.7 | +| junctions w2 | hard | 6 / 0 / 1 / 0 | 1.31 | 20/23 | 0.51 (-0.20) | 1 | 0.698 / – | -8.4% / – | – | 48 | – | 3.7 | +| junctions w4 | aa | 5 / 1 / 1 / 2 | 1.67 | 20/23 | 0.30 (-0.24) | 1 | 0.841 / – | -3.3% / – | – | 91 | – | 3.9 | +| junctions w4 | hard | 5 / 1 / 1 / 2 | 1.67 | 20/23 | 0.54 (-0.24) | 1 | 0.788 / – | -2.5% / – | – | 74 | – | 3.9 | +| junctions w8 | aa | 4 / 2 / 1 / 4 | 3.00 | 20/23 | 0.45 (-0.31) | 1 | 0.857 / – | -6.2% / – | – | 104 | – | 4.6 | +| junctions w8 | hard | 5 / 1 / 1 / 2 | 3.02 | 20/23 | 0.73 (-0.34) | 1 | 0.836 / – | -6.0% / – | – | 89 | – | 4.4 | +| crossings w3 | aa | 0 / 3 / 2 / 5 | 2.91 | 16/20 | 0.39 (-0.31) | 2 | 0.757 / – | -20.4% / – | – | 68 | – | 2.8 | +| crossings w3 | hard | 0 / 3 / 2 / 6 | 2.81 | 16/20 | 0.58 (-0.31) | 2 | 0.658 / – | -23.9% / – | – | 61 | – | 2.8 | +| crossings w6 | aa | 0 / 3 / 2 / 8 | 5.00 | 17/20 | 0.20 (-0.15) | 1 | 0.754 / – | -21.3% / – | – | 103 | – | 3.5 | +| crossings w6 | hard | 0 / 3 / 2 / 8 | 5.00 | 17/20 | 0.43 (-0.27) | 1 | 0.718 / – | -21.8% / – | – | 78 | – | 3.4 | +| dashed w2 | aa | 0 / 0 / 0 / 0 | – | 62/62 | 0.34 (-0.31) | 0 | 0.861 / – | 8.4% / – | – | 62 | – | 2.0 | +| dashed w2 | hard | 0 / 0 / 0 / 0 | – | 62/62 | 0.36 (-0.26) | 0 | 0.843 / – | 9.9% / – | – | 62 | – | 2.3 | +| dashed w4 | aa | 0 / 0 / 0 / 0 | – | 58/58 | 0.78 (-0.52) | 0 | 0.849 / – | 3.5% / – | – | 61 | – | 2.2 | +| dashed w4 | hard | 0 / 0 / 0 / 0 | – | 58/58 | 0.88 (-0.53) | 0 | 0.850 / – | -0.3% / – | – | 60 | – | 2.0 | +| letters w3 | aa | 5 / 2 / 0 / 3 | 1.42 | 17/17 | 0.30 (-0.24) | 0 | 0.941 / – | -0.6% / – | – | 106 | – | 3.2 | +| letters w3 | hard | 5 / 2 / 0 / 3 | 1.42 | 17/17 | 0.44 (-0.27) | 0 | 0.860 / – | -0.5% / – | – | 82 | – | 3.2 | +| letters w5 | aa | 5 / 2 / 0 / 8 | 1.81 | 16/17 | 0.80 (-0.65) | 1 | 0.925 / – | -1.1% / – | – | 134 | – | 3.5 | +| letters w5 | hard | 5 / 2 / 0 / 8 | 1.81 | 16/17 | 0.89 (-0.67) | 1 | 0.889 / – | -2.0% / – | – | 117 | – | 3.4 | +| mix | aa | 0 / 0 / 0 / 0 | – | 6/7 | 0.21 (-0.19) | 0 | 0.987 / – | 0.1% / – | 99.7 | 11 | – | 5.9 | +| mix | hard | 0 / 0 / 0 / 0 | – | 6/7 | 0.61 (-0.52) | 0 | 0.971 / – | -0.1% / – | 99.1 | 10 | – | 5.9 | +| varying | aa | 0 / 0 / 0 / 0 | – | 10/10 | 2.11 (-1.12) | 0 | 0.738 / – | 6.0% / – | – | 22 | – | 4.7 | +| varying | hard | 0 / 0 / 0 / 0 | – | 10/10 | 2.17 (-1.17) | 0 | 0.732 / – | 6.0% / – | – | 21 | – | 4.8 | + +**aa totals**: junctions 25 correct, 13 wrong degree, 7 missed, 112 extra (of 45); ends 638 of 688; spurs 154. + +**hard totals**: junctions 26 correct, 12 wrong degree, 7 missed, 29 extra (of 45); ends 631 of 688; spurs 210. + ## Revision history | date | change | |:--|:--| +| 2026-10-17 | M12: add the ignored centre-lines class and measure the existing Create Strokes output on 23 code-drawn scenes, aa and hard; centre/width/topology/end/coverage metrics, no new images, dependencies or behaviour | | 2026-10-16 | Image Trace: a loop or junction-returning piece whose fit collapsed to one cubic keeps the fewest segments that hold its area and course instead of every positioned point, so a higher Paths no longer gives fewer segments on code-drawn noisy images (six falling runs to none; one other cause remains). | | 2026-10-16 | Image Trace: a notch corner is cut only when the straight edges beside it lie in the shared piece, so both regions of the piece get the same points (code-drawn noisy colour images: 93 uncovered sample points to 0). | | 2026-10-16 | Image Trace: a crack's sub-pixel shift is the mean of the readings from its two sides, exactly antisymmetric, so abutting regions get the same points on thin slivers (a code-drawn noisy ripple went from 482–1,228 uncovered sample points per trace to 0). | diff --git a/docs/roadmap.md b/docs/roadmap.md index 8f0ce4e2d..8ec752658 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: M12's Image Trace quick presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: per-corner radius, #1682; Width Point Edit from the Width tool, #1814, #1630; guides, #1782, #1836; anchor X/Y/W/H, #1824; toolbar and All Tools, #1751; macOS save sheets, #1855; rendering caches, #1667–#1792; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; Dim Images, #1683; estimates unchanged; M12 row: Image Trace's enhanced presets; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Expand on repeats, #1756; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace #1647, #1657; estimates unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1601; Warp Arc, #1578; Pathfinder strokes, #1570; saves, #1620 in part; OpenType features, #1607; intersect Smart Guides, #1602 in part; estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; M12 row: Image Trace's presets and options as they are now; Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; M2: Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; legacy `.ai` of version 7 saved, #868; estimates unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: per-corner radius, #1682; Width Point Edit from the Width tool, #1814, #1630; guides, #1782, #1836; anchor X/Y/W/H, #1824; toolbar and All Tools, #1751; macOS save sheets, #1855; rendering caches, #1667–#1792; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; Dim Images, #1683; estimates unchanged; M12 row: Image Trace's enhanced presets; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Expand on repeats, #1756; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace #1647, #1657; estimates unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1601; Warp Arc, #1578; Pathfinder strokes, #1570; saves, #1620 in part; OpenType features, #1607; intersect Smart Guides, #1602 in part; estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; M12 row: Image Trace's presets and options as they are now; Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (M12 centre-lines baseline measured; scores and hours unchanged; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; correction: M12's Image Trace quick presets leave the panel as it was, the Image Trace button opens it; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: per-corner radius, #1682; Width Point Edit from the Width tool, #1814, #1630; guides, #1782, #1836; anchor X/Y/W/H, #1824; toolbar and All Tools, #1751; macOS save sheets, #1855; rendering caches, #1667–#1792; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; Dim Images, #1683; estimates unchanged; M12 row: Image Trace's enhanced presets; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Expand on repeats, #1756; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace #1647, #1657; estimates unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1601; Warp Arc, #1578; Pathfinder strokes, #1570; saves, #1620 in part; OpenType features, #1607; intersect Smart Guides, #1602 in part; estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; M12 row: Image Trace's presets and options as they are now; Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; M2: Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; legacy `.ai` of version 7 saved, #868; estimates unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: per-corner radius, #1682; Width Point Edit from the Width tool, #1814, #1630; guides, #1782, #1836; anchor X/Y/W/H, #1824; toolbar and All Tools, #1751; macOS save sheets, #1855; rendering caches, #1667–#1792; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; Dim Images, #1683; estimates unchanged; M12 row: Image Trace's enhanced presets; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Expand on repeats, #1756; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace #1647, #1657; estimates unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1601; Warp Arc, #1578; Pathfinder strokes, #1570; saves, #1620 in part; OpenType features, #1607; intersect Smart Guides, #1602 in part; estimates unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; M12 row: Image Trace's presets and options as they are now; Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) Forward-looking plan. Where VectorCraft stands is in [`ROADMAP.md`](../ROADMAP.md) and [target-app-parity.md](target-app-parity.md); the ranked work list is [gaps.md](gaps.md). Hours are Opus 5.5 @@ -87,6 +87,7 @@ M3 follow-up (#1968): native saves retain the per-document transparency grid set | Date | Change | Summary | |---|---|---| +| 2026-10-17 | minor | M12: code-drawn centre-line scenes and metrics added to the ignored quality harness; current Create Strokes baseline in `image-trace-quality.md`. Tests only; scores, missing features and estimates unchanged. | | 2026-10-16 | minor | Image Trace: a loop or junction-returning piece whose fit collapsed to one cubic keeps the fewest segments that hold its area and course instead of every positioned point, so a higher Paths no longer gives fewer segments on code-drawn noisy images (six falling runs to none; one fall with another cause remains). Scores and hours unchanged. | | 2026-10-16 | minor | Image Trace: a notch corner is cut only when the straight edges beside it lie in the shared piece, so both regions of the piece get the same points (code-drawn noisy colour images: 93 uncovered sample points to 0). Scores and hours unchanged. | | 2026-10-16 | minor | Image Trace: a crack's sub-pixel shift is the mean of the readings from its two sides, exactly antisymmetric, so abutting regions get the same points on thin slivers (a code-drawn noisy ripple went from 482–1,228 uncovered sample points per trace to 0). Scores and hours unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4c3e43771..33af28d1a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese complete under the tests, #1914, 71% → 74%; G6 image variables: Automation ~60% → ~62% / detail ~65% → ~68%, hours lowered; Warp Options controls verified by focused checks and original-image review; estimates unchanged; G11: Asset Export Art Optimized seam (#983 / #1037) and supersampled thumbs; Export for Screens 85% → 90%; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; File interop: legacy `.ai` of version 7 saved with type as text, hidden objects kept and an artboard picker, #868, scores unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (M12 centre-lines baseline measured; scores and hours unchanged; installed plug-ins stay installed after a restart, #2541; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; Image Trace: a higher Paths never gives fewer segments on collapsed loops; Image Trace: the notch rule reads only its own piece; Image Trace: an edge's sub-pixel shift is the same from either side; Image Trace Flat Logo traces line art on white paper instead of a blank result; score and hours unchanged; Image Trace: a simpler contour walk with no precomputed direction table; transparency grid visibility saved in native documents, #1968; MCP cancellation, progress and busy answers during long exports, #2165, #2171, #2172; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; native family-name fallback from English-only typographic names, #2208; scores unchanged; batch exporting, #1935; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); G5: Symbol Options Export Type (Graphic / Movie Clip); Localization: Brazilian Portuguese complete under the tests, #1914, 71% → 74%; G6 image variables: Automation ~60% → ~62% / detail ~65% → ~68%, hours lowered; Warp Options controls verified by focused checks and original-image review; estimates unchanged; G11: Asset Export Art Optimized seam (#983 / #1037) and supersampled thumbs; Export for Screens 85% → 90%; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged.; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; File interop: legacy `.ai` of version 7 saved with type as text, hidden objects kept and an artboard picker, #868, scores unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: anchor X/Y/W/H, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar and All Tools, #1751; Actions panel selection, #1629 in part; redo during a drag, #1719; preferences written on OK, #1747; type in symbols after reopening, #1738; macOS save sheets, #1855, the Format menu withdrawn; stroke outlines shared, #1667; untrusted files, #1852, #1862–#1873; failed-save dialog, #1685; effects, #1864, Dim Images, #1683; duplicated rows of the 2026-10-05 table noted for the owner; scores unchanged; Image Trace: Shapes rework, stroke caps, enhanced presets, gradients over noise, measured in `image-trace-quality.md`, score unchanged; verified PngSuite downloads, #1729; synthetic control-input budgets, #1709; 2026-10-14 merges: anchor Align and Distribute, #1636; Direct Selection and Delete fixes, #1674, #1666, #1743; Escape while typing, #1686; Pen clicks, #1663; Expand on repeats, #1756; Roughen and Scribble, #1643, #1648; file and input hardening, #1776–#1783, #1786, #1807; MCP limits and envelope, #1727, #1728; Image Trace levelling and Flat Logo on scans, #1647, #1657; scores unchanged; later 2026-10-13 merges: SVG import, #1565, #1596, #1568, #1598, #1601; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment, #1584; saves, #1620 in part; Warp Arc, #1578; Zig Zag, #1558; Pathfinder strokes, #1570; OpenType features, #1607; intersect Smart Guides, #1602 in part; a guide's position in the Transform panel, #1597 in part, a convenience with no parity claim; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks; Image Trace: colour presets no longer shatter noisy flat art and photos trace into smooth tones, measured in `image-trace-quality.md`, score unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user score ~75% added; ready-for-real-work re-examined: 50% → 55%, mainstream 65% added; earlier the same day: full re-measure against Illustrator 2026 30.x; merged the ROADMAP's parity estimate, honest assessment and "Shipped so far" into this file; SVG knockout groups of opaque art export without masks) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is the work list made from it. Deep areas have their own checklists: [UI and interaction](ui-parity.md), @@ -606,6 +606,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Disable, Enable, Unlink and Link Opacity Mask and Opacity Mask Options (`transparency.setOpacityMask`) record no undo step when every mask they act on already has the flags they set. - **Image Trace, thin closed outlines:** generated `fit::snap_area_tests` verify nonzero interiors after straightening, opposite closed-boundary winding and Noise controls; see [quality notes](image-trace-quality.md#thin-closed-outlines-with-snap-curves-to-lines). Scores and hours unchanged; open-piece fitting is unchanged. - **Image Trace small contours:** closed fits too small to form an editable subpath retain their positioned outline, including holes. Source-generated island, hole/winding and stepped-region regressions cover Corners and Noise; defaults, parity scores and estimates are unchanged. +- **Create Strokes baseline (2026-10-17, measured):** the ignored centre-lines class covers 23 exact code-drawn scenes in aa and hard variants, including junctions, crossings, fills meeting strokes and varying widths. [Baseline and metric definitions](image-trace-quality.md#centre-lines). Topology is recovered from emitted geometry; width-profile metrics remain unavailable. No behaviour change or score increase. - **Image Trace, collapsed loops keep few segments:** a loop whose fit collapses to one cubic keeps the fewest segments that hold its area and course, so a higher Paths gives no fewer segments; a code-drawn Paths sweep in `crates/trace` (see [quality notes](image-trace-quality.md#collapsed-loops-keep-few-segments)). - **Image Trace, notches read only their own piece:** the notch rule of `subpixel::outline` reads only the shared piece, so both regions of an edge cut a corner pixel alike and leave no gap; a code-drawn noisy multi-colour coverage test in `crates/trace` (see [quality notes](image-trace-quality.md#notches-read-only-their-own-piece)). - **Image Trace, same shift from either side of an edge:** a crack's sub-pixel shift is averaged from its two sides, so abutting regions on one- and two-pixel slivers get the same points and leave nothing uncovered; code-drawn regressions in `crates/trace` (see [quality notes](image-trace-quality.md#same-sub-pixel-shift-from-either-side-of-an-edge)). @@ -760,6 +761,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur | Date | Change | Summary | |---|---|---| +| 2026-10-17 | minor | M12: code-drawn centre-line scenes and metrics added to the ignored quality harness; current Create Strokes baseline in `image-trace-quality.md`. Tests only; scores, missing features and estimates unchanged. | | 2026-10-17 | minor | Plug-ins: Install Plug-in keeps the module in a Plug-ins folder next to the preferences, installed again at startup (#2541); scores unchanged | | 2026-10-17 | minor | Evidence: Group Selection picks and moves one path of a compound path (#2441, in part); scores unchanged | | 2026-10-16 | minor | Create Gradient Mesh on an embedded image (#1676): colours sampled as area averages, transform kept, linked images refused. Scores and hours unchanged |