Merge pull request #1632 from sanyamsingh001/codex/r61-screens-tiff
G13: offer TIFF in Export for Screens and Asset Export
This commit is contained in:
commit
dbd2497b82
|
|
@ -9,7 +9,7 @@
|
|||
//! `boards` ([bool] per artboard), `assets` ([asset id] checked on the Assets tab), `includeBleed`,
|
||||
//! `folder`, `openLocation`, `subfolders`, `prefix`, `preset` (""|mobile|density: where the rows
|
||||
//! came from), `formats` (rows of `document.exportForScreens`, `scale` as text), `settings`
|
||||
//! ({png|png8|jpg|webp|gif|svg|pdf: {…}}); `__settings` names the format whose Format Settings
|
||||
//! ({png|png8|jpg|webp|gif|tiff|svg|pdf: {…}}); `__settings` names the format whose Format Settings
|
||||
//! show ("" when closed).
|
||||
|
||||
use serde::Deserialize;
|
||||
|
|
@ -39,7 +39,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
|||
pub const KIND: &str = "exportForScreens";
|
||||
|
||||
/// The format choices of a row: (label, format, JPEG quality).
|
||||
const FORMATS: [(&str, &str, Option<u8>); 9] = [
|
||||
const FORMATS: [(&str, &str, Option<u8>); 10] = [
|
||||
("PNG", "png", None),
|
||||
("PNG 8", "png8", None),
|
||||
("JPG 100", "jpg", Some(100)),
|
||||
|
|
@ -47,15 +47,16 @@ const FORMATS: [(&str, &str, Option<u8>); 9] = [
|
|||
("JPG 50", "jpg", Some(50)),
|
||||
("WebP", "webp", None),
|
||||
("GIF", "gif", None),
|
||||
("TIFF", "tiff", None),
|
||||
("SVG", "svg", None),
|
||||
("PDF", "pdf", None),
|
||||
];
|
||||
const FORMAT_LABELS: [&str; 9] = ["PNG", "PNG 8", "JPG 100", "JPG 80", "JPG 50", "WebP", "GIF", "SVG", "PDF"];
|
||||
const FORMAT_LABELS: [&str; 10] = ["PNG", "PNG 8", "JPG 100", "JPG 80", "JPG 50", "WebP", "GIF", "TIFF", "SVG", "PDF"];
|
||||
|
||||
/// The formats Format Settings has options for: (format, label).
|
||||
const SETTINGS: [(&str, &str); 7] =
|
||||
[("png", "PNG"), ("png8", "PNG 8"), ("jpg", "JPG"), ("webp", "WebP"), ("gif", "GIF"), ("svg", "SVG"), ("pdf", "PDF")];
|
||||
const SETTINGS_LABELS: [&str; 7] = ["PNG", "PNG 8", "JPG", "WebP", "GIF", "SVG", "PDF"];
|
||||
const SETTINGS: [(&str, &str); 8] =
|
||||
[("png", "PNG"), ("png8", "PNG 8"), ("jpg", "JPG"), ("webp", "WebP"), ("gif", "GIF"), ("tiff", "TIFF"), ("svg", "SVG"), ("pdf", "PDF")];
|
||||
const SETTINGS_LABELS: [&str; 8] = ["PNG", "PNG 8", "JPG", "WebP", "GIF", "TIFF", "SVG", "PDF"];
|
||||
|
||||
/// The sizes a row's scale list offers (any can be typed).
|
||||
const SIZES: [&str; 12] = ["0.5x", "0.75x", "1x", "1.5x", "2x", "3x", "4x", "512w", "512h", "72ppi", "150ppi", "300ppi"];
|
||||
|
|
@ -106,11 +107,7 @@ pub fn open(app: &mut VectorcraftApp) {
|
|||
// Every format's settings: its defaults, then the saved ones.
|
||||
let mut settings = Map::new();
|
||||
for (id, _) in SETTINGS {
|
||||
let mut s = defaults(app, id);
|
||||
if let Some(o) = get("settings").and_then(|v| v.get(id)).and_then(Value::as_object) {
|
||||
s.extend(o.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
}
|
||||
settings.insert(id.into(), Value::Object(s));
|
||||
settings.insert(id.into(), Value::Object(saved_options(app, &saved, id)));
|
||||
}
|
||||
let folder = get("folder").and_then(Value::as_str).filter(|f| !f.is_empty()).map(str::to_string).or_else(fileio::export_folder);
|
||||
let fields = json!({
|
||||
|
|
@ -150,6 +147,21 @@ fn defaults(app: &VectorcraftApp, id: &str) -> Map<String, Value> {
|
|||
o
|
||||
}
|
||||
|
||||
/// The format's defaults with the document's saved choices overlaid.
|
||||
pub(crate) fn saved_options(app: &VectorcraftApp, saved: &Map<String, Value>, id: &str) -> Map<String, Value> {
|
||||
let mut options = defaults(app, id);
|
||||
// TIFF's new defaults must not mask an existing shared anti-aliasing choice.
|
||||
if id == "tiff"
|
||||
&& let Some(aa) = saved.get("antiAlias")
|
||||
{
|
||||
options.insert("antiAlias".into(), aa.clone());
|
||||
}
|
||||
if let Some(o) = saved.get("settings").and_then(|v| v.get(id)).and_then(Value::as_object) {
|
||||
options.extend(o.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
/// A row with its size as the text the Scale field shows (`2x`, `100w`, `100h`, `72ppi`).
|
||||
fn editable_row(row: &Value) -> Value {
|
||||
let num = |k: &str| row.get(k).and_then(Value::as_f64);
|
||||
|
|
@ -444,7 +456,13 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) {
|
|||
widgets::dim_label(ui, tl!("Formats:"));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if widgets::icon_button(ui, "settings", tl!("Format Settings"), false, 24.0).clicked() {
|
||||
let first = rows.first().and_then(|r| r["format"].as_str()).filter(|f| SETTINGS.iter().any(|(id, _)| id == f));
|
||||
// Preserve row spellings; format settings use canonical IDs.
|
||||
let first = rows
|
||||
.first()
|
||||
.and_then(|r| r["format"].as_str())
|
||||
.and_then(fileio::format)
|
||||
.map(|f| f.id)
|
||||
.filter(|f| SETTINGS.iter().any(|(id, _)| id == f));
|
||||
d.fields.insert("__settings".into(), json!(first.unwrap_or("png")));
|
||||
}
|
||||
let preset = d.str("preset");
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@ pub use effect::open as open_effect_dialog;
|
|||
pub use export_as::open as open_export_as;
|
||||
pub use export_for_screens::open as open_export_for_screens;
|
||||
pub(crate) use export_for_screens::{
|
||||
KIND as EXPORT_FOR_SCREENS, formats as screen_formats, open_assets as open_export_for_screens_assets, saved_rows as screen_saved_rows,
|
||||
KIND as EXPORT_FOR_SCREENS, formats as screen_formats, open_assets as open_export_for_screens_assets, saved_options as screen_saved_options,
|
||||
saved_rows as screen_saved_rows,
|
||||
};
|
||||
pub(crate) use new_document::preset_name;
|
||||
pub use new_document::{open as open_new_document, preset_card};
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ fn opens_on_defaults_and_draws_every_view() {
|
|||
assert_eq!(field(&app, "folder"), json!(fileio::export_folder().unwrap_or_default()), "the Desktop, else home");
|
||||
assert_eq!((field(&app, "openLocation"), field(&app, "subfolders"), field(&app, "preset")), (json!(true), json!(false), json!("")));
|
||||
let settings = field(&app, "settings");
|
||||
for f in ["png", "png8", "jpg", "webp", "gif", "svg", "pdf"] {
|
||||
for f in ["png", "png8", "jpg", "webp", "gif", "tiff", "svg", "pdf"] {
|
||||
assert!(settings[f].is_object(), "{f} has settings");
|
||||
}
|
||||
assert!(settings["jpg"].get("quality").is_none() && settings["jpg"].get("imageMap").is_none(), "rows choose quality; no maps");
|
||||
|
|
@ -69,7 +69,7 @@ fn opens_on_defaults_and_draws_every_view() {
|
|||
frame(&mut app);
|
||||
set(&mut app, "tab", json!("artboards"));
|
||||
set(&mut app, "select", json!("full"));
|
||||
for f in ["png", "png8", "jpg", "webp", "gif", "svg", "pdf"] {
|
||||
for f in ["png", "png8", "jpg", "webp", "gif", "tiff", "svg", "pdf"] {
|
||||
set(&mut app, "__settings", json!(f));
|
||||
frame(&mut app);
|
||||
assert_eq!(field(&app, "__settings"), json!(f));
|
||||
|
|
@ -190,3 +190,261 @@ fn a_preset_export_reopens_on_its_rows() {
|
|||
let scales: Vec<Value> = field(&app, "formats").as_array().unwrap().iter().map(|r| r["scale"].clone()).collect();
|
||||
assert_eq!(scales, [json!("64w"), json!("3x"), json!("144ppi")]);
|
||||
}
|
||||
|
||||
/// Persistent frames keep dropdowns open between the pointer press and release.
|
||||
struct ScreensFrame {
|
||||
ctx: egui::Context,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
impl ScreensFrame {
|
||||
fn new() -> Self {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
Self { ctx, time: 0.0 }
|
||||
}
|
||||
|
||||
fn frame(&mut self, app: &mut VectorcraftApp, events: Vec<egui::Event>) -> egui::FullOutput {
|
||||
self.time += 0.1;
|
||||
let input = egui::RawInput {
|
||||
events,
|
||||
time: Some(self.time),
|
||||
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1100.0, 900.0))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut out = self.ctx.run_ui(input, |ui| super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
out
|
||||
}
|
||||
|
||||
fn settle(&mut self, app: &mut VectorcraftApp) -> egui::FullOutput {
|
||||
self.frame(app, vec![]);
|
||||
self.frame(app, vec![])
|
||||
}
|
||||
|
||||
fn asset_panel(&mut self, app: &mut VectorcraftApp, events: Vec<egui::Event>) -> egui::FullOutput {
|
||||
self.time += 0.1;
|
||||
let input = egui::RawInput {
|
||||
events,
|
||||
time: Some(self.time),
|
||||
screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(236.0, 700.0))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut out = self.ctx.run_ui(input, |ui| crate::panels::show_icon_panel(app, ui, "assetExport"));
|
||||
out.textures_delta.clear();
|
||||
out
|
||||
}
|
||||
|
||||
fn click(&mut self, app: &mut VectorcraftApp, at: egui::Pos2) -> egui::FullOutput {
|
||||
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
self.frame(app, vec![egui::Event::PointerMoved(at)]);
|
||||
self.frame(app, vec![button(true)]);
|
||||
self.frame(app, vec![button(false)]);
|
||||
self.settle(app)
|
||||
}
|
||||
|
||||
fn click_label(&mut self, app: &mut VectorcraftApp, label: &str) -> egui::FullOutput {
|
||||
let out = self.settle(app);
|
||||
self.click(app, screen_text_at(&out, label))
|
||||
}
|
||||
|
||||
fn choose(&mut self, app: &mut VectorcraftApp, current: &str, next: &str) {
|
||||
let out = self.click_label(app, current);
|
||||
self.click(app, screen_text_at(&out, next));
|
||||
}
|
||||
}
|
||||
|
||||
fn screen_text_at(out: &egui::FullOutput, label: &str) -> egui::Pos2 {
|
||||
fn find(shape: &egui::Shape, label: &str) -> Option<egui::Pos2> {
|
||||
match shape {
|
||||
egui::Shape::Text(t) if t.galley.text().trim() == label => Some(t.visual_bounding_rect().center()),
|
||||
egui::Shape::Vec(v) => v.iter().find_map(|s| find(s, label)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
out.shapes.iter().find_map(|s| find(&s.shape, label)).unwrap_or_else(|| panic!("missing {label}: {}", crate::tests_labels::shapes_text(out)))
|
||||
}
|
||||
|
||||
/// First-directory entries of the big-endian TIFF chosen below: tag, type, count, inline value.
|
||||
fn big_tiff_tags(bytes: &[u8]) -> Vec<(u16, u16, u32, u16)> {
|
||||
assert_eq!(&bytes[..4], b"MM\0*");
|
||||
let short = |at| u16::from_be_bytes(bytes[at..at + 2].try_into().unwrap());
|
||||
let long = |at| u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap());
|
||||
let dir = long(4) as usize;
|
||||
(0..usize::from(short(dir)))
|
||||
.map(|i| {
|
||||
let at = dir + 2 + 12 * i;
|
||||
(short(at), short(at + 2), long(at + 4), short(at + 8))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Locate the gear from the painted Formats label and its production column width.
|
||||
fn screen_format_gear(out: &egui::FullOutput, width: f32) -> egui::Pos2 {
|
||||
fn label(shape: &egui::Shape) -> Option<egui::Rect> {
|
||||
match shape {
|
||||
egui::Shape::Text(t) if t.galley.text().trim() == "Formats:" => Some(t.visual_bounding_rect()),
|
||||
egui::Shape::Vec(shapes) => shapes.iter().find_map(label),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
let label = out.shapes.iter().find_map(|s| label(&s.shape)).expect("painted Formats row");
|
||||
// The existing 24 pt gear is right-aligned in the 350 pt dialog or 236 pt panel.
|
||||
egui::pos2(label.left() + width - 12.0, label.center().y)
|
||||
}
|
||||
|
||||
fn saved_tiff_alias_gear(alias: &str, asset_panel: bool) {
|
||||
let (mut app, log) = app(1, true);
|
||||
app.run("paint.setStroke", json!({"none": true})).unwrap();
|
||||
app.run("object.convertDocumentColorMode", json!({"mode": "cmyk"})).unwrap();
|
||||
if asset_panel {
|
||||
app.run("assets.add", json!({"name": "Badge"})).unwrap();
|
||||
}
|
||||
let rows = json!([{"format": alias, "scale": "2x", "suffix": "@2x", "antiAlias": "none"}]);
|
||||
app.run(
|
||||
"assets.settings.set",
|
||||
json!({
|
||||
"formats": rows.clone(),
|
||||
"settings": {
|
||||
"tiff": {"colorModel": "rgb", "byteOrder": "little", "lzw": false, "embedIcc": false, "antiAlias": "type"},
|
||||
"png": {"interlaced": true}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let saved_before = app.session.active().unwrap().doc.export_settings.clone();
|
||||
if !asset_panel {
|
||||
open(&mut app);
|
||||
}
|
||||
let mut h = ScreensFrame::new();
|
||||
let draw = |h: &mut ScreensFrame, app: &mut VectorcraftApp, events| {
|
||||
if asset_panel { h.asset_panel(app, events) } else { h.frame(app, events) }
|
||||
};
|
||||
draw(&mut h, &mut app, vec![]);
|
||||
let out = draw(&mut h, &mut app, vec![]);
|
||||
let gear = screen_format_gear(&out, if asset_panel { 236.0 } else { 350.0 });
|
||||
draw(&mut h, &mut app, vec![egui::Event::PointerMoved(gear)]);
|
||||
for pressed in [true, false] {
|
||||
draw(
|
||||
&mut h,
|
||||
&mut app,
|
||||
vec![egui::Event::PointerButton { pos: gear, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }],
|
||||
);
|
||||
}
|
||||
assert_eq!(field(&app, "__settings"), "tiff", "gear for {alias}, asset panel={asset_panel}");
|
||||
assert_eq!(field(&app, "tab"), if asset_panel { "assets" } else { "artboards" });
|
||||
let out = h.settle(&mut app);
|
||||
for label in ["Format Settings", "TIFF", "Color Model:", "RGB", "Byte Order:", "Little-endian (II)", "LZW Compression", "Embed ICC Profile"] {
|
||||
screen_text_at(&out, label);
|
||||
}
|
||||
assert_eq!(field(&app, "formats"), rows, "opening the gear preserves the saved spelling and row options");
|
||||
assert_eq!(app.session.active().unwrap().doc.export_settings, saved_before, "opening settings does not save them");
|
||||
let mut expected = field(&app, "settings");
|
||||
assert_eq!(expected["tiff"]["colorModel"], "rgb", "saved RGB overrides the CMYK document default");
|
||||
assert_eq!(expected["tiff"]["lzw"], false);
|
||||
assert_eq!(expected["tiff"]["embedIcc"], false);
|
||||
assert_eq!(expected["tiff"]["antiAlias"], "type");
|
||||
assert_eq!(expected["png"]["interlaced"], true);
|
||||
h.choose(&mut app, "Little-endian (II)", "Big-endian (MM)");
|
||||
expected["tiff"]["byteOrder"] = json!("big");
|
||||
assert_eq!(field(&app, "settings"), expected, "only the chosen TIFF option changes");
|
||||
h.click_label(&mut app, "Done");
|
||||
assert_eq!(field(&app, "formats"), rows);
|
||||
h.click_label(&mut app, "Download");
|
||||
assert!(app.ui.dialog.is_none(), "{}", app.ui.status);
|
||||
{
|
||||
let log = log.borrow();
|
||||
let [(name, bytes)] = log.as_slice() else { panic!("expected one TIFF download for {alias}") };
|
||||
assert_eq!(name, if asset_panel { "Badge@2x.tif" } else { "Artboard-1@2x.tif" });
|
||||
let tags = big_tiff_tags(bytes); // The gear's edited canonical settings reach the aliased row.
|
||||
assert!(tags.contains(&(259, 3, 1, 1)), "saved uncompressed option survives");
|
||||
assert!(tags.contains(&(262, 3, 1, 2)), "saved RGB survives");
|
||||
assert!(!tags.iter().any(|t| t.0 == 34675), "saved profile-off option survives");
|
||||
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Tiff).unwrap();
|
||||
assert_eq!((img.width(), img.height()), if asset_panel { (60, 40) } else { (120, 80) });
|
||||
}
|
||||
let saved = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone();
|
||||
assert_eq!(saved["formats"], rows, "export preserves the alias, scale, suffix and row override");
|
||||
assert_eq!(saved["settings"], expected, "canonical TIFF edits and other format settings persist");
|
||||
open(&mut app);
|
||||
assert_eq!(field(&app, "formats"), rows);
|
||||
assert_eq!(field(&app, "settings"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiff_saved_alias_gear_opens_and_edits_settings_in_screens() {
|
||||
for alias in ["tiff", "tif", "TIFF", ".tif"] {
|
||||
saved_tiff_alias_gear(alias, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiff_saved_alias_gear_opens_and_edits_settings_in_asset_export() {
|
||||
for alias in ["tiff", "tif", "TIFF", ".tif"] {
|
||||
saved_tiff_alias_gear(alias, true);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiff_dropdown_exports_scaled_artboards_with_options_and_reopens() {
|
||||
let (mut app, log) = app(2, true);
|
||||
app.run("paint.setStroke", json!({"none": true})).unwrap();
|
||||
app.run("paint.setFill", json!({"color": "#c03020"})).unwrap();
|
||||
open(&mut app);
|
||||
// Begin with an existing PNG row; TIFF must be chosen from the actual popup.
|
||||
set(&mut app, "formats", json!([{"format": "png", "scale": "2x", "suffix": "@2x"}]));
|
||||
set(&mut app, "prefix", json!("proof-"));
|
||||
let mut h = ScreensFrame::new();
|
||||
h.choose(&mut app, "PNG", "TIFF");
|
||||
assert_eq!(field(&app, "formats"), json!([{"format": "tiff", "scale": "2x", "suffix": "@2x"}]));
|
||||
// Leave only the first of two boards checked.
|
||||
h.click_label(&mut app, "Artboard 2");
|
||||
assert_eq!(field(&app, "boards"), json!([true, false]));
|
||||
assert_eq!(field(&app, "range"), json!("1"));
|
||||
|
||||
// Exercise the settings dropdown independently of the row's format choice.
|
||||
set(&mut app, "__settings", json!("png"));
|
||||
h.choose(&mut app, "PNG", "TIFF");
|
||||
assert_eq!(field(&app, "__settings"), json!("tiff"));
|
||||
let settings = field(&app, "settings");
|
||||
assert_eq!(settings["tiff"]["colorModel"], "rgb");
|
||||
assert_eq!(settings["tiff"]["byteOrder"], "little");
|
||||
assert_eq!(settings["tiff"]["lzw"], true);
|
||||
assert_eq!(settings["tiff"]["embedIcc"], true);
|
||||
h.choose(&mut app, "Little-endian (II)", "Big-endian (MM)");
|
||||
h.click_label(&mut app, "LZW Compression");
|
||||
h.click_label(&mut app, "Embed ICC Profile");
|
||||
let settings = field(&app, "settings");
|
||||
assert_eq!(settings["tiff"]["byteOrder"], "big");
|
||||
assert_eq!(settings["tiff"]["lzw"], false);
|
||||
assert_eq!(settings["tiff"]["embedIcc"], false);
|
||||
h.click_label(&mut app, "Done");
|
||||
h.click_label(&mut app, "Download");
|
||||
assert!(app.ui.dialog.is_none(), "{}", app.ui.status);
|
||||
|
||||
{
|
||||
let log = log.borrow();
|
||||
let [(name, bytes)] = log.as_slice() else { panic!("expected one TIFF download") };
|
||||
assert_eq!(name, "proof-Artboard-1@2x.tif");
|
||||
let tags = big_tiff_tags(bytes);
|
||||
assert!(tags.contains(&(259, 3, 1, 1)), "uncompressed as selected");
|
||||
assert!(tags.contains(&(262, 3, 1, 2)), "RGB");
|
||||
assert!(!tags.iter().any(|t| t.0 == 34675), "ICC profile disabled");
|
||||
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Tiff).unwrap().to_rgba8();
|
||||
assert_eq!(img.dimensions(), (120, 80));
|
||||
assert_eq!(img.get_pixel(0, 0).0[3], 0, "transparent board corner");
|
||||
let inside = img.get_pixel(40, 28).0;
|
||||
assert!(inside[0] > inside[1] && inside[3] == 255, "ellipse at the scaled position: {inside:?}");
|
||||
}
|
||||
|
||||
open(&mut app);
|
||||
assert_eq!(field(&app, "formats"), json!([{"format": "tiff", "scale": "2x", "suffix": "@2x"}]));
|
||||
assert_eq!(field(&app, "boards"), json!([true, false]));
|
||||
assert_eq!(field(&app, "prefix"), "proof-");
|
||||
assert_eq!(field(&app, "settings")["tiff"], settings["tiff"]);
|
||||
// Existing vector and raster choices remain selectable.
|
||||
h.choose(&mut app, "TIFF", "SVG");
|
||||
assert_eq!(field(&app, "formats")[0]["format"], "svg");
|
||||
h.choose(&mut app, "SVG", "PNG");
|
||||
assert_eq!(field(&app, "formats")[0]["format"], "png");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use vectorcraft_doc::{ExportAsset, Node, NodeId};
|
|||
use vectorcraft_engine::DocState;
|
||||
|
||||
use super::{pstate, set_pstate};
|
||||
use crate::dialogs::{EXPORT_FOR_SCREENS, open_export_for_screens_assets, screen_formats, screen_saved_rows};
|
||||
use crate::dialogs::{EXPORT_FOR_SCREENS, open_export_for_screens_assets, screen_formats, screen_saved_options, screen_saved_rows};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, menu_item};
|
||||
|
|
@ -191,6 +191,18 @@ fn export(app: &mut VectorcraftApp, ids: Vec<u64>, files_each: usize) {
|
|||
|
||||
fn export_now(app: &mut VectorcraftApp, ids: Vec<u64>, files_each: usize) {
|
||||
let mut p = json!({ "assets": ids });
|
||||
// Match the shared dialog's TIFF defaults, retaining saved choices and other formats.
|
||||
if let Some(st) = app.session.active()
|
||||
&& screen_saved_rows(&st.doc.export_settings)
|
||||
.0
|
||||
.iter()
|
||||
.any(|row| row["format"].as_str().and_then(vectorcraft_engine::cmd::fileio::format).is_some_and(|f| f.id == "tiff"))
|
||||
{
|
||||
let saved = &st.doc.export_settings;
|
||||
let mut settings = saved.get("settings").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
settings.insert("tiff".into(), Value::Object(screen_saved_options(app, saved, "tiff")));
|
||||
p["settings"] = Value::Object(settings);
|
||||
}
|
||||
if io::is_web(app) {
|
||||
p["zip"] = json!(ids.len() * files_each > 1);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -204,3 +204,217 @@ fn export_writes_into_the_picked_folder() {
|
|||
assert!(dir.join("Badge.png").is_file(), "{:?}", std::fs::read_dir(&dir).map(|d| d.count()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiff_dropdown_exports_only_the_selected_asset_and_shares_settings() {
|
||||
let (mut app, log, [a, b]) = app(true);
|
||||
app.run("paint.setFill", json!({"ids": [a], "color": "#c03020"})).unwrap();
|
||||
app.run("paint.setFill", json!({"ids": [b], "color": "#2040c0"})).unwrap();
|
||||
app.run("assets.add", json!({"ids": [a], "name": "Badge"})).unwrap();
|
||||
app.run("assets.add", json!({"ids": [b], "name": "Other"})).unwrap();
|
||||
app.run(
|
||||
"assets.settings.set",
|
||||
json!({
|
||||
"formats": [{"format": "png", "scale": "2x", "suffix": "@2x"}],
|
||||
"settings": {"tiff": {"byteOrder": "big", "lzw": false, "embedIcc": false}}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "PNG")), false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "TIFF")), false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
assert!(has(&texts, "TIFF"));
|
||||
let saved = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone();
|
||||
assert_eq!(saved["formats"], json!([{"format": "tiff", "scale": "2x", "suffix": "@2x"}]));
|
||||
frame(&ctx, &mut app, click(at(&texts, "Badge")), false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "Export 1 Selected")), false);
|
||||
{
|
||||
let log = log.borrow();
|
||||
let [(name, bytes)] = log.as_slice() else { panic!("expected only the selected asset") };
|
||||
assert_eq!(name, "Badge@2x.tif");
|
||||
assert_eq!(&bytes[..4], b"MM\0*", "the saved TIFF byte order reaches asset export");
|
||||
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Tiff).unwrap().to_rgba8();
|
||||
assert_eq!(img.dimensions(), (60, 40), "the selected 30x20 asset at 2x");
|
||||
let pixel = img.get_pixel(30, 20).0;
|
||||
assert!(pixel[0] > pixel[1] && pixel[0] > pixel[2] && pixel[3] == 255, "the red Badge, not the blue Other asset: {pixel:?}");
|
||||
}
|
||||
app.run("file.exportForScreens", json!({})).unwrap();
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.fields["formats"], saved["formats"]);
|
||||
assert_eq!(d.fields["settings"]["tiff"]["byteOrder"], "big");
|
||||
assert_eq!(d.fields["settings"]["tiff"]["lzw"], false);
|
||||
assert_eq!(d.fields["settings"]["tiff"]["embedIcc"], false);
|
||||
}
|
||||
|
||||
/// TIFF tag 262 identifies RGB (2) or separated CMYK (5), independently of a decoder's conversion.
|
||||
fn tiff_photometric(bytes: &[u8]) -> u16 {
|
||||
let big = match &bytes[..4] {
|
||||
b"II*\0" => false,
|
||||
b"MM\0*" => true,
|
||||
magic => panic!("not TIFF: {magic:?}"),
|
||||
};
|
||||
let short = |at: usize| {
|
||||
let b = bytes[at..at + 2].try_into().unwrap();
|
||||
if big { u16::from_be_bytes(b) } else { u16::from_le_bytes(b) }
|
||||
};
|
||||
let long = |at: usize| {
|
||||
let b = bytes[at..at + 4].try_into().unwrap();
|
||||
if big { u32::from_be_bytes(b) } else { u32::from_le_bytes(b) }
|
||||
};
|
||||
let dir = long(4) as usize;
|
||||
let entry = (0..usize::from(short(dir))).map(|i| dir + 2 + 12 * i).find(|&at| short(at) == 262).expect("PhotometricInterpretation");
|
||||
assert_eq!((short(entry + 2), long(entry + 4)), (3, 1), "one SHORT");
|
||||
short(entry + 8)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiff_cmyk_defaults_match_screens_and_preserve_explicit_rgb() {
|
||||
for rgb_override in [false, true] {
|
||||
let mut models = vec![];
|
||||
for screens in [false, true] {
|
||||
let (mut app, log, [a, _]) = app(true);
|
||||
app.run("object.convertDocumentColorMode", json!({"mode": "cmyk"})).unwrap();
|
||||
app.run("assets.add", json!({"ids": [a], "name": "Badge"})).unwrap();
|
||||
assert!(app.session.active().unwrap().doc.export_settings.get("settings").is_none(), "fresh format settings");
|
||||
if rgb_override {
|
||||
app.run("assets.settings.set", json!({"settings": {"tiff": {"colorModel": "rgb"}}})).unwrap();
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "PNG")), false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "TIFF")), false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
assert!(has(&texts, "TIFF"));
|
||||
if !rgb_override {
|
||||
assert!(app.session.active().unwrap().doc.export_settings.get("settings").and_then(|s| s.get("tiff")).is_none());
|
||||
}
|
||||
if screens {
|
||||
// The 24 pt gear is the rightmost control in the Formats row of this 236 pt panel.
|
||||
frame(&ctx, &mut app, click(pos2(236.0 - 12.0, at(&texts, "Formats:").y)), false);
|
||||
let d = app.ui.dialog.as_ref().expect("the panel gear opens Format Settings");
|
||||
assert_eq!((d.kind.as_str(), d.str("tab"), d.str("__settings")), ("exportForScreens", "assets".into(), "tiff".into()));
|
||||
assert_eq!(d.fields["settings"]["tiff"]["colorModel"], if rgb_override { "rgb" } else { "cmyk" });
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
} else {
|
||||
frame(&ctx, &mut app, click(at(&texts, "Export")), false);
|
||||
}
|
||||
let log = log.borrow();
|
||||
let [(name, bytes)] = log.as_slice() else { panic!("expected one TIFF: screens={screens}, rgb_override={rgb_override}") };
|
||||
assert_eq!(name, "Badge.tif");
|
||||
models.push(tiff_photometric(bytes));
|
||||
}
|
||||
assert_eq!(models, vec![if rgb_override { 2 } else { 5 }; 2], "direct panel and screens; explicit RGB={rgb_override}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiff_panel_preserves_shared_antialias_and_explicit_overrides() {
|
||||
for (format_aa, row_aa, effective_aa) in [(None, None, "none"), (Some("art"), None, "art"), (Some("art"), Some("none"), "none")] {
|
||||
let (mut app, log, _) = app(true);
|
||||
// The fractional ellipse reuses the raster-export hard/smooth edge oracle.
|
||||
let id = app.run("shape.ellipse", json!({"x": 5.3, "y": 4.7, "width": 25.8, "height": 21.5})).unwrap()["id"].clone();
|
||||
app.run("assets.add", json!({"ids": [id], "name": "Edge"})).unwrap();
|
||||
let mut row = json!({"format": "png", "scale": "1x", "suffix": ""});
|
||||
if let Some(aa) = row_aa {
|
||||
row["antiAlias"] = json!(aa);
|
||||
}
|
||||
let other_settings = json!({"png": {"antiAlias": "type", "interlaced": true}, "jpg": {"quality": 80}});
|
||||
let mut settings = other_settings.clone();
|
||||
if let Some(aa) = format_aa {
|
||||
settings["tiff"] = json!({"antiAlias": aa});
|
||||
}
|
||||
app.run("assets.settings.set", json!({"formats": [row], "antiAlias": "none", "settings": settings})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "PNG")), false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "TIFF")), false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
assert!(has(&texts, "TIFF"));
|
||||
frame(&ctx, &mut app, click(at(&texts, "Export")), false);
|
||||
{
|
||||
let log = log.borrow();
|
||||
let [(name, bytes)] = log.as_slice() else { panic!("expected one TIFF: format={format_aa:?}, row={row_aa:?}") };
|
||||
assert_eq!(name, "Edge.tif");
|
||||
let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Tiff).unwrap().to_rgba8();
|
||||
assert!(img.pixels().any(|p| p[3] == 255), "the ellipse is present");
|
||||
assert!(img.pixels().any(|p| p[3] == 0), "transparent outside the ellipse");
|
||||
let hard = img.pixels().all(|p| p[3] == 0 || p[3] == 255);
|
||||
assert_eq!(hard, effective_aa == "none", "effective anti-aliasing: format={format_aa:?}, row={row_aa:?}");
|
||||
}
|
||||
let saved = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone();
|
||||
assert_eq!(saved["antiAlias"], "none", "the shared choice stays saved");
|
||||
assert_eq!(saved["settings"], settings, "direct export preserves saved format options, including an absent TIFF map");
|
||||
assert_eq!(saved["formats"][0]["format"], "tiff");
|
||||
assert_eq!(saved["formats"][0].get("antiAlias").and_then(Value::as_str), row_aa, "row overrides stay on the row");
|
||||
for format in ["png", "jpg"] {
|
||||
assert_eq!(saved["settings"][format], other_settings[format], "other format choices survive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn tiff_saved_aliases_use_document_defaults_and_explicit_rgb() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-asset-tiff-aliases-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
// The first case is the historical control: saved tif without an explicit model must be CMYK.
|
||||
// Native picked folders let the mixed-row case inspect each file without introducing a ZIP decoder.
|
||||
for (case, (alias, rgb_override, mixed)) in
|
||||
[("tif", false, false), ("TIFF", false, false), (".tif", false, true), ("tif", true, false)].into_iter().enumerate()
|
||||
{
|
||||
let folder = dir.join(case.to_string());
|
||||
let picked = folder.to_string_lossy().replace('\\', "/");
|
||||
let (mut app, _, [a, _]) = app(false);
|
||||
app.services.pick_folder = Some(Box::new(move || Some(picked.clone())));
|
||||
app.run("object.convertDocumentColorMode", json!({"mode": "cmyk"})).unwrap();
|
||||
app.run("assets.add", json!({"ids": [a], "name": "Badge"})).unwrap();
|
||||
let mut rows = vec![json!({"format": alias, "scale": "2x", "suffix": "@2x"})];
|
||||
if mixed {
|
||||
rows.push(json!({"format": "png", "scale": "1x", "suffix": ""}));
|
||||
}
|
||||
let mut settings = json!({"png": {"interlaced": true}});
|
||||
if rgb_override {
|
||||
settings["tiff"] = json!({"colorModel": "rgb"});
|
||||
}
|
||||
let saved = json!({"formats": rows, "settings": settings});
|
||||
app.run("assets.settings.set", saved.clone()).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame(&ctx, &mut app, vec![], false);
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
// Keep the valid saved spelling; choosing the TIFF dropdown would hide the alias bug.
|
||||
frame(&ctx, &mut app, click(at(&texts, "Export")), false);
|
||||
let bytes = std::fs::read(folder.join("Badge@2x.tif")).unwrap();
|
||||
assert_eq!(tiff_photometric(&bytes), if rgb_override { 2 } else { 5 }, "saved alias {alias}, explicit RGB={rgb_override}");
|
||||
if rgb_override {
|
||||
let img = image::load_from_memory_with_format(&bytes, image::ImageFormat::Tiff).unwrap();
|
||||
assert_eq!((img.width(), img.height()), (60, 40), "the saved TIFF row is still 2x");
|
||||
}
|
||||
if mixed {
|
||||
let png = std::fs::read(folder.join("Badge.png")).unwrap();
|
||||
let img = image::load_from_memory_with_format(&png, image::ImageFormat::Png).unwrap();
|
||||
assert_eq!((img.width(), img.height()), (30, 20), "the PNG row keeps its own 1x scale");
|
||||
assert_eq!(png.get(28), Some(&1), "the saved PNG interlace option survives the TIFF defaults");
|
||||
}
|
||||
assert_eq!(std::fs::read_dir(&folder).unwrap().count(), if mixed { 2 } else { 1 });
|
||||
assert_eq!(
|
||||
app.run("document.exportSettings", json!({})).unwrap()["settings"],
|
||||
saved,
|
||||
"direct export preserves saved aliases and explicit options"
|
||||
);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (compact native saves for documents with a lot of path data, #1492; PDF and EPS paste as vector art, #1499; `.ai` gradient stops listed upwards, #1530; SVG text on basic shapes, #1527; PDF soft masks drawn as images, #1551; Type 42 fonts in EPS, #1553; clips that only seem to cover the page, #1542; images too large to decode are reported, #1540; a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; compact native saves for documents with a lot of path data, #1492; PDF and EPS paste as vector art, #1499; `.ai` gradient stops listed upwards, #1530; SVG text on basic shapes, #1527; PDF soft masks drawn as images, #1551; Type 42 fonts in EPS, #1553; clips that only seem to cover the page, #1542; images too large to decode are reported, #1540; a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/mod.rs`). Illustrator's list comes from its public
|
||||
|
|
@ -68,7 +68,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
|||
| EMF / WMF | open, export | yes | yes | |
|
||||
| PSD | open, place, export | yes (merged image, every mode and depth) | yes (layered, max editability) | placing as layers (Photoshop Import Options) missing |
|
||||
| PSB | open | yes (merged image) | no | |
|
||||
| PNG / JPEG / GIF / BMP / TIFF / Targa | open, export | yes (Targa: export only) | yes | Targa import missing; TIFF: grayscale with an alpha channel or other extra channels reads (#1466 in part), CCITT Group 3 and planar files with extra channels don't |
|
||||
| PNG / JPEG / GIF / BMP / TIFF / Targa | open, export | yes (Targa: export only) | yes; TIFF is also selectable in Export for Screens and Asset Export with its existing options | Targa import missing; TIFF: grayscale with an alpha channel or other extra channels reads (#1466 in part), CCITT Group 3 and planar files with extra channels don't |
|
||||
| WebP | export (Export for Screens) | yes | yes (lossless only; lossy written lossless with a warning) | |
|
||||
| HEIC / HEIF | open, place | no | no | |
|
||||
| JPEG 2000 | open, place | no | no | |
|
||||
|
|
@ -89,6 +89,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
|||
| 2026-10-13 | minor | 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) (the `.ai` row for #1528 was written by that PR); scores unchanged (estimated) |
|
||||
| 2026-10-13 | minor | `.ai` read as its PDF part: a page whose art reaches past its artboard to the left (notes beside a design) moves right in the row until that art clears the page before it, so it is no longer taken for that page's art; a multi-artboard `.ai` with such notes opened as its PDF part ("the art on them differs from the page's"), without the art outside its artboards, and now opens from its editing data |
|
||||
| 2026-10-13 | minor | TIFF: grayscale files with an alpha channel or other extra channels place, relink and open, read as CMYK files with alpha already were (8 and 16 bits, either polarity; #1489, part of #1466); the rows for #1434 (strokes kept apart from a faded or masked fill, a `.ai`'s RGB numbers) and #1435 (SVG knockout groups without masks, `--decimals`) were written by those PRs; scores unchanged (estimated) |
|
||||
| 2026-10-12 | minor | TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Affinity import: a text frame too short for its first line (a 64 pt title in a 54 pt frame) is made taller from its top, with a warning in the open result, instead of showing nothing (#1423), as `.ai` area type is (#1392); the spacing Affinity itself fits the line with isn't read; scores unchanged |
|
||||
| 2026-10-12 | minor | `.ai` editing data: a compound path takes the paint, opacity, blend mode and fill rule of its last path (the last painted one when that has none), so gradient-filled compound paths no longer come in black, and paths grouped inside a compound path stay its paths (#1426, fixes #1410); hand-written editing data test; scores unchanged |
|
||||
| 2026-10-12 | minor | `editingData: "always"` (`convert --editing-data`) opens a `.ai` from its editing data even where it draws differently from its page, which can be older, with a note |
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; 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)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; 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
|
||||
|
|
@ -52,11 +52,10 @@ Checked 2026-10-10 from the code, tests and open issues; no workflow fails, so V
|
|||
| M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (bounded control socket waits and connection-slot recovery, #1726; no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending) | 4–6 |
|
||||
| M2 | Drawing tools + smart guides | ✅ mostly done (Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending) | 12–18 |
|
||||
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork; gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries. Left: freeform and mixed spot/process gradients export as stops or process colours to SVG/PDF; confirm the Scale Strokes & Effects default | 4–8 |
|
||||
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (closed SPLINE flags round-trip, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish | 3–6 (existing scope; Affinity export unestimated) |
|
||||
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (closed SPLINE flags round-trip, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish | 3–6 (existing scope; Affinity export unestimated) |
|
||||
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
|
||||
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; auto/slider/advanced UI remains open); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open) | 3–6 |
|
||||
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), 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 |
|
||||
| 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, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), 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 |
|
||||
| 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 (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; tab leaders, spell check, vertical type pending | 45–60 |
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | ✅ done (M8.1–M8.20 on 2026-10-06, fidelity pass): Liquify Tool Options with one shared brush, pen pressure, hold-to-apply, a safe scope and incremental strokes; tool options that last; Puppet Warp rotation, multi-select, Control bar and a rest-shape session that follows Undo; Width tool on compound paths; Envelope dialogs with preview, Reset with Warp/Mesh, full Envelope Options, type/images/symbols/appearance/gradients/patterns distorted on canvas and in every export, warps that keep their frame, Edit Contents, mesh handles and Control bar; Blend Options, anchor-targeted Blend tool, editable spine, keys picked on the canvas, faithful interpolation and knockout; Perspective Grid with Define Grid, presets, lock/snap/rulers, widgets, the 1–4 plane switch, movable planes, Perspective Selection scale/copy/perpendicular/Transform Again, and type and symbols truly projected with Edit Text. Left: Anti-Alias and Preserve Shape output, warp-envelope point editing, brush interpolation in blends, Free Transform/Scale/Reflect along a rotated box | 2–4 |
|
||||
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done incl. Effect → Pathfinder, Color Adjustments and Document Raster Effects Settings; PDF export warns when the pixel cap lowers raster-effect resolution; Artistic › Film Grain done (29/57 Photoshop filters); initial Revolve, 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 |
|
||||
|
|
@ -93,6 +92,7 @@ After placed documents (File › Place of a `.vectorcraft` file, linked), M4.14
|
|||
| 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-12 | minor | Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours 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 |
|
||||
| 2026-10-12 | minor | TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged |
|
||||
| 2026-10-12 | minor | Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged |
|
||||
| 2026-10-12 | minor | M9: Extrude & Bevel, Inflate and Rotate (initial 3D types, [three-d.md](three-d.md)) |
|
||||
| 2026-10-12 | minor | M4: a `.ai` compound path takes its last path's paint (#1410), PDF and `.ai` import keep overprint (#1388), a `.ai` whose PDF part draws only text opens from it (#1387); M2: the Transform dialogs preview and step with the arrow keys (#1412), the transform tools and commands act on direct-selected anchors (#1408), the bounding box hides during a corner drag (#1229); M5: a stroke wider than its closed shape paints solid on the canvas (#1396); estimates unchanged |
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,8 +22,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
|||
| Area Type Options (rows, columns, inset, first baseline, Auto Size, Shrink Text to Fit) | done | the commands that make or restyle type say when it does not fit its frame; MCP `add_text` takes Auto Size and Shrink Text to Fit (#1471) |
|
||||
| Threaded text, Fit Headline, Text Wrap | done | Fit Headline sees wrapping in a frame one line tall (#1444) |
|
||||
| Type on a Path effects and options; brackets | done | #429 |
|
||||
| Character and Paragraph panels, styles with overrides | done | Character-style Auto-leading resets, partial definitions, local leading overrides, override markers, Undo/Redo and native serialization have regression coverage; mixed-value display for differing paragraphs remains missing |
|
||||
| Character and Paragraph panels, styles with overrides | done | differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields missing |
|
||||
| Character and Paragraph panels, styles with overrides | done | Character-style Auto-leading resets, partial definitions, local leading overrides, override markers, Undo/Redo and native serialization have regression coverage; differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields remains missing |
|
||||
| Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. |
|
||||
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
|
||||
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference unused (#394) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue