mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
Merge pull request #1305 from haranishi/fix/issue-1272-simplify-preview
fix: Simplify後の選択を更新して点数を表示する #1272
This commit is contained in:
@@ -645,7 +645,7 @@ fn path_ids(d: &Document, roots: &[NodeId]) -> Vec<NodeId> {
|
||||
|
||||
fn edit_paths(s: &mut Session, label: &str, f: impl Fn(&PathData) -> PathData) -> Result<(Vec<NodeId>, usize, usize)> {
|
||||
let roots = selected_roots(s)?;
|
||||
s.edit(label, |d, _| {
|
||||
s.edit(label, |d, sel| {
|
||||
let ids = path_ids(d, &roots);
|
||||
if ids.is_empty() {
|
||||
return Err(EngineError::Other(format!("{label}: select paths")));
|
||||
@@ -660,6 +660,11 @@ fn edit_paths(s: &mut Session, label: &str, f: impl Fn(&PathData) -> PathData) -
|
||||
continue;
|
||||
}
|
||||
after += np.anchor_count();
|
||||
if *path != np {
|
||||
// Refit and subdivision change what the old component indexes refer to.
|
||||
// Keep the object selected whole so its new points remain editable.
|
||||
sel.anchors.remove(id);
|
||||
}
|
||||
*path = np;
|
||||
*live = None;
|
||||
}
|
||||
|
||||
@@ -279,6 +279,89 @@ fn simplify_dense_polyline() {
|
||||
assert!(close(bounds(&s, id), Rect::new(0.0, 60.0, 300.0, 140.0), 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplify_refreshes_changed_path_selection_and_undo() {
|
||||
let mut s = session();
|
||||
let anchors: Vec<Value> = (0..20).map(|i| json!({"x": 30 + i * 15, "y": 100.0 + (i % 2) as f64 * 0.1})).collect();
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
// A one-anchor path cannot be simplified; its component selection must stay.
|
||||
let kept = NodeId(s.execute("path.create", &json!({"anchors": [{"x": 350, "y": 180}]})).unwrap()["id"].as_u64().unwrap());
|
||||
let unrelated = rect(&mut s, 400.0, 200.0, 40.0, 30.0);
|
||||
s.edit("selection fixture", |d, sel| {
|
||||
d.guides.push(vectorcraft_doc::Guide::new(true, 30.0));
|
||||
sel.set([kept, id]);
|
||||
sel.anchors.insert(id, [(0, 19)].into_iter().collect());
|
||||
sel.anchors.insert(kept, [(0, 0)].into_iter().collect());
|
||||
sel.key = Some(kept);
|
||||
sel.target = Some(id);
|
||||
sel.guides.push(0);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let undo_len = s.doc().unwrap().history.undo.len();
|
||||
let out = s.execute("object.path.simplify", &json!({"tolerance": 1})).unwrap();
|
||||
assert_eq!((out["before"].as_u64(), out["after"].as_u64()), (Some(21), Some(3)));
|
||||
assert_eq!(path_of(&s, id).0.anchor_count(), 2);
|
||||
let mut expected_selection = selection.clone();
|
||||
expected_selection.anchors.remove(&id);
|
||||
assert_eq!(s.doc().unwrap().selection, expected_selection, "only the changed path becomes selected whole");
|
||||
assert_eq!(node(&s, unrelated), before.node(unrelated).unwrap().clone());
|
||||
let after = s.doc().unwrap().doc.clone();
|
||||
let mut restored = after.clone();
|
||||
std::sync::Arc::make_mut(&mut restored).node_mut(id).unwrap().kind = before.node(id).unwrap().kind.clone();
|
||||
assert_eq!(restored, before, "all document state except the simplified geometry is preserved");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo_len + 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc, after);
|
||||
assert_eq!(s.doc().unwrap().selection, expected_selection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_anchor_points_refreshes_components_even_when_old_indexes_still_exist() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 20.0, 30.0, 100.0, 70.0);
|
||||
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 3]]})).unwrap();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let out = s.execute("object.path.addAnchorPoints", &json!({})).unwrap();
|
||||
assert_eq!((out["before"].as_u64(), out["after"].as_u64()), (Some(4), Some(8)));
|
||||
assert!(s.doc().unwrap().selection.anchors.is_empty());
|
||||
assert_eq!(s.doc().unwrap().selection.objects, selection.objects);
|
||||
assert!(close(bounds(&s, id), before.node(id).unwrap().geometric_bounds().unwrap(), 1e-9));
|
||||
let after = s.doc().unwrap().doc.clone();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc, before);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc, after);
|
||||
assert!(s.doc().unwrap().selection.anchors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplify_refreshes_components_inside_selected_groups() {
|
||||
let mut s = session();
|
||||
let anchors: Vec<Value> = (0..20).map(|i| json!({"x": 30 + i * 15, "y": 100.0 + (i % 2) as f64 * 0.1})).collect();
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
let other = rect(&mut s, 400.0, 200.0, 40.0, 30.0);
|
||||
s.execute("select.set", &json!({"ids": [id.0, other.0]})).unwrap();
|
||||
let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
s.select(|_, sel| {
|
||||
sel.anchors.insert(id, [(0, 19)].into_iter().collect());
|
||||
})
|
||||
.unwrap();
|
||||
let other_before = node(&s, other);
|
||||
s.execute("object.path.simplify", &json!({"tolerance": 1})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![group]);
|
||||
assert!(s.doc().unwrap().selection.anchors.is_empty());
|
||||
assert_eq!(path_of(&s, id).0.anchor_count(), 2);
|
||||
assert_eq!(node(&s, other).appearance, other_before.appearance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_anchor_points_doubles() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -469,6 +469,19 @@ pub(super) const PREVIEWED: &str = "__previewed";
|
||||
/// again on the interaction's snapshot whenever they differ from the last preview (whether a widget
|
||||
/// or `ui.dialog.set` changed them); turning it off rolls back.
|
||||
pub(super) fn preview(app: &mut crate::VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, label: &str, cmd: &str, params: Value) {
|
||||
let _ = preview_result(app, ui, d, label, cmd, params);
|
||||
}
|
||||
|
||||
/// As [`preview`], returning the command's result only when a new preview succeeds.
|
||||
/// A dialog can retain a small summary (such as point counts) in its private fields.
|
||||
pub(super) fn preview_result(
|
||||
app: &mut crate::VectorcraftApp,
|
||||
ui: &mut egui::Ui,
|
||||
d: &mut Dialog,
|
||||
label: &str,
|
||||
cmd: &str,
|
||||
params: Value,
|
||||
) -> Option<Value> {
|
||||
const LAST: &str = PREVIEWED;
|
||||
ui.add_space(8.0);
|
||||
let mut on = d.bool("preview");
|
||||
@@ -480,15 +493,18 @@ pub(super) fn preview(app: &mut crate::VectorcraftApp, ui: &mut egui::Ui, d: &mu
|
||||
if d.fields.remove(LAST).is_some() {
|
||||
let _ = app.session.cancel_interaction();
|
||||
}
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let mut result = None;
|
||||
if d.fields.get(LAST) != Some(¶ms) || !app.session.in_interaction() {
|
||||
let _ = app.session.begin_interaction(label);
|
||||
if let Err(e) = app.session.preview(cmd, ¶ms) {
|
||||
app.status(e.to_string());
|
||||
match app.session.preview(cmd, ¶ms) {
|
||||
Ok(r) => result = Some(r),
|
||||
Err(e) => app.status(e.to_string()),
|
||||
}
|
||||
d.fields.insert(LAST.into(), params);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// OK of a dialog using [`preview`]: keep the previewed `cmd` as one undo step (or run it when no
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Path dialogs: Average, Offset Path (with Preview), Simplify and Split Into Grid.
|
||||
//! Path dialogs: Average, Offset Path and Simplify (with Preview), and Split Into Grid.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -31,21 +31,38 @@ fn command(d: &Dialog) -> (&'static str, Value) {
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
form::grid(ui, d, app.session.general_unit());
|
||||
if d.kind == "offsetPath" {
|
||||
if matches!(d.kind.as_str(), "offsetPath" | "simplify") {
|
||||
let (id, params) = command(d);
|
||||
form::preview(app, ui, d, "Offset Path", id, params);
|
||||
if d.fields.get(form::PREVIEWED) != Some(¶ms) || !d.bool("preview") {
|
||||
d.fields.remove("__counts");
|
||||
}
|
||||
let label = if d.kind == "simplify" { "Simplify" } else { "Offset Path" };
|
||||
if let Some(r) = form::preview_result(app, ui, d, label, id, params)
|
||||
&& d.kind == "simplify"
|
||||
{
|
||||
d.fields.insert("__counts".into(), json!({"before": r["before"], "after": r["after"]}));
|
||||
}
|
||||
if d.kind == "simplify"
|
||||
&& d.bool("preview")
|
||||
&& let Some(counts) = d.fields.get("__counts")
|
||||
&& let (Some(before), Some(after)) = (counts["before"].as_u64(), counts["after"].as_u64())
|
||||
{
|
||||
ui.label(format!("{}: {before} → {after}", tl!("Anchor Points")));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let (id, params) = command(d);
|
||||
if d.kind != "offsetPath" {
|
||||
if !matches!(d.kind.as_str(), "offsetPath" | "simplify") {
|
||||
return run_and_close(app, id, params);
|
||||
}
|
||||
let r = form::commit_preview(app, id, params)?;
|
||||
// The fields as the dialog held them, so they show as typed when it opens again.
|
||||
let kept = ["offset", "joins", "miterLimit"].into_iter().filter_map(|k| Some((k.to_string(), d.fields.get(k)?.clone())));
|
||||
app.ui.offset_path = Value::Object(kept.collect());
|
||||
if d.kind == "offsetPath" {
|
||||
let kept = ["offset", "joins", "miterLimit"].into_iter().filter_map(|k| Some((k.to_string(), d.fields.get(k)?.clone())));
|
||||
app.ui.offset_path = Value::Object(kept.collect());
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
@@ -217,3 +217,82 @@ fn offset_path_remembers_its_last_values() {
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.f64("offset", 0.0), d.str("joins"), d.f64("miterLimit", 0.0)), (-4.5, "round".to_string(), 4.0));
|
||||
}
|
||||
|
||||
fn app_with_dense_path() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let anchors: Vec<_> = (0..20).map(|i| json!({"x": 30 + i * 15, "y": 100.0 + (i % 2) as f64 * 0.1})).collect();
|
||||
let id = app.run("path.create", json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.anchors", json!({"id": id, "anchors": [[0, 19]]})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplify_dialog_preview_counts_and_cancel_restore_the_original() {
|
||||
let (ctx, mut app) = (ctx(), app_with_dense_path());
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let selection = app.session.doc().unwrap().selection.clone();
|
||||
let undo_len = app.session.doc().unwrap().history.undo.len();
|
||||
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
|
||||
settle(&ctx, &mut app);
|
||||
let texts = frame(&ctx, &mut app, vec![]);
|
||||
let preview = texts.iter().find(|(t, _)| t == "Preview").map(|(_, r)| *r).expect("a Preview checkbox");
|
||||
assert!(texts.iter().any(|(t, _)| t == "Anchor Points: 20 → 2"), "the reduction is visible: {texts:?}");
|
||||
assert!(app.session.in_interaction());
|
||||
assert!(app.session.doc().unwrap().selection.anchors.is_empty());
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len);
|
||||
// The dialog focuses its first field. Type as a user does, then Tab commits it
|
||||
// without confirming the dialog; replacing only its bound value leaves old focused text.
|
||||
frame(&ctx, &mut app, vec![Event::Text("0.01 pt".into())]);
|
||||
frame(&ctx, &mut app, vec![Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().f64("tolerance", 0.0), 0.01);
|
||||
let fine = app.session.doc().unwrap().doc.clone();
|
||||
click(&ctx, &mut app, preview.center());
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
assert_eq!(app.session.doc().unwrap().selection, selection);
|
||||
assert!(!app.session.in_interaction());
|
||||
let texts = frame(&ctx, &mut app, vec![]);
|
||||
assert!(!texts.iter().any(|(t, _)| t.starts_with("Anchor Points:")), "no stale counts while Preview is off");
|
||||
let preview = texts.iter().find(|(t, _)| t == "Preview").unwrap().1;
|
||||
click(&ctx, &mut app, preview.center());
|
||||
assert_eq!(app.session.doc().unwrap().doc, fine);
|
||||
cancel(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
assert_eq!(app.session.doc().unwrap().selection, selection);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len);
|
||||
app.run("object.path.simplify", json!({"tolerance": 0.01})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, fine, "changing tolerance refits the original, not the previous preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplify_dialog_ok_keeps_exactly_one_undo_with_or_without_preview() {
|
||||
for on in [true, false] {
|
||||
let (ctx, mut app) = (ctx(), app_with_dense_path());
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let selection = app.session.doc().unwrap().selection.clone();
|
||||
let undo_len = app.session.doc().unwrap().history.undo.len();
|
||||
crate::menus::invoke(&mut app, "object.path.simplify", json!({}));
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(on));
|
||||
settle(&ctx, &mut app);
|
||||
if on {
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("tolerance".into(), json!("0.01 pt"));
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("tolerance".into(), json!("1 pt"));
|
||||
frame(&ctx, &mut app, vec![]);
|
||||
}
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len);
|
||||
confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none() && !app.session.in_interaction());
|
||||
assert!(app.session.doc().unwrap().selection.anchors.is_empty());
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len + 1);
|
||||
let after = app.session.doc().unwrap().doc.clone();
|
||||
assert_ne!(after, before);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
assert_eq!(app.session.doc().unwrap().selection, selection);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||||
assert!(app.session.doc().unwrap().selection.anchors.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3013,7 +3013,7 @@ fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static str, Value)>
|
||||
// Offset Path opens on the values it was last confirmed with.
|
||||
"object.path.offsetPath" if app.ui.offset_path.is_object() => ("offsetPath", app.ui.offset_path.clone()),
|
||||
"object.path.offsetPath" => ("offsetPath", json!({"offset": "10 pt", "joins": "miter", "miterLimit": 4})),
|
||||
"object.path.simplify" => ("simplify", json!({"tolerance": "1 pt"})),
|
||||
"object.path.simplify" => ("simplify", json!({"tolerance": "1 pt", "preview": true})),
|
||||
"object.path.splitIntoGrid" => ("splitIntoGrid", json!({"rows": 2, "columns": 2, "gutter": "12 pt"})),
|
||||
"object.vectorHalftone" => (crate::dialogs::halftone::KIND, crate::dialogs::halftone::fields()),
|
||||
"artboard.rearrange" => (crate::dialogs::rearrange_artboards::KIND, crate::dialogs::rearrange_artboards::fields()),
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# Gaps
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
|
||||
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
|
||||
@@ -95,6 +95,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
## G11 Correctness bugs in core tools
|
||||
|
||||
- **Done:** Simplify and Add Anchor Points clear stale component selections on rewritten paths; Simplify previews and reports original/new point counts (#1272).
|
||||
- **Done:** no-op anchor Align preserves Undo, Redo, saved state and path geometry (#1180 follow-up),
|
||||
with the same 1e-9 pt tolerance as whole-object Align; actual alignment remains one undo step.
|
||||
- **Done:** scaled text's size and horizontal-scale controls account for object/group transforms (#1034),
|
||||
@@ -219,6 +220,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | G11: clear stale component refs after Simplify/Add Anchor Points; preview and point counts give visible feedback (#1272); estimates unchanged |
|
||||
| 2026-10-12 | minor | G2: Artistic › Film Grain added with live controls and raster exports; measured Photoshop filters 29/57, 28 remaining (23–39 h). |
|
||||
| 2026-10-12 | minor | G10: RPM required dependencies and glibc floor metadata checked (#1275); real-distro install/start-up QA remains; estimates unchanged |
|
||||
| 2026-10-12 | minor | G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged |
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (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-12 · **Change:** minor (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
|
||||
@@ -57,6 +57,9 @@ 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; 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; 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 |
|
||||
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; 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, 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); 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 |
|
||||
| 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, 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 |
|
||||
| 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 (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 |
|
||||
@@ -85,6 +88,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | M6: Simplify preview/counts and valid selection after topology changes (#1272); auto/slider/advanced UI remains open; estimates unchanged |
|
||||
| 2026-10-12 | minor | G2: Artistic › Film Grain added with live controls and raster exports; measured Photoshop filters 29/57, 28 remaining (23–39 h). |
|
||||
| 2026-10-12 | minor | M14: RPM runtime dependencies use shared-library capabilities and glibc 2.35 (#1275); metadata verified, distro QA pending; estimates unchanged |
|
||||
| 2026-10-12 | minor | M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged |
|
||||
|
||||
File diff suppressed because one or more lines are too long
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
How VectorCraft's tools, handles, snapping and panels behave next to Illustrator's. A vector illustrator's
|
||||
trust is in the details: where a handle lands, what Shift and Alt do mid-drag, what a smart guide snaps to,
|
||||
@@ -55,7 +55,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Add/Delete/Anchor Point tools; Anchor Point tool drags segments to reshape | done | |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | partial | Keep selected and Alt to Smooth are missing |
|
||||
| Join tool scrubbing, Average, Join with corner/smooth choice | done | |
|
||||
| Simplify (slider, auto, corner angle threshold) | done | |
|
||||
| Simplify (slider, auto, corner angle threshold) | partial | Tolerance, default-on Preview and original/new point counts (#1272); slider, auto-simplify, corner-angle UI and Show Original remain missing |
|
||||
|
||||
## Direct Selection, anchors and handles
|
||||
|
||||
@@ -178,6 +178,7 @@ re-measure of the panels added since.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Simplify previews and shows original/new point counts; refits/subdivisions select changed paths whole (#1272); auto/slider/corner-angle UI and Show Original remain partial |
|
||||
| 2026-10-12 | minor | Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged |
|
||||
| 2026-10-12 | minor | Curvature: circular point markers, no direction handles or duplicate points; existing-path interpolation and #975 remain separate (#1236, in part); scores unchanged (estimated) |
|
||||
| 2026-10-12 | minor | Direct Selection: repeating anchor Align and rounding-only Align leave Undo, Redo and saved state intact (#1180 follow-up); scores unchanged (estimated) |
|
||||
|
||||
Reference in New Issue
Block a user