Browse / github.com/storytold/vectorcraft / patch
EPS in Save As, and the Tools panel docks at the right edge too
EPS in Save As, and the Tools panel docks at the right edge too ## What and why Two independent changes, one commit each. **1. File formats: EPS in Save As and Save a Copy** (`4aff6cf6`) EPS was only reachable through Export, although an EPS file VectorCraft writes already carries the native document and reopens exactly. EPS now joins `SAVE_FORMATS` (appended), so it can be the document's own file: - Save As / Save a Copy in EPS ask with the existing EPS Options dialog (its OK reads "OK" there and saves instead of exporting); the web Save As dialog hands EPS on to it, like SVG and PDF. - Save writes the EPS back with the options it was saved with. -
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- vecteza (imported from pull request #3365, open)
- Base
75dacb4fec73dac539538c91f92d41e09b255d23- Size
- +389 −68 in 42 files
- Review
- pending
- Shared
- 2026-10-28
- Id
cbd1b87e485ad06d9fc954d7
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/fileio/load.rs crates/engine/src/cmd/fileio/save.rs crates/engine/src/tests_recovery.rs crates/engine/src/tests_save.rs crates/mcp/src/tests.rs crates/ui-egui/src/dialogs/eps_options.rs crates/ui-egui/src/dialogs/save_options.rs crates/ui-egui/src/dialogs/tests_eps.rs crates/ui-egui/src/edit_toolbar.rs crates/ui-egui/src/floating.rs crates/ui-egui/src/i18n/bg.tsv crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/da.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/el.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fi.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/id.tsv crates/ui-egui/src/i18n/it.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/ka.tsv crates/ui-egui/src/i18n/ko.tsv crates/ui-egui/src/i18n/nl.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-br.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/sv.tsv crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/src/i18n/zh-hans.tsv crates/ui-egui/src/i18n/zh-hant.tsv crates/ui-egui/src/io.rs crates/ui-egui/src/lib.rs crates/ui-egui/src/menus.rs crates/ui-egui/src/panel_docking.rs crates/ui-egui/src/state.rs crates/ui-egui/src/toolbar.rs crates/ui-egui/src/workspaces.rs docs/file-format-parity.md docs/mcp.md
@@ -443,11 +443,11 @@ fn open_loaded(s: &mut Session, loaded: Loaded, links: Resolved, path: Option<St doc.layers_open = Some(vec![]); } // Save writes the file back only in a format it writes, only a whole PDF (see- // [`LoadOptions::is_partial`]), and a .ai file only when it carried the native document (Save- // writes .ai that way).+ // [`LoadOptions::is_partial`]), and a .ai or EPS file only when it carried the native document+ // (Save writes them that way). let partial = format.id == "pdf" && opts.is_partial();- let lossy_ai = format.id == "ai" && !restored;- let path = path.filter(|_| !template && !partial && !lossy_ai && SAVE_FORMATS.contains(&format.id));+ let lossy = matches!(format.id, "ai" | "eps") && !restored;+ let path = path.filter(|_| !template && !partial && !lossy && SAVE_FORMATS.contains(&format.id)); let saves_back = path.is_some(); let converted = saves_back && converted && s.prefs.append_converted; let index = s.add_document(doc, path);@@ -20,7 +20,7 @@ use crate::{DocState, Prefs}; /// The formats Save As offers, in menu order (append-only). The other writable formats are exports: /// they never become the document's own file.-pub const SAVE_FORMATS: &[&str] = &["vectorcraft", "template", "pdf", "svg", "svgz", "ai"];+pub const SAVE_FORMATS: &[&str] = &["vectorcraft", "template", "pdf", "svg", "svgz", "ai", "eps"]; /// The formats Save as Template writes, the default first: a native template, or a /// PDF-compatible `.ait` (written as a `.ai` file is).@@ -312,9 +312,10 @@ fn doc_to_save(st: &DocState, f: &Format) -> Arc<Document> { Arc::new(d) } -/// What a format loses against a native file (reported whenever a save writes it).+/// What a format loses against a native file (reported whenever a save writes it). An EPS file+/// always carries the native document, which reopening it restores. fn fidelity_warning(f: &Format) -> Option<String> {- (!is_native(f)).then(|| {+ (!is_native(f) && f.id != "eps").then(|| { format!( "{} keeps the artwork but not everything a VectorCraft document holds (editable effects, symbols, swatches, styles): save as VectorCraft to keep it all editable", f.label@@ -778,7 +779,7 @@ pub(super) fn specs() -> Vec<CommandSpec> { "Save Document", [], None,- "{path?, format?: vectorcraft|template|pdf|svg|svgz|ai (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead, compress?: bool (native, also in options: gzip; default: the useCompression preference), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: false for native (also carry a PDF of every artboard) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly; separateArtboards writes more files, one per artboard. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified",+ "{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead, compress?: bool (native, also in options: gzip; default: the useCompression preference), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), separateArtboards?: false (native and .ai: also save each artboard of `range` to <name>-<artboard>.<ext> beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: false for native (also carry a PDF of every artboard) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), …for .eps the options of document.exportEps (level, previewFormat, flattenerPreset; one file of the visible art, or of an artboard with useArtboards), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file: a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly; an EPS file carries the native document too, which document.open restores; separateArtboards writes more files, one per artboard. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified", has_doc, |s, p| save(s, SaveMode::Save, p) ),@@ -787,7 +788,7 @@ pub(super) fn specs() -> Vec<CommandSpec> { "Save As…", ["File"], Some("Cmd+Shift+S"),- "{…as document.save} {path?, format?: vectorcraft|template|pdf|svg|svgz|ai (default: the path's extension, else the document's own; ait, a .ait template, is written by file.saveAsTemplate), options?, svg?, modified?} the document takes on the new path, name and format (except a template, which is always a copy) → {path, format, bytes, warnings}; no path → {dataBase64, format, name, folder?, warnings}",+ "{…as document.save} {path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own; ait, a .ait template, is written by file.saveAsTemplate), options?, svg?, modified?} the document takes on the new path, name and format (except a template, which is always a copy) → {path, format, bytes, warnings}; no path → {dataBase64, format, name, folder?, warnings}", has_doc, |s, p| save(s, SaveMode::SaveAs, p) ),@@ -138,6 +138,30 @@ fn a_crash_leaves_copies_that_restore_modified_and_titled() { assert!(s.execute("file.recovery.restore", &json!({"file": format!("{mine}/Poster-1")})).is_err(), "an open document's copy"); } +/// A document whose own file is an EPS gets recovery copies too, and a restored one saves as EPS+/// again (once a path is chosen).+#[test]+fn an_eps_documents_copy_restores_as_eps() {+ let (mut s, store) = session();+ let dir = std::env::temp_dir().join(format!("vc-recovery-eps-{}", std::process::id()));+ std::fs::create_dir_all(&dir).unwrap();+ let eps = dir.join("Poster.eps").to_string_lossy().to_string();+ s.execute("file.saveAs", &json!({"path": eps, "options": {"previewFormat": "none"}})).unwrap();+ s.execute("shape.ellipse", &json!({"x": 100, "y": 10, "width": 20, "height": 20})).unwrap();+ let r = s.execute("file.recovery.save", &json!({})).unwrap();+ assert_eq!(r["saved"].as_array().unwrap().len(), 1, "the modified EPS document is copied: {r}");+ let nodes = s.doc().unwrap().doc.node_count();+ drop(s);++ let mut s = relaunch(&store);+ assert_eq!(restored(s.execute("file.recovery.restore", &json!({})).unwrap()), 1);+ let st = s.doc().unwrap();+ assert_eq!((st.path.as_deref(), st.format, st.doc.node_count()), (Some(eps.as_str()), "eps", nodes));+ let saved = s.execute("document.save", &json!({})).unwrap();+ assert_eq!((saved["format"].as_str(), saved["name"].as_str()), (Some("eps"), Some("Poster.eps")), "{saved}");+ let _ = std::fs::remove_dir_all(dir);+}+ #[test] fn a_restored_copy_still_refuses_to_replace_the_file_an_import_left_things_out_of() { let (mut s, store) = session();@@ -67,6 +67,43 @@ fn an_svg_path_writes_svg_and_save_writes_it_back() { let _ = std::fs::remove_dir_all(d); } +#[test]+fn save_as_eps_carries_the_document_and_save_writes_it_back() {+ let d = dir("eps");+ let mut s = session();+ rect(&mut s);+ let eps = path(&d, "art.eps");+ let r = s.execute("file.saveAs", &json!({"path": eps, "options": {"previewFormat": "none", "level": 2}})).unwrap();+ assert_eq!(r["format"], "eps");+ assert_eq!(r["warnings"], json!([]), "the file carries the whole document: {r}");+ assert!(std::fs::read(&eps).unwrap().starts_with(b"%!PS-Adobe-3.0 EPSF-3.0"));+ let st = s.doc().unwrap();+ assert_eq!((st.path.as_deref(), st.format, st.is_dirty()), (Some(eps.as_str()), "eps", false));+ assert_eq!(Value::Object(st.save_options.clone()), json!({"previewFormat": "none", "level": 2}));+ // Reopened, it is the same document, keeps its path and saves as EPS again.+ let mut s = Session::new();+ let r = s.execute("document.open", &json!({"path": eps})).unwrap();+ assert_eq!((&r["restored"], &r["format"]), (&json!(true), &json!("eps")), "{r}");+ assert_eq!((s.doc().unwrap().path.as_deref(), s.doc().unwrap().format), (Some(eps.as_str()), "eps"));+ let n = objects(&s);+ rect(&mut s);+ assert_eq!(s.execute("document.save", &json!({})).unwrap()["format"], "eps");+ let mut s = Session::new();+ s.execute("document.open", &json!({"path": eps})).unwrap();+ assert_eq!(objects(&s), n + 1, "the second rectangle was saved");+ // An EPS file another app wrote (no document of ours in it) is never saved over.+ let foreign = path(&d, "foreign.eps");+ let bytes = std::fs::read(&eps).unwrap();+ let at = bytes.windows(30).position(|w| w == b"%VectorCraft_BeginData: native").unwrap();+ let mut patched = bytes.clone();+ patched[at + 29] = b'x';+ std::fs::write(&foreign, patched).unwrap();+ let r = s.execute("document.open", &json!({"path": foreign})).unwrap();+ assert_eq!(r["restored"], false, "{r}");+ assert_eq!(s.doc().unwrap().path, None);+ let _ = std::fs::remove_dir_all(d);+}+ #[test] fn save_as_takes_on_path_format_and_options() { let d = dir("saveas");@@ -84,7 +121,7 @@ fn save_as_takes_on_path_format_and_options() { let svg = s.execute("file.formatOptions", &json!({"format": "svg"})).unwrap(); assert_eq!(svg["options"]["outlineText"]["value"], false, "another format shows its defaults"); let ids: Vec<&str> = o["saveFormats"].as_array().unwrap().iter().map(|f| f["id"].as_str().unwrap()).collect();- assert_eq!(ids, ["vectorcraft", "template", "pdf", "svg", "svgz", "ai"]);+ assert_eq!(ids, ["vectorcraft", "template", "pdf", "svg", "svgz", "ai", "eps"]); // SVGZ is gzip-compressed SVG and reopens as SVGZ. let svgz = path(&d, "doc.svgz"); s.execute("file.saveAs", &json!({"path": svgz})).unwrap();@@ -455,7 +455,7 @@ fn add_text_sets_writing_direction_for_every_kind() { fn save_file_describes_the_formats_it_writes() { let tools = tool_definitions(); let d = tools.iter().find(|t| t["name"] == "save_file").and_then(|t| t["description"].as_str()).unwrap();- for ext in [".vectorcraft", ".vctemplate", ".pdf", ".svg", ".svgz", ".ai"] {+ for ext in [".vectorcraft", ".vctemplate", ".pdf", ".svg", ".svgz", ".ai", ".eps"] { assert!(d.contains(ext), "{ext}: {d}"); } assert!(d.contains("its own format"), "{d}");@@ -5,9 +5,11 @@ //! //! Fields: `document.export` params (`format: eps`, `path?`, the artboard choice) and the EPS //! options of `document.exportEps`. OK remembers the options for next time and writes the file-//! (asking where when there is no path); bad options keep the dialog open.+//! (asking where when there is no path); bad options keep the dialog open. File → Save As and Save a+//! Copy in EPS ask with it too (`__save`: the save command), and its OK then saves the file. use serde_json::{Map, Value, json};+use vectorcraft_engine::cmd::fileio::SaveMode; use vectorcraft_engine::cmd::{FlattenOptions, fileio}; use vectorcraft_eps::{Level, Overprint, Preview}; @@ -23,6 +25,8 @@ pub(super) const SPEC: DialogSpec = DialogSpec { body, confirm, ok: Some("Export"),+ // OK when it finishes a Save As or Save a Copy.+ ok_label: Some(|app| if app.ui.dialog.as_ref().is_some_and(saving) { "OK" } else { "Export" }), min_width: 420.0, max_width: Some(460.0), ..DialogSpec::FORM@@ -121,16 +125,29 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { false } -/// Check the options, remember them, and write the file(s).+/// Does the dialog finish a save (Save As or Save a Copy in EPS) rather than an export?+fn saving(d: &Dialog) -> bool {+ SaveMode::of(&d.str("__save")).is_some()+}++/// Check the options, remember them, and write the file(s): an export, or the save `__save` names+/// (the document's own EPS file, which carries the whole document; the export's artboard choice+/// doesn't apply). fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> { let params = fileio::eps::resolve(&super::form::params(d), &app.session.prefs.flattener_presets)?;+ let save = SaveMode::of(&d.str("__save")); // Nothing selected keeps the dialog open too.- if let Some(st) = app.session.active() {+ if let (Some(st), None) = (app.session.active(), save) { fileio::export_source(st, ¶ms).map_err(|e| e.to_string())?; } app.ui.eps_options = Value::Object(d.fields.iter().filter(|(k, _)| !PER_EXPORT.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect()); app.ui.dialog = None; let path = d.fields.get("path").and_then(Value::as_str).map(str::to_string);- io::export(app, Some("eps"), path, ¶ms)+ let Some(mode) = save else { return io::export(app, Some("eps"), path, ¶ms) };+ let options: Map<String, Value> = params+ .as_object()+ .map(|o| o.iter().filter(|(k, _)| !PER_EXPORT.contains(&k.as_str())).map(|(k, v)| (k.clone(), v.clone())).collect())+ .unwrap_or_default();+ io::save(app, mode, &json!({ "path": path, "format": "eps", "options": options }), false) }@@ -1,6 +1,6 @@ //! A save format's options before the file is written; on the web, where there is no save panel,-//! also the Save As dialog that names the file and picks the format (SVG and PDF then go on to-//! their own dialogs). The rows come from the format's options in the engine's format table, so a+//! also the Save As dialog that names the file and picks the format (SVG, PDF and EPS then go on+//! to their own dialogs). The rows come from the format's options in the engine's format table, so a //! format gains rows as its encoder gains options. //! //! Fields: `__action` (the save command it finishes), `__pick` (file name and format@@ -62,10 +62,10 @@ pub(crate) fn option_values(app: &mut VectorcraftApp, f: &Format) -> Map<String, .collect() } -/// Formats whose options have a dialog of their own (SVG Options, Save PDF), which follows this-/// one.+/// Formats whose options have a dialog of their own (SVG Options, Save PDF, EPS Options), which+/// follows this one. fn own_dialog(f: &Format) -> bool {- matches!(f.id, "svg" | "svgz" | "pdf")+ matches!(f.id, "svg" | "svgz" | "pdf" | "eps") } /// Does `f` save each artboard to a separate file on request (native and `.ai` files)? Its@@ -544,7 +544,7 @@ mod tests { // The save panel offers every save format, the document's own first. let pick = &picks.borrow()[0]; let labels: Vec<&str> = pick.filters.iter().map(|f| f.0.as_str()).collect();- assert_eq!(labels, ["VectorCraft", "VectorCraft Template", "PDF", "SVG", "SVG Compressed", "PDF-compatible .ai"]);+ assert_eq!(labels, ["VectorCraft", "VectorCraft Template", "PDF", "SVG", "SVG Compressed", "PDF-compatible .ai", "EPS"]); assert_eq!(pick.name, "Untitled-1.vectorcraft"); let d = dialog(&app); assert_eq!((d.kind.as_str(), d.str("path").as_str(), d.str("mode").as_str()), ("svgOptions", "art.svg", "save"));@@ -67,6 +67,36 @@ fn eps_options_take_saved_flattener_presets_and_refuse_bad_ones() { assert!(written.borrow()[0].1.starts_with(b"%!PS-Adobe-3.0 EPSF-3.0"), "{:?}", &written.borrow()[0].1[..8]); } +#[test]+fn save_as_eps_asks_with_eps_options_and_save_reuses_them() {+ let (mut app, written) = app(1);+ assert_eq!(app.run("file.saveAs", json!({"format": "eps"})).unwrap()["pending"], "epsOptions");+ let d = app.ui.dialog.as_ref().unwrap();+ assert_eq!((d.str("path"), d.str("__save")), ("/out/Untitled-1.eps".into(), "file.saveAs".into()));+ frame(&mut app);+ set(&mut app, "previewFormat", json!("none"));+ set(&mut app, "level", json!("2"));+ confirm(&mut app).unwrap();+ assert!(app.ui.dialog.is_none());+ assert_eq!(names(&written), ["/out/Untitled-1.eps"]);+ assert!(written.borrow()[0].1.starts_with(b"%!PS-Adobe-3.0 EPSF-3.0"), "no preview: plain PostScript");+ let st = app.session.active().unwrap();+ assert_eq!((st.path.as_deref(), st.format, st.is_dirty()), (Some("/out/Untitled-1.eps"), "eps", false));+ assert_eq!((st.save_options["previewFormat"].clone(), st.save_options["level"].clone()), (json!("none"), json!("2")));+ assert!(vectorcraft_eps::has_native(&written.borrow()[0].1), "the file carries the document");+ // Save writes EPS again with those options, without asking.+ app.run("shape.rectangle", json!({"x": 40, "y": 5, "width": 10, "height": 10})).unwrap();+ app.run("file.save", json!({})).unwrap();+ assert!(app.ui.dialog.is_none());+ assert_eq!(names(&written), ["/out/Untitled-1.eps", "/out/Untitled-1.eps"]);+ assert!(String::from_utf8_lossy(&written.borrow()[1].1).contains("%%LanguageLevel: 2"));+ // Save a Copy as EPS starts from them, and its OK reads OK, not Export.+ app.run("file.saveCopy", json!({"format": "eps"})).unwrap();+ let d = app.ui.dialog.as_ref().unwrap();+ assert_eq!((d.str("__save"), d.str("previewFormat")), ("file.saveCopy".into(), "none".into()));+ assert_eq!(super::spec("epsOptions").ok_label.map(|f| f(&app)), Some("OK"));+}+ #[test] fn the_eps_options_dialog_needs_a_document() { let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());@@ -411,7 +411,7 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) { let screen = ctx.content_rect(); let id = drawer_id(); let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(WIDTH, 200.0), |r| r.size());- let x = (bar.right() + GAP).min(screen.right() - size.x - GAP).max(screen.left());+ let x = crate::toolbar::beside(bar, size.x, GAP, crate::toolbar::room(app, ctx).shrink(GAP)); // Never above the toolbar's top: higher up are the window's title bar (and on macOS its // buttons) and the bars under it. let top = bar.top().max(screen.top() + GAP);@@ -74,6 +74,8 @@ struct Move { pub(crate) enum Drop { /// Back in the dock (the Tools panel: at the window's left edge). Dock,+ /// The Tools panel docked on the right, left of the panel dock.+ DockRight, /// Stacked with the floating group holding this panel. Stack(&'static str), /// In a set with the floating group holding this panel, below it.@@ -154,6 +156,11 @@ pub(crate) fn tools_zone_id() -> Id { Id::new("floating-tools-zone") } +/// Where a Tools panel dropped docks on the right: the strip left of the panel dock.+pub(crate) fn tools_zone_right_id() -> Id {+ Id::new("floating-tools-zone-right")+}+ /// The area of the floating group whose first panel is `first` (of the set it heads). pub fn area_id(first: &str) -> Id { Id::new(("floating-panels", first))@@ -418,7 +425,16 @@ pub(crate) fn drag_tools(app: &mut VectorcraftApp, ctx: &egui::Context, title: & /// Where the pointer at `at` would drop `what`, and the zone that lights up for it. pub(crate) fn drop_target(app: &VectorcraftApp, ctx: &egui::Context, what: Moving, at: Pos2) -> Option<(Drop, Rect)> { let zone = |id: Id| ctx.data(|d| d.get_temp::<Rect>(id)).filter(|r| r.contains(at));- let Moving::Panel(id) = what else { return zone(tools_zone_id()).map(|r| (Drop::Dock, r)) };+ let Moving::Panel(id) = what else {+ // Left of the panel dock, or anywhere along the window's right edge: it docks left of+ // the panels either way, so that place lights up.+ let right = ctx.data(|d| d.get_temp::<Rect>(tools_zone_right_id()));+ let at_edge = at.x >= ctx.content_rect().right() - crate::toolbar::WIDTH && right.is_some_and(|r| r.y_range().contains(at.y));+ return zone(tools_zone_id())+ .map(|r| (Drop::Dock, r))+ .or_else(|| zone(tools_zone_right_id()).map(|r| (Drop::DockRight, r)))+ .or_else(|| right.filter(|_| at_edge).map(|r| (Drop::DockRight, r)));+ }; let own = group_of(&app.ui, id); let moving = own.map(|i| set_of(&app.ui, i)).unwrap_or_default(); // The left edge of the icon column or of a dock column docks the moved groups as a new column@@ -560,7 +576,7 @@ pub(crate) fn preview(ctx: &egui::Context, target: Drop, rect: Rect) { match target { Drop::Below(_) | Drop::Above(_) | Drop::Column(_) => insertion_line(ctx, rect), Drop::Stack(_) | Drop::Root(..) => tab_zone(ctx, rect),- Drop::Dock | Drop::Icons => highlight(ctx, rect),+ Drop::Dock | Drop::DockRight | Drop::Icons => highlight(ctx, rect), } } @@ -588,6 +604,7 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) { app.ui.open_panel = m.original.open_panel; app.ui.dock_tab = m.original.dock_tab; app.ui.toolbar_pos = m.original.toolbar_pos;+ app.ui.toolbar_right = m.original.toolbar_right; ctx.data_mut(|data| { data.remove::<Move>(move_id()); data.insert_temp(suppressed_id(), true);@@ -626,6 +643,7 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) { let Some((drop, _)) = target else { return }; let Moving::Panel(id) = m.what else { app.ui.toolbar_pos = None;+ app.ui.toolbar_right = drop == Drop::DockRight; return; }; // The moved set: its groups' panels, showing the top group's.@@ -657,7 +675,7 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) { } match drop { Drop::Root(..) | Drop::Icons => {}- Drop::Dock => {+ Drop::Dock | Drop::DockRight => { dock(&mut app.ui, &ids, active); // One that went back to its icon pops out of it, so it doesn't seem to vanish. if app.ui.dock_collapsed || DockTab::from_id(active).is_none() {@@ -1105,9 +1123,20 @@ fn float_or_dock(app: &mut VectorcraftApp, float_it: bool, p: &Value) -> Result< let group = crate::menus::opt_bool(p, "group")?.unwrap_or(false); let at = position(p)?; if raw.eq_ignore_ascii_case("tools") || raw.eq_ignore_ascii_case("toolbar") {+ // Docking: at the window's left or right edge (default: the side it docked at last).+ let right = match p.get("side").and_then(Value::as_str) {+ None => app.ui.toolbar_right,+ Some(s) if s.eq_ignore_ascii_case("left") => false,+ Some(s) if s.eq_ignore_ascii_case("right") => true,+ Some(s) => return Err(format!("side is left or right, not `{s}`")),+ }; app.ui.toolbar_pos = float_it.then(|| at.map_or(TOOLS_AT, |p| [p.x, p.y])); app.ui.toolbar |= float_it;- return Ok(json!({ "panel": "tools", "floating": float_it, "pos": app.ui.toolbar_pos }));+ if !float_it {+ app.ui.toolbar_right = right;+ }+ let side = if app.ui.toolbar_right { "right" } else { "left" };+ return Ok(json!({ "panel": "tools", "floating": float_it, "pos": app.ui.toolbar_pos, "side": side })); } let Some(id) = crate::menus::normalize_panel(raw) else { return Err(format!("unknown panel `{raw}`")) }; let own = group_of(&app.ui, id);@@ -1628,6 +1657,73 @@ mod tests { assert_ne!(h.app.ui.toolbar_double, double); } + /// The Tools panel also docks on the right, dropped at the window's right edge or just left of+ /// the panel dock: it stands between the canvas and the panels (left of their icon column),+ /// stays there through a restart and a workspace, and its flyouts open on its left.+ #[test]+ fn the_tools_panel_docks_on_the_right_edge_too() {+ let mut h = Harness::new();+ let zone = h.temp_rect(tools_zone_id());+ h.drag(zone.left_top() + vec2(30.0, 7.0), pos2(400.0, 300.0));+ let float = h.ctx.memory(|m| m.area_rect(Id::new("toolbar-floating"))).unwrap();+ let edge = pos2(SCREEN.x - 10.0, 400.0);+ h.hold(float.left_top() + vec2(30.0, 7.0), edge);+ assert_eq!(drop_target(&h.app, &h.ctx, Moving::Tools, edge).map(|t| t.0), Some(Drop::DockRight));+ h.release(edge);+ assert_eq!((h.app.ui.toolbar_pos, h.app.ui.toolbar_right), (None, true), "docked on the right");+ h.settle();+ let bar = h.temp_rect(crate::edit_toolbar::toolbar_id());+ let dock = crate::toolbar::dock_left(&h.app, &h.ctx).expect("the panel dock shows");+ assert!(dock < SCREEN.x - 40.0, "the panels keep the window's right edge: {dock}");+ assert!(bar.right() <= dock + 1.0 && bar.right() > dock - 12.0, "left of the panel dock at {dock}: {bar:?}");+ // A flyout opens on the toolbar's left, toward the canvas, not over the panels.+ let screen = Rect::from_min_size(Pos2::ZERO, SCREEN);+ let room = crate::toolbar::room(&h.app, &h.ctx);+ assert!((room.right() - dock).abs() < 1.0, "room ends at the panels: {room:?}");+ let x = crate::toolbar::beside(bar, 160.0, 8.0, room);+ assert!(x + 160.0 <= bar.left() - 7.0, "left of the toolbar: {x}");+ assert_eq!(crate::toolbar::beside(Rect::from_min_size(pos2(0.0, 50.0), vec2(40.0, 40.0)), 160.0, 8.0, screen), 48.0, "right of it elsewhere");+ // Kept in the preferences and in a saved workspace; Essentials docks it on the left.+ let prefs: UiState = serde_json::from_slice(&serde_json::to_vec(&h.app.ui).unwrap()).unwrap();+ assert!(prefs.sanitized().toolbar_right);+ h.app.run("window.workspace.new", json!({"name": "Right Tools"})).unwrap();+ h.app.run("window.workspace", json!({"name": "Essentials"})).unwrap();+ assert!(!h.app.ui.toolbar_right);+ h.app.run("window.workspace", json!({"name": "Right Tools"})).unwrap();+ assert!(h.app.ui.toolbar_right);+ // Dragged back to the left edge, it docks there again.+ h.settle();+ let bar = h.temp_rect(crate::edit_toolbar::toolbar_id());+ h.drag(bar.left_top() + vec2(30.0, 7.0), pos2(400.0, 300.0));+ let float = h.ctx.memory(|m| m.area_rect(Id::new("toolbar-floating"))).unwrap();+ h.drag(float.left_top() + vec2(30.0, 7.0), pos2(10.0, 400.0));+ assert_eq!((h.app.ui.toolbar_pos, h.app.ui.toolbar_right), (None, false));+ // Dropped just left of the panel dock (not at the window's edge), it docks there too.+ h.settle();+ let bar = h.temp_rect(crate::edit_toolbar::toolbar_id());+ h.drag(bar.left_top() + vec2(30.0, 7.0), pos2(400.0, 300.0));+ let float = h.ctx.memory(|m| m.area_rect(Id::new("toolbar-floating"))).unwrap();+ let dock = crate::toolbar::dock_left(&h.app, &h.ctx).expect("the panel dock shows");+ let beside = pos2(dock - 10.0, 400.0);+ h.hold(float.left_top() + vec2(30.0, 7.0), beside);+ assert_eq!(drop_target(&h.app, &h.ctx, Moving::Tools, beside).map(|t| t.0), Some(Drop::DockRight));+ h.release(beside);+ assert_eq!((h.app.ui.toolbar_pos, h.app.ui.toolbar_right), (None, true));+ }++ #[test]+ fn the_dock_command_takes_the_tools_panels_side() {+ let mut app = VectorcraftApp::new(Session::new(), Default::default());+ let r = app.run("window.panel.dock", json!({"panel": "tools", "side": "right"})).unwrap();+ assert_eq!((r["side"].as_str(), app.ui.toolbar_right), (Some("right"), true));+ app.run("window.panel.float", json!({"panel": "tools"})).unwrap();+ let r = app.run("window.panel.dock", json!({"panel": "tools"})).unwrap();+ assert_eq!(r["side"], "right", "back where it docked last");+ app.run("window.panel.dock", json!({"panel": "tools", "side": "LEFT"})).unwrap();+ assert!(!app.ui.toolbar_right);+ assert!(app.run("window.panel.dock", json!({"panel": "tools", "side": "top"})).is_err());+ }+ #[test] fn floating_panels_are_kept_in_workspaces_and_preferences_and_clamped_into_the_window() { let mut h = Harness::new();@@ -3693,7 +3693,7 @@ axis Both И двете Drag to float the panel group Плъзнете, за да направите групата от панели плаваща Put back in the dock Връщане в дока Drag to float the toolbar Плъзнете, за да направите лентата с инструменти плаваща- Drag to the window's left edge to dock Плъзнете до левия ръб на прозореца за закотвяне+ Drag to the window's left or right edge to dock Плъзнете до левия или десния ръб на прозореца за закотвяне Float Tool Group Плаваща група инструменти @msg no library `{id}` няма библиотека `{id}` @msg select some type first първо изберете текст@@ -5472,3 +5472,5 @@ axis Y Axis Y ос Rectangular Grid Tool Options Опции на инструмента „Правоъгълна мрежа“ Polar Grid Tool Options Опции на инструмента „Полярна мрежа“ Fill Line Запълване на линия+@msg the EPS Options dialog didn't open диалоговият прозорец „Опции за EPS“ не се отвори+@msg side is left or right, not `{s}` side е left или right, а не `{s}`@@ -1192,7 +1192,7 @@ lighting Light Světlo X: X: Y: Y: Drag to float the toolbar Přetažením vytvoříte plovoucí panel nástrojů- Drag to the window's left edge to dock Přetažením k levému okraji okna panel ukotvíte+ Drag to the window's left or right edge to dock Přetažením k levému nebo pravému okraji okna panel ukotvíte # Tools Crystallize Krystalizace@@ -4556,6 +4556,7 @@ imageTrace Outline Obrys @msg the PDF is password-protected: give its password PDF je chráněno heslem: zadejte heslo @msg the PDF password is wrong heslo k PDF je chybné @msg the Save PDF dialog didn't open dialogové okno Uložit PDF se neotevřelo+@msg the EPS Options dialog didn't open dialogové okno Možnosti EPS se neotevřelo @msg the WMF header is damaged hlavička WMF je poškozená @msg the art could not be sampled z kresby nelze nabrat vzorek @msg the art has no bounds kresba nemá hranice@@ -5533,3 +5534,4 @@ axis Y Axis osa Y Rectangular Grid Tool Options Možnosti nástroje Obdélníková mřížka Polar Grid Tool Options Možnosti nástroje Polární mřížka Fill Line Výplň úsečky+@msg side is left or right, not `{s}` side je left nebo right, ne `{s}`@@ -1113,7 +1113,7 @@ lighting Light Lys X: X: Y: Y: Drag to float the toolbar Træk for at frigøre værktøjslinjen- Drag to the window's left edge to dock Træk til vinduets venstre kant for at fastgøre+ Drag to the window's left or right edge to dock Træk til vinduets venstre eller højre kant for at fastgøre Crystallize Krystallisér Add Anchor Point Tool Tilføj ankerpunkt-værktøj Anchor Point Tool Ankerpunktværktøj@@ -5488,3 +5488,5 @@ axis Y Axis Y-akse Rectangular Grid Tool Options Indstillinger for rektangulært gitterværktøj Polar Grid Tool Options Indstillinger for polært gitterværktøj Fill Line Udfyld linje+@msg the EPS Options dialog didn't open dialogen EPS-indstillinger blev ikke åbnet+@msg side is left or right, not `{s}` side er left eller right, ikke `{s}`@@ -3690,7 +3690,7 @@ theme Appearance Erscheinungsbild Drag to float the panel group Ziehen, um die Bedienfeldgruppe abzulösen Put back in the dock Zurück ins Dock setzen Drag to float the toolbar Ziehen, um die Werkzeugleiste abzulösen- Drag to the window's left edge to dock Zum Andocken an den linken Fensterrand ziehen+ Drag to the window's left or right edge to dock Zum Andocken an den linken oder rechten Fensterrand ziehen Float Tool Group Werkzeuggruppe ablösen # Status and error messages (`@msg`), translated where the status bar shows them; the message itself@@ -4179,6 +4179,7 @@ theme Appearance Erscheinungsbild @msg the PDF is password-protected: give its password die PDF-Datei ist kennwortgeschützt: Geben Sie ihr Kennwort an @msg the PDF password is wrong das PDF-Kennwort ist falsch @msg the Save PDF dialog didn't open das Dialogfeld „PDF speichern“ wurde nicht geöffnet+@msg the EPS Options dialog didn't open das Dialogfeld „EPS-Optionen“ wurde nicht geöffnet @msg the WMF header is damaged der WMF-Header ist beschädigt @msg the art could not be sampled die Grafik konnte nicht abgetastet werden @msg the art has no bounds die Grafik hat keine Begrenzung@@ -5115,3 +5116,4 @@ axis Y Axis Y-Achse Rectangular Grid Tool Options Optionen für das Rechteckiges-Raster-Werkzeug Polar Grid Tool Options Optionen für das Radiales-Raster-Werkzeug Fill Line Linie füllen+@msg side is left or right, not `{s}` side ist left oder right, nicht `{s}`@@ -3894,7 +3894,7 @@ theme Appearance Εμφάνιση Drag to float the panel group Σύρετε για αποδέσμευση της ομάδας πινάκων Put back in the dock Επαναφορά στην περιοχή σύνδεσης Drag to float the toolbar Σύρετε για αποδέσμευση της γραμμής εργαλείων- Drag to the window's left edge to dock Σύρετε στο αριστερό άκρο του παραθύρου για σύνδεση+ Drag to the window's left or right edge to dock Σύρετε στο αριστερό ή στο δεξί άκρο του παραθύρου για σύνδεση Float Tool Group Αποδέσμευση ομάδας εργαλείων @msg a PostScript error ({name} in `{op}`) σφάλμα PostScript ({name} στην εντολή `{op}`) @msg a PostScript error ({name} in `{op}`), in {procs} σφάλμα PostScript ({name} στην εντολή `{op}`), στις διαδικασίες {procs}@@ -5485,3 +5485,5 @@ axis Y Axis Άξονας Y Rectangular Grid Tool Options Επιλογές εργαλείου ορθογώνιου πλέγματος Polar Grid Tool Options Επιλογές εργαλείου πολικού πλέγματος Fill Line Γέμισμα γραμμής+@msg the EPS Options dialog didn't open δεν άνοιξε το παράθυρο Επιλογές EPS+@msg side is left or right, not `{s}` το side είναι left ή right, όχι `{s}`@@ -3766,7 +3766,7 @@ axis Both Ambos Drag to float the panel group Arrastrar para hacer flotar el grupo de paneles Put back in the dock Devolver al área de paneles Drag to float the toolbar Arrastrar para hacer flotar la barra de herramientas- Drag to the window's left edge to dock Arrastrar al borde izquierdo de la ventana para anclar+ Drag to the window's left or right edge to dock Arrastrar al borde izquierdo o derecho de la ventana para anclar Float Tool Group Hacer flotar el grupo de herramientas # Status and error messages (`@msg`), translated where the status bar shows them; the message itself@@ -4254,6 +4254,7 @@ axis Both Ambos @msg the PDF is password-protected: give its password el PDF está protegido con contraseña: indica su contraseña @msg the PDF password is wrong la contraseña del PDF es incorrecta @msg the Save PDF dialog didn't open el cuadro de diálogo Guardar PDF no se abrió+@msg the EPS Options dialog didn't open el cuadro de diálogo Opciones EPS no se abrió @msg the WMF header is damaged el encabezado WMF está dañado @msg the art could not be sampled no se pudo muestrear la ilustración @msg the art has no bounds la ilustración no tiene límites@@ -5475,3 +5476,4 @@ axis Y Axis Eje Y Rectangular Grid Tool Options Opciones de la herramienta Cuadrícula rectangular Polar Grid Tool Options Opciones de la herramienta Cuadrícula polar Fill Line Rellenar línea+@msg side is left or right, not `{s}` side es left o right, no `{s}`@@ -1170,7 +1170,7 @@ lighting Light Valo X: X: Y: Y: Drag to float the toolbar Kelluta työkaluriviä vetämällä- Drag to the window's left edge to dock Telakoi vetämällä ikkunan vasempaan reunaan+ Drag to the window's left or right edge to dock Telakoi vetämällä ikkunan vasempaan tai oikeaan reunaan Crystallize Kiteytä Add Anchor Point Tool Lisää ankkuripiste -työkalu Anchor Point Tool Ankkuripistetyökalu@@ -5564,3 +5564,5 @@ axis Y Axis Y-akseli Rectangular Grid Tool Options Suorakulmaisen ruudukon työkalun asetukset Polar Grid Tool Options Napakoordinaattiruudukon työkalun asetukset Fill Line Täytä viiva+@msg the EPS Options dialog didn't open valintaikkuna EPS-asetukset ei avautunut+@msg side is left or right, not `{s}` side on left tai right, ei `{s}`@@ -3763,7 +3763,7 @@ axis Both Les deux Drag to float the panel group Faire glisser pour rendre le groupe de panneaux flottant Put back in the dock Remettre dans la zone d’ancrage Drag to float the toolbar Faire glisser pour rendre la barre d’outils flottante- Drag to the window's left edge to dock Faire glisser vers le bord gauche de la fenêtre pour ancrer+ Drag to the window's left or right edge to dock Faire glisser vers le bord gauche ou droit de la fenêtre pour ancrer Float Tool Group Rendre le groupe d’outils flottant # Status and error messages (`@msg`), translated where the status bar shows them; the message itself@@ -4251,6 +4251,7 @@ axis Both Les deux @msg the PDF is password-protected: give its password le PDF est protégé par un mot de passe : indiquez-le @msg the PDF password is wrong le mot de passe du PDF est incorrect @msg the Save PDF dialog didn't open la boîte de dialogue Enregistrer au format PDF ne s’est pas ouverte+@msg the EPS Options dialog didn't open la boîte de dialogue Options EPS ne s’est pas ouverte @msg the WMF header is damaged l’en-tête WMF est endommagé @msg the art could not be sampled impossible d’échantillonner l’illustration @msg the art has no bounds l’illustration n’a pas de limites@@ -5532,3 +5533,4 @@ axis Y Axis Axe Y Rectangular Grid Tool Options Options de l’outil Grille rectangulaire Polar Grid Tool Options Options de l’outil Grille à coordonnées polaires Fill Line Remplir le trait+@msg side is left or right, not `{s}` side vaut left ou right, pas `{s}`@@ -1102,7 +1102,7 @@ font-technology Variable Változó Drag to float the toolbar Húzd el az eszköztár lebegtetéséhez Drag to move Húzással áthelyezhető Drag to move out of the set Húzással kivehető a csoportból- Drag to the window's left edge to dock Dokkoláshoz húzd az ablak bal szélére+ Drag to the window's left or right edge to dock Dokkoláshoz húzd az ablak bal vagy jobb szélére Step up or down Léptetés fel vagy le Drag values or labels. Double-click a value to type. Húzd az értékeket vagy a címkéket. Gépeléshez kattints duplán egy értékre. Draw Rajzolás@@ -5034,6 +5034,7 @@ imageTrace Outline Körvonal @msg the PSD file's image data uses an unknown compression ({m}) a PSD-fájl képadatai ismeretlen tömörítést használnak ({m}) @msg the PSD image is too large to read ({_1} × {_2} pixels) a PSD-kép túl nagy a beolvasáshoz ({_1} × {_2} pixel) @msg the Save PDF dialog didn't open a PDF mentése párbeszédablak nem nyílt meg+@msg the EPS Options dialog didn't open az EPS-beállítások párbeszédablak nem nyílt meg @msg the scale is not a usable number a méretarány nem használható szám @msg the search couldn't start: try again in a moment a keresés nem indult el: próbáld újra egy kicsit később @msg the selected stroke's profile is already listed as {_1} a kijelölt körvonal profilja már szerepel a listán: {_1}@@ -5519,3 +5520,4 @@ axis Y Axis Y tengely Rectangular Grid Tool Options Téglalaprács beállításai Polar Grid Tool Options Polárrács beállításai Fill Line Szakasz kitöltése+@msg side is left or right, not `{s}` a side értéke left vagy right lehet, nem `{s}`@@ -1113,7 +1113,7 @@ lighting Light Cahaya X: X: Y: Y: Drag to float the toolbar Seret untuk mengambangkan bilah alat- Drag to the window's left edge to dock Seret ke tepi kiri jendela untuk memasang+ Drag to the window's left or right edge to dock Seret ke tepi kiri atau kanan jendela untuk memasang Crystallize Kristalisasi Add Anchor Point Tool Alat tambah titik jangkar Anchor Point Tool Alat titik jangkar@@ -5488,3 +5488,5 @@ axis Y Axis Sumbu Y Rectangular Grid Tool Options Opsi alat kisi persegi panjang Polar Grid Tool Options Opsi alat kisi polar Fill Line Isi garis+@msg the EPS Options dialog didn't open dialog Opsi EPS tidak terbuka+@msg side is left or right, not `{s}` side adalah left atau right, bukan `{s}`@@ -3806,7 +3806,7 @@ axis Both Entrambi Drag to float the panel group Trascina per rendere mobile il gruppo di pannelli Put back in the dock Riporta nell'area dei pannelli Drag to float the toolbar Trascina per rendere mobile la barra degli strumenti- Drag to the window's left edge to dock Trascina sul bordo sinistro della finestra per ancorare+ Drag to the window's left or right edge to dock Trascina sul bordo sinistro o destro della finestra per ancorare Float Tool Group Rendi mobile gruppo strumenti # Status and error messages (`@msg`), translated where the status bar shows them; the message itself@@ -4294,6 +4294,7 @@ axis Both Entrambi @msg the PDF is password-protected: give its password il PDF è protetto da password: specifica la password @msg the PDF password is wrong la password del PDF è errata @msg the Save PDF dialog didn't open la finestra di dialogo Salva PDF non si è aperta+@msg the EPS Options dialog didn't open la finestra di dialogo Opzioni EPS non si è aperta @msg the WMF header is damaged l'intestazione WMF è danneggiata @msg the art could not be sampled impossibile campionare la grafica @msg the art has no bounds la grafica non ha limiti@@ -5149,3 +5150,4 @@ axis Y Axis Asse Y Rectangular Grid Tool Options Opzioni strumento Griglia rettangolare Polar Grid Tool Options Opzioni strumento Griglia polare Fill Line Riempi linea+@msg side is left or right, not `{s}` side è left o right, non `{s}`@@ -3743,7 +3743,7 @@ axis Both 両方 Drag to float the panel group ドラッグしてパネルグループをフローティング Put back in the dock ドックに戻す Drag to float the toolbar ドラッグしてツールバーをフローティング- Drag to the window's left edge to dock ウィンドウの左端までドラッグしてドッキング+ Drag to the window's left or right edge to dock ウィンドウの左端または右端までドラッグしてドッキング Float Tool Group ツールグループをフローティング # The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.@@ -4424,6 +4424,7 @@ swatch Bright Violet ビビッドバイオレット @msg no file manager here この環境にはファイルマネージャーがありません @msg no revert command 「保存済みの状態に戻す」コマンドがありません @msg the Save PDF dialog didn't open PDF 保存ダイアログが開きませんでした+@msg the EPS Options dialog didn't open EPS オプションダイアログが開きませんでした @msg the document has never been saved ドキュメントはまだ一度も保存されていません @msg the export returned unreadable data 書き出し処理から読み取れないデータが返されました @msg {status} ({e}) {status} ({e})@@ -5211,3 +5212,4 @@ axis Y Axis Y 軸 Rectangular Grid Tool Options 長方形グリッドツールオプション Polar Grid Tool Options 同心円グリッドツールオプション Fill Line 線を塗りつぶす+@msg side is left or right, not `{s}` side は left または right です(`{s}` ではありません)@@ -1043,7 +1043,7 @@ lighting Light სინათლე Toggle single/double column ერთი/ორი სვეტის გადართვა W: სიგ.: Drag to float the toolbar გადაათრიეთ ხელსაწყოთა ზოლის მცურავად გადასაქცევად- Drag to the window's left edge to dock მისამაგრებლად გადაათრიეთ ფანჯრის მარცხენა კიდესთან+ Drag to the window's left or right edge to dock მისამაგრებლად გადაათრიეთ ფანჯრის მარცხენა ან მარჯვენა კიდესთან Crystallize კრისტალიზაცია Add Anchor Point Tool საყრდენი წერტილის დამატების ხელსაწყო Anchor Point Tool საყრდენი წერტილის ხელსაწყო@@ -5449,3 +5449,5 @@ axis Y Axis Y ღერძი Rectangular Grid Tool Options მართკუთხა ბადის ხელსაწყოს პარამეტრები Polar Grid Tool Options პოლარული ბადის ხელსაწყოს პარამეტრები Fill Line ხაზის შევსება+@msg the EPS Options dialog didn't open EPS-ის პარამეტრების დიალოგი არ გაიხსნა+@msg side is left or right, not `{s}` side არის left ან right და არა `{s}`@@ -3732,7 +3732,7 @@ axis Both 양쪽 Drag to float the panel group 드래그하여 패널 그룹 분리 Put back in the dock 독으로 되돌리기 Drag to float the toolbar 드래그하여 도구 모음 분리- Drag to the window's left edge to dock 창 왼쪽 가장자리로 드래그하여 고정+ Drag to the window's left or right edge to dock 창 왼쪽 또는 오른쪽 가장자리로 드래그하여 고정 Float Tool Group 도구 그룹 분리 Services 서비스 Hide VectorCraft VectorCraft 숨기기@@ -5112,3 +5112,5 @@ axis Y Axis Y축 Rectangular Grid Tool Options 사각형 격자 도구 옵션 Polar Grid Tool Options 극좌표 격자 도구 옵션 Fill Line 선 채우기+@msg the EPS Options dialog didn't open EPS 옵션 대화상자가 열리지 않았습니다+@msg side is left or right, not `{s}` side는 left 또는 right입니다(`{s}` 아님)@@ -1127,7 +1127,7 @@ lighting Light Licht X: X: Y: Y: Drag to float the toolbar Sleep om de werkbalk zwevend te maken- Drag to the window's left edge to dock Sleep naar de linkerrand van het venster om te dokken+ Drag to the window's left or right edge to dock Sleep naar de linker- of rechterrand van het venster om te dokken Crystallize Kristalliseren Add Anchor Point Tool Ankerpunt toevoegen Anchor Point Tool Ankerpunt-gereedschap@@ -5531,3 +5531,5 @@ axis Y Axis Y-as Rectangular Grid Tool Options Opties rechthoekig-rastergereedschap Polar Grid Tool Options Opties polair-rastergereedschap Fill Line Lijn vullen+@msg the EPS Options dialog didn't open het dialoogvenster EPS-opties is niet geopend+@msg side is left or right, not `{s}` side is left of right, niet `{s}`@@ -1121,7 +1121,7 @@ lighting Light Światło Toggle single/double column Przełącz jedną/dwie kolumny W: S: Drag to float the toolbar Przeciągnij, aby odpiąć pasek narzędzi- Drag to the window's left edge to dock Przeciągnij do lewej krawędzi okna, aby zadokować+ Drag to the window's left or right edge to dock Przeciągnij do lewej lub prawej krawędzi okna, aby zadokować Crystallize Krystalizacja Add Anchor Point Tool Narzędzie Dodaj punkt kontrolny Anchor Point Tool Narzędzie Punkt kontrolny@@ -5385,3 +5385,5 @@ axis Y Axis Oś Y Rectangular Grid Tool Options Opcje narzędzia Siatka prostokątna Polar Grid Tool Options Opcje narzędzia Siatka biegunowa Fill Line Wypełnij odcinek+@msg the EPS Options dialog didn't open okno Opcje EPS nie otworzyło się+@msg side is left or right, not `{s}` side to left lub right, a nie `{s}`@@ -3356,7 +3356,7 @@ axis Both Ambos Drag to float the panel group Arraste para flutuar o grupo de painéis Put back in the dock Devolver ao dock Drag to float the toolbar Arraste para flutuar a barra de ferramentas- Drag to the window's left edge to dock Arraste até a borda esquerda da janela para encaixar+ Drag to the window's left or right edge to dock Arraste até a borda esquerda ou direita da janela para encaixar @msg no library `{id}` nenhuma biblioteca `{id}` @msg select some type first selecione algum texto primeiro @msg {_1} has no {_2} style {_1} não tem o estilo {_2}@@ -3827,6 +3827,7 @@ axis Both Ambos @msg the PDF is password-protected: give its password o PDF é protegido por senha: informe a senha @msg the PDF password is wrong a senha do PDF está incorreta @msg the Save PDF dialog didn't open a caixa de diálogo Salvar PDF não abriu+@msg the EPS Options dialog didn't open a caixa de diálogo Opções de EPS não abriu @msg the WMF header is damaged o cabeçalho WMF está danificado @msg the art could not be sampled não foi possível amostrar a arte @msg the art has no bounds a arte não tem limites@@ -5450,3 +5451,4 @@ axis Y Axis Eixo Y Rectangular Grid Tool Options Opções da Ferramenta Grade Retangular Polar Grid Tool Options Opções da Ferramenta Grade Polar Fill Line Preencher linha+@msg side is left or right, not `{s}` side é left ou right, não `{s}`@@ -3802,7 +3802,7 @@ axis Both Оба Drag to float the panel group Перетащите, чтобы сделать группу панелей плавающей Put back in the dock Вернуть в док Drag to float the toolbar Перетащите, чтобы сделать панель инструментов плавающей- Drag to the window's left edge to dock Перетащите к левому краю окна для прикрепления+ Drag to the window's left or right edge to dock Перетащите к левому или правому краю окна для прикрепления Float Tool Group Сделать группу инструментов плавающей # Status and error messages (`@msg`), translated where the status bar shows them; the message itself@@ -4290,6 +4290,7 @@ axis Both Оба @msg the PDF is password-protected: give its password PDF защищён паролем: укажите его пароль @msg the PDF password is wrong неверный пароль PDF @msg the Save PDF dialog didn't open диалоговое окно «Сохранить PDF» не открылось+@msg the EPS Options dialog didn't open диалоговое окно «Параметры EPS» не открылось @msg the WMF header is damaged заголовок WMF повреждён @msg the art could not be sampled не удалось взять образец рисунка @msg the art has no bounds у рисунка нет границ@@ -5145,3 +5146,4 @@ axis Y Axis Ось Y Rectangular Grid Tool Options Параметры инструмента «Прямоугольная сетка» Polar Grid Tool Options Параметры инструмента «Полярная сетка» Fill Line Заливка отрезка+@msg side is left or right, not `{s}` side — это left или right, а не `{s}`@@ -3693,7 +3693,7 @@ axis Both Båda Drag to float the panel group Dra för att göra panelgruppen flytande Put back in the dock Lägg tillbaka i dockan Drag to float the toolbar Dra för att göra verktygsfältet flytande- Drag to the window's left edge to dock Dra till fönstrets vänsterkant för att docka+ Drag to the window's left or right edge to dock Dra till fönstrets vänster- eller högerkant för att docka Float Tool Group Gör verktygsgruppen flytande @msg no library `{id}` inget bibliotek `{id}` @msg select some type first markera någon text först@@ -5489,3 +5489,5 @@ axis Y Axis Y-axel Rectangular Grid Tool Options Alternativ för verktyget för rektangulärt rutnät Polar Grid Tool Options Alternativ för verktyget för polärt rutnät Fill Line Fyll linje+@msg the EPS Options dialog didn't open dialogrutan EPS-alternativ öppnades inte+@msg side is left or right, not `{s}` side är left eller right, inte `{s}`@@ -3795,7 +3795,7 @@ axis Both Обидві Drag to float the panel group Перетягніть, щоб від’єднати групу панелей Put back in the dock Повернути у док Drag to float the toolbar Перетягніть, щоб від’єднати панель інструментів- Drag to the window's left edge to dock Перетягніть до лівого краю вікна для прикріплення+ Drag to the window's left or right edge to dock Перетягніть до лівого або правого краю вікна для прикріплення Float Tool Group Від’єднати групу інструментів @msg no library `{id}` немає бібліотеки `{id}` @msg select some type first спочатку виділіть текст@@ -4278,6 +4278,7 @@ axis Both Обидві @msg the PDF is password-protected: give its password PDF захищений паролем: вкажіть його пароль @msg the PDF password is wrong неправильний пароль PDF @msg the Save PDF dialog didn't open діалогове вікно «Зберегти PDF» не відкрилося+@msg the EPS Options dialog didn't open діалогове вікно «Параметри EPS» не відкрилося @msg the WMF header is damaged заголовок WMF пошкоджений @msg the art could not be sampled не вдалося взяти зразок малюнка @msg the art has no bounds малюнок не має меж@@ -5129,3 +5130,4 @@ axis Y Axis Вісь Y Rectangular Grid Tool Options Параметри інструменту «Прямокутна сітка» Polar Grid Tool Options Параметри інструменту «Полярна сітка» Fill Line Заливка відрізка+@msg side is left or right, not `{s}` side — це left або right, а не `{s}`@@ -986,7 +986,7 @@ font-technology Variable Biến thiên Drag to float the toolbar Kéo để thả nổi thanh công cụ Drag to move Kéo để di chuyển Drag to move out of the set Kéo để tách khỏi bộ- Drag to the window's left edge to dock Kéo đến cạnh trái cửa sổ để neo+ Drag to the window's left or right edge to dock Kéo đến cạnh trái hoặc phải cửa sổ để neo Step up or down Tăng hoặc giảm một bước Drag values or labels. Double-click a value to type. Kéo giá trị hoặc nhãn. Bấm đúp vào giá trị để nhập. Draw Vẽ@@ -5129,3 +5129,5 @@ axis Y Axis Trục Y Rectangular Grid Tool Options Tùy chọn công cụ Lưới chữ nhật Polar Grid Tool Options Tùy chọn công cụ Lưới cực Fill Line Tô đoạn thẳng+@msg the EPS Options dialog didn't open hộp thoại Tùy chọn EPS không mở+@msg side is left or right, not `{s}` side là left hoặc right, không phải `{s}`@@ -4764,7 +4764,7 @@ axis Both 两者 Drag to float the panel group 拖动以浮动面板组 Put back in the dock 放回停放区 Drag to float the toolbar 拖动以浮动工具栏- Drag to the window's left edge to dock 拖到窗口左边缘以停放+ Drag to the window's left or right edge to dock 拖到窗口左边缘或右边缘以停放 Float Tool Group 浮动工具组 # The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.@@ -5372,3 +5372,5 @@ axis Y Axis Y 轴 Rectangular Grid Tool Options 矩形网格工具选项 Polar Grid Tool Options 极坐标网格工具选项 Fill Line 填充线段+@msg the EPS Options dialog didn't open “EPS 选项”对话框未打开+@msg side is left or right, not `{s}` side 应为 left 或 right,而不是 `{s}`@@ -4568,7 +4568,7 @@ axis Both 兩者 Drag to float the panel group 拖曳以浮動面板群組 Put back in the dock 放回停駐區 Drag to float the toolbar 拖曳以浮動工具列- Drag to the window's left edge to dock 拖曳到視窗左側邊緣以停駐+ Drag to the window's left or right edge to dock 拖曳到視窗左側或右側邊緣以停駐 Float Tool Group 浮動工具組 # The macOS menu bar (native_menu): the app and Window menus' system items, Settings pages.@@ -640,10 +640,11 @@ fn has_options(p: &Value) -> bool { } /// Ask for format `f`'s options before a save (`mode`) to `path`: SVG Options for SVG, the Save-/// PDF dialog for PDF, else the save options dialog. OK finishes the save.+/// PDF dialog for PDF, EPS Options for EPS, else the save options dialog. OK finishes the save. pub fn ask_format_options(app: &mut VectorcraftApp, mode: SaveMode, f: &Format, path: &str) -> Result<Value, String> { match f.id { "pdf" => ask_pdf_options(app, mode, path),+ "eps" => ask_eps_options(app, mode, path), "svg" | "svgz" => { let m = if mode == SaveMode::Copy { svg_options::Mode::SaveCopy } else { svg_options::Mode::Save }; svg_options::open(app, m, Some(path));@@ -664,6 +665,17 @@ pub fn ask_pdf_options(app: &mut VectorcraftApp, mode: SaveMode, path: &str) -> Ok(json!({ "pending": d.kind })) } +/// A save as EPS asks with EPS Options, filled with the EPS options the document was last saved+/// with (else those used last); its OK finishes the save.+fn ask_eps_options(app: &mut VectorcraftApp, mode: SaveMode, path: &str) -> Result<Value, String> {+ let mut p = Value::Object(plan(app, mode, &json!({ "path": path, "format": "eps" }))?.options);+ p["path"] = json!(path);+ dialogs::eps_options::open(app, &p);+ let d = app.ui.dialog.as_mut().ok_or("the EPS Options dialog didn't open")?;+ d.fields.insert("__save".into(), json!(mode.command()));+ Ok(json!({ "pending": d.kind }))+}+ /// The Save PDF dialog's OK during a save (`mode`): `params` are its `document.exportPdf` options /// and `path`. pub fn save_pdf(app: &mut VectorcraftApp, mode: SaveMode, mut params: Value) -> Result<Value, String> {@@ -1610,13 +1610,19 @@ impl VectorcraftApp { // Keeps the previous frame's value valid for menus drawn above, before the bars. widgets::set_menu_floor(&ctx, ctx.content_rect().bottom()); }- if self.ui.toolbar && self.ui.screen_mode < 3 {+ // Docked right, the Tools panel stands between the canvas and the panel dock: laid out+ // after the dock, which takes the window's right edge first.+ let tools_after_dock = self.ui.toolbar_right && self.ui.toolbar_pos.is_none();+ if self.ui.toolbar && self.ui.screen_mode < 3 && !tools_after_dock { toolbar::show(self, ui); } panels::library_panel::tidy(self); if self.ui.dock && self.ui.screen_mode < 3 { dock::show(self, ui); }+ if self.ui.toolbar && self.ui.screen_mode < 3 && tools_after_dock {+ toolbar::show(self, ui);+ } egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| { if self.ui.screen_mode < 3 { chrome::doc_tabs(self, ui);@@ -487,7 +487,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "window.panel.dock", "Dock Panel", "",- "{panel: id or \"tools\", group?: bool, column?: true or n, anchor?: id, zone?: \"tab\" | \"top\" | \"bottom\" | \"left\" | \"right\" | \"center\", before?: id, icon?: bool} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; with `anchor`, in the dock group holding that panel instead: `zone` \"tab\" (the default) as a tab of it (before the `before` panel's tab), as dropping it on that group's tabs does, \"top\" / \"bottom\" as a new group above / below it, as dropping it on that group's upper / lower half does (\"left\", \"right\" and \"center\" split or join it likewise); with `icon: true` (no `anchor`), as icons in the icon column, the tabbed group's panels too, as dropping it on the icon column does: it lives there from then on and pops out of its icon; with `column`, dock its group and the rest of its set as a dock column instead, as dropping it on the left edge of the icon column or of a dock column does: `true` at the dock's left, n at that place (1: next to the icon column, counting left); \"tools\" docks the Tools panel at the window's left edge. Returns {panel, floating, column}, or with `anchor` or `icon: true` (the shared layout) {panel, floating, layout} and no `column`",+ "{panel: id or \"tools\", group?: bool, column?: true or n, anchor?: id, zone?: \"tab\" | \"top\" | \"bottom\" | \"left\" | \"right\" | \"center\", before?: id, icon?: bool} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; with `anchor`, in the dock group holding that panel instead: `zone` \"tab\" (the default) as a tab of it (before the `before` panel's tab), as dropping it on that group's tabs does, \"top\" / \"bottom\" as a new group above / below it, as dropping it on that group's upper / lower half does (\"left\", \"right\" and \"center\" split or join it likewise); with `icon: true` (no `anchor`), as icons in the icon column, the tabbed group's panels too, as dropping it on the icon column does: it lives there from then on and pops out of its icon; with `column`, dock its group and the rest of its set as a dock column instead, as dropping it on the left edge of the icon column or of a dock column does: `true` at the dock's left, n at that place (1: next to the icon column, counting left); \"tools\" docks the Tools panel at the window's left edge, or with side: right on the right, left of the panel dock (default: the side it docked at last), as dropping it there or on the window's right edge does. Returns {panel, floating, column, side?}, or with `anchor` or `icon: true` (the shared layout) {panel, floating, layout} and no `column`", ), ( "window.collapseDock",@@ -829,7 +829,8 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> { fn drop_command(panel: &str, target: crate::floating::Drop, whole_group: bool) -> (&'static str, Value) { use crate::floating::Drop; match target {- Drop::Dock => ("window.panel.dock", json!({"panel": panel, "group": whole_group})),+ // Only the Tools panel docks on the right; a panel dropped there docks as usual.+ Drop::Dock | Drop::DockRight => ("window.panel.dock", json!({"panel": panel, "group": whole_group})), Drop::Stack(onto) => ("window.panel.float", json!({"panel": panel, "group": whole_group, "onto": onto})), Drop::Below(onto) => ("window.panel.float", json!({"panel": panel, "group": whole_group, "below": onto})), Drop::Above(onto) => ("window.panel.float", json!({"panel": panel, "group": whole_group, "above": onto})),@@ -388,9 +388,13 @@ pub struct UiState { #[serde(skip)] pub dock_width_pending: bool, /// The Tools panel floats with its top-left corner here (dragged out by its title bar); `None`:- /// docked at the window's left edge.+ /// docked at the window's left edge (or right edge, [`Self::toolbar_right`]). #[serde(default)] pub toolbar_pos: Option<[f32; 2]>,+ /// The docked Tools panel stands on the right, left of the panel dock, instead of at the window's+ /// left edge (dropped there or on the window's right edge).+ #[serde(default)]+ pub toolbar_right: bool, pub status_bar: bool, pub dock: bool, /// The bars that Tab (`window.dock`) hid, which the next Tab shows again; none when Tab has not@@ -848,6 +852,7 @@ impl Default for UiState { dock_width: None, dock_width_pending: false, toolbar_pos: None,+ toolbar_right: false, status_bar: true, dock: true, tab_hid: None,@@ -16,7 +16,7 @@ use crate::theme::{self, Tokens}; use crate::{VectorcraftApp, icons, widgets}; const PITCH: f32 = 30.0;-const WIDTH: f32 = 48.0;+pub(crate) const WIDTH: f32 = 48.0; /// Seconds a press on a tool group's button is held before its flyout opens. const LONG_PRESS: f64 = 0.35; /// Width of the grab bar down a flyout's right side: dragging it tears the flyout off.@@ -101,13 +101,24 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // Two 36 pt buttons with 2 pt either side inside the border and separator line (at 76 they // overran the right border). let w = if cols == 2 { 80.0 } else { WIDTH };- // Where the panel docks: along the window's left edge, under the bars.+ // Where the panel docks: along the window's left or right edge, under the bars. let edge = ui.available_rect_before_wrap();- ui.ctx().data_mut(|d| d.insert_temp(crate::floating::tools_zone_id(), egui::Rect::from_min_size(edge.min, vec2(WIDTH, edge.height()))));+ // Docked right, it stands left of the panel dock (its icon column or its leftmost column),+ // between the canvas and the panels.+ let right = dock_left(app, ui.ctx()).map_or(edge.max.x, |x| x.min(edge.max.x));+ ui.ctx().data_mut(|d| {+ d.insert_temp(crate::floating::tools_zone_id(), egui::Rect::from_min_size(edge.min, vec2(WIDTH, edge.height())));+ d.insert_temp(crate::floating::tools_zone_right_id(), egui::Rect::from_min_max(pos2(right - WIDTH, edge.min.y), pos2(right, edge.max.y)));+ }); match app.ui.toolbar_pos { None => {- let margin = egui::Margin { left: 6, right: 0, top: 6, bottom: 6 };- egui::Panel::left("toolbar").resizable(false).exact_size(w + 6.0).frame(egui::Frame::NONE).show(ui, |ui| {+ // The card's outer gap faces the window edge it docks at.+ let (panel, margin) = if app.ui.toolbar_right {+ (egui::Panel::right("toolbar"), egui::Margin { left: 0, right: 6, top: 6, bottom: 6 })+ } else {+ (egui::Panel::left("toolbar"), egui::Margin { left: 6, right: 0, top: 6, bottom: 6 })+ };+ panel.resizable(false).exact_size(w + 6.0).frame(egui::Frame::NONE).show(ui, |ui| { crate::theme::card(ui, &t, margin, |ui| body(app, ui, cols, None)); }); }@@ -168,7 +179,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu { app.ui.status = e; }- let grip_tip = if floating.is_some() { tl!("Drag to the window's left edge to dock") } else { tl!("Drag to float the toolbar") };+ let grip_tip = if floating.is_some() { tl!("Drag to the window's left or right edge to dock") } else { tl!("Drag to float the toolbar") }; gresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), grip_tip)); gresp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(grip_tip); // The tools and the controls under them scroll in a window too short for them.@@ -572,6 +583,38 @@ fn raise_floating(ctx: &egui::Context, key: &str) { } /// A flyout's frame: drop shadow and border (the accent while it flashes).+/// The panel dock's left edge as last drawn (its icon column, or the leftmost dock column), while+/// it shows: where a toolbar docked right stands.+pub(crate) fn dock_left(app: &VectorcraftApp, ctx: &egui::Context) -> Option<f32> {+ if !app.ui.dock || app.ui.screen_mode >= 3 {+ return None;+ }+ let (left, dock) = ctx.data(|d| (d.get_temp::<f32>(crate::dock::dock_left_id()), d.get_temp::<egui::Rect>(crate::floating::dock_rect_id())));+ [left, dock.map(|r| r.left())].into_iter().flatten().filter(|x| x.is_finite()).reduce(f32::min)+}++/// Where the toolbar's flyouts and drawer may open: the window, or with the toolbar docked right+/// the part left of the panel dock, so they open toward the canvas instead of over the panels.+pub(crate) fn room(app: &VectorcraftApp, ctx: &egui::Context) -> egui::Rect {+ let mut r = ctx.content_rect();+ if app.ui.toolbar_right+ && app.ui.toolbar_pos.is_none()+ && let Some(x) = dock_left(app, ctx)+ {+ r.max.x = r.max.x.min(x);+ }+ r+}++/// The left edge of something `width` wide opened beside `anchor` (a tool, the toolbar), `gap`+/// from it: on its right when it fits in `screen`, else on its left (a toolbar docked at the+/// window's right edge), kept inside the screen either way.+pub(crate) fn beside(anchor: egui::Rect, width: f32, gap: f32, screen: egui::Rect) -> f32 {+ let right = anchor.right() + gap;+ let x = if right + width <= screen.right() { right } else { anchor.left() - gap - width };+ x.min(screen.right() - width).max(screen.left())+}+ pub(crate) fn flyout_frame(t: &Tokens, flash: bool) -> egui::Frame { egui::Frame::NONE .fill(t.panel)@@ -741,12 +784,12 @@ fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) { let t = Tokens::get(ctx); let active = app.session.tool_id(); let mut input = FlyoutInput::default();- let resp = egui::Area::new(egui::Id::new("tool-flyout")).order(egui::Order::Foreground).fixed_pos(anchor.right_top() + vec2(8.0, -1.0)).show(- ctx,- |ui| {- flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, active, &tools, false));- },- );+ // Beside the tool, on the side with room for it (left of a toolbar docked at the right edge).+ let size = ctx.memory(|m| m.area_rect(egui::Id::new("tool-flyout"))).map_or(vec2(160.0, 0.0), |r| r.size());+ let x = beside(anchor, size.x, 8.0, room(app, ctx));+ let resp = egui::Area::new(egui::Id::new("tool-flyout")).order(egui::Order::Foreground).fixed_pos(pos2(x, anchor.top() - 1.0)).show(ctx, |ui| {+ flyout_frame(&t, false).show(ui, |ui| input = flyout_body(ui, &t, active, &tools, false));+ }); // Dragging the bar tears the flyout off, and so does releasing a press over it (a click, or the // end of the long press that opened the flyout): it floats where the pointer is. let at = ctx.input(|i| i.pointer.interact_pos());@@ -52,6 +52,8 @@ pub struct Workspace { /// Main dock column width in screen points (#1753). #[serde(default, skip_serializing_if = "Option::is_none")] pub dock_width: Option<f32>,+ /// The docked Tools panel stands on the right, left of the panel dock (built-in workspaces: left).+ pub toolbar_right: bool, } impl Default for Workspace {@@ -186,6 +188,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace { docking_collapsed: ui.docking_collapsed.clone(), toolbar_pos: ui.toolbar_pos, dock_width: ui.dock_width,+ toolbar_right: ui.toolbar_right, } } @@ -213,6 +216,7 @@ pub fn apply(ui: &mut UiState, w: &Workspace) { ui.toolbar_pos = w.toolbar_pos; ui.dock_width = w.dock_width; ui.dock_width_pending = true;+ ui.toolbar_right = w.toolbar_right; FloatingPanels::sanitize(&mut ui.floating_panels, &mut ui.toolbar_pos); ui.flyout = None; ui.workspace = w.name.clone();@@ -18,7 +18,7 @@ from the public specifications, and users' own `.ai` files are used locally only | | Illustrator | VectorCraft | Notes | |---|---:|---:|---| | Formats opened or placed | ~30 | 25 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, JPEG 2000, PICT, PCX, Pixar, Word `.doc`, PS |-| Formats saved (Save As) | 6 (AI, AIT, PDF, EPS, SVG, SVGZ) | 6 + native | `.ai` written PDF-compatible (no Illustrator editing data) or as legacy version 7 (layers and groups, one artboard per file) |+| Formats saved (Save As) | 6 (AI, AIT, PDF, EPS, SVG, SVGZ) | 6 + native | `.ai` written PDF-compatible (no Illustrator editing data) or as legacy version 7 (layers and groups, one artboard per file); EPS saved with EPS Options, carrying the native document (reopens exactly) | | Formats exported | ~16 | 17 | missing: DWG, 3D (OBJ/USDA/glTF); extra: GIF/PNG-8 palette export, DXF up to 2018, layered PSD | | **Overall (weighted by use)** | | **~75%** | `.ai` and PDF dominate the weighting | @@ -130,6 +130,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | Date | Change | Summary | |---|---|---|+| 2026-10-18 | minor | EPS joins Save As and Save a Copy: EPS Options ask first, the file carries the native document, and Save writes it back | | 2026-10-24 | minor | `.ai` import: an image larger than the editing data's reader takes (2^25 pixels, a halftone texture at print resolution) no longer leaves its layer blank: the file comes in as its PDF part, which reads larger images, with a note saying why (#1479). Own-fixture test; scores and hours unchanged | | 2026-10-24 | minor | PDF export: a pattern fill or stroke is written as a PDF tiling pattern (ISO 32000-1, 8.7.3.1) of one period of the tiling, as the canvas repeats it, so grid, brick and hex tilings and overlapping tiles keep their look in a few kilobytes, and PDF import reads it back as one object with a pattern swatch instead of hundreds of tile paths (#3849). Patterns nested more than four deep, or with tiles beyond a billion points, are still written as their tiles, now with a warning. Tests: the issue's hatch round trip, a brick tiling read back against the canvas, the nesting warning; scores and hours unchanged | | 2026-10-23 | minor | SVG: type written as outlines stacks each run's colour glyphs (bitmap images, COLR and SVG art) under the run's paint, as the canvas does, so a plain character set over a colour one stays on top; scores unchanged |@@ -373,7 +373,7 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr | `open_panel` | `{panel}` | Remote only. `panel` is a panel id (`layers`, `swatches`, `colorGuide`, …, as `window.panel` takes) or its display label (`"Color Guide"`), in any case. | | `screenshot` | `{path?, scale?, artboard?, window?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard; `window:true` captures the app window (remote only). With `path` the backend (the headless session or the app) writes the PNG there too. | | `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, PNG, JPEG, GIF, WebP, TIFF, BMP, Targa (`.tga`/`.vda`/`.icb`/`.vst`), PSD/PSB (an image opens as a document of its pixel size; a Photoshop file as its merged image). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). `run_command document.formats` lists the formats. |-| `save_file` | `{path?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). |+| `save_file` | `{path?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF, or a restorable `.ai` or EPS; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable; `.eps`: carries it too). | | `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF writes one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based); the other formats write one artboard. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. | | `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. | | `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG, SVGZ and EPS write one file per chosen artboard. Rasters, DXF, EMF and WMF write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |