mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
fix(pathfinder): Divide splits shapes with an open cutting line (#1307)
This commit is contained in:
1 parent
6ab08dd407
commit
18e538ec94
7 files changed
+186
-3
No files matched your search
@@ -313,10 +313,17 @@ fn run_pf(s: &mut Session, op: PathfinderOp) -> Result<Value> {
|
||||
// Build the stack (back → front) with a style per entry.
|
||||
let mut styles: Vec<Style> = vec![];
|
||||
let mut shapes: Vec<po::Shape> = vec![];
|
||||
let mut push = |path: PathData, rule: FillRule, st: Style| {
|
||||
let mut push = |mut path: PathData, rule: FillRule, st: Style| {
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
if op == PathfinderOp::Divide && st.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if !f.paint.is_none())) {
|
||||
// Filled open contours keep their implicit closing edge. A zero-area line
|
||||
// stays a cutter; normalised area also catches zero-signed-area self crossings.
|
||||
for sub in path.subpaths.iter_mut().filter(|sp| !sp.closed) {
|
||||
sub.closed = po::encloses_area(sub) || po::area(&PathData::single(sub.clone()), rule) > 0.0;
|
||||
}
|
||||
}
|
||||
let key = match styles.iter().position(|x| *x == st) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
|
||||
@@ -584,3 +584,89 @@ fn commands_reject_bad_input() {
|
||||
let _ = s.execute(c, &p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divide_cutting_lines_keep_appearance_unrelated_art_and_one_undo() {
|
||||
for (line_has_fill, curved) in [(false, false), (true, false), (false, true)] {
|
||||
let mut s = session();
|
||||
let base = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
let cutter =
|
||||
NodeId(s.execute("path.create", &json!({"anchors": [{"x": -10, "y": 50}, {"x": 110, "y": 50}]})).unwrap()["id"].as_u64().unwrap());
|
||||
let kept = rect(&mut s, 200.0, 0.0, 30.0, 30.0);
|
||||
s.edit("divide fixture", |d, sel| {
|
||||
let n = d.node_mut(base).unwrap();
|
||||
n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.2, 0.5, 0.7)), Paint::solid(Color::BLACK), 3.0).into();
|
||||
n.opacity = 0.65;
|
||||
n.blend = vectorcraft_color::BlendMode::Multiply;
|
||||
let n = d.node_mut(cutter).unwrap();
|
||||
n.appearance =
|
||||
Appearance::basic(if line_has_fill { Paint::solid(Color::BLACK) } else { Paint::None }, Paint::solid(Color::rgb(0.8, 0.2, 0.1)), 2.0)
|
||||
.into();
|
||||
if curved {
|
||||
let mut bp = vectorcraft_geom::BezPath::new();
|
||||
bp.move_to((-10.0, 50.0));
|
||||
bp.curve_to((30.0, 15.0), (70.0, 85.0), (110.0, 50.0));
|
||||
n.kind = NodeKind::Path { path: PathData::from_bezpath(&bp), rule: FillRule::NonZero, live: None, clipping: false, guide: false };
|
||||
}
|
||||
sel.set([cutter, base]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selected = s.doc().unwrap().selection.clone();
|
||||
let undo_len = s.doc().unwrap().history.undo.len();
|
||||
let result = ids(&s.execute("object.pathfinder.divide", &json!({})).unwrap())[0];
|
||||
let group = node(&s, result);
|
||||
let pieces = group.children().unwrap();
|
||||
assert_eq!(pieces.len(), 2, "line fill {line_has_fill}, curved {curved}");
|
||||
let source = before.node(base).unwrap();
|
||||
assert!(pieces.iter().all(|n| n.appearance == source.appearance && n.opacity == source.opacity && n.blend == source.blend));
|
||||
let total: f64 = pieces.iter().map(|n| area_of(&s, n.id)).sum();
|
||||
assert!((total - 10000.0).abs() < 0.01);
|
||||
assert_eq!(node(&s, kept), *before.node(kept).unwrap());
|
||||
assert!(s.doc().unwrap().doc.node(base).is_none() && s.doc().unwrap().doc.node(cutter).is_none());
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![result]);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo_len + 1);
|
||||
let after = s.doc().unwrap().doc.clone();
|
||||
let result_selection = s.doc().unwrap().selection.clone();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
assert_eq!(s.doc().unwrap().selection, selected);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc, after);
|
||||
assert_eq!(s.doc().unwrap().selection, result_selection);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divide_keeps_implicitly_closed_filled_paths_including_zero_signed_area() {
|
||||
for anchors in [
|
||||
json!([{"x": 10, "y": 10}, {"x": 50, "y": 90}, {"x": 90, "y": 10}]),
|
||||
json!([{"x": 0, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}, {"x": 100, "y": 0}]),
|
||||
] {
|
||||
let mut s = session();
|
||||
let filled = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
let cutter =
|
||||
NodeId(s.execute("path.create", &json!({"anchors": [{"x": -10, "y": 150}, {"x": 110, "y": 150}]})).unwrap()["id"].as_u64().unwrap());
|
||||
s.edit("open fill fixture", |d, sel| {
|
||||
let mut appearance = Appearance::basic(Paint::solid(Color::rgb(0.1, 0.7, 0.2)), Paint::None, 0.0);
|
||||
// An empty top fill must not hide the nonempty fill below it.
|
||||
appearance.items.insert(0, vectorcraft_doc::AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(Paint::None)));
|
||||
d.node_mut(filled).unwrap().appearance = appearance.into();
|
||||
d.node_mut(cutter).unwrap().appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0).into();
|
||||
sel.set([cutter, filled]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let source = node(&s, filled);
|
||||
assert!(source.path_data().unwrap().subpaths.iter().all(|sp| !sp.closed));
|
||||
let expected = area_of(&s, filled);
|
||||
assert!(expected > 3000.0);
|
||||
let result = ids(&s.execute("object.pathfinder.divide", &json!({})).unwrap())[0];
|
||||
let group = node(&s, result);
|
||||
let pieces = group.children().unwrap();
|
||||
let actual: f64 = pieces.iter().map(|n| area_of(&s, n.id)).sum();
|
||||
assert!((actual - expected).abs() < 0.01, "{actual} != {expected}");
|
||||
assert!(pieces.iter().all(|n| n.appearance == source.appearance));
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,18 @@ pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
|
||||
if op == PathfinderOp::Unite {
|
||||
return unite();
|
||||
}
|
||||
// Open paths are cutting edges for Divide; the filled-only operations keep their
|
||||
// implicit closure. Reuse Shape Builder's bounded planar map and its fallback.
|
||||
if op == PathfinderOp::Divide && shapes.iter().flat_map(|s| &s.path.subpaths).any(|sp| !sp.closed && sp.anchors.len() >= 2) {
|
||||
return crate::shape_builder(shapes, false)
|
||||
.regions
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
let key = shapes.get(r.top()?)?.key;
|
||||
Some(Shape::new(r.path, FillRule::NonZero, key))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let Some(arr) = arrangement(shapes) else { return Vec::new() };
|
||||
let p = &arr.tidy;
|
||||
match op {
|
||||
@@ -564,4 +576,74 @@ mod tests {
|
||||
same_as_one_mask_at_a_time(&v);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divide_open_paths_cut_faces_without_taking_the_cutters_paint() {
|
||||
let line = |a: (f64, f64), b: (f64, f64)| shapes::line(Point::from(a), Point::from(b));
|
||||
let mut cases = vec![
|
||||
("horizontal", vec![line((-10.0, 50.0), (110.0, 50.0))], 2),
|
||||
("reverse", vec![line((110.0, 50.0), (-10.0, 50.0))], 2),
|
||||
("vertical", vec![line((50.0, -10.0), (50.0, 110.0))], 2),
|
||||
("diagonal", vec![line((-10.0, -10.0), (110.0, 110.0))], 2),
|
||||
("crossed", vec![line((-10.0, 50.0), (110.0, 50.0)), line((50.0, -10.0), (50.0, 110.0))], 4),
|
||||
("outside", vec![line((-10.0, 150.0), (110.0, 150.0))], 1),
|
||||
("dangling", vec![line((-10.0, 50.0), (50.0, 50.0))], 1),
|
||||
("boundary", vec![line((-10.0, 0.0), (110.0, 0.0))], 1),
|
||||
("tangent", vec![line((-10.0, 10.0), (10.0, -10.0))], 1),
|
||||
];
|
||||
let mut curve = BezPath::new();
|
||||
curve.move_to((-10.0, 50.0));
|
||||
curve.curve_to((30.0, 15.0), (70.0, 85.0), (110.0, 50.0));
|
||||
cases.push(("curved", vec![PathData::from_bezpath(&curve)], 2));
|
||||
let mut bent = BezPath::new();
|
||||
bent.move_to((-10.0, 25.0));
|
||||
bent.line_to((50.0, 75.0));
|
||||
bent.line_to((110.0, 25.0));
|
||||
cases.push(("bent", vec![PathData::from_bezpath(&bent)], 2));
|
||||
for (label, cutters, count) in cases {
|
||||
let mut inputs = vec![Shape::new(rect(0.0, 0.0, 100.0, 100.0), FillRule::NonZero, 7)];
|
||||
inputs.extend(cutters.into_iter().map(|p| Shape::new(p, FillRule::NonZero, 99)));
|
||||
let divided = pathfinder(PathfinderOp::Divide, &inputs);
|
||||
assert_eq!(divided.len(), count, "{label}");
|
||||
assert!(divided.iter().all(|s| s.key == 7 && s.path.subpaths.iter().all(|sp| sp.closed)), "{label}");
|
||||
let total: f64 = divided.iter().map(|s| crate::area(&s.path, s.rule)).sum();
|
||||
assert!((total - 10000.0).abs() < 0.01, "{label}: area {total}");
|
||||
for p in [Point::new(20.0, 31.0), Point::new(79.0, 37.0), Point::new(81.0, 68.0), Point::new(17.0, 85.0)] {
|
||||
assert_eq!(divided.iter().filter(|s| s.path.to_bezpath().winding(p) != 0).count(), 1, "{label}: {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divide_open_paths_preserve_frontmost_fills_and_holes() {
|
||||
let cutter = || Shape::new(shapes::line(Point::new(-10.0, 50.0), Point::new(160.0, 50.0)), FillRule::NonZero, 99);
|
||||
let inputs = [
|
||||
Shape::new(rect(0.0, 0.0, 100.0, 100.0), FillRule::NonZero, 7),
|
||||
Shape::new(rect(50.0, 0.0, 150.0, 100.0), FillRule::NonZero, 8),
|
||||
cutter(),
|
||||
];
|
||||
let faces = pathfinder(PathfinderOp::Divide, &inputs);
|
||||
assert_eq!(faces.len(), 6);
|
||||
assert_eq!(faces.iter().filter(|s| s.key == 7).count(), 2);
|
||||
assert_eq!(faces.iter().filter(|s| s.key == 8).count(), 4);
|
||||
let total: f64 = faces.iter().map(|s| crate::area(&s.path, s.rule)).sum();
|
||||
assert!((total - 15000.0).abs() < 1e-6);
|
||||
let mut ring = rect(0.0, 0.0, 100.0, 100.0);
|
||||
ring.subpaths.extend(rect(30.0, 30.0, 70.0, 70.0).subpaths);
|
||||
let faces = pathfinder(PathfinderOp::Divide, &[Shape::new(ring, FillRule::EvenOdd, 7), cutter()]);
|
||||
assert_eq!(faces.len(), 2);
|
||||
assert!(faces.iter().all(|s| s.key == 7 && s.path.to_bezpath().winding(Point::new(50.0, 40.0)) == 0));
|
||||
let total: f64 = faces.iter().map(|s| crate::area(&s.path, s.rule)).sum();
|
||||
assert!((total - 8400.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn divide_open_paths_retains_the_existing_segment_cap() {
|
||||
let mut inputs = vec![Shape::new(rect(0.0, 0.0, 100.0, 100.0), FillRule::NonZero, 7)];
|
||||
inputs.extend((0..4093).map(|_| Shape::new(shapes::line(Point::new(-10.0, 50.0), Point::new(110.0, 50.0)), FillRule::NonZero, 99)));
|
||||
let faces = pathfinder(PathfinderOp::Divide, &inputs);
|
||||
assert_eq!(faces.len(), 1, "4097 segments fall back to filled areas");
|
||||
assert_eq!(faces[0].key, 7);
|
||||
assert!((crate::area(&faces[0].path, faces[0].rule) - 10000.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
## G11 Correctness bugs in core tools
|
||||
|
||||
- **Done:** Divide splits filled paths along open cutting lines and curves (#1307), preserving the frontmost filled appearance, implicit filled closure, holes, unrelated objects and one Undo/Redo; the existing bounded-map fallback remains.
|
||||
|
||||
- **Done:** Numeric fields keep a text drag's start when the pointer gains focus and movement exceeds the click threshold (#1326, in part). A completed focus click selects the whole value; keyboard focus, double-click and numeric stepping remain regression-tested. Short movements still classified as clicks can select all; that case remains open.
|
||||
|
||||
- **Done:** DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves keep their open state. Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests_distortkeys.rs`.
|
||||
@@ -245,6 +247,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
| 2026-10-12 | minor | G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged |
|
||||
| 2026-10-12 | minor | G11: Direct Selection segment Delete retains neighboring geometry and appearance (#1278); estimates unchanged |
|
||||
| 2026-10-12 | minor | G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged |
|
||||
| 2026-10-12 | minor | G11: Divide uses open cutting paths without taking their paint (#1307); implicit filled closure, holes and history regression-tested; estimates unchanged |
|
||||
| 2026-10-12 | minor | G11: numeric fields keep the text-drag start instead of replacing it with select-all on press (#1326, in part); keyboard and click selection regression-tested; estimates unchanged; short movements below the click threshold remain open |
|
||||
| 2026-10-11 | minor | G15: large-selection edits, cached live effects and faster Pathfinder (#1173); a 25k-path perf run meets every budget; estimate unchanged |
|
||||
| 2026-10-11 | minor | G4: Middle Eastern digit types done (Character panel menu › Digits); estimate unchanged |
|
||||
|
||||
+2
-1
@@ -54,7 +54,7 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
|
||||
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork; gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool, Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries. Left: freeform and mixed spot/process gradients export as stops or process colours to SVG/PDF; confirm the Scale Strokes & Effects default | 4–8 |
|
||||
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (closed SPLINE flags round-trip, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish | 3–6 (existing scope; Affinity export unestimated) |
|
||||
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
|
||||
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Simplify preview/counts and rewritten-path component selection verified (#1272; auto/slider/advanced UI remains open); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 |
|
||||
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; auto/slider/advanced UI remains open); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 |
|
||||
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles, per-paragraph paragraph attributes, Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 |
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 |
|
||||
| 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); initial Revolve only; SVG Filters, remaining raster effects and 3D pending | 81–130 |
|
||||
@@ -103,6 +103,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
||||
| 2026-10-11 | minor | M12: Object › Graph › Design… and Marker… (graph designs, marker designs); column designs remain. Scores/hours unchanged. |
|
||||
| 2026-10-12 | minor | M14: native macOS save Format menu (#866); filename regression tests, actual SVG save and PDF selection/cancel verified; estimates unchanged |
|
||||
| 2026-10-12 | minor | M6: compound paths inherit the backmost appearance; selected members read and change the compound fill rule (#1197); estimates unchanged |
|
||||
| 2026-10-12 | minor | M6: Divide accepts open cutting paths (#1307), preserving filled-face appearance and Undo; the existing segment cap remains; estimates unchanged |
|
||||
| 2026-10-12 | minor | M14: numeric-field focus and text-drag selection regression-tested (#1326, in part); estimates unchanged; short movements below the click threshold remain open |
|
||||
| 2026-10-11 | minor | M9: PDF raster-effect resolution-cap warnings verified with real capped/uncapped exports; scores and hours unchanged |
|
||||
| 2026-10-11 | minor | #893 (Shape Builder seams on grids of lines) fixed; G11 keeps #937 and its 8–15 h |
|
||||
|
||||
@@ -69,7 +69,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
|
||||
|---|---:|---:|---|---:|---|
|
||||
| Selection, transform & align tools | 6 | 85% | Direct Selection segment click/Delete corrected (#1278); remaining: Free Transform/Scale/Reflect along a rotated box, Start Global Edit, transform nuances (no-op anchor Align history regression-tested, #1180 follow-up) | 4–6 | estimated |
|
||||
| Drawing tools (Pen, Curvature, Pencil, shapes, Shaper) | 7 | 85% | Shaper construction widget, Pencil/Pen nuances, Blob Brush options, Touch Type, Dimension tool, Live Corners' Relative rounding; see [ui-parity.md](ui-parity.md) | 8–13 | estimated; Curvature point display regression-tested (#1236, in part) |
|
||||
| Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 80% | Shape Builder misses planar regions in real files (#937), its gap detection and options; Live Paint Gap Options; Simplify slider/auto, advanced UI and Show Original | 4–8 | estimated (lowered from 86%: open issues); compound attributes regression-tested (#1197); Simplify component selection and preview regression-tested (#1272) |
|
||||
| Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 80% | Shape Builder misses planar regions in real files (#937), its gap detection and options; Live Paint Gap Options; Simplify slider/auto, advanced UI and Show Original | 4–8 | estimated (lowered from 86%: open issues); compound attributes regression-tested (#1197); Simplify component selection and preview regression-tested (#1272); Divide open-cutting-path geometry and appearance regression-tested (#1307) |
|
||||
| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | 30.0's gradient dither, perceptual blending and gradient presets; freeform and mixed spot/process gradients in SVG/PDF | 4–7 | estimated (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251) |
|
||||
| Strokes, brushes, width profiles | 5 | 88% | Art brush Overlap, Pattern brush auto corners, brush libraries, pen pressure on macOS, tilt/bearing/rotation | 8–15 | estimated |
|
||||
| Appearance, transparency, graphic styles, masks | 5 | 97% | raster effect reach in preview bounds | 1–2 | estimated |
|
||||
@@ -452,6 +452,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Choosing the Artboard tool (Shift+O, its toolbar button, Edit Artboards in Properties or Document Setup, or the Artboards panel menu's Artboard Options…) starts it on the active artboard: the canvas outlines that artboard, and Delete, Cut, Copy, the Transform panel and Artboard Options act on it.
|
||||
- **Geometry and effects:**
|
||||
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected. With anchors selected, the Control bar and the Properties panel also convert them to corner or smooth (`path.convertAnchors`), connect end points (Join) and cut the path at them (`path.cutAtAnchors`, also on the task bar and the context menu): a closed path opens there and an open one becomes one path per piece, each cut leaving one of its two coincident anchors selected so a drag pulls the path apart.
|
||||
- Divide uses open lines, bent paths and curves as cutting edges (#1307), preserving the frontmost filled face's appearance and holes. Engine tests cover implicit closure of filled open contours and unrelated objects; native automation confirms independent piece movement, one Undo/Redo, canvas/window captures and an exact saved-document read by a separate CLI process. The shared planar map retains its 4,096-segment cap and filled-area fallback; other Pathfinder operations are unchanged.
|
||||
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Open ends are uncapped, shading is flat with one light, and intersecting surfaces' visibility is approximate ([revolve.md](revolve.md)).
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
|
||||
- **Simplify feedback and component selection (#1272):** the existing tolerance dialog previews from the original interaction snapshot and shows original/new anchor counts. Preview off and Cancel restore the document and selection; OK is one Undo. A refit or Add Anchor Points clears component indexes only on paths whose geometry changes, keeping their objects selected whole and unchanged paths' components intact. Engine and real-egui frame regressions cover document/selection restoration and grouped paths.
|
||||
@@ -536,6 +537,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
| 2026-10-11 | minor | Graph designs and marker designs (Object › Graph › Design… and Marker…): menu stubs 27 → 25; the graphs row's missing items are column designs and Image Trace depth. Scores unchanged within rounding |
|
||||
| 2026-10-12 | minor | UI chrome: native macOS save sheets gain a Format menu (#866), with extension regression tests and actual save/cancel verification; score and hours unchanged (estimated) |
|
||||
| 2026-10-12 | minor | Compound-path appearance and fill-rule edits through directly selected members (#1197) verified with rendered holes and Undo/Redo; scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Path operations: Divide accepts open cutting paths (#1307); filled-face appearance, holes and bounded fallback regression-tested; scores and hours unchanged |
|
||||
| 2026-10-12 | minor | UI chrome: numeric focus clicks select on release while drags retain their press position (#1326, in part); keyboard focus, stepping and double-click regressions pass; scores and hours unchanged; short movements below the click threshold remain open |
|
||||
| 2026-10-12 | minor | DXF reads control-point SPLINE closure (#1299 follow-up after #1320); open curves stay open and curves retained; rational/non-rational import and export versions regression-tested; scores/hours unchanged |
|
||||
| 2026-10-11 | minor | Type core: Middle Eastern digit types (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; `{digits}`) leave the missing list; score and hours unchanged at this rounding (estimated) |
|
||||
|
||||
@@ -163,6 +163,7 @@ Layers panel.
|
||||
| Contextual task bar | done | |
|
||||
| Control bar with Transform and Stroke popovers | done | |
|
||||
| Attributes fill-rule controls with a compound member selected | done | Edits the compound's rule; direction still edits only the member; rendered-hole and Undo/Redo regression tests (#1197) |
|
||||
| Pathfinder Divide: an open cutting line splits a filled path | done | #1307; straight, crossed, bent and curved cutters, holes and frontmost appearance regression-tested; native automation: independent piece movement, one Undo/Redo, saved-document round trip and window capture; existing bounded-map fallback remains |
|
||||
| Canvas context menu | done | Layers rows, panels and type have none (gap) |
|
||||
| Blend panel (30.7) | missing | Blend Options dialog exists |
|
||||
| SVG Interactivity, Retype, Comments, Version History panels | missing | the last two are cloud features (out of scope) |
|
||||
@@ -195,6 +196,7 @@ re-measure of the panels added since.
|
||||
| 2026-10-12 | minor | Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged |
|
||||
| 2026-10-12 | minor | macOS native save sheets gain a localised Format menu (#866); filename regression tests and an actual SVG save, PDF selection and cancellation verified; scores unchanged (estimated) |
|
||||
| 2026-10-12 | minor | Attributes: directly selected compound members read and change the compound fill rule; path direction stays member-local (#1197); score unchanged |
|
||||
| 2026-10-12 | minor | Pathfinder Divide: open cutting lines split filled paths with one Undo (#1307); scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Numeric entry: click-to-focus selection waits for release so text drags keep their start (#1326, in part); keyboard focus, double-click, arrows and wheel preserved; scores and hours unchanged; short movements below the click threshold remain open |
|
||||
| 2026-10-11 | minor | A marquee inside a filled shape no longer selects it: only touching its path does (#1103); score unchanged (estimated) |
|
||||
| 2026-10-11 | minor | Live Corners: corners between curved sides round too, the curve trimmed to what the cut leaves (#1144); score unchanged (estimated) |
|
||||
|
||||
Reference in new issue
Block a user