mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Image Trace: retain pieces returning to a junction
Squashed from #2205 onto main.
This commit is contained in:
@@ -199,6 +199,15 @@ pub(crate) fn fit_piece(pts: &[Point], style: &Style, fixed_ends: bool) -> Vec<C
|
||||
if style.upright {
|
||||
snap_upright(&mut segs, false, fixed_ends);
|
||||
}
|
||||
// A fixed-end piece can return to its junction. Fewer than two cubics
|
||||
// cannot form its own editable closed boundary. Preserve the positioned
|
||||
// edge on both sides, including when it belongs to a larger compound loop.
|
||||
if fixed_ends && first == last && segs.len() < 2 {
|
||||
let area2: f64 = pts.windows(2).map(|p| (p[0] - first).cross(p[1] - first)).sum();
|
||||
if area2.is_finite() && area2.abs() > 1e-8 {
|
||||
segs = pts.windows(2).filter(|p| p[0] != p[1]).map(|p| [p[0], p[0], p[1], p[1]]).collect();
|
||||
}
|
||||
}
|
||||
if flip { reversed(segs) } else { segs }
|
||||
}
|
||||
|
||||
@@ -271,6 +280,9 @@ fn polygon(pts: &[Point], tol: f64) -> Vec<Cubic> {
|
||||
(0..kept.len()).map(|i| [kept[i], kept[i], kept[(i + 1) % kept.len()], kept[(i + 1) % kept.len()]]).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod junction_return_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod component_survival_tests {
|
||||
use crate::{Raster, TraceParams, preset, trace};
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//! Original geometry regressions for a shared piece returning to one junction.
|
||||
use super::*;
|
||||
|
||||
fn hard_style() -> Style {
|
||||
let mut s = crate::TraceParams { corners: 0., paths: 0., ..crate::TraceParams::default() }.outline_style();
|
||||
s.tol *= 2.5;
|
||||
s.line_tol *= 1.8;
|
||||
s.circle_tol *= 1.8;
|
||||
s.sigma = 0.9;
|
||||
s
|
||||
}
|
||||
|
||||
fn returning_rect(width: f64) -> Vec<Point> {
|
||||
let d = 1.0 / 512.0;
|
||||
vec![Point::new(width, 1.), Point::new(-d, 1. + d), Point::new(-d, -d), Point::new(width + d, -d), Point::new(width, 1.)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_piece_keeps_its_junction_and_interior() {
|
||||
let style = crate::TraceParams { corners: 0., paths: 0., ..crate::TraceParams::default() }.outline_style();
|
||||
for width in [1, 2] {
|
||||
for turns in 0..4 {
|
||||
for offset in [(10, 10), (1_000_000_000, 1_000_000_000)] {
|
||||
let pts: Vec<_> = [(width, 1), (0, 1), (0, 0), (width, 0)]
|
||||
.into_iter()
|
||||
.map(|(mut x, mut y)| {
|
||||
for _ in 0..turns {
|
||||
(x, y) = (-y, x);
|
||||
}
|
||||
(x + offset.0, y + offset.1)
|
||||
})
|
||||
.collect();
|
||||
let junction = pts[0];
|
||||
let at = |x, y| ((x, y) == junction).then(|| Point::new(x as f64, y as f64));
|
||||
let raw = Loop { pts: pts.clone(), area2: 2 * i64::from(width) };
|
||||
let w = offset.0 as usize + 20;
|
||||
let h = offset.1 as usize + 20;
|
||||
let (path, _) = fit_loop(&raw, w, h, &|_| (0., 0), &at, &style).expect("a positive-area returning loop must be editable");
|
||||
assert_eq!(path.anchors[0].p, Point::new(junction.0 as f64, junction.1 as f64));
|
||||
let mut local = path.clone();
|
||||
let shift = Point::new(offset.0 as f64, offset.1 as f64).to_vec2();
|
||||
for a in &mut local.anchors {
|
||||
a.p -= shift;
|
||||
a.h_in -= shift;
|
||||
a.h_out -= shift;
|
||||
}
|
||||
assert!(local.area().abs() > 0.9 * width as f64, "{width}, {turns}, {offset:?}");
|
||||
let reverse = Loop { pts: pts.into_iter().rev().collect(), area2: -raw.area2 };
|
||||
let (back, _) = fit_loop(&reverse, w, h, &|_| (0., 0), &at, &style).unwrap();
|
||||
let mut expected = path;
|
||||
expected.reverse();
|
||||
let start = expected.anchors.iter().position(|a| a.p == back.anchors[0].p).unwrap();
|
||||
expected.anchors.rotate_left(start);
|
||||
assert_eq!(back, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_piece_does_not_promote_a_zero_area_chain() {
|
||||
let raw = Loop { pts: vec![(2, 2), (1, 2), (1, 1), (2, 1)], area2: 2 };
|
||||
let junction = |x, y| ((x, y) == (2, 2)).then_some(Point::new(1.5, 1.5));
|
||||
let style = crate::TraceParams { corners: 0., paths: 0., ..crate::TraceParams::default() }.outline_style();
|
||||
// All four subpixel corners coincide; the raw pixel has area but its
|
||||
// positioned boundary does not. The fallback must not invent ink.
|
||||
assert!(fit_loop(&raw, 4, 4, &|_| (-0.5, 0), &junction, &style).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_piece_leaves_free_end_fitting_unchanged() {
|
||||
assert_eq!(fit_piece(&returning_rect(1.), &hard_style(), false).len(), 1);
|
||||
let pts = [Point::new(0., 0.), Point::new(2., 0.), Point::new(4., 0.)];
|
||||
assert_eq!(fit_piece(&pts, &hard_style(), true), fit_piece(&pts, &hard_style(), false));
|
||||
}
|
||||
|
||||
fn trace_art(hole: bool, noise: u32) -> crate::TraceResult {
|
||||
let image = crate::Raster::from_fn(12, 12, |x, y| {
|
||||
let ink = if hole {
|
||||
(3..9).contains(&x) && (3..9).contains(&y) && !(x == 5 && y == 5)
|
||||
} else {
|
||||
((4..9).contains(&x) && (4..9).contains(&y)) || (x == 3 && y == 3)
|
||||
};
|
||||
if ink { [0, 0, 0, 255] } else { [255, 255, 255, 255] }
|
||||
});
|
||||
crate::trace(
|
||||
&image,
|
||||
&crate::TraceParams {
|
||||
corners: if hole { 100. } else { 0. },
|
||||
paths: if hole { 100. } else { 0. },
|
||||
noise,
|
||||
ignore_white: true,
|
||||
..crate::TraceParams::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_piece_keeps_a_corner_touching_mark_until_noise_removes_it() {
|
||||
assert_eq!(trace_art(false, 0).paths.len(), 2);
|
||||
assert_eq!(trace_art(false, 2).paths.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_piece_preserves_the_existing_hole_noise_control() {
|
||||
let holes = |noise| trace_art(true, noise).paths.iter().map(|p| p.path.subpaths.len().saturating_sub(1)).sum::<usize>();
|
||||
assert_eq!(holes(0), 1);
|
||||
assert_eq!(holes(2), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_piece_has_the_same_boundary_in_its_abutting_neighbour() {
|
||||
for inverse in [false, true] {
|
||||
for turns in 0..4 {
|
||||
let image = crate::Raster::from_fn(12, 12, |mut x, mut y| {
|
||||
for _ in 0..turns {
|
||||
(x, y) = (y, 11 - x);
|
||||
}
|
||||
let mark = ((4..9).contains(&x) && (4..9).contains(&y)) || (x == 3 && y == 3);
|
||||
if mark != inverse { [0, 0, 0, 255] } else { [255, 255, 255, 255] }
|
||||
});
|
||||
let res =
|
||||
crate::trace(&image, &crate::TraceParams { corners: 0., paths: 0., noise: 0, ignore_white: false, ..crate::TraceParams::default() });
|
||||
let ink = if inverse { [255, 255, 255] } else { [0, 0, 0] };
|
||||
let dot = res.paths.iter().find(|p| p.color == ink && p.pixels == 1).expect("corner mark is retained");
|
||||
let cubics = |p: &vectorcraft_geom::PathData| {
|
||||
p.subpaths
|
||||
.iter()
|
||||
.flat_map(|s| {
|
||||
(0..s.segment_count()).map(move |i| {
|
||||
let c = s.segment(i);
|
||||
[c.p0, c.p1, c.p2, c.p3]
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let other: Vec<_> = res.paths.iter().filter(|p| p.color != ink).flat_map(|p| cubics(&p.path)).collect();
|
||||
for c in cubics(&dot.path) {
|
||||
assert!(other.contains(&[c[3], c[2], c[1], c[0]]), "inverse {inverse}, turns {turns}: unshared recovered edge: {c:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,7 +460,6 @@ fn an_outline_returning_to_its_junction_is_shared() {
|
||||
/// and its neighbour keeps a single curve from the junction back to itself, so a hole the size of
|
||||
/// the region is left uncovered (1 to 6.7 px wide here, 0.3 to 108 px²).
|
||||
#[test]
|
||||
#[ignore = "a piece returning to its own junction collapses at Paths 0 / Corners 0 (fit.rs fit_piece and fit_loop): the region is dropped and its area left uncovered; draft #2205 (retain pieces returning to a junction) fixes it"]
|
||||
fn an_outline_returning_to_its_junction_survives_the_loosest_fit() {
|
||||
let mut wrong = vec![];
|
||||
for art in returning_arts() {
|
||||
|
||||
@@ -6,6 +6,21 @@ What Image Trace (`crates/trace`) makes of images whose right answer is known, m
|
||||
compared with the numbers before each change. Nothing here is a judgement by eye, and nothing is compared
|
||||
with any other product except the open tracer named under [Yardstick](#4-yardstick).
|
||||
|
||||
## Pieces returning to a junction
|
||||
|
||||
An outline piece can return to its starting junction while being fitted as an open chain. At loose
|
||||
Corners/Paths settings, a nonzero positioned boundary could collapse to one cubic and lose a small
|
||||
corner-touching mark. Fixed-end pieces with identical endpoints and fewer than two fitted segments
|
||||
now retain their positioned boundary when its area is finite and nonzero. Fitting the shared piece
|
||||
this way also gives an abutting fill exactly the reversed boundary, including within a compound path.
|
||||
|
||||
Six source-generated regressions check 1- and 2-pixel regions through four rotations, both windings
|
||||
and large translations; fixed junctions; zero positioned area; free ends; Noise controls; and shared
|
||||
edges through four rotations and black/white inversion. Noise 2 still removes the 1-pixel mark.
|
||||
The white-hole Noise check is a control, not a reproduced returning-hole case. Defaults are unchanged;
|
||||
these tests establish survival and edge consistency, not a new artistic-quality or parity score.
|
||||
This is separate from the closed-contour recovery in #2012 and straightening guard in #2025.
|
||||
|
||||
## Thin closed outlines with Snap Curves To Lines
|
||||
|
||||
A four-pixel stair-step mark can fit as two straight cubics along the same diagonal, leaving a closed path with zero area. The outline-distance checks accept this because both sides are close to the chord. Closed fitting now checks analytic curve area after straightening and upright levelling; if a nondegenerate source loop has lost its interior, it retains the positioned boundary as an unsimplified polygon. The area integral is translated to a local origin to reduce cancellation. This numerical degeneracy check is not a new Noise or minimum-feature-area setting.
|
||||
@@ -36,6 +51,7 @@ anti-aliasing):
|
||||
|
||||
| kind | inputs |
|
||||
|:--|:--|
|
||||
| 2026-10-15 | Fixed-end pieces returning to their junction retain their positioned nonzero boundary when fitted to fewer than two cubics; source-generated tests cover survival, winding, shared-edge equality, translations and Noise controls. Defaults and parity scores unchanged. |
|
||||
| curved shapes | a circle; two ellipses; a rounded badge with a round hole; a letter D with its counter |
|
||||
| polygons | a triangle, a turned square and a hexagon; a five-pointed star; a letter A with its counter |
|
||||
| thin lines | five lines 0.5, 1, 2, 3 and 5 units wide (of a 256-unit square), a few degrees off the pixel grid |
|
||||
|
||||
+2
-1
@@ -64,7 +64,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
|
||||
| M11 | Artboards & views (selected-set deletion shared by Artboards menu, trash and Properties, retaining artboard 1 when all are selected; Artboard deletion keys keep the first of an all-selected set and protect artwork; history prunes invalid indices while preserving valid multiselection (history tests and full workspace CI pass); click-to-fit objects with the Artboard tool (#1227), artboard escape routing and document setup modal handoff done; artboard panel/tool with Move Artwork for X/Y fields and Artboard Options, Trim View, middle-button pan, print tiling and the status bar's artboard navigator with synchronized Artboard tool selection done; multiple windows, presentation polish) | 🟡 | 15–25 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 |
|
||||
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 MCP `add_text` exposes existing vertical point, area and path type; broader scripting and batch work remains | 18–27 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (quick presets reveal adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (junction-returning pieces retain their positioned boundaries; quick presets reveal adjustment controls; Properties reopens them; tiny fitted contours retain islands and holes; 15 presets, the 14 documented ones each with an enhanced version; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; closed outlines retain their interior after straightening; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 |
|
||||
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 18–27 |
|
||||
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Numeric-field pointer selection and focus regression-tested (#1326, in part; short movements still select all); RPM runtime capabilities and glibc floor checked (#1275; real-distro install/start-up pending), Eyedropper cursor with precise-setting checks (#1269), Preferences, shortcut editor (chords with Shift and a punctuation key fire as typed; other apps' text shortcut sets imported), workspaces, a custom title bar on Windows/Linux with a Home button, content-sized dialogs, menus that scroll when longer than the window and macOS native menu dismissal done (M14.8), and native macOS save sheets with a Format menu (#866); an experimental, unsupported Windows 7 x64 portable OpenGL build (not verified by maintainers); accessibility and broader Windows/Linux packaging/QA pending | 18–28 |
|
||||
| — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed) | ⬜ (new ruler-guide Alt orientation implemented; existing-guide Escape cancellation regression-tested; broader pass remains) | 60–90 |
|
||||
@@ -84,6 +84,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | Image Trace retains nonzero positioned boundaries when a fixed-end returning piece has fewer than two fitted cubics. Source-generated tests check survival, winding, shared-edge reversal and Noise controls; see `image-trace-quality.md`. Scores and hours unchanged. |
|
||||
| 2026-10-16 | minor | G2: Effect › Sketch › Halftone Pattern (Size, Contrast, Pattern Type Circle, Dot or Line; black ink on white paper, Size in points at any Document Raster Effects resolution); measured Photoshop filters 30/57, 27 remaining (22–38 h). |
|
||||
| 2026-10-14 | minor | Effect > Warp: style, orientation and percent controls; focused checks passed and original captures reviewed; scores and hours unchanged |
|
||||
| 2026-10-14 | minor | G11: open-path anchor Delete removes adjoining segments without bridging the gap; explicit Remove Anchors keeps its reconnect/refit behavior. Scores and hours unchanged. |
|
||||
|
||||
@@ -11,6 +11,7 @@ the work list made from it. Deep areas have their own checklists: [UI and intera
|
||||
|
||||
| Number | Value | Kind |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | Image Trace retains nonzero positioned boundaries when a fixed-end returning piece has fewer than two fitted cubics. Source-generated tests check survival, winding, shared-edge reversal and Noise controls; see `image-trace-quality.md`. Scores and hours unchanged. |
|
||||
| **Feature breadth** (does each Illustrator menu item, tool, effect and format exist?) | **~88%** | measured in part: menu tree 353 of 375 items wired (94.1%), tools 89 of 92 (97%), vector effects 44 of 50 (88%), Photoshop-style effects 30 of 57 (53%), 3D 4 of 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate); blended by the area weights below |
|
||||
| **Feature depth** (each area scored by behaviour, weighted) | **~75%** | estimated, area table below |
|
||||
| **Ready for real work, mainstream illustrator** | **~65%** (60–70%) | estimated, see [Ready for real work](#ready-for-real-work) |
|
||||
@@ -79,7 +80,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
|
||||
| Type core | 9 | 80% | Auto-leading style resets are implemented; remaining: hyphenation and justification options, Optical Margin Alignment, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass |
|
||||
| Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), variable font axes, tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged |
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, symbol libraries, Start Global Edit, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration and Export Type (Graphic / Movie Clip) are done | 5–9 | estimated |
|
||||
| Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them |
|
||||
| Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them |
|
||||
| Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set |
|
||||
| View & navigation | 3 | 70% | New View/Edit Views, multiple windows and Arrange, Snap to Glyph | 9–14 | estimated |
|
||||
| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), guides lost (#779), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC/JPEG 2000 import, 3D export; see [file-format-parity.md](file-format-parity.md) | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests_reader_streaming.rs` and `xtask/src/pdf_deps.rs` |
|
||||
|
||||
Reference in New Issue
Block a user