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:
dotCooCoo
2026-11-08 05:35:23 -08:00
parent 58f5789e9a
commit d65207dc62
10 changed files with 89 additions and 21 deletions
+3 -3
View File
@@ -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)]
+18 -4
View File
@@ -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,
+15 -6
View File
@@ -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)
}
+15
View File
@@ -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();