Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a91be7983 | ||
|
|
5bfe774420 | ||
|
|
e918615347 | ||
|
|
17b4396a97 | ||
|
|
5adeada26d | ||
|
|
175ad4a1e3 | ||
|
|
95304ad7f6 | ||
|
|
02da9bbdc9 | ||
|
|
331fc48b24 |
No files matched your search
+2
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (community PRs and fixes of 2026-10-13: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; `.ai`, PDF, EPS and SVG import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; headline numbers unchanged; community PRs and fixes of late 2026-10-12: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; French catalog complete again, #1511; headline numbers unchanged; Image Trace colour and photo presets, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-12: Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; many-object edits, #1429 in part; headline numbers unchanged; Image Trace places edges to a fraction of a pixel, measured in `docs/image-trace-quality.md`, headline numbers unchanged; Transform dialogs preview and the transform tools act on direct-selected anchors, #1412 and #1408; `.ai` compound path paint, #1410, overprint, #1388, and text-only PDF parts, #1387; community PRs: strokes wider than their shape, corner drags without the bounding box, pasted pictures, compound paths in `document.inspect`, MCP guide; headline numbers unchanged; community PRs: `.ai` editing-data follow-ups (gradient stops, type of objects with an appearance, short area-type frames), PDF bleed with editing data, dialog keyboard focus, art flush with an artboard, font rescan, symbol instance redraw, `--control` port check; headline numbers unchanged; Rectangular Grid and Polar Grid tool options, #1377 in part; `.ai` files with placed (linked) pictures open from their editing data, #1264; community PRs: `.ai` and PDF bleed and editing-data import, gradient shapes imported as paths, ruler origin, Align To, Live Corners corner selection; headline numbers unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; community PRs: graph designs, Graph Data worksheet and Graph Type options; Align both centres; Pen endpoint fixes; earlier: Effect › Brush Strokes landed: Photoshop-style filters 28 of 57, raster effects 40% → 48%; `.ai` gradients, meshes and artboard names from the editing data; headline numbers unchanged; `.ai` art styles and clipping fills from the editing data; headline numbers unchanged; `editingData: "always"`; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Image Trace edges, strokes, memory and three new options, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-13: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; `.ai`, PDF, EPS and SVG import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; headline numbers unchanged; community PRs and fixes of late 2026-10-12: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; French catalog complete again, #1511; headline numbers unchanged; Image Trace colour and photo presets, measured in `docs/image-trace-quality.md`, headline numbers unchanged; community PRs and fixes of 2026-10-12: Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; many-object edits, #1429 in part; headline numbers unchanged; Image Trace places edges to a fraction of a pixel, measured in `docs/image-trace-quality.md`, headline numbers unchanged; Transform dialogs preview and the transform tools act on direct-selected anchors, #1412 and #1408; `.ai` compound path paint, #1410, overprint, #1388, and text-only PDF parts, #1387; community PRs: strokes wider than their shape, corner drags without the bounding box, pasted pictures, compound paths in `document.inspect`, MCP guide; headline numbers unchanged; community PRs: `.ai` editing-data follow-ups (gradient stops, type of objects with an appearance, short area-type frames), PDF bleed with editing data, dialog keyboard focus, art flush with an artboard, font rescan, symbol instance redraw, `--control` port check; headline numbers unchanged; Rectangular Grid and Polar Grid tool options, #1377 in part; `.ai` files with placed (linked) pictures open from their editing data, #1264; community PRs: `.ai` and PDF bleed and editing-data import, gradient shapes imported as paths, ruler origin, Align To, Live Corners corner selection; headline numbers unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; community PRs: graph designs, Graph Data worksheet and Graph Type options; Align both centres; Pen endpoint fixes; earlier: Effect › Brush Strokes landed: Photoshop-style filters 28 of 57, raster effects 40% → 48%; `.ai` gradients, meshes and artboard names from the editing data; headline numbers unchanged; `.ai` art styles and clipping fills from the editing data; headline numbers unchanged; `editingData: "always"`; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs
|
||||
on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON
|
||||
@@ -127,6 +127,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-13 | Image Trace: neighbouring colours share one fitted edge (gaps between them 22 to 50 px of area down to under 2), a 16 megapixel photo traces in 0.9 GB (was 3.9), stroked centre lines lie in the middle of their ink at its width (0.30 to 0.05 px off, 18 to 3 anchors a stroke), nearly level lines are levelled, more sharp corners are kept; new: Ignore Color, the Auto Color and Outline presets, and the Shapes, Auto Grouping and Gradients options (off by default) ([`docs/image-trace-quality.md`](docs/image-trace-quality.md)). |
|
||||
| 2026-10-13 | The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498). Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492). Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499). Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479). Import: a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537). Live Corners: a round corner next to a curved side curves smoothly instead of sticking out of the shape, and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512). Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533). Guides: Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534). Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). Fonts: the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux). |
|
||||
| 2026-10-13 | Image Trace's colour presets trace flat art with its own colours however many are asked for (noise no longer shatters it: 16 Colors 70 → 4 paths on the harness's flat art; no slivers of in-between colour along anti-aliased edges), and the photo presets trace a photo into smooth tones without specks or hairlines (High Fidelity Photo 3,653 → 121 paths, colour error 2.30 → 1.75; 16 megapixels traced where 4 were refused) ([`docs/image-trace-quality.md`](docs/image-trace-quality.md)). |
|
||||
| 2026-10-13 | Image Trace traces anti-aliased and blurred images to a fraction of a pixel in Black and White, Grayscale and Color, with corners put back, straight edges as lines and round outlines as circles; a harness measures every preset before and after ([`docs/image-trace-quality.md`](docs/image-trace-quality.md)): outlines 0.31 → 0.09 px off on average (Default), 0.27 → 0.06 (Black and White Logo). |
|
||||
|
||||
@@ -249,7 +249,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"distort.freeDistort",
|
||||
"Free Distort…",
|
||||
DT,
|
||||
"{corners: [[x,y]×4] new positions of the bounding-box corners TL, TR, BR, BL in unit box coordinates (identity [[0,0],[1,0],[1,1],[0,1]])}",
|
||||
"{corners: [[x,y]×4] (new positions of the bounding-box corners TL, TR, BR, BL in unit box coordinates; identity [[0,0],[1,0],[1,1],[0,1]])}",
|
||||
json!({"corners": [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]}),
|
||||
),
|
||||
g("distort.puckerBloat", "Pucker & Bloat…", DT, "{amount: % (-200 pucker … 200 bloat, default 0)}", json!({"amount": 0.0})),
|
||||
|
||||
@@ -280,6 +280,20 @@ fn move_effect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "ids": ids_json(&ids), "index": index, "item": item.flatten() }))
|
||||
}
|
||||
|
||||
/// The ids of the effects `effect.setParams` with `p` edits, for the check of their parameters
|
||||
/// (none when `p` doesn't say which: running it reports why).
|
||||
pub(crate) fn set_params_effects(s: &Session, p: &Value) -> Vec<String> {
|
||||
const C: &str = "effect.setParams";
|
||||
let (Ok(index), Ok(item), Ok(ids), Ok(st)) = (index_param(p, "index", C), item_target(s, p, C), appearance_targets(s, p), s.doc()) else {
|
||||
return vec![];
|
||||
};
|
||||
let effect = |id: &NodeId| {
|
||||
let ap = &st.doc.node(*id)?.appearance;
|
||||
Some(ap.effects_at(item.effects_item(ap, C).ok()?)?.get(index)?.id.clone())
|
||||
};
|
||||
ids.iter().filter_map(effect).collect()
|
||||
}
|
||||
|
||||
fn set_params(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "effect.setParams";
|
||||
let index = index_param(p, "index", C)?;
|
||||
@@ -566,7 +580,7 @@ mod tests {
|
||||
for other in ["threeD.revolve", "threeD.extrude", "threeD.rotate"] {
|
||||
assert!(s.execute("effect.apply", &json!({"effect": other, "ids": [id.0], "item": null})).is_err(), "{effect} + {other}");
|
||||
}
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"rotationY": 50, "depth": 120}, "ids": [id.0], "item": null})).unwrap();
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"rotationY": 50, "rotationX": 10}, "ids": [id.0], "item": null})).unwrap();
|
||||
assert_ne!(effects::solid_art(&node(&s, id)).unwrap(), art, "{effect}: options change the art");
|
||||
assert!(s.execute("effect.setParams", &json!({"index": 0, "params": {"bevel": "nope"}, "ids": [id.0], "item": null})).is_err());
|
||||
let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap();
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
//!
|
||||
//! Callers outside the app (the control channel, MCP, the CLI) are answered with a parameter error
|
||||
//! for a key the description doesn't name ([`check_params`]): a misspelt `widht` no longer runs the
|
||||
//! command with its default. [`Session::execute`] itself takes what it is given, so the journal of
|
||||
//! an older version still replays.
|
||||
//! command with its default. So are the `params` of an effect (#1592): `effect.apply` and
|
||||
//! `effect.setParams` answer a key the effect doesn't take, or a value of another type than its
|
||||
//! default, with a parameter error ([`check_effect_params`]). [`Session::execute`] itself takes
|
||||
//! what it is given, so the journal of an older version still replays.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -189,15 +191,56 @@ pub fn check_keys(id: &str, doc: &'static str, params: &Value) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(keys) = accepted(doc, 0) else { return Ok(()) };
|
||||
let Some(key) = given.keys().find(|k| !keys.contains(&k.as_str())) else { return Ok(()) };
|
||||
Err(bad(
|
||||
match given.keys().find(|k| !keys.contains(&k.as_str())) {
|
||||
Some(key) => Err(unknown(id, key, &keys)),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameter error of command `id` for `key`, which isn't one of `keys`: with the nearest of
|
||||
/// them, or all of them.
|
||||
fn unknown(id: &str, key: &str, keys: &[&str]) -> crate::EngineError {
|
||||
bad(
|
||||
id,
|
||||
match nearest(key, &keys) {
|
||||
match nearest(key, keys) {
|
||||
Some(near) => format!("unknown parameter `{key}`; did you mean `{near}`?"),
|
||||
None if keys.is_empty() => format!("unknown parameter `{key}`; the command takes no parameters"),
|
||||
None => format!("unknown parameter `{key}`; the command takes: {}", keys.join(", ")),
|
||||
},
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
/// `params` of built-in effect `effect`, as command `id` was given them (#1592): only keys the
|
||||
/// effect takes (those of its dialog defaults and of its params description), each of its
|
||||
/// default's type. Numbers out of range pass: the effect clamps them when it draws. An effect
|
||||
/// plug-in resolves its own against its manifest, and an unknown effect is the command's to report.
|
||||
pub fn check_effect_params(id: &str, effect: &str, params: &Value) -> Result<()> {
|
||||
let Value::Object(given) = params else { return Ok(()) };
|
||||
if effect.starts_with(vectorcraft_plugins::effect::PREFIX) {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(info) = vectorcraft_render::effects::effect_info(effect) else { return Ok(()) };
|
||||
let defaults = info.defaults.as_object();
|
||||
let mut keys: Vec<&str> = defaults.into_iter().flat_map(|d| d.keys().map(String::as_str)).collect();
|
||||
// A description that doesn't parse names no more keys (the tests rule that out).
|
||||
for key in doc_keys(info.params).map(|d| d.keys).unwrap_or_default() {
|
||||
if !keys.contains(&key) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
for (key, v) in given {
|
||||
if !keys.contains(&key.as_str()) {
|
||||
return Err(unknown(id, key, &keys));
|
||||
}
|
||||
match defaults.and_then(|d| d.get(key)) {
|
||||
Some(Value::Number(_)) if !v.is_number() => return Err(bad(id, format!("`{key}` must be a number"))),
|
||||
Some(Value::Bool(_)) if !v.is_boolean() => return Err(bad(id, format!("`{key}` must be true or false"))),
|
||||
// (A colour is also taken as `[r, g, b]` or as a colour object.)
|
||||
Some(Value::String(_)) if v.is_number() || v.is_boolean() => return Err(bad(id, format!("`{key}` must be a string"))),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commands whose params aren't checked yet (#1442): they read keys in ways the audit of the
|
||||
@@ -333,10 +376,16 @@ pub const UNCHECKED: &[&str] = &[
|
||||
"webExport.settings",
|
||||
];
|
||||
|
||||
/// [`check_keys`] for an engine command, the steps of a `command.batch` included. A command the
|
||||
/// engine doesn't have passes: running it reports that.
|
||||
/// [`check_keys`] for an engine command, the steps of a `command.batch` included, and
|
||||
/// [`check_effect_params`] for the effect `effect.apply` adds. A command the engine doesn't have
|
||||
/// passes: running it reports that.
|
||||
pub fn check_params(id: &str, params: &Value) -> Result<()> {
|
||||
let Some(spec) = find_command(id) else { return Ok(()) };
|
||||
if let ("effect.apply" | "appearance.addEffect", Some(effect), Some(given)) =
|
||||
(id, params.get("effect").or_else(|| params.get("id")).and_then(Value::as_str), params.get("params"))
|
||||
{
|
||||
check_effect_params(id, effect, given)?;
|
||||
}
|
||||
if UNCHECKED.contains(&id) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -350,3 +399,18 @@ pub fn check_params(id: &str, params: &Value) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl crate::Session {
|
||||
/// [`check_params`], and [`check_effect_params`] for the effects `effect.setParams` edits in
|
||||
/// this session's document (not as a step of a `command.batch`: what it edits is only there
|
||||
/// once the steps before it ran).
|
||||
pub fn check_params(&self, id: &str, params: &Value) -> Result<()> {
|
||||
check_params(id, params)?;
|
||||
if let ("effect.setParams", Some(given)) = (id, params.get("params")) {
|
||||
for effect in super::effectcmd::set_params_effects(self, params) {
|
||||
check_effect_params(id, &effect, given)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1317,7 +1317,7 @@ impl Session {
|
||||
// The crate's own tests are the audit of the params descriptions (#1442): a key a test
|
||||
// passes that its command's description doesn't name fails the test.
|
||||
#[cfg(test)]
|
||||
cmd::params::check_params(id, params)?;
|
||||
self.check_params(id, params)?;
|
||||
// Clear's menu needs a selection; an agent supplying explicit targets only needs a
|
||||
// document. The command validates those targets before making any edit.
|
||||
let enabled = if id == "edit.clear" && params.get("ids").is_some() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::cmd::params::{UNCHECKED, check_keys, check_params, doc_keys};
|
||||
use crate::cmd::params::{UNCHECKED, check_effect_params, check_keys, check_params, doc_keys};
|
||||
use crate::{EngineError, Session, command_specs, find_command};
|
||||
|
||||
fn keys(doc: &str) -> Vec<&str> {
|
||||
@@ -95,6 +95,50 @@ fn a_batch_checks_its_steps() {
|
||||
assert_eq!(refusal("command.batch", json!({"steps": []})), "unknown parameter `steps`; the command takes: label, commands, artboard");
|
||||
}
|
||||
|
||||
/// #1592: the `params` of an effect are held to the effect's own keys and types, where the effect
|
||||
/// is named (`effect.apply`) and where the document says which it is (`effect.setParams`).
|
||||
#[test]
|
||||
fn effect_params_are_the_effects_own() {
|
||||
use vectorcraft_render::effects::effect_catalog;
|
||||
for e in effect_catalog() {
|
||||
doc_keys(e.params).unwrap_or_else(|why| panic!("{}: {why}", e.id));
|
||||
check_effect_params("effect.apply", e.id, &e.defaults).unwrap_or_else(|why| panic!("{}'s defaults: {why}", e.id));
|
||||
}
|
||||
let apply = |effect: &str, params| json!({"effect": effect, "params": params});
|
||||
assert_eq!(refusal("effect.apply", apply("blur.gaussian", json!({"blurr": 3}))), "unknown parameter `blurr`; the command takes: radius");
|
||||
assert_eq!(
|
||||
refusal("appearance.addEffect", apply("stylize.dropShadow", json!({"opacty": 50}))),
|
||||
"unknown parameter `opacty`; did you mean `opacity`?"
|
||||
);
|
||||
assert_eq!(refusal("effect.apply", apply("blur.gaussian", json!({"radius": "big"}))), "`radius` must be a number");
|
||||
assert_eq!(refusal("effect.apply", apply("threeD.revolve", json!({"shade": 1}))), "`shade` must be true or false");
|
||||
assert_eq!(refusal("effect.apply", apply("threeD.revolve", json!({"edge": 1}))), "`edge` must be a string");
|
||||
// Out of range is the effect's to clamp; a key only its description names is one it takes;
|
||||
// a plug-in's are its manifest's; an unknown effect is the command's to report.
|
||||
for (effect, params) in [
|
||||
("stylize.dropShadow", json!({"opacity": 500, "x": -3, "color": "#ff0000"})),
|
||||
("stylize.dropShadow", json!({"color": [1, 0, 0]})),
|
||||
("threeD.revolve", json!({"segments": 100000})),
|
||||
("path.outlineStroke", json!({"width": 2})),
|
||||
("plugin.anything", json!({"whatever": 1})),
|
||||
("no.such", json!({"whatever": 1})),
|
||||
] {
|
||||
assert!(check_params("effect.apply", &apply(effect, params.clone())).is_ok(), "{effect} {params}");
|
||||
}
|
||||
// `effect.setParams` names an index: the session knows the effect there.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
s.execute("effect.apply", &apply("blur.gaussian", json!({"radius": 2}))).unwrap();
|
||||
for (params, why) in [(json!({"blurr": 3}), "unknown parameter `blurr`"), (json!({"radius": true}), "`radius` must be a number")] {
|
||||
let e = s.execute("effect.setParams", &json!({"index": 0, "params": params})).unwrap_err().to_string();
|
||||
assert!(e.contains(why), "{e}");
|
||||
}
|
||||
s.execute("effect.setParams", &json!({"index": 0, "params": {"radius": 1e9}, "visible": false})).unwrap();
|
||||
// No effect at the index: the command says so, not the check.
|
||||
assert!(s.check_params("effect.setParams", &json!({"index": 7, "params": {"blurr": 3}})).is_ok());
|
||||
}
|
||||
|
||||
/// What a command notes for its journal entry is a key its description names, so the entry
|
||||
/// passes the check a caller's params get.
|
||||
#[test]
|
||||
|
||||
@@ -106,10 +106,19 @@ fn listed_applied_edited_and_undone() {
|
||||
assert_eq!((&e["raster"], &e["menu"]), (&json!(true), &json!(["Effect", menu])), "{id}");
|
||||
}
|
||||
}
|
||||
// Junk parameters are stored as given and read clamped; the canvas still draws.
|
||||
// Numbers far out of range are stored as given and read clamped; the canvas still draws.
|
||||
for (id, _) in EFFECTS {
|
||||
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": {"amount": 1e308, "radius": -1e308, "threshold": "x", "quality": 7}}))
|
||||
.unwrap();
|
||||
let defaults = vectorcraft_render::effects::default_params(id).unwrap();
|
||||
let junk: serde_json::Map<String, Value> = defaults
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(_, v)| v.is_number())
|
||||
.enumerate()
|
||||
.map(|(i, (k, _))| (k.clone(), json!(if i % 2 == 0 { 1e308 } else { -1e308 })))
|
||||
.collect();
|
||||
assert!(!junk.is_empty(), "{id}");
|
||||
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": junk})).unwrap();
|
||||
}
|
||||
let _ = render(&s);
|
||||
let n = s.doc().unwrap().doc.node(NodeId(g)).unwrap().clone();
|
||||
|
||||
@@ -22,6 +22,28 @@ fn a_journal_with_a_key_no_longer_read_still_replays() {
|
||||
assert_eq!((st.doc.artboards[0].rect.width(), st.doc.layers[0].children().unwrap().len()), (300.0, 1));
|
||||
}
|
||||
|
||||
/// So do an effect's own parameters (#1592): a journal or a file with a key the effect doesn't
|
||||
/// take, or a value of another type, replays and opens; the effect reads what it knows, clamped.
|
||||
#[test]
|
||||
fn effect_params_no_longer_taken_still_replay_and_open() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
let old = [
|
||||
("effect.apply", json!({"effect": "blur.gaussian", "params": {"blurr": 3, "radius": "big"}})),
|
||||
("effect.setParams", json!({"index": 0, "params": {"amount": 1e308}})),
|
||||
];
|
||||
for (id, params) in &old {
|
||||
assert!(s.check_params(id, params).is_err(), "{id}");
|
||||
s.execute(id, params).unwrap();
|
||||
}
|
||||
let doc = &s.active().unwrap().doc;
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(doc, false)).unwrap();
|
||||
let stored = |d: &vectorcraft_doc::Document| d.layers[0].children().unwrap()[0].appearance.effects[0].params.clone();
|
||||
assert_eq!(stored(&back), stored(doc));
|
||||
assert_eq!(stored(&back)["blurr"], json!(3));
|
||||
}
|
||||
|
||||
/// What a command adds to its own journal entry passes the check, so a recorded journal can be
|
||||
/// sent back through the control channel or MCP.
|
||||
#[test]
|
||||
|
||||
@@ -128,7 +128,7 @@ impl Headless {
|
||||
// A key the command's description doesn't name is an error, not a default (#1442).
|
||||
match HOST_COMMANDS.iter().find(|c| c.0 == id) {
|
||||
Some((_, _, doc)) => vectorcraft_engine::check_keys(id, doc, params),
|
||||
None => vectorcraft_engine::check_params(id, params),
|
||||
None => self.session.check_params(id, params),
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
match id {
|
||||
|
||||
@@ -1241,11 +1241,50 @@ impl<'p> Builder<'p> {
|
||||
}
|
||||
|
||||
fn flush_text(&mut self) {
|
||||
let Some((t, opacity, facts)) = self.text.take().and_then(TextLine::finish) else { return };
|
||||
let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t)));
|
||||
n.opacity = opacity;
|
||||
self.lines.insert(n.id, facts);
|
||||
self.push_node(n);
|
||||
let Some(mut line) = self.text.take() else { return };
|
||||
let held = std::mem::take(&mut line.held);
|
||||
if let Some((t, opacity, facts)) = line.finish() {
|
||||
let mut n = Node::new(self.id(), NodeKind::Text(Box::new(t)));
|
||||
n.opacity = opacity;
|
||||
self.lines.insert(n.id, facts);
|
||||
self.push_node(n);
|
||||
}
|
||||
// The strokes of its glyphs' outlines, right after it ([`import_lines`] makes them its stroke).
|
||||
for n in held {
|
||||
self.push_node(n);
|
||||
}
|
||||
}
|
||||
|
||||
/// A stroke of the outline of the glyph just drawn, in the glyph's own paint, while every glyph
|
||||
/// before it on the open line was stroked so too, is held with the line instead of ending it
|
||||
/// (an editor writes type on a path glyph by glyph, each followed by the stroke added to the
|
||||
/// type). `false`: it isn't one.
|
||||
fn hold_glyph_stroke(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'_>, draw_mode: &PathDrawMode) -> bool {
|
||||
let PathDrawMode::Stroke(props) = draw_mode else { return false };
|
||||
if self.mask.is_some() || self.blend != BlendMode::Normal || matches!(paint, hayro_interpret::Paint::Pattern(_)) {
|
||||
return false;
|
||||
}
|
||||
let (p, opacity) = self.paint(paint, true);
|
||||
let Some(line) = &self.text else { return false };
|
||||
let Some(ink) = line.ink.last() else { return false };
|
||||
if line.held.len() + 1 != line.ink.len() || opacity < 0.999 || !line.last_fill().is_some_and(|f| *f == p) {
|
||||
return false;
|
||||
}
|
||||
let mut bp = path.clone();
|
||||
bp.apply_affine(transform);
|
||||
let b = bp.bounding_box();
|
||||
let tol = 0.01 * (ink.width() + ink.height()) + 0.01;
|
||||
if ![(b.x0, ink.x0), (b.y0, ink.y0), (b.x1, ink.x1), (b.y1, ink.y1)].iter().all(|(a, c)| (a - c).abs() <= tol) {
|
||||
return false;
|
||||
}
|
||||
let mut st = stroke_layer(p, opacity, props, mean_scale(transform));
|
||||
// Every painted path takes its turn in the scan's overprint list, a held one too.
|
||||
st.overprint = self.path_overprint();
|
||||
let n = Node::path(self.id(), PathData::from_bezpath(&bp), Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() });
|
||||
if let Some(line) = &mut self.text {
|
||||
line.held.push(n);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Convert a hayro paint to a VectorCraft paint and opacity (`stroke`: for a stroke).
|
||||
@@ -1667,6 +1706,10 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||
|
||||
fn draw_path(&mut self, path: &BezPath, transform: Affine, paint: &hayro_interpret::Paint<'a>, draw_mode: &PathDrawMode) {
|
||||
self.flush_glyphs();
|
||||
if self.hold_glyph_stroke(path, transform, paint, draw_mode) {
|
||||
self.pending = false;
|
||||
return;
|
||||
}
|
||||
self.flush_text();
|
||||
self.pending = false;
|
||||
let overprint = self.path_overprint();
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::collections::HashMap;
|
||||
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{CharStyle, ParaDirection, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_doc::{CharStyle, Node, ParaDirection, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_text::TextLayout;
|
||||
|
||||
/// One glyph's placement: its baseline origin, advance direction, size and horizontal scale.
|
||||
@@ -144,6 +144,9 @@ pub(crate) struct TextLine {
|
||||
gaps: Vec<(usize, f64)>,
|
||||
/// Each glyph's advance in the file's font (points), with [`Self::baseline`].
|
||||
advances: Vec<f64>,
|
||||
/// The strokes of its glyphs' outlines drawn between them, one per glyph with ink so far
|
||||
/// (written right after the line).
|
||||
pub held: Vec<Node>,
|
||||
}
|
||||
|
||||
/// What a finished line of type tells beyond its text object (see [`crate::import_lines`]).
|
||||
@@ -177,9 +180,15 @@ impl TextLine {
|
||||
ink: vec![],
|
||||
gaps: vec![],
|
||||
advances: vec![],
|
||||
held: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// The paint the last glyph is filled with.
|
||||
pub fn last_fill(&self) -> Option<&Paint> {
|
||||
self.runs.last()?.0.fill.as_ref()
|
||||
}
|
||||
|
||||
/// Add glyph `text` set vertically (`upright`) if it continues this column (or makes this
|
||||
/// line's single glyph a column): below the last one, in the same column. `false`: it starts
|
||||
/// another line.
|
||||
@@ -350,6 +359,20 @@ impl TextLine {
|
||||
t.runs = tracked_each(std::mem::take(&mut t.runs), own, self.at.size);
|
||||
}
|
||||
if let Some(path) = path {
|
||||
// Evenly spaced type keeps its length along the path: the path runs through the
|
||||
// glyphs' origins, one curve each, to the end of the last glyph, and tracking makes up
|
||||
// the difference from the advances.
|
||||
if own.is_none() {
|
||||
let length: f64 = path.segments().take(self.baseline.len()).map(|seg| kurbo::ParamCurveArclen::arclen(&seg, 1e-3)).sum();
|
||||
let laid = vectorcraft_text::layout(db, &t);
|
||||
let natural: f64 = laid.glyphs.iter().map(|g| g.advance).sum();
|
||||
let chars: usize = t.runs.iter().map(|r| r.text.chars().count()).sum();
|
||||
let size = self.at.size;
|
||||
if chars > 1 && length.is_finite() && (length - natural).abs() > size * 0.01 {
|
||||
let (tracking, spaces) = spacing(&laid, &[], length - natural, chars, size);
|
||||
t.runs = tracked(std::mem::take(&mut t.runs), tracking, &spaces);
|
||||
}
|
||||
}
|
||||
t.kind = TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&path), start: 0.0, end: None };
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.cached_bounds = Some(vectorcraft_text::layout(db, &t).bounds);
|
||||
|
||||
@@ -153,6 +153,25 @@ fn a_stroke_on_the_type_of_a_moved_ai_page_is_still_the_types_stroke() {
|
||||
assert!(matches!(art[1].appearance.items.as_slice(), [AppearanceItem::Stroke(_)]), "{:?}", art[1].appearance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyphs_each_followed_by_their_stroked_outline_stay_one_object() {
|
||||
// Type on a path is written glyph by glyph: each glyph, then its outline stroked in the same
|
||||
// colour (a shadow under the type).
|
||||
let glyph = |c: char, x: f64, stroke: &str| format!("BT 0 0 1 rg /F1 24 Tf {x} 50 Td ({c}) Tj ET {stroke} 0.5 w {}", outline(c, x));
|
||||
let i_at = 10.0 + 0.722 * 24.0;
|
||||
let d = import(&page(&(glyph('H', 10.0, "0 0 1 RG") + &glyph('i', i_at, "0 0 1 RG")))).unwrap();
|
||||
let art = d.layers[0].children().unwrap();
|
||||
assert_eq!(art.len(), 1, "one object: {art:?}");
|
||||
let NodeKind::Text(t) = &art[0].kind else { panic!("{:?}", art[0].kind) };
|
||||
assert_eq!(t.plain_text(), "Hi");
|
||||
let [AppearanceItem::Stroke(st)] = art[0].appearance.items.as_slice() else { panic!("{:?}", art[0].appearance) };
|
||||
assert_eq!(st.paint.color(), Some(Color::rgb(0.0, 0.0, 1.0)));
|
||||
// A stroke of another colour drawn over each glyph in turn would also cover the next one's
|
||||
// neighbour as one object's stroke: the glyphs stay as the file draws them.
|
||||
let d = import(&page(&(glyph('H', 10.0, "1 1 1 RG") + &glyph('i', i_at, "1 1 1 RG")))).unwrap();
|
||||
assert_eq!(texts(&d).len(), 2);
|
||||
}
|
||||
|
||||
/// A 300 × 200 pt page with `lines` of (x, y from the top, size, text) in Source Sans 3.
|
||||
fn source_sans(lines: &[(f32, f32, f32, &str)]) -> Vec<u8> {
|
||||
crate::tests_live_text::text_pdf(lines, false)
|
||||
|
||||
@@ -424,3 +424,22 @@ fn glyphs_spaced_each_their_own_way_keep_their_place() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Evenly tracked type on a path comes back with its tracking: each glyph where it was along the
|
||||
/// curve, not closed up to the font's advances.
|
||||
#[test]
|
||||
fn evenly_tracked_type_on_a_path_keeps_its_length() {
|
||||
let mut arc = BezPath::new();
|
||||
arc.move_to((40.0, 200.0));
|
||||
arc.curve_to((100.0, 80.0), (260.0, 80.0), (320.0, 200.0));
|
||||
let mut t = TextObject::point(Point::ORIGIN, "ARCHING TYPE", CharStyle { tracking: 80.0, ..style(24.0) });
|
||||
t.kind = TextKind::OnPath { path: PathData::from_bezpath(&arc), start: 0.0, end: None };
|
||||
let d = doc(vec![t]);
|
||||
let want = origins(&d);
|
||||
let back = import_as(&pdf(&d, false).bytes, TextAs::default());
|
||||
let got = origins(&back);
|
||||
assert_eq!(got.len(), want.len(), "{:?}", texts(&back));
|
||||
for (g, w) in got.iter().zip(&want) {
|
||||
assert!(g.distance(*w) < 0.3, "{got:?}\nvs {want:?}");
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
let params = p.get("params").cloned().unwrap_or(json!({}));
|
||||
let params = if params.is_null() { json!({}) } else { params };
|
||||
// A key the command's description doesn't name is an error, not a default (#1442).
|
||||
if let Err(e) = crate::menus::check_params(id, ¶ms) {
|
||||
if let Err(e) = crate::menus::check_params(&app.session, id, ¶ms) {
|
||||
return err(e);
|
||||
}
|
||||
wrap(app.run(id, params))
|
||||
|
||||
@@ -623,7 +623,7 @@ impl VectorcraftApp {
|
||||
// In debug builds the app's own calls are held to the params descriptions too (#1442): a
|
||||
// panel passing a key its command doesn't read shows up in the tests.
|
||||
if cfg!(debug_assertions) {
|
||||
menus::check_params(id, ¶ms)?;
|
||||
menus::check_params(&self.session, id, ¶ms)?;
|
||||
}
|
||||
if let Some(r) = menus::run_ui_command(self, id, ¶ms) {
|
||||
return r;
|
||||
|
||||
@@ -905,11 +905,11 @@ pub(crate) fn opt_bool(p: &Value, key: &str) -> Result<Option<bool>, String> {
|
||||
/// `params` has only keys the params description of engine command `id` names, or the parameter
|
||||
/// error naming the one it doesn't (#1442). The UI's own commands aren't checked yet: their
|
||||
/// descriptions haven't been audited against what they read.
|
||||
pub fn check_params(id: &str, params: &Value) -> Result<(), String> {
|
||||
pub fn check_params(session: &vectorcraft_engine::Session, id: &str, params: &Value) -> Result<(), String> {
|
||||
if UI_COMMANDS.iter().any(|c| c.0 == id) {
|
||||
return Ok(());
|
||||
}
|
||||
vectorcraft_engine::check_params(id, params).map_err(|e| e.to_string())
|
||||
session.check_params(id, params).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
|
||||
|
||||
+14
-2
@@ -675,8 +675,20 @@ is refused before the command runs:
|
||||
→ invalid parameters for `file.new`: unknown parameter `widht`; did you mean `width`?
|
||||
```
|
||||
|
||||
Only the top level is checked: the keys of an object inside a value (`artboardLayout: {…}`, an effect's `params`) are
|
||||
the command's to read. A description with a `…` entry takes more keys than it lists: those of the command it names
|
||||
Only the top level is checked: the keys of an object inside a value (`artboardLayout: {…}`) are the command's to
|
||||
read. The one exception is an effect's `params`: `effect.apply` (and `appearance.addEffect`) and `effect.setParams`
|
||||
check them against the effect's own entry in `effect.list`, its `defaults` and the keys its `params` text names. A key
|
||||
the effect doesn't take, or a value of another type than its default (a string for a number, a number for a switch),
|
||||
is refused the same way:
|
||||
|
||||
```json
|
||||
{"id": "effect.apply", "params": {"effect": "blur.gaussian", "params": {"blurr": 3}}}
|
||||
→ invalid parameters for `effect.apply`: unknown parameter `blurr`; the command takes: radius
|
||||
```
|
||||
|
||||
A number outside the range the effect documents is still stored as given and clamped when the effect draws. A
|
||||
plug-in effect's parameters are resolved against its manifest instead, and `effect.setParams` as a step of
|
||||
`command.batch` isn't checked (the effect it edits may only exist once the steps before it ran). A description with a `…` entry takes more keys than it lists: those of the command it names
|
||||
(`…settings as file.new`), or any key when it names none, as `document.export` and `document.save` do for the options
|
||||
of the format they write (one object can carry several formats' options). A command that takes its own reply back
|
||||
(`document.setup`, `text.areaOptions`) says so the same way. The app's own journal isn't checked: an action or
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (M12 row: Image Trace's presets and options as they are now; Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts, #1539; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481; print-size images, #1517; French catalog, #1511; estimates unchanged; Split Into Grid options, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; estimates unchanged; M9: Extrude & Bevel, Inflate and Rotate; `.ai` compound path paint, #1410, overprint on import, #1388, text-only PDF parts, #1387; Transform dialogs preview, #1412, and transforms of direct-selected anchors, #1408; strokes wider than their shape, #1396; estimates unchanged; `.ai` editing-data follow-ups, #1390, #1391, #1392; dialog keyboard focus, #1367 and #1395; symbol instance redraw cache, #1386; font rescan, #1376; estimates unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click, #1239; custom ruler origin, #1212; M11: Artboard object clicks create a fitted artboard (#1227); estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; M6: Simplify preview/counts and valid topology selection (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; M14: RPM runtime metadata (#1275); estimates unchanged; M5: squared-distance texture redraws (#1257); clean-machine/platform drag measurements still pending; estimates unchanged; M2: Curvature point display improved (#1236, in part); estimates unchanged; M7: full-name font matching (#1258); Windows font-service QA pending; estimates unchanged; M1: no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); estimates unchanged; M14: dedicated Eyedropper cursor (#1269); estimates unchanged; M3: Eyedropper reads visible locked and outside-isolation sources (#1251); estimates unchanged; M2: Direct Selection segment picks and Delete (#1278); estimates unchanged; M3: adding paint to an unpainted zero-width stroke restores 1 pt (#1253); estimates unchanged; M12: graph designs, graph data, Graph Type and label options; M14: native macOS save Format menu, #866; M6: compound appearance and member fill-rule fixes, #1197; estimates unchanged; M9: PDF raster-effect cap warnings; G11: #893 fixed, #937 open; M7: accurate character controls after scaling, #1034; estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Forward-looking plan. Where VectorCraft stands is in [`ROADMAP.md`](../ROADMAP.md) and
|
||||
[target-app-parity.md](target-app-parity.md); the ranked work list is [gaps.md](gaps.md). Hours are Opus 5.5
|
||||
@@ -60,7 +60,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); initial Revolve, 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 `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 23–37 |
|
||||
| M11 | Artboards & views (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 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 (13 presets, 18 ms/1k² image; 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 (15 presets; Ignore Color, Shapes, Auto Grouping and Gradients options; measured in `image-trace-quality.md`; the Flat Logo mode places edges to a fraction of a pixel, restores sharp corners, snaps lines and circles, and refuses gradients), Recolor Artwork, colour management (ICC, soft proofing, separations preview), Graphs (all 9 tools, Graph Data/Type, group-selected series colours persist through regenerate/save/open) done; Graph Design and Marker designs, Transpose and Switch x/y, `|` label line breaks, the pie options, drop shadows, the legend across the top, overlapping widths, First Row/Column in Front, filled lines, kept label formatting and the Graph Data worksheet done; graph Column designs pending | 6–10 |
|
||||
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 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), 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) | ⬜ | 60–90 |
|
||||
@@ -80,6 +80,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | M12 row: Image Trace has 15 presets and the Ignore Color, Shapes, Auto Grouping and Gradients options (#1574, #1609, #1618, #1627); its speed figure, measured before these, is dropped for the measurements in `image-trace-quality.md` |
|
||||
| 2026-10-13 | minor | The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498); Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479); a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542); Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539); Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536); the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); estimates unchanged |
|
||||
| 2026-10-13 | minor | Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1508); commands and MCP `add_text` report area type that does not fit its frame (#1471), a tab stop in the Tabs panel drags from where the press landed (#1458); M9: 3D and Materials on outlined type and other shapes of many small curves (#1481), Twist 0° to 360° (#1524), twisted walls keep their faces (#1525); print-size images draw, export and crop (#1517), `vectorcraft-cli perf` reports load and memory on Windows (#1502); sliders in dialogs show their rail (#1483), a template layer stays in Preview in Outline view (#1486), French catalog complete again (#1511), a Hungarian message in the informal address (#1474); `graph.setData`'s CSV keeps each value exactly (#1485); estimates unchanged |
|
||||
| 2026-10-13 | minor | M6: Split Into Grid has its documented options (#1462), the Pathfinder panel works on a group selected alone (#1450); M2: Transform Objects in the Transform dialogs and Transform Each, Reflect across an angle (#1448, #1460); M4: grayscale TIFFs with an alpha channel or extra channels place (#1466 in part), strokes kept apart from a faded or masked fill on PDF and `.ai` import (#1434), SVG knockout groups without masks (#1435); M7: Fit Headline in a frame one line tall (#1444), frames beyond a billion points hold no text (#1451); M13: commands refuse a parameter key they don't take (#1442); M5: edits and Alt-drags of thousands of objects (#1429 in part); M14: a focused field follows its value (#1457), selected text in value fields is readable (#1465 in part), a window surface past 8192 px no longer closes the app (#1459 in part); estimates unchanged |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: document-point character controls after object/group scaling; full readiness recomputed as the standard's additive weighted sum, ~62%; per-audience hours; essentials-user scoreLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel cap lowers raster-effect resolution; Drawing tools: Live Corners round corners between curved sides, #1144; Type advanced: proportional vertical metrics (`vpal`) done, #966; Shape Builder: #893 fixed, merged regions of crossing lines no longer keep seams; scores unchanged while #937 is open; #1034: docLine truncated
|
||||
|
||||
The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is
|
||||
the work list made from it. Deep areas have their own checklists: [UI and interaction](ui-parity.md),
|
||||
@@ -468,7 +468,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 `<mask>` 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, 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. Still weak, by the same numbers: abutting paths leave 22 to 50 px of gaps (each region's edge is still fitted on its own), thin lines in blurred and scanned images take several times the art's anchors, hairlines under half a pixel are lost to the threshold, a scan's uneven paper splits into tones when more colours are asked for than the art has, and a 16 megapixel photo takes about 4 GB while it is traced.
|
||||
- **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). 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, Flat Logo shatters scans, stroke junctions stay at their skeleton pixels, blurred, noisy and scanned gradients are found in part or in pieces, and radial gradients stay bands.
|
||||
- **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.
|
||||
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own, and regions they bound merge without seams where the lines cross at rounded coordinates (#893); filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points is refused with advice rather than running out of memory, an image over 64 megapixels is traced scaled down to that size (aspect kept, paths at the image's own size) and one over 192 megapixels is refused, and a selection too large to outline shows its bounds (#525); Object › Image Trace › Make and Make and Expand trace at once with the Default preset, #543; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image (a crop box on the image, starting on its part over the artboard, dragged by its handles or inside it, the outside dimmed, with X/Y/W/H, Apply and Cancel in the Control bar, Enter and Escape, #734; `object.cropImage {rect}` for agents, `{trim: true}` cuts an image to its pixels that aren't fully transparent, pixel for pixel) and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), View, the Image Trace panel and Expand, plus Release in Properties; the five View modes (Tracing Result, with Outlines, Outlines, Outlines with Source Image, Source Image) redraw a trace on screen without tracing again, are saved with it and `imageTrace.setView` sets them, while exports and Expand keep the result; the panel's Advanced section is a header that stays open or closed as it was left (part of #641); Color mode's Palette: Limited, Full Tone (as many colours as the tones need, Less … More), Automatic (flat art with its own colours, else Full Tone; a heuristic) and Document Library (the most used colours of the document's swatches or a swatch library, filled with the swatch colours as they are), part of #641; Advanced › Create: Fills and/or Strokes, where lines up to the Stroke width trace as stroked centre lines (distance transform, thinning, a skeleton graph with spurs pruned, curve fitting; one width per line; without Fills, wider areas become stroked outlines), part of #641), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror).
|
||||
- **Paint and appearance (M3):**
|
||||
@@ -528,6 +528,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Image Trace, measured in `image-trace-quality.md`: shared edges fitted once (#1535), 16 megapixel photos in 0.9 GB (#1546), thin lines (#1563), Ignore Color and two presets (#1574), upright snapping (#1593), stroked centre lines (#1595), corners and Shapes (#1609), Auto Grouping (#1618), Gradients (#1627). Score and hours unchanged (estimated) |
|
||||
| 2026-10-13 | minor | Files: Native documents with a lot of path data (traced or generated art, in layers, symbols or patterns) are saved as compact JSON, about a fifth of the size; small documents stay pretty-printed (#1492); Paste takes a PDF or EPS another app copied as editable vector art: from the macOS pasteboard (with SVG), EPS on Windows beside PDF, and `.eps` files copied in a file manager, with the new command `clipboard.importEps` (#1538, fixes #1499); Opening or placing a file with an image too large to decode says so, and the Links panel shows it on the row (#1540, part of #1479); a multi-artboard `.ai` with notes beside its artboards opens from its editing data, a page's art past its artboard no longer lying over the page before it (#1528); a `.ai` gradient whose stops are listed upwards keeps each midpoint on the stop that starts its part of the ramp (#1530); SVG text on a path follows basic shapes (circle, ellipse, rect, line, polyline, polygon) as their SVG 2 equivalent paths (#1531, fixes #1527); PDFs whose alpha soft masks are drawn as images import with the mask instead of empty art (#1551); EPS and PostScript type set in Type 42 (TrueType) fonts imports at its size instead of 1000 times too large (#1553); PDF and EPS import keep a clipping path whose bounding box covers the page but whose shape doesn't (#1544, fixes #1542). Rendering: Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). Fonts: the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux). Localization: The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498) (4,510 rows); scores and hours unchanged (estimated) |
|
||||
| 2026-10-13 | minor | Tools and UI: Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537); a round corner next to a curved side curves smoothly instead of sticking out of the shape and the Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512); Join merges the selected anchor points that lie on one another, as after Average, into one anchor (#1582, fixes #1533); Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534); Edit › Keyboard Shortcuts lists Edit › Duplicate and the other commands that are in no menu but run without parameters (panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1549, #1580, fixes #1539). Correction: the combined Average and Join with its corner or smooth choice was listed as done in [ui-parity.md](ui-parity.md) and does not exist in the app (`path.join` and `path.average` are separate commands, and Join has no such choice); it is now listed as missing there and in [gap 1](gaps.md); scores and hours unchanged (estimated) |
|
||||
| 2026-10-13 | minor | UI and tools: sliders in Save for Web, the raster export options, Preferences and the opacity popup show their rail, filled up to the handle (#1506, fixes #1483); in Outline view a template layer stays in Preview, dimmed (#1505, fixes #1486); Direct Selection takes the selected object's anchor where anchors of overlapping objects coincide (#1509, fixes #1508); a tab stop in the Tabs panel drags from where the press landed (#1487, fixes #1458); type: commands and MCP `add_text` report area type that does not fit its frame (#1500, fixes #1471); graphs: `graph.setData`'s CSV keeps each value exactly (#1485); scores and hours unchanged (estimated) |
|
||||
|
||||
Reference in new issue
Block a user