diff --git a/crates/trace/src/fit.rs b/crates/trace/src/fit.rs index ca2b71995..3a7521c26 100644 --- a/crates/trace/src/fit.rs +++ b/crates/trace/src/fit.rs @@ -1,11 +1,15 @@ //! Loop → Bézier curves, and the pieces of that which centre lines use too. +use kurbo::ParamCurveArea; use vectorcraft_geom::{Anchor, AnchorKind, Point, SubPath}; use crate::contour::Loop; use crate::logo::{Cubic, Stats, Style, all_within, fit_open, fit_outline, resample_closed, resample_open, sample_cubics}; use crate::subpixel::{self, Crack}; +#[cfg(test)] +mod snap_area_tests; + fn seg_dist(p: Point, a: Point, b: Point) -> f64 { let ab = b - a; let l2 = ab.hypot2(); @@ -149,6 +153,21 @@ pub(crate) fn fit_closed(pts: &[Point], style: &Style) -> Option> { if style.upright { snap_upright(&mut segs, true, false); } + // Opposite sides of a thin loop can both straighten onto the same chord. + // Distance-to-outline checks allow that fit, but it has no filled interior. + // Keep the positioned boundary when a nondegenerate loop loses all its area. + // Translate before integrating to avoid cancellation far from the origin. + let origin = pts.first().copied().unwrap_or(Point::new(0.0, 0.0)); + let fitted_area: f64 = segs + .iter() + .map(|c| { + let [a, b, c, d] = c.map(|p| Point::new(p.x - origin.x, p.y - origin.y)); + kurbo::PathSeg::Cubic(kurbo::CubicBez::new(a, b, c, d)).signed_area() + }) + .sum(); + if area2.abs() > 1e-8 && fitted_area.abs() < 1e-8 { + segs = polygon(&pts, 0.0); + } Some(if flip { reversed(segs) } else { segs }) } diff --git a/crates/trace/src/fit/snap_area_tests.rs b/crates/trace/src/fit/snap_area_tests.rs new file mode 100644 index 000000000..7eae8f851 --- /dev/null +++ b/crates/trace/src/fit/snap_area_tests.rs @@ -0,0 +1,96 @@ +use super::*; +use crate::{Raster, preset, trace}; + +#[test] +fn snap_preserves_the_interior_of_a_thin_zig_at_different_positions() { + for (ox, oy) in [(10, 10), (210, 85)] { + let image = Raster::from_fn(256, 128, |x, y| { + if [(ox, oy), (ox + 1, oy), (ox + 1, oy + 1), (ox + 2, oy + 1)].contains(&(x, y)) { [0, 0, 0, 255] } else { [255; 4] } + }); + for corners in [0.0, 25.0, 50.0, 75.0, 100.0] { + for snap in [false, true] { + let mut p = preset("Black and White Logo").unwrap(); + p.paths = 50.0; + p.corners = corners; + p.noise = 1; + p.ignore_white = true; + p.snap_curves_to_lines = snap; + let out = trace(&image, &p); + assert_eq!(out.paths.len(), 1); + assert!(out.paths[0].path.subpaths[0].area().abs() > 1.0, "position=({ox},{oy}), corners={corners}, snap={snap}"); + p.noise = 5; + assert!(trace(&image, &p).paths.is_empty(), "Noise must still remove the four-pixel mark"); + } + } + } +} + +fn thin_outline() -> Vec { + vec![ + Point::new(0.0, 0.0), + Point::new(2.00390625, 0.501953125), + Point::new(2.501953125, 1.0), + Point::new(3.00390625, 2.00390625), + Point::new(1.0, 1.501953125), + Point::new(0.501953125, 1.00390625), + ] +} + +fn hard_style() -> Style { + let mut p = crate::preset("Black and White Logo").unwrap(); + p.paths = 50.0; + p.corners = 0.0; + let mut st = p.outline_style(); + st.tol *= 2.5; + st.line_tol *= 1.8; + st.circle_tol *= 1.8; + st.sigma = 0.9; + st +} + +#[test] +fn a_thin_closed_outline_and_its_hole_keep_opposite_winding() { + let points = thin_outline(); + let forward = fit_closed(&points, &hard_style()).unwrap(); + let area = subpath(&forward, true).unwrap().area(); + assert!(area > 1.0); + for shift in 0..points.len() { + let mut backward = points.clone(); + backward.rotate_left(shift); + backward.reverse(); + let reverse = fit_closed(&backward, &hard_style()).unwrap(); + assert_eq!(reverse, reversed(forward.clone()), "shared closed boundary must be the same curve in reverse"); + assert!((subpath(&reverse, true).unwrap().area() + area).abs() < 1e-10); + } +} + +#[test] +fn a_thin_white_hole_remains_open_with_snap_enabled() { + let image = Raster::from_fn(32, 32, |x, y| { + if (4..28).contains(&x) && (4..28).contains(&y) && ![(14, 14), (15, 14), (15, 15), (16, 15)].contains(&(x, y)) { + [0, 0, 0, 255] + } else { + [255; 4] + } + }); + let mut p = preset("Black and White Logo").unwrap(); + p.paths = 50.0; + p.corners = 0.0; + p.noise = 1; + p.ignore_white = true; + let out = trace(&image, &p); + assert_eq!(out.paths.len(), 1); + let subs = &out.paths[0].path.subpaths; + assert_eq!(subs.len(), 2); + assert!(subs[0].area() * subs[1].area() < -1.0, "hole must retain a nonzero opposite interior"); + p.noise = 5; + assert_eq!(trace(&image, &p).paths[0].path.subpaths.len(), 1, "Noise must still fill the four-pixel hole"); +} + +#[test] +fn a_narrow_rectangle_remains_a_filled_region() { + let points = vec![Point::new(0.0, 0.0), Point::new(20.0, 0.0), Point::new(20.0, 1.0), Point::new(0.0, 1.0)]; + let curves = fit_closed(&points, &hard_style()).unwrap(); + let area = subpath(&curves, true).unwrap().area().abs(); + assert!((15.0..25.0).contains(&area), "a narrow filled region must remain visible: {area}"); +} diff --git a/docs/image-trace-quality.md b/docs/image-trace-quality.md index fb23a903d..ea29c60fa 100644 --- a/docs/image-trace-quality.md +++ b/docs/image-trace-quality.md @@ -6,18 +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). -## Tiny closed contours +## 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. + +`fit::snap_area_tests` covers the mark at two positions and five Corners settings, a white stair-step hole, reversed/rotated closed boundaries with exactly matching opposite curves, and a narrow filled rectangle. Noise still removes the four-pixel mark or hole when set to 5. It does not change open-piece fitting, global Snap tolerances or defaults, and it does not address every possible zero-area compound outline. The preset quality table below is the previous measurement, not a rerun of that full report. + +## Tiny closed contours A coarse fit can reduce a small closed outline to one cubic, which the editable-path builder cannot represent as a closed subpath with two anchors. Previously that dropped the component or hole after it survived Noise. The fitter now retains the positioned contour without further simplification when that minimum is not met. Corners and Noise defaults are unchanged. - Three source-generated regressions cover a one-pixel island, a one-pixel hole with opposite winding, and a stepped seven-pixel region across Corners values. They also check that Noise still removes the island and fills the hole when its threshold requests that. These are survival and winding checks, not a new quality-harness baseline or a general claim about small-feature fidelity. - ## How it is measured `crates/trace/tests/quality` draws its own inputs from vector art (no image files), traces each with every @@ -967,6 +970,7 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab | date | change | |:--|:--| +| 2026-10-14 | Closed fitting retains the positioned boundary when straightening collapses a nonzero-area loop; four generated regressions; preset scores unchanged | | 2026-10-14 | Tiny closed fits retain their positioned outlines instead of losing islands/holes; three generated regression tests; quality-harness baseline unchanged | | 2026-10-14 | Gradients: a gradient that noise or a soft rim broke into pieces is found in its largest piece and grown over the rest (blurred and noisy linear gradients: 3 to 11 paths beside the gradient down to 0.5 with 6 Colors, 24 to 56 down to 0.5 to 1.5 with High Fidelity Photo) | | 2026-10-14 | Enhanced versions of the presets (Auto Grouping, and Shapes in all but High Fidelity Photo; Flat Logo has none), each measured beside its preset; a hole that one colour fills is the same shape as what fills it, rectangles are sized by where their sides lie, and a group keeps its paths in the order they were traced in (grouped, a letter with a counter was painted with a light seam: Default with Auto Grouping, edge ΔE 10.24 to 9.88) | diff --git a/docs/roadmap.md b/docs/roadmap.md index a6ace0d33..5722106f4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -62,7 +62,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V | M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Film Grain done (29/57 Photoshop filters); Revolve with automatic smooth vector shading, default-on circular caps and continuous rims, Extrude & Bevel, Inflate and Rotate; SVG Filters, remaining raster effects and 3D materials, lights and shadows pending | 81–130 | | M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG ``/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 | | 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 (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`; 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 (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; broader pass remains) | 60–90 | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 0111c291e..37303aa7b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -523,6 +523,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); German, Spanish, French, Italian, Japanese, Russian and Ukrainian also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498). - **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `` and PDF soft masks. The constant alpha soft mask that PDF and `.ai` export draw a CMYK page with transparency through is white, so applications that read every soft mask by its luminosity show the page too (#1359, part of #1286). 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. - **Image Trace, measured:** `crates/trace/tests/quality` traces drawings made from known vector art with every preset and [`image-trace-quality.md`](image-trace-quality.md) records the numbers against the baseline. Black and White, Grayscale and Color place each edge by the blend of the pixels across it (to a fraction of a pixel on anti-aliased and blurred images; hard-edged images are read as staircases) and fit outlines as Flat Logo does. *Measured* on each preset's own inputs, all sizes and variants: Default's outlines went from 0.31 px off on average to 0.09 (largest 1.51 to 1.02), Black and White Logo's from 0.27 to 0.06 with 0.3 kinks per trace where it had 15, and the gaps between abutting paths from 64 px to 22. On clean curved shapes Black and White Logo is 0.02 px off (the open tracer measured beside it: 0.20). The palette is made of the picture's own colours (blends along edges take none of it, colours that noise made of one colour merge, a blend pixel goes to one of the two colours it lies between; #1504): 16 Colors on flat art, all variants, went from 70 paths to 4.2 and from 5.1 px off to 0.13, with no path left in a colour the art doesn't have (5.9 before). Photos are smoothed within their tones before they are quantised and stacked layers go by lightness (#1516): High Fidelity Photo went from 3,653 paths to 121 with its colour error down from 2.30 to 1.75, and a 16 megapixel photo-like image is traced in 3.5 s where 4 megapixels were refused. Since then, by the same harness: outlines are cut at their junctions and each shared piece is fitted once (#1535: gaps and overlaps between abutting paths 22 to 50 px of area down to under 2); layers are traced within the box of their pixels and a few at a time (#1546: a 16 megapixel photo 3.9 GB to 0.9 GB); black and white are read against the image's own black and the paper near each edge (#1563, for thin lines in blurred and scanned art); Ignore Color and the Auto Color and Outline presets (#1574); Snap Curves To Lines levels lines within 2.5 degrees of level or upright (#1593, the angle our choice); Create: Strokes puts centre lines in the middle of their ink and makes them as wide as it (#1595: Outline 0.295 to 0.052 px from the true centre line, 18.4 to 2.7 anchors a stroke); sharp corners whose pixel is mostly outside the shape stay corners (#1609: Default 69% to 76% kept). Three options, each off unless asked for as the legacy presets have them: Shapes (#1609: 57 to 62% of plain circles, ellipses, rectangles and regular polygons come out as live shapes, rounded rectangles rarely), Auto Grouping (#1618: paths grouped with what lies inside them, the same picture) and Gradients (#1627: linear gradients traced as one gradient-filled path; on gradient inputs 6 Colors' colour error 2.12 to 0.62 with 6.4 to 3.4 paths, none found in flat art). Snap Curves To Lines moves no end of a line by more than 1.6 px, so long lines a degree or two off stay as the image has them (#1647: Technical Drawing's largest distance 4.21 px to 1.46). Flat Logo takes the tones a scan's uneven paper makes of one colour for one colour (#1657: on scans 470 paths and 21 px off to 8.5 paths and 0.09 px). Shapes finds rounded rectangles and shapes in aliased, blurred and scanned art (#1845: 57 to 62% of the plain shapes up to 98 to 100%, no false shape but one line 2 px wide). Stroked lines get square caps where their ink is cut square, and a wide end that turned towards a corner goes back on its line (#1846: Outline's largest distance 2.94 px to 0.88, 14 anchors to 10). Each of the 14 documented presets has an enhanced version with Auto Grouping and, in all but High Fidelity Photo, Shapes (#1847), measured beside its preset: equal or better within 0.005 px and 1% of edge ΔE on every preset's own inputs (Default 0.091 to 0.083 px); Auto Grouping keeps traced order in its groups, which removed a light seam along the counters of letters. Gradients grows a gradient over the pixels noise and soft rims took off it (#1848: blurred and noisy linear gradients 3 to 11 paths beside the gradient down to 0.5 with 6 Colors, 24 to 56 down to 0.5 to 1.5 with High Fidelity Photo). Silhouettes is 0.52 px off because its threshold is 200 (0.11 px at 128). Still weak, by the same numbers: rounded rectangles and other line-and-arc outlines take 12 to 16 anchors (the fitter does not split lines from arcs), blurred and scanned polygons lose corners, hairlines under half a pixel are lost to the threshold, aliased art is about 0.25 px off, a scan's uneven paper splits into tones when more colours are asked for than the art has, stroke junctions stay at their skeleton pixels, a three-stop gradient in a small or scanned disc is not found, radial gradients stay bands (a draft, #1849, traces them; the documented option is for linear ones, so it waits for a decision), near-regular polygons a few dozen pixels across are made regular with Shapes (up to 0.5 px off), and aliased stroked lines are 0.8 px off at worst. - **Image Trace Flat Logo mode:** a fourth mode beside Black and White, Grayscale and Color, for flat-colour logos. Each pixel is read as a blend of the logo's colours (found automatically or named), the blend is interpolated smoothly and every outline is placed where two colours are equally present, so edges are accurate to a fraction of a pixel. Outlines are then cleaned: corners the blur rounded are put back, near-straight runs and near-circular arcs become exact lines and arcs, the rest is fitted with as few Béziers as the tolerance allows, touching colours overlap by a hair so no gap opens, and an outline that strays from the raw contour is fitted plainly instead. Gradients and photos are refused with advice to use Color. *Measured* (a local experiment, not run in CI): a flat-colour logo's vector original was rendered down to a soft 199 × 100 px transparent PNG, traced, and the result compared with the original at 1600 px wide. The built-in colour presets (6 Colors, 16 Colors, High Fidelity Photo, Low Fidelity Photo) had a mean edge error of 2.5 to 3.4 px and region overlap of 0.88 to 0.91; Flat Logo had 0.78 px and 0.966 (0.48 px and 0.975 from a crisp source), in about 0.1 s. Limits: flat-colour art only; needle-thin sharp tips are shortened by about 1.7 source pixels because the blur erases them; it works at no more than 1024 px on the long side (larger images are averaged down first); only one logo family has been scored against a vector original. @@ -639,6 +640,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur | 2026-10-14 | minor | Quick Image Trace presets reveal the adjustment panel with Advanced open; Properties reopens it. Sliders keep their preset row until release so the re-trace is committed. UI regressions added; scores and remaining estimates unchanged. | | 2026-10-14 | minor | Revolve: automatic smooth vector shading, default-on Cap with continuous curved rims, and bounded seam joins; angular cut sides remain open. Scores and hour estimates unchanged at this rounding | | 2026-10-14 | minor | Evidence for the later merges of 2026-10-14 that did not write their own: anchor X/Y/W/H (#1839), Snap to Grid over Snap to Pixel (#1840), guides in a marquee (#1836), the toolbar and All Tools drawer (#1751), the Actions panel selection (#1853), redo during a drag (#1856), preferences written on OK (#1857), character styles from a selected range (#1634), type in symbols after reopening (#1833), swatches in opacity masks (#1830), macOS save sheets (#1855, the Format menu withdrawn), shared stroke outlines (#1667), SVG and DXF fixes (#1799, #1834, #1870, #1789), untrusted files (#1852, #1862, #1863, #1871–#1873), failed saves (#1865), effects and Dim Images (#1864), krilla (#1874) and the wasm gate (#1875). #1630, #1631, #1632, #1633, #1682, #1687–#1690, #1744, #1750, #1755, #1767, #1782, #1784, #1792, #1809, #1814 and #1850 wrote their own evidence and are not repeated. A note above the 2026-10-05 table says its duplicated "Selection, transform & align tools" and "Drawing tools" rows need the owner's judgement. Scores and hours unchanged (estimated) | +| 2026-10-14 | minor | Image Trace: closed outlines keep their interior when straightening would collapse them; generated mark/hole/winding/Noise regressions; score and hours unchanged | | 2026-10-14 | minor | Image Trace, measured in `image-trace-quality.md`: Shapes rework (#1845), square stroke caps (#1846), enhanced presets (#1847), gradients grown over noise and soft rims (#1848). Score and hours unchanged (estimated) | | 2026-10-14 | minor | Width-point finite-arithmetic follow-up: atomicity, history and native round-trip regressions and current full CI passed; scores and hours unchanged. | | 2026-10-13 | minor | Pinned PngSuite HTTPS files verified by SHA-256 before publication; bounded downloads replace archive extraction (#1729). Scores and hours unchanged. |