Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a52ad5cac8 | ||
|
|
acd5dec214 | ||
|
|
aef5790e01 | ||
|
|
f59be80671 | ||
|
|
b411a3d158 | ||
|
|
a27c912f8a | ||
|
|
5d74c5bb77 | ||
|
|
1056a42a56 | ||
|
|
3e94f6ffd6 |
No files matched your search
+1
-1
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
_Last updated: 2026-10-09 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
|
||||
## Where we are
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ pub use pdfimport::{LoadOptions, page_document};
|
||||
pub(crate) use save::job_for;
|
||||
pub use save::{
|
||||
SAVE_FORMATS, SaveJob, SaveMode, SavePlan, export_folder, save_filters, save_format, save_job, save_plan, save_with, stamp_save_dates,
|
||||
templates_folder,
|
||||
templates_dialog_folder, templates_folder,
|
||||
};
|
||||
pub use screens::{PRESETS as SCREEN_PRESETS, ScreenSize, preset_rows as screen_preset_rows};
|
||||
pub(crate) use screens::{
|
||||
|
||||
@@ -98,13 +98,52 @@ pub fn stamp_save_dates(st: &mut DocState, at: i64) {
|
||||
d.metadata.modified = Some(at);
|
||||
}
|
||||
|
||||
/// The Templates folder: the `templatesFolder` preference, else `Documents/VectorCraft Templates`
|
||||
/// in the user's home (none where there is no home folder, as on the web).
|
||||
/// The Templates folder: the `templatesFolder` preference, else `VectorCraft Templates` in the
|
||||
/// user's documents folder (none where there is no home folder, as on the web).
|
||||
pub fn templates_folder(prefs: &Prefs) -> Option<String> {
|
||||
if !prefs.templates_folder.is_empty() {
|
||||
return Some(prefs.templates_folder.clone());
|
||||
}
|
||||
Some(std::path::Path::new(&home_folder()?).join("Documents").join("VectorCraft Templates").to_string_lossy().to_string())
|
||||
Some(documents_folder()?.join("VectorCraft Templates").to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// The Templates folder for a file dialog to start in (#703): made when it's missing; where it
|
||||
/// can't be, the nearest folder above it that exists, else the home folder. A dialog asked to
|
||||
/// start in a folder that isn't there shows an error on some desktops.
|
||||
pub fn templates_dialog_folder(prefs: &Prefs) -> Option<String> {
|
||||
let folder = std::path::PathBuf::from(templates_folder(prefs)?);
|
||||
if !folder.is_dir() {
|
||||
// Best effort: a folder that can't be made leaves the nearest one that exists.
|
||||
let _ = std::fs::create_dir_all(&folder);
|
||||
}
|
||||
folder.ancestors().find(|a| a.is_dir()).map(|a| a.to_string_lossy().into_owned()).or_else(home_folder)
|
||||
}
|
||||
|
||||
/// The user's documents folder: on Linux and the BSDs the one the desktop names
|
||||
/// (`XDG_DOCUMENTS_DIR` in `user-dirs.dirs`: `~/Documenti`, `~/Dokumente`…), elsewhere `Documents`
|
||||
/// in the home folder.
|
||||
fn documents_folder() -> Option<std::path::PathBuf> {
|
||||
let home = std::path::PathBuf::from(home_folder()?);
|
||||
if cfg!(all(unix, not(target_os = "macos"))) {
|
||||
let config =
|
||||
std::env::var_os("XDG_CONFIG_HOME").map(std::path::PathBuf::from).filter(|p| p.is_absolute()).unwrap_or_else(|| home.join(".config"));
|
||||
if let Some(dir) = std::fs::read_to_string(config.join("user-dirs.dirs")).ok().and_then(|t| xdg_documents(&t, &home)) {
|
||||
return Some(dir);
|
||||
}
|
||||
}
|
||||
Some(home.join("Documents"))
|
||||
}
|
||||
|
||||
/// `XDG_DOCUMENTS_DIR` in a `user-dirs.dirs` file's `text`, `$HOME` being `home`. None when it isn't
|
||||
/// set, is relative, or is the home folder itself (the spec's way of turning it off).
|
||||
fn xdg_documents(text: &str, home: &std::path::Path) -> Option<std::path::PathBuf> {
|
||||
let line = text.lines().map(str::trim).find(|l| l.starts_with("XDG_DOCUMENTS_DIR="))?;
|
||||
let value = line.split_once('=')?.1.trim().trim_matches('"');
|
||||
let dir = match value.strip_prefix("$HOME") {
|
||||
Some(rest) => home.join(rest.trim_start_matches('/')),
|
||||
None => std::path::PathBuf::from(value),
|
||||
};
|
||||
(dir.has_root() && dir != home).then_some(dir)
|
||||
}
|
||||
|
||||
/// The user's home folder (`HOME`, else `USERPROFILE` on Windows; none on the web).
|
||||
@@ -189,7 +228,7 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
|
||||
SaveMode::Save | SaveMode::SaveAs => format!("{stem}.{ext}"),
|
||||
};
|
||||
let folder = match mode {
|
||||
SaveMode::Template => templates_folder(&s.prefs),
|
||||
SaveMode::Template => templates_dialog_folder(&s.prefs),
|
||||
_ => st.path.as_deref().and_then(parent_folder),
|
||||
};
|
||||
let modified = date_param(p, "modified", cmd)?;
|
||||
@@ -614,3 +653,40 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_folders {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// #703: the documents folder a desktop names in `user-dirs.dirs`, not an English `Documents`.
|
||||
#[test]
|
||||
fn the_desktops_documents_folder_is_read_from_user_dirs() {
|
||||
let home = Path::new("/home/david");
|
||||
let file = "# comment\nXDG_DESKTOP_DIR=\"$HOME/Scrivania\"\nXDG_DOCUMENTS_DIR=\"$HOME/Documenti\"\n";
|
||||
assert_eq!(super::xdg_documents(file, home), Some(PathBuf::from("/home/david/Documenti")));
|
||||
assert_eq!(super::xdg_documents("XDG_DOCUMENTS_DIR=\"/data/docs\"", home), Some(PathBuf::from("/data/docs")));
|
||||
// "$HOME/" turns it off; a relative or missing one isn't used.
|
||||
assert_eq!(super::xdg_documents("XDG_DOCUMENTS_DIR=\"$HOME/\"", home), None);
|
||||
assert_eq!(super::xdg_documents("XDG_DOCUMENTS_DIR=\"docs\"", home), None);
|
||||
assert_eq!(super::xdg_documents("XDG_MUSIC_DIR=\"$HOME/Musica\"", home), None);
|
||||
}
|
||||
|
||||
/// #703: the Templates folder a dialog starts in exists: made when missing, else the nearest
|
||||
/// folder above it.
|
||||
#[test]
|
||||
fn the_templates_dialog_folder_exists() {
|
||||
let base = std::env::temp_dir().join(format!("vc-templates-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
std::fs::create_dir_all(&base).unwrap();
|
||||
let mut prefs = crate::Prefs::default();
|
||||
let wanted = base.join("Modelli").join("VectorCraft Templates");
|
||||
prefs.templates_folder = wanted.to_string_lossy().into_owned();
|
||||
assert_eq!(super::templates_dialog_folder(&prefs).map(PathBuf::from), Some(wanted.clone()));
|
||||
assert!(wanted.is_dir(), "made");
|
||||
// A file in the way: the nearest folder that exists.
|
||||
std::fs::write(base.join("blocked"), b"x").unwrap();
|
||||
prefs.templates_folder = base.join("blocked").join("Templates").to_string_lossy().into_owned();
|
||||
assert_eq!(super::templates_dialog_folder(&prefs).map(PathBuf::from), Some(base.clone()));
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
}
|
||||
@@ -185,7 +185,12 @@ fn polygon_rotation(corners: &[Point], step: f64) -> f64 {
|
||||
})
|
||||
.sum::<f64>()
|
||||
};
|
||||
[0.0, 90.0, 180.0, 270.0].into_iter().filter(|r| r % step == 0.0).min_by(|a, b| error(*a).total_cmp(&error(*b))).unwrap_or(0.0)
|
||||
// A candidate a whole number of periods from an earlier one is the same polygon (a hexagon at 270°
|
||||
// is the one at 90°) and scores the same up to rounding, so it is dropped: otherwise the platform's
|
||||
// `atan2` rounding picks between them (FreeBSD's libm gave 270° where Linux gives 90°).
|
||||
let candidates = [0.0, 90.0, 180.0, 270.0].into_iter().filter(|r| r % step == 0.0);
|
||||
let distinct = candidates.clone().enumerate().filter(|&(i, r)| !candidates.clone().take(i).any(|q| (r - q) % period == 0.0));
|
||||
distinct.map(|(_, r)| r).min_by(|a, b| error(*a).total_cmp(&error(*b))).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn rectangle_rotation(corners: &[Point]) -> f64 {
|
||||
@@ -533,6 +538,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hexagon_rotation_never_depends_on_rounding_between_equivalent_angles() {
|
||||
// 0° and 180° (and 90° and 270°) are the same hexagon, so only 0° and 90° may come back, whatever
|
||||
// rounding does to the tied scores: on FreeBSD the tilted test above got 270°.
|
||||
for direction in [0.0_f64, 90.0] {
|
||||
for tilt in [-9.0_f64, -7.0, -3.0, 3.0, 7.0, 9.0] {
|
||||
for offset in [0.0, 1e-7, 0.1, 1e3, 1e6] {
|
||||
let corners: Vec<Point> = (0..6)
|
||||
.map(|i| {
|
||||
let a = (direction + tilt - 90.0).to_radians() + std::f64::consts::TAU * i as f64 / 6.0;
|
||||
Point::new(offset + 70.0 * a.cos(), offset + 70.0 * a.sin())
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(polygon_rotation(&corners, 90.0), direction, "tilt {tilt}, offset {offset}");
|
||||
let mut shifted = corners.clone();
|
||||
shifted.rotate_left(3);
|
||||
assert_eq!(polygon_rotation(&shifted, 90.0), direction, "tilt {tilt}, offset {offset}, from the opposite corner");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Triangles have no such twin among the candidates: 0° and 180° stay distinct.
|
||||
let down: Vec<Point> = (0..3).map(|i| Point::new(0.0, 0.0) + Vec2::from_angle((90.0_f64 + 120.0 * i as f64).to_radians()) * 50.0).collect();
|
||||
assert_eq!(polygon_rotation(&down, 180.0), 180.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_fit_ignores_sampling_density_and_document_offset() {
|
||||
let mut stroke = wobbly(&[Point::new(0.0, 0.0), Point::new(100.0, 8.0), Point::new(94.0, 100.0), Point::new(10.0, 91.0)], 24, 4.0);
|
||||
|
||||
@@ -106,17 +106,21 @@ fn double_arrow(p: &mut Ink, o: Pos2, dir: egui::Vec2) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A curved double arrow over the hotspot: an arc with an arrowhead at each end, pointing on along
|
||||
/// the arc (#701).
|
||||
fn rotate(p: &mut Ink, o: Pos2) {
|
||||
let pts: Vec<Pos2> = (0..=10)
|
||||
.map(|i| {
|
||||
let a = std::f32::consts::PI * (0.15 + 0.7 * i as f32 / 10.0);
|
||||
o + vec2(a.cos() * 9.0, -a.sin() * 9.0)
|
||||
})
|
||||
.collect();
|
||||
p.add(Shape::line(pts.clone(), Stroke::new(3.0, HALO)));
|
||||
p.add(Shape::line(pts.clone(), Stroke::new(1.2, INK)));
|
||||
for end in [pts[0], pts[pts.len() - 1]] {
|
||||
poly(p, vec![end + vec2(-3.0, -1.0), end + vec2(3.0, -1.0), end + vec2(0.0, 4.0)], INK, INK);
|
||||
const R: f32 = 8.0;
|
||||
let (a0, a1) = (std::f32::consts::PI * 0.12, std::f32::consts::PI * 0.88);
|
||||
let at = |a: f32| o + vec2(a.cos() * R, -a.sin() * R);
|
||||
let pts: Vec<Pos2> = (0..=12).map(|i| at(a0 + (a1 - a0) * i as f32 / 12.0)).collect();
|
||||
p.add(Shape::line(pts.clone(), Stroke::new(3.2, HALO)));
|
||||
p.add(Shape::line(pts, Stroke::new(1.3, INK)));
|
||||
// Each head points away from the arc, along its tangent there.
|
||||
for (a, away) in [(a0, -1.0f32), (a1, 1.0)] {
|
||||
let t = vec2(-a.sin(), -a.cos()) * away;
|
||||
let n = vec2(-t.y, t.x);
|
||||
let (end, tip) = (at(a) - t * 1.5, at(a) + t * 4.5);
|
||||
poly(p, vec![tip, end + n * 1.8, end - n * 1.8], INK, INK);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,11 +114,17 @@ pub(super) fn show_preview(ui: &mut egui::Ui, tex: Option<&egui::TextureHandle>,
|
||||
|
||||
/// Open `bytes` (or place them with the `file.place` params `place`) through the dialog when the
|
||||
/// file is a PDF (or `.ai`/`.ait`) with several pages or a password; false when there's nothing to
|
||||
/// ask (the caller goes on).
|
||||
/// ask (the caller goes on), including a file that carries its VectorCraft document when opening.
|
||||
pub fn offer(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Option<String>, place: Option<&Value>) -> bool {
|
||||
if !fileio::detect(name, bytes).is_some_and(|f| matches!(f.id, "pdf" | "ai" | "ait")) {
|
||||
return false;
|
||||
}
|
||||
// A file carrying its VectorCraft document (a saved .ai, or a PDF saved with Preserve Editing)
|
||||
// opens as that document, every artboard and layer: there are no pages to pick. Placing still
|
||||
// asks, since a place takes one page.
|
||||
if place.is_none() && vectorcraft_pdf::editing_with(bytes, None).is_some() {
|
||||
return false;
|
||||
}
|
||||
let (pages, locked) = match vectorcraft_pdf::info(bytes, None) {
|
||||
Ok(i) if i.pages.len() > 1 => (i.pages.len(), false),
|
||||
Err(PdfError::NeedsPassword) => (0, true),
|
||||
|
||||
@@ -165,3 +165,35 @@ fn placing_a_pdf_page_asks_for_the_page_and_crop_box() {
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(placed(&app).1.width(), 80.0);
|
||||
}
|
||||
|
||||
/// A saved `.ai` with several artboards carries its VectorCraft document: opening it restores that
|
||||
/// document (artboards, layers) instead of asking which pages to import.
|
||||
#[test]
|
||||
fn opening_a_saved_ai_with_several_artboards_restores_it_without_asking() {
|
||||
let mut app = app();
|
||||
app.run("artboard.new", json!({"width": 200, "height": 100, "name": "Second"})).unwrap();
|
||||
app.run("layer.new", json!({"name": "Front"})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
let saved = app.run("document.save", json!({"format": "ai"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap();
|
||||
assert!(vectorcraft_pdf::info(&bytes, None).unwrap().pages.len() > 1, "a page per artboard");
|
||||
|
||||
io::open_bytes(&mut app, "two.ai", &bytes, Some("/tmp/two.ai".into())).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "no Import PDF dialog");
|
||||
assert_eq!(app.session.documents().len(), 2);
|
||||
let doc = &app.session.active().unwrap().doc;
|
||||
assert_eq!(doc.artboards.len(), 2);
|
||||
assert_eq!(doc.artboards[1].name, "Second");
|
||||
assert!(doc.layers.iter().any(|l| l.name.as_deref() == Some("Front")), "layers come back");
|
||||
}
|
||||
|
||||
/// A PDF without a VectorCraft document still asks, and placing a saved `.ai` still asks for the page.
|
||||
#[test]
|
||||
fn placing_a_saved_ai_still_asks_for_the_page() {
|
||||
let mut app = app();
|
||||
app.run("artboard.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
let saved = app.run("document.save", json!({"format": "ai"})).unwrap();
|
||||
let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap();
|
||||
assert!(import_pdf::offer(&mut app, "two.ai", &bytes, None, Some(&json!({}))));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("mode"), "place");
|
||||
}
|
||||
@@ -121,7 +121,7 @@ pub fn new_from_template(app: &mut VectorcraftApp, path: Option<String>) -> Resu
|
||||
None if app.services.open_async.is_some() => return open_dialog(app).map(|_| Value::Null),
|
||||
None => {
|
||||
let filters = std::iter::once(("Templates", TEMPLATE_EXTS)).chain(fileio::open_filters()).collect();
|
||||
pick_open(app, &FilePick { folder: fileio::templates_folder(&app.session.prefs), filters, ..Default::default() })?
|
||||
pick_open(app, &FilePick { folder: fileio::templates_dialog_folder(&app.session.prefs), filters, ..Default::default() })?
|
||||
}
|
||||
};
|
||||
let bytes = read(app, &path)?;
|
||||
|
||||
Reference in new issue
Block a user