mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
Plug-ins: only File › Open lists importers' files, and imported art at the canvas edge keeps an editable artboard
New from Template, Load Character/Paragraph/All Styles, Recolor's Other Library… and the library panels' Other Library… read files with the built-in readers, so their open dialogs now use builtin_open_filters(), which leaves out the extensions only importer plug-ins read. File › Open keeps open_filters() with the importers' files. The artboard of a document an importer opens is now kept within ±4,000,000 pt. Art at the edge (a zero-width path at x = 4,000,000, or a stroke past the edge) gave an artboard beyond it, and every later edit of the document was rejected.
This commit is contained in:
@@ -130,9 +130,9 @@ pub(crate) use export_names::{default_name, merge, unique_file_names, with_black
|
||||
use export_names::{distinct_names_by, write_encoded};
|
||||
use formats::{ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, import_name, writable};
|
||||
pub use formats::{
|
||||
DOC_EXTS, DOCX_EXTS, FORMATS, Format, FormatOption, OPEN_EXTS, PLACE_EXTS, RTF_EXTS, TEXT_EXTS, UNASSOCIATED_EXTS, export_extensions, extension,
|
||||
file_stem, format, format_for_name, has_own_title, importer_formats, library_open_filters, open_extensions, open_filters, place_filters,
|
||||
plugin_formats, read_extensions, readable_format, readable_format_for_name, titled_for_export, writable_format,
|
||||
DOC_EXTS, DOCX_EXTS, FORMATS, Format, FormatOption, OPEN_EXTS, PLACE_EXTS, RTF_EXTS, TEXT_EXTS, UNASSOCIATED_EXTS, builtin_open_filters,
|
||||
export_extensions, extension, file_stem, format, format_for_name, has_own_title, importer_formats, library_open_filters, open_extensions,
|
||||
open_filters, place_filters, plugin_formats, read_extensions, readable_format, readable_format_for_name, titled_for_export, writable_format,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -693,7 +693,7 @@ pub fn open_extensions() -> &'static [&'static str] {
|
||||
exts
|
||||
}
|
||||
|
||||
/// Open-dialog filters: "All readable files" first, then one per readable format, then one per
|
||||
/// File › Open's filters: "All readable files" first, then one per readable format, then one per
|
||||
/// importer plug-in (under its name), then swatch libraries (which open in the library panel),
|
||||
/// Libraries panel libraries, flattener, PDF, print and export presets (imported), scripts (checked
|
||||
/// without running them) and plug-ins (installed).
|
||||
@@ -701,7 +701,20 @@ pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static
|
||||
std::iter::once(("All readable files", open_extensions()))
|
||||
.chain(format_filters())
|
||||
.chain(importer_formats().into_iter().map(|f| (f.label, f.extensions)))
|
||||
.chain(std::iter::once(("Swatch libraries", crate::cmd::swatchlib::LIBRARY_EXTS)))
|
||||
.chain(other_open_filters())
|
||||
}
|
||||
|
||||
/// [`open_filters`] without the importer plug-ins: "All readable files" lists [`OPEN_EXTS`]. For
|
||||
/// open dialogs whose files the built-in readers read (New from Template, Load Styles, a document
|
||||
/// picked for its swatches, symbols, brushes or graphic styles).
|
||||
pub fn builtin_open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
|
||||
std::iter::once(("All readable files", OPEN_EXTS)).chain(format_filters()).chain(other_open_filters())
|
||||
}
|
||||
|
||||
/// The open-dialog filters after the document formats: swatch libraries, Libraries panel
|
||||
/// libraries, presets, shortcut sets, scripts and plug-ins.
|
||||
fn other_open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
|
||||
std::iter::once(("Swatch libraries", crate::cmd::swatchlib::LIBRARY_EXTS))
|
||||
.chain(std::iter::once(("Libraries", crate::cmd::library::LIBRARY_EXTS)))
|
||||
.chain(std::iter::once(("Flattener presets", crate::cmd::flatten::PRESET_EXTS)))
|
||||
.chain(std::iter::once(("PDF presets", crate::cmd::pdfcmds::PRESET_EXTS)))
|
||||
@@ -715,12 +728,13 @@ pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static
|
||||
/// Open-dialog filters for Swatch Libraries and Graphic Style Libraries Other Library… and Locate
|
||||
/// Library…. The library formats come first so Linux file portals (which grey out files that don't
|
||||
/// match the default filter) still list `.ase` and the other library extensions; then the same
|
||||
/// filters as [`open_filters`] so a document can be picked for its swatches or graphic styles.
|
||||
/// filters as [`builtin_open_filters`] so a document can be picked for its swatches or graphic
|
||||
/// styles.
|
||||
pub fn library_open_filters(
|
||||
library_label: &'static str,
|
||||
library_exts: &'static [&'static str],
|
||||
) -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
|
||||
std::iter::once((library_label, library_exts)).chain(open_filters())
|
||||
std::iter::once((library_label, library_exts)).chain(builtin_open_filters())
|
||||
}
|
||||
|
||||
/// File → Place dialog filters: "All placeable files", then one per placeable format, then text,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_doc::{Document, MAX_COORD};
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_plugins::{Plugin, export, import, objects};
|
||||
|
||||
@@ -101,15 +101,24 @@ pub(super) fn load(name: &str, bytes: &[u8], plugin: &Plugin, p: &Value) -> Resu
|
||||
let ids =
|
||||
objects::apply_output(&mut doc, &[], imported.objects, layer, &admit).map_err(|e| err(format!("can't open `{}`: {e}", file_name(name))))?;
|
||||
if let (Some(bounds), Some(board)) = (doc.bounds_of(&ids, true), doc.artboards.first_mut()) {
|
||||
board.rect = at_least_a_point(bounds);
|
||||
board.rect = artboard_rect(bounds);
|
||||
}
|
||||
doc.title = file_name(name);
|
||||
Ok(Loaded { doc, format, warnings: imported.warnings, restored: false, converted: false, preview_only: false, editing_data: false })
|
||||
}
|
||||
|
||||
/// `r` at least 1 pt wide and high, about its center: the artboard of a single line or point.
|
||||
fn at_least_a_point(r: Rect) -> Rect {
|
||||
let grow = |lo: f64, hi: f64| if hi - lo >= 1.0 { (lo, hi) } else { ((lo + hi) / 2.0 - 0.5, (lo + hi) / 2.0 + 0.5) };
|
||||
let ((x0, x1), (y0, y1)) = (grow(r.x0, r.x1), grow(r.y0, r.y1));
|
||||
/// The artboard of art with bounds `r`: `r` within ±[`MAX_COORD`], at least 1 pt wide and high
|
||||
/// about its center (the artboard of a single line or point). A side that would pass the edge
|
||||
/// moves inward, so art at the edge keeps an artboard the document's edits accept.
|
||||
fn artboard_rect(r: Rect) -> Rect {
|
||||
let side = |lo: f64, hi: f64| {
|
||||
let (lo, hi) = (lo.max(-MAX_COORD), hi.min(MAX_COORD));
|
||||
if hi - lo >= 1.0 {
|
||||
return (lo, hi);
|
||||
}
|
||||
let mid = ((lo + hi) / 2.0).clamp(-MAX_COORD + 0.5, MAX_COORD - 0.5);
|
||||
(mid - 0.5, mid + 0.5)
|
||||
};
|
||||
let ((x0, x1), (y0, y1)) = (side(r.x0, r.x1), side(r.y0, r.y1));
|
||||
Rect::new(x0, y0, x1, y1)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,21 @@ fn a_file_an_importer_reads_opens_as_editable_art() {
|
||||
open(&mut s, "Line.tso", LINE.as_bytes(), json!({})).unwrap();
|
||||
assert_eq!(artboard(&s), (0.0, 4.5, 50.0, 5.5));
|
||||
|
||||
// Art at the edge of the canvas keeps its artboard within ±4,000,000 pt, so the document can
|
||||
// still be edited: a vertical line at x = 4,000,000 (its 1 pt moves inward) and a stroked line
|
||||
// along y = -4,000,000 (its stroke passes the edge).
|
||||
let edge = r#"{"objects":[{"type":"path","path":{"subpaths":[{"closed":false,"anchors":[{"p":[4000000,0]},{"p":[4000000,50]}]}]}}]}"#;
|
||||
open(&mut s, "Edge.tso", edge.as_bytes(), json!({})).unwrap();
|
||||
assert_eq!(artboard(&s), (3_999_999.0, 0.0, 4_000_000.0, 50.0));
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
s.execute("object.move", &json!({"dx": -5, "dy": 0})).unwrap();
|
||||
let stroked = r#"{"objects":[{"type":"path","path":{"subpaths":[{"closed":false,"anchors":[{"p":[0,-4000000]},{"p":[50,-4000000]}]}]},"strokes":[{"paint":{"type":"solid","color":{"model":"rgb","r":0,"g":0,"b":0}},"width":10}]}]}"#;
|
||||
open(&mut s, "Stroked.tso", stroked.as_bytes(), json!({})).unwrap();
|
||||
let (x0, y0, x1, y1) = artboard(&s);
|
||||
assert!(x0 < 0.0 && x1 > 50.0 && y0 == -4_000_000.0 && y1 > y0 + 1.0, "{:?}", artboard(&s));
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
s.execute("object.move", &json!({"dx": 0, "dy": 5})).unwrap();
|
||||
|
||||
// From a path: the document keeps no path, so Save asks for a name.
|
||||
let path = dir.join("Shapes.tso");
|
||||
std::fs::write(&path, RECT).unwrap();
|
||||
|
||||
@@ -604,8 +604,8 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
// Import a library (.ase, .acb, .gpl, .vcswatches or a document's swatches) and limit to it.
|
||||
if app.services.pick_open.is_some() && widgets::icon_button(ui, "folder-open", tl!("Other Library…"), false, 24.0).clicked() {
|
||||
let r = crate::picks::in_dialog(app, d, |app, d| {
|
||||
let pick =
|
||||
crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::open_filters()), ..Default::default() };
|
||||
let filters = crate::FilePick::filters(vectorcraft_engine::cmd::fileio::builtin_open_filters());
|
||||
let pick = crate::FilePick { filters, ..Default::default() };
|
||||
let Some(path) = crate::picks::open(app, &pick) else { return Ok(()) };
|
||||
let r = app.run("swatch.library.load", json!({ "path": path }))?;
|
||||
d.fields.insert("limitTo".into(), r["library"].clone());
|
||||
|
||||
@@ -135,6 +135,35 @@ fn open_with_asks_for_an_importer_s_parameters_and_passes_them_on() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// File › Open's dialog lists an importer's files; the open dialogs whose files only the built-in
|
||||
/// readers read (New from Template, Load Styles, the library panels' Other Library…) don't.
|
||||
#[test]
|
||||
fn only_file_open_s_dialog_lists_an_importer_s_files() {
|
||||
let mut app = app();
|
||||
install(&mut app, &tpl::module(&tpl::importer("test.ui.filters", r#"["tuf"]"#, "{}"), tpl::ECHO, "", 1));
|
||||
let asked: std::rc::Rc<std::cell::RefCell<Vec<crate::FilePick>>> = Default::default();
|
||||
let seen = asked.clone();
|
||||
app.services.pick_open = Some(Box::new(move |pick: &crate::FilePick| {
|
||||
seen.borrow_mut().push(pick.clone());
|
||||
None
|
||||
}));
|
||||
let lists = |filters: &[(String, Vec<String>)]| filters.iter().any(|(_, exts)| exts.iter().any(|e| e == "tuf"));
|
||||
let shown = |app: &mut VectorcraftApp, cmd: &str| {
|
||||
let _ = app.run(cmd, json!({}));
|
||||
let pick = asked.borrow_mut().pop().unwrap_or_else(|| panic!("{cmd} showed no open dialog"));
|
||||
lists(&pick.filters)
|
||||
};
|
||||
assert!(shown(&mut app, "file.open"), "File › Open");
|
||||
for cmd in ["file.newFromTemplate", "ui.loadCharStyles", "ui.loadParaStyles", "ui.loadAllStyles"] {
|
||||
assert!(!shown(&mut app, cmd), "{cmd}");
|
||||
}
|
||||
for kind in ["swatches", "graphicStyles", "symbols"] {
|
||||
let filters = crate::FilePick::filters(crate::panels::library_panel::library_pick_filters(kind));
|
||||
assert!(!lists(&filters), "{kind}");
|
||||
}
|
||||
app.run("plugin.remove", json!({"id": "test.ui.filters"})).unwrap();
|
||||
}
|
||||
|
||||
/// File › Open reads a file of an extension only an importer lists, and refuses one past the
|
||||
/// importer's input limit before reading it, as Open With does.
|
||||
#[test]
|
||||
|
||||
@@ -461,7 +461,7 @@ pub fn new_from_template(app: &mut VectorcraftApp, path: Option<String>) -> Resu
|
||||
Some(p) => p,
|
||||
None if app.services.open_async.is_some() => return open_dialog(app).map(|_| Value::Null),
|
||||
None => {
|
||||
let filters = FilePick::filters(std::iter::once(("Templates", TEMPLATE_EXTS)).chain(fileio::open_filters()));
|
||||
let filters = FilePick::filters(std::iter::once(("Templates", TEMPLATE_EXTS)).chain(fileio::builtin_open_filters()));
|
||||
pick_open(app, &FilePick { folder: fileio::templates_dialog_folder(&app.session.prefs), filters, ..Default::default() })?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -573,7 +573,7 @@ pub(crate) fn library_pick_filters(kind: &str) -> Vec<(&'static str, &'static [&
|
||||
match kind {
|
||||
super::swatches::SwatchLibraries::KIND => fileio::library_open_filters("Swatch libraries", swatchlib::LIBRARY_EXTS).collect(),
|
||||
super::graphic_styles::GraphicStyleLibraries::KIND => fileio::library_open_filters("Graphic Style Libraries", &[STYLES_EXT]).collect(),
|
||||
_ => fileio::open_filters().collect(),
|
||||
_ => fileio::builtin_open_filters().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -421,7 +421,7 @@ pub(crate) fn load(app: &mut VectorcraftApp, load: &str, path: Option<String>) -
|
||||
let path = match path {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
let pick = crate::FilePick { filters: crate::FilePick::filters(fileio::open_filters()), ..Default::default() };
|
||||
let pick = crate::FilePick { filters: crate::FilePick::filters(fileio::builtin_open_filters()), ..Default::default() };
|
||||
let Some(path) = crate::picks::open(app, &pick) else { return Ok(Value::Null) };
|
||||
path
|
||||
}
|
||||
|
||||
+4
-3
@@ -47,8 +47,8 @@ reads the file, by its content or by its extension, and the importer lists the f
|
||||
one whose id sorts first opens it. `document.open {"importer": "plugin.<id>", "params"?: {…}}` names the importer,
|
||||
which must list the file's extension, also one a built-in format reads (an importer that lists `svg` opens an `.svg`
|
||||
only this way). A damaged file of a built-in format reports that format's error, and the file is not handed to an
|
||||
importer. The importer's objects become a new document whose artboard is their bounds (at least 1 pt wide and high);
|
||||
the document keeps no path, so Save asks for a name. File › Place reads only the built-in formats. These rules are
|
||||
importer. The importer's objects become a new document whose artboard is their bounds (at least 1 pt wide and high,
|
||||
and within ±4,000,000 pt); the document keeps no path, so Save asks for a name. File › Place reads only the built-in formats. These rules are
|
||||
VectorCraft's own.
|
||||
|
||||
In the app, installed filters appear under **Object › Plug-ins**, next to **Install Plug-in…** and **Reload
|
||||
@@ -59,7 +59,8 @@ parameters it runs at once. Parameters too many for the window's height scroll i
|
||||
heading and buttons in view. Exporters are formats of **File › Export › Export As**, where Use
|
||||
Artboards is greyed out (the plug-in writes the whole document); one with parameters asks for them in a dialog of the
|
||||
same kind once the file is picked, and its Export button writes the file. Importers' files are listed under File ›
|
||||
Open's "All readable files" and under a filter of their own, named after the importer; **File › Open With** lists the
|
||||
Open's "All readable files" and under a filter of their own, named after the importer (other open dialogs, such as New
|
||||
from Template and Load Styles, list only the files the built-in formats read); **File › Open With** lists the
|
||||
installed importers by name (`file.openWith {"importer", "path"?, "params"?}`), and each opens a file picked among its
|
||||
extensions with that importer (on the web, File › Open's picker opens instead) and adds it to Open Recent. An importer
|
||||
with parameters, given no `params`, asks for them in a dialog of the same kind once the file is picked, and its Open
|
||||
|
||||
Reference in New Issue
Block a user