diff --git a/apps/figcraft/src/apple_events.rs b/apps/figcraft/src/apple_events.rs index 6b792094..41ae5cc3 100644 --- a/apps/figcraft/src/apple_events.rs +++ b/apps/figcraft/src/apple_events.rs @@ -50,7 +50,7 @@ pub fn poll(inbox: &Inbox, fig: &mut FigApp, ctx: &egui::Context) { /// Open `paths` as File › Open does; a file that fails shows its error and the others still open. pub fn open_documents(fig: &mut FigApp, paths: Vec) { for p in paths { - if fig.run("file.open", json!({"path": p.to_string_lossy()})).is_some() { + if fig.run_user("file.open", json!({"path": p.to_string_lossy()})).is_some() { fig.ui.home = false; } } diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 54348372..8319dee2 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -30,7 +30,7 @@ pub fn specs() -> Vec { spec!( "file.open", "Open", - "{path, variables? (REST: the local variables JSON as an object, JSON text or a path; default `.variables.json` or `variables.json` next to the file)} (.figcraft, .json: FigCraft or REST API file JSON, .fig (best effort), .svg, .png, .jpg …); REST and .fig imports return `warnings`", + "{path, variables? (REST: the local variables JSON as an object, JSON text or a path; default `.variables.json` or `variables.json` next to the file)} (.figcraft, .json: FigCraft or REST API file JSON, .fig (best effort), .svg, .png, .jpg …); REST and .fig imports return `warnings`; a file opened anew returns `summary` {name, pages, layers, images} (one that was already open just returns its `index`)", false, open ), @@ -74,7 +74,7 @@ pub fn specs() -> Vec { spec!( "file.import", "Import", - "{path | data (base64) + name, x?, y?, center? (SVG: centre it on x, y), variables? (REST: the local variables JSON as an object, JSON text or a path)} SVG becomes an editable frame (shapes, text, gradients, masks, effects; with `warnings` for what it left out), images become image layers, .figcraft, REST API file JSON and .fig pages are appended with their variables and flows (REST and .fig: with `warnings`)", + "{path | data (base64) + name, x?, y?, center? (SVG: centre it on x, y), variables? (REST: the local variables JSON as an object, JSON text or a path)} SVG becomes an editable frame (shapes, text, gradients, masks, effects; with `warnings` for what it left out), images become image layers, .figcraft, REST API file JSON and .fig pages are appended with their variables and flows (REST and .fig: with `warnings`; those return `summary` {name, pages, layers, images} of the file read)", true, import ), @@ -461,6 +461,11 @@ fn file_stem(path: &str) -> String { std::path::Path::new(path).file_stem().and_then(|e| e.to_str()).unwrap_or("Untitled").to_string() } +/// The counts the import report shows for a freshly loaded document. +fn import_summary(doc: &Document) -> Value { + json!({"name": doc.name, "pages": doc.pages.len(), "layers": doc.nodes().filter(|n| !n.is_page()).count(), "images": doc.images.len()}) +} + /// Load a document from any supported file (REST API file JSON images come from an `images` /// folder next to the file). pub fn load(path: &str) -> Result<(Document, Vec)> { @@ -672,9 +677,10 @@ fn open(s: &mut Session, p: &Value) -> Result { more => format!("{}: {} problems were repaired or left out while opening it, e.g. {}", file_stem(&path), more.len(), more[0]), }); } + let summary = import_summary(&loaded.doc); s.docs.push(DocState::new(loaded.doc, native.then_some(path)).with_versions(loaded.versions).with_branches(loaded.branches)); s.active = s.docs.len() - 1; - let mut out = json!({"index": s.active}); + let mut out = json!({"index": s.active, "summary": summary}); out[if warnings { "warnings" } else { "repairs" }] = json!(loaded.issues); Ok(out) } @@ -1215,8 +1221,9 @@ fn import(s: &mut Session, p: &Value) -> Result { }; let sibling = if variables.is_none() { path.as_deref().and_then(sibling_variables) } else { None }; let (other, rest) = load_with(&bytes, &name, variables.as_ref().or(sibling.as_ref()), images)?; + let summary = import_summary(&other.doc); let pages = append_pages(s.doc_mut()?, &other.doc)?; - let mut out = json!({"pages": pages}); + let mut out = json!({"pages": pages, "summary": summary}); if rest { out["warnings"] = json!(other.issues); } diff --git a/crates/ui-egui/locales/de.toml b/crates/ui-egui/locales/de.toml index cd1e3482..c7bd13f7 100644 --- a/crates/ui-egui/locales/de.toml +++ b/crates/ui-egui/locales/de.toml @@ -261,6 +261,7 @@ status = "draft — needs native review" "Commits" = "Commits" "Company" = "Unternehmen" "Compare changes" = "Änderungen vergleichen" +"Compatibility notes" = "Hinweise zur Kompatibilität" "Completed" = "Abgeschlossen" "Component" = "Komponente" "Component name" = "Komponentenname" @@ -464,6 +465,7 @@ status = "draft — needs native review" "False" = "Falsch" "Fetching from the server…" = "Wird vom Server abgerufen…" "FigCraft couldn't open its window: {error}" = "FigCraft konnte sein Fenster nicht öffnen: {error}" +"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft unterstützt noch nicht alle Funktionen dieser Dateien. Die Hinweise nennen, was in der importierten Kopie weggelassen oder geändert wurde; die Originaldatei bleibt unverändert." "File" = "Datei" "File name" = "Dateiname" "File published" = "Datei veröffentlicht" @@ -580,11 +582,14 @@ status = "draft — needs native review" "If {condition}" = "Wenn {condition}" "Ignore auto layout (absolute position)" = "Auto-Layout ignorieren (absolute Position)" "Image" = "Bild" +"Images" = "Bilder" "Import" = "Importieren" "Import path" = "Importpfad" "Import plugin from manifest" = "Plugin aus Manifest importieren" "Import plugin from manifest…" = "Plugin aus Manifest importieren…" +"Import report" = "Importbericht" "Import variables" = "Variablen importieren" +"Imported document" = "Importiertes Dokument" "Imported {name}" = "{name} importiert" "Imported {n} new variables, updated {updated}" = { one = "{n} neue Variable importiert, {updated} aktualisiert", other = "{n} neue Variablen importiert, {updated} aktualisiert" } "Import…" = "Importieren…" @@ -774,6 +779,7 @@ status = "draft — needs native review" "No colour variables fit here." = "Hier passen keine Farbvariablen." "No colour variables yet. Create them in Local variables." = "Noch keine Farbvariablen. Erstellen Sie sie unter Lokale Variablen." "No comments yet" = "Noch keine Kommentare" +"No compatibility warnings reported" = "Keine Kompatibilitätswarnungen gemeldet" "No components yet. Select layers and press {keys}." = "Noch keine Komponenten. Wählen Sie Ebenen aus und drücken Sie {keys}." "No conflicts." = "Keine Konflikte." "No contributor data was built into this copy." = "Diese Kopie enthält keine Daten zu Mitwirkenden." @@ -860,6 +866,7 @@ status = "draft — needs native review" "Open overlay" = "Overlay öffnen" "Open prototype preview" = "Prototyp-Vorschau öffnen" "Open variables ({count})" = "Variablen öffnen ({count})" +"Opened as a new document" = "Als neues Dokument geöffnet" "Opened “{name}” in a new tab" = "„{name}“ in neuem Tab geöffnet" "Opening the file…" = "Datei wird geöffnet…" "Opening {n} files as new files" = { one = "Wird als neue Datei geöffnet", other = "{n} Dateien werden als neue Dateien geöffnet" } @@ -885,6 +892,8 @@ status = "draft — needs native review" "Padding" = "Innenabstand" "Page" = "Seite" "Pages" = "Seiten" +"Pages added to the document" = "Seiten zum Dokument hinzugefügt" +"Placed on the current page" = "Auf der aktuellen Seite platziert" "Pages: {pages}" = "Seiten: {pages}" "Paint bucket" = "Farbeimer" "Pan the canvas" = "Arbeitsfläche verschieben" @@ -1435,25 +1444,87 @@ status = "draft — needs native review" "call any command its access allows" = "jeden Befehl aufrufen, den sein Zugriff erlaubt" "call only: {commands}" = "nur aufrufen: {commands}" "can edit the document" = "kann das Dokument bearbeiten" +"conditional prototype actions nested too deeply were skipped" = "zu tief verschachtelte bedingte Prototyp-Aktionen wurden übersprungen" +"conditional prototype actions with conditions that can't be evaluated were skipped" = "bedingte Prototyp-Aktionen mit nicht auswertbaren Bedingungen wurden übersprungen" +"connector labels were skipped" = "Verbinderbeschriftungen wurden übersprungen" +"connectors whose ends can't be placed are empty" = "Verbinder, deren Enden sich nicht platzieren lassen, sind leer" "crop marks" = "Schnittmarken" +"damaged vector geometry was skipped" = "beschädigte Vektorgeometrie wurde übersprungen" +"damaged vector networks import as outlines" = "beschädigte Vektornetze werden als Umrisse importiert" "development: {path}" = "Entwicklung: {path}" +"document children other than pages were skipped" = "Dokumentinhalte, die keine Seiten sind, wurden übersprungen" "e.g. count + 1" = "z. B. count + 1" "e.g. count >= 3 and not done" = "z. B. count >= 3 and not done" +"elbowed and curved connectors without a saved path import as straight lines" = "gewinkelte und gebogene Verbinder ohne gespeicherten Pfad werden als gerade Linien importiert" +"entries without a node type were skipped" = "Einträge ohne Knotentyp wurden übersprungen" "figcraft:// links from a browser, chat or email open this app and join the session." = "figcraft://-Links aus Browser, Chat oder E-Mail öffnen diese App und treten der Sitzung bei." +"filled areas of a vector with their own fill use the layer's fill" = "gefüllte Bereiche eines Vektors mit eigener Füllung verwenden die Füllung der Ebene" "from {server}" = "von {server}" +"gamepad triggers without exactly one known controller button were skipped" = "Gamepad-Auslöser ohne genau eine bekannte Controllertaste wurden übersprungen" +"grid tracks of fixed or content size import as equal tracks" = "Rasterspuren mit fester oder inhaltsabhängiger Größe werden als gleich große Spuren importiert" "hidden" = "ausgeblendet" "iOS" = "iOS" +"instance swap properties whose default component isn't in the file were skipped" = "Instanztausch-Eigenschaften, deren Standardkomponente nicht in der Datei ist, wurden übersprungen" +"instances inside their own main component were imported as frames" = "Instanzen innerhalb ihrer eigenen Hauptkomponente wurden als Rahmen importiert" +"instances of components that aren't in the file show no layers" = "Instanzen von Komponenten, die nicht in der Datei sind, zeigen keine Ebenen" +"instances of components that aren't in the file were imported as frames" = "Instanzen von Komponenten, die nicht in der Datei sind, wurden als Rahmen importiert" +"instances that would nest a component inside itself were imported as frames" = "Instanzen, die eine Komponente in sich selbst verschachteln würden, wurden als Rahmen importiert" +"instances without a main component were imported as empty frames" = "Instanzen ohne Hauptkomponente wurden als leere Rahmen importiert" +"invalid or oversized vector networks import as outlines" = "ungültige oder zu große Vektornetze werden als Umrisse importiert" +"invalid or oversized vector path data was skipped" = "ungültige oder zu große Vektorpfaddaten wurden übersprungen" +"key triggers without a supported key were skipped" = "Tastenauslöser ohne unterstützte Taste wurden übersprungen" +"key up triggers import as key presses" = "Auslöser beim Loslassen einer Taste werden als Tastendruck importiert" +"layers without a position were placed at their parent's origin" = "Ebenen ohne Position wurden am Ursprung ihres übergeordneten Elements platziert" +"layers without a type were skipped" = "Ebenen ohne Typ wurden übersprungen" "locked" = "gesperrt" "no bleed" = "kein Anschnitt" "nobody else here yet" = "noch niemand sonst hier" "now" = "jetzt" "or" = "oder" +"points with their own stroke join use the layer's" = "Punkte mit eigener Konturecke verwenden die der Ebene" +"prototype actions of unknown types were skipped" = "Prototyp-Aktionen unbekannter Art wurden übersprungen" +"prototype actions without a destination in the file were skipped" = "Prototyp-Aktionen ohne Ziel in der Datei wurden übersprungen" +"prototype expressions on variables that aren't in the variables JSON were skipped" = "Prototyp-Ausdrücke mit Variablen, die nicht im Variablen-JSON sind, wurden übersprungen" +"prototype expressions with unsupported functions were skipped" = "Prototyp-Ausdrücke mit nicht unterstützten Funktionen wurden übersprungen" +"prototype interactions without supported actions were skipped" = "Prototyp-Interaktionen ohne unterstützte Aktionen wurden übersprungen" +"prototype navigations of unknown types were skipped" = "Prototyp-Navigationen unbekannter Art wurden übersprungen" +"prototype transitions of unknown types play instantly" = "Prototyp-Übergänge unbekannter Art werden sofort ausgeführt" +"prototype triggers of unknown types were skipped" = "Prototyp-Auslöser unbekannter Art wurden übersprungen" "put a widget on the canvas" = "ein Widget auf die Arbeitsfläche setzen" "read and edit the document" = "das Dokument lesen und bearbeiten" "read the document" = "das Dokument lesen" "reads the document" = "liest das Dokument" +"set-variable actions on variables that aren't in the variables JSON were skipped" = "Aktionen „Variable festlegen“ für Variablen, die nicht im Variablen-JSON sind, wurden übersprungen" +"set-variable actions with values that can't be evaluated were skipped" = "Aktionen „Variable festlegen“ mit nicht auswertbaren Werten wurden übersprungen" +"set-variable actions with values that don't fit the variable were skipped" = "Aktionen „Variable festlegen“ mit Werten, die nicht zur Variable passen, wurden übersprungen" +"set-variable actions without a value were skipped" = "Aktionen „Variable festlegen“ ohne Wert wurden übersprungen" +"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "Aktionen „Variablenmodus festlegen“ für Sammlungen oder Modi, die nicht im Variablen-JSON sind, wurden übersprungen" +"stars and polygons without point counts or geometry use the default point count" = "Sterne und Polygone ohne Punktanzahl oder Geometrie verwenden die Standard-Punktanzahl" +"text style overrides refer to missing table entries" = "Textstil-Überschreibungen verweisen auf fehlende Tabelleneinträge" +"the file has no pages" = "die Datei hat keine Seiten" "the plugin" = "das Plugin" +"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "das Variablen-JSON enthält keine `variableCollections` (erwartet wurde die Antwort mit den lokalen Variablen) und wurde ignoriert" "this session only" = "nur diese Sitzung" +"unknown component property types were skipped" = "unbekannte Komponenteneigenschaftstypen wurden übersprungen" +"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "Variablen-Aliase auf Variablen, die nicht im Variablen-JSON sind (oder einen anderen Typ haben), wurden übersprungen" +"variable bindings to properties the layer doesn't have were skipped" = "Variablenbindungen an Eigenschaften, die die Ebene nicht hat, wurden übersprungen" +"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "Variablenbindungen an Variablen, die nicht im Variablen-JSON sind (Bibliotheksvariablen), wurden übersprungen" +"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "Variablenbindungen wurden übersprungen: Das Datei-JSON enthält keine Variablendefinitionen (übergeben Sie das lokale Variablen-JSON als `variables` oder betten Sie dessen `meta` neben dem Datei-JSON ein)" +"variable bindings whose variable type doesn't fit the property were skipped" = "Variablenbindungen, deren Variablentyp nicht zur Eigenschaft passt, wurden übersprungen" +"variable collections without modes were skipped" = "Variablensammlungen ohne Modi wurden übersprungen" +"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "Variablenmodus-Abfragen in Prototyp-Ausdrücken, die nicht aus einer Variable und einem ihrer Modi bestehen, wurden übersprungen" +"variable modes of collections that aren't in the file were skipped" = "Variablenmodi von Sammlungen, die nicht in der Datei sind, wurden übersprungen" +"variable modes of collections that aren't in the variables JSON were skipped" = "Variablenmodi von Sammlungen, die nicht im Variablen-JSON sind, wurden übersprungen" +"variable values that don't match their type (or composed colours) were skipped" = "Variablenwerte, die nicht zu ihrem Typ passen (oder zusammengesetzte Farben), wurden übersprungen" +"variables of unknown types were skipped" = "Variablen unbekannter Typen wurden übersprungen" +"variables whose collection isn't in the variables JSON were skipped" = "Variablen, deren Sammlung nicht im Variablen-JSON ist, wurden übersprungen" +"variables without readable values were skipped" = "Variablen ohne lesbare Werte wurden übersprungen" +"vectors without geometry are empty" = "Vektoren ohne Geometrie sind leer" +"vectors without geometry are empty (export the file JSON with geometry=paths)" = "Vektoren ohne Geometrie sind leer (exportieren Sie das Datei-JSON mit geometry=paths)" +"very long text was cut to 1,048,576 characters" = "sehr langer Text wurde auf 1.048.576 Zeichen gekürzt" +"very long vector paths were skipped" = "sehr lange Vektorpfade wurden übersprungen" +"video actions are not supported and were skipped" = "Videoaktionen werden nicht unterstützt und wurden übersprungen" +"video triggers are not supported and were skipped" = "Videoauslöser werden nicht unterstützt und wurden übersprungen" "{instances} in {files}" = "{instances} in {files}" "{language}: draft translation, waiting for a native speaker's review." = "{language}: Übersetzungsentwurf, wartet auf die Prüfung durch Muttersprachler." "{length} bleed" = "{length} Anschnitt" @@ -1496,6 +1567,7 @@ status = "draft — needs native review" "{n} matches" = { one = "{n} Treffer", other = "{n} Treffer" } "{n} minutes ago" = { one = "vor {n} Minute", other = "vor {n} Minuten" } "{n} months ago" = { one = "vor {n} Monat", other = "vor {n} Monaten" } +"{n} more notes omitted from this report" = { one = "{n} weitere Notiz in diesem Bericht nicht aufgeführt", other = "{n} weitere Notizen in diesem Bericht nicht aufgeführt" } "{n} of {total}" = "{n} von {total}" "{n} people joined" = { one = "{n} Person ist beigetreten", other = "{n} Personen sind beigetreten" } "{n} preferred components" = { one = "{n} bevorzugte Komponente", other = "{n} bevorzugte Komponenten" } diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index 3c127fc6..c3e75570 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -270,6 +270,7 @@ name = "English" "Commits" = "Commits" "Company" = "Company" "Compare changes" = "Compare changes" +"Compatibility notes" = "Compatibility notes" "Completed" = "Completed" "Component" = "Component" "Component name" = "Component name" @@ -482,6 +483,7 @@ name = "English" "Feb" = "Feb" "Fetching from the server…" = "Fetching from the server…" "FigCraft couldn't open its window: {error}" = "FigCraft couldn't open its window: {error}" +"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." "File" = "File" "File name" = "File name" "File not found" = "File not found" @@ -606,11 +608,14 @@ name = "English" "If {condition}" = "If {condition}" "Ignore auto layout (absolute position)" = "Ignore auto layout (absolute position)" "Image" = "Image" +"Images" = "Images" "Import" = "Import" "Import path" = "Import path" "Import plugin from manifest" = "Import plugin from manifest" "Import plugin from manifest…" = "Import plugin from manifest…" +"Import report" = "Import report" "Import variables" = "Import variables" +"Imported document" = "Imported document" "Imported {name}" = "Imported {name}" "Imported {n} new variables, updated {updated}" = { one = "Imported {n} new variable, updated {updated}", other = "Imported {n} new variables, updated {updated}" } "Import…" = "Import…" @@ -812,6 +817,7 @@ name = "English" "No colour variables fit here." = "No colour variables fit here." "No colour variables yet. Create them in Local variables." = "No colour variables yet. Create them in Local variables." "No comments yet" = "No comments yet" +"No compatibility warnings reported" = "No compatibility warnings reported" "No components yet. Select layers and press {keys}." = "No components yet. Select layers and press {keys}." "No conflicts." = "No conflicts." "No contributor data was built into this copy." = "No contributor data was built into this copy." @@ -903,6 +909,7 @@ name = "English" "Open overlay" = "Open overlay" "Open prototype preview" = "Open prototype preview" "Open variables ({count})" = "Open variables ({count})" +"Opened as a new document" = "Opened as a new document" "Opened “{name}” in a new tab" = "Opened “{name}” in a new tab" "Opening the file…" = "Opening the file…" "Opening {n} files as new files" = { one = "Opening as a new file", other = "Opening {n} files as new files" } @@ -929,6 +936,8 @@ name = "English" "Padding" = "Padding" "Page" = "Page" "Pages" = "Pages" +"Pages added to the document" = "Pages added to the document" +"Placed on the current page" = "Placed on the current page" "Pages: {pages}" = "Pages: {pages}" "Paint bucket" = "Paint bucket" "Pan the canvas" = "Pan the canvas" @@ -1506,26 +1515,88 @@ name = "English" "call any command its access allows" = "call any command its access allows" "call only: {commands}" = "call only: {commands}" "can edit the document" = "can edit the document" +"conditional prototype actions nested too deeply were skipped" = "conditional prototype actions nested too deeply were skipped" +"conditional prototype actions with conditions that can't be evaluated were skipped" = "conditional prototype actions with conditions that can't be evaluated were skipped" +"connector labels were skipped" = "connector labels were skipped" +"connectors whose ends can't be placed are empty" = "connectors whose ends can't be placed are empty" "crop marks" = "crop marks" +"damaged vector geometry was skipped" = "damaged vector geometry was skipped" +"damaged vector networks import as outlines" = "damaged vector networks import as outlines" "development: {path}" = "development: {path}" +"document children other than pages were skipped" = "document children other than pages were skipped" "e.g. count + 1" = "e.g. count + 1" "e.g. count >= 3 and not done" = "e.g. count >= 3 and not done" +"elbowed and curved connectors without a saved path import as straight lines" = "elbowed and curved connectors without a saved path import as straight lines" +"entries without a node type were skipped" = "entries without a node type were skipped" "figcraft:// links from a browser, chat or email open this app and join the session." = "figcraft:// links from a browser, chat or email open this app and join the session." +"filled areas of a vector with their own fill use the layer's fill" = "filled areas of a vector with their own fill use the layer's fill" "from {server}" = "from {server}" +"gamepad triggers without exactly one known controller button were skipped" = "gamepad triggers without exactly one known controller button were skipped" +"grid tracks of fixed or content size import as equal tracks" = "grid tracks of fixed or content size import as equal tracks" "hidden" = "hidden" "iOS" = "iOS" +"instance swap properties whose default component isn't in the file were skipped" = "instance swap properties whose default component isn't in the file were skipped" +"instances inside their own main component were imported as frames" = "instances inside their own main component were imported as frames" +"instances of components that aren't in the file show no layers" = "instances of components that aren't in the file show no layers" +"instances of components that aren't in the file were imported as frames" = "instances of components that aren't in the file were imported as frames" +"instances that would nest a component inside itself were imported as frames" = "instances that would nest a component inside itself were imported as frames" +"instances without a main component were imported as empty frames" = "instances without a main component were imported as empty frames" +"invalid or oversized vector networks import as outlines" = "invalid or oversized vector networks import as outlines" +"invalid or oversized vector path data was skipped" = "invalid or oversized vector path data was skipped" "just now" = "just now" +"key triggers without a supported key were skipped" = "key triggers without a supported key were skipped" +"key up triggers import as key presses" = "key up triggers import as key presses" +"layers without a position were placed at their parent's origin" = "layers without a position were placed at their parent's origin" +"layers without a type were skipped" = "layers without a type were skipped" "locked" = "locked" "no bleed" = "no bleed" "nobody else here yet" = "nobody else here yet" "now" = "now" "or" = "or" +"points with their own stroke join use the layer's" = "points with their own stroke join use the layer's" +"prototype actions of unknown types were skipped" = "prototype actions of unknown types were skipped" +"prototype actions without a destination in the file were skipped" = "prototype actions without a destination in the file were skipped" +"prototype expressions on variables that aren't in the variables JSON were skipped" = "prototype expressions on variables that aren't in the variables JSON were skipped" +"prototype expressions with unsupported functions were skipped" = "prototype expressions with unsupported functions were skipped" +"prototype interactions without supported actions were skipped" = "prototype interactions without supported actions were skipped" +"prototype navigations of unknown types were skipped" = "prototype navigations of unknown types were skipped" +"prototype transitions of unknown types play instantly" = "prototype transitions of unknown types play instantly" +"prototype triggers of unknown types were skipped" = "prototype triggers of unknown types were skipped" "put a widget on the canvas" = "put a widget on the canvas" "read and edit the document" = "read and edit the document" "read the document" = "read the document" "reads the document" = "reads the document" +"set-variable actions on variables that aren't in the variables JSON were skipped" = "set-variable actions on variables that aren't in the variables JSON were skipped" +"set-variable actions with values that can't be evaluated were skipped" = "set-variable actions with values that can't be evaluated were skipped" +"set-variable actions with values that don't fit the variable were skipped" = "set-variable actions with values that don't fit the variable were skipped" +"set-variable actions without a value were skipped" = "set-variable actions without a value were skipped" +"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" +"stars and polygons without point counts or geometry use the default point count" = "stars and polygons without point counts or geometry use the default point count" +"text style overrides refer to missing table entries" = "text style overrides refer to missing table entries" +"the file has no pages" = "the file has no pages" "the plugin" = "the plugin" +"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" "this session only" = "this session only" +"unknown component property types were skipped" = "unknown component property types were skipped" +"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" +"variable bindings to properties the layer doesn't have were skipped" = "variable bindings to properties the layer doesn't have were skipped" +"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "variable bindings to variables that aren't in the variables JSON (library variables) were skipped" +"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" +"variable bindings whose variable type doesn't fit the property were skipped" = "variable bindings whose variable type doesn't fit the property were skipped" +"variable collections without modes were skipped" = "variable collections without modes were skipped" +"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" +"variable modes of collections that aren't in the file were skipped" = "variable modes of collections that aren't in the file were skipped" +"variable modes of collections that aren't in the variables JSON were skipped" = "variable modes of collections that aren't in the variables JSON were skipped" +"variable values that don't match their type (or composed colours) were skipped" = "variable values that don't match their type (or composed colours) were skipped" +"variables of unknown types were skipped" = "variables of unknown types were skipped" +"variables whose collection isn't in the variables JSON were skipped" = "variables whose collection isn't in the variables JSON were skipped" +"variables without readable values were skipped" = "variables without readable values were skipped" +"vectors without geometry are empty" = "vectors without geometry are empty" +"vectors without geometry are empty (export the file JSON with geometry=paths)" = "vectors without geometry are empty (export the file JSON with geometry=paths)" +"very long text was cut to 1,048,576 characters" = "very long text was cut to 1,048,576 characters" +"very long vector paths were skipped" = "very long vector paths were skipped" +"video actions are not supported and were skipped" = "video actions are not supported and were skipped" +"video triggers are not supported and were skipped" = "video triggers are not supported and were skipped" "yesterday" = "yesterday" "{instances} in {files}" = "{instances} in {files}" "{language}: draft translation, waiting for a native speaker's review." = "{language}: draft translation, waiting for a native speaker's review." @@ -1570,6 +1641,7 @@ name = "English" "{n} matches" = { one = "{n} match", other = "{n} matches" } "{n} minutes ago" = { one = "{n} minute ago", other = "{n} minutes ago" } "{n} months ago" = { one = "{n} month ago", other = "{n} months ago" } +"{n} more notes omitted from this report" = { one = "{n} more note omitted from this report", other = "{n} more notes omitted from this report" } "{n} of {total}" = "{n} of {total}" "{n} people joined" = { one = "{n} person joined", other = "{n} people joined" } "{n} preferred components" = { one = "{n} preferred component", other = "{n} preferred components" } diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index 901283f1..49dd765f 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -8,6 +8,7 @@ name = "Español" status = "draft — needs native review" [ui] +"Compatibility notes" = "Notas de compatibilidad" "Couldn't download the variables" = "No se pudieron descargar las variables" "% of all commits" = "% del total de commits" @@ -483,6 +484,7 @@ status = "draft — needs native review" "Feb" = "feb" "Fetching from the server…" = "Obteniendo del servidor…" "FigCraft couldn't open its window: {error}" = "FigCraft no pudo abrir su ventana: {error}" +"FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged." = "FigCraft aún no admite todas las funciones de estos archivos. Las notas indican qué se omitió o cambió en la copia importada; el archivo original no se modifica." "File" = "Archivo" "File name" = "Nombre del archivo" "File not found" = "No se encuentra el archivo" @@ -607,11 +609,14 @@ status = "draft — needs native review" "If {condition}" = "Si {condition}" "Ignore auto layout (absolute position)" = "Ignorar diseño automático (posición absoluta)" "Image" = "Imagen" +"Images" = "Imágenes" "Import" = "Importar" "Import path" = "Ruta de importación" "Import plugin from manifest" = "Importar plugin desde manifiesto" "Import plugin from manifest…" = "Importar plugin desde manifiesto…" +"Import report" = "Informe de importación" "Import variables" = "Importar variables" +"Imported document" = "Documento importado" "Imported {name}" = "{name} importado" "Imported {n} new variables, updated {updated}" = { one = "Variables importadas: {n} nueva; actualizadas: {updated}", other = "Variables importadas: {n} nuevas; actualizadas: {updated}" } "Import…" = "Importar…" @@ -813,6 +818,7 @@ status = "draft — needs native review" "No colour variables fit here." = "Ninguna variable de color encaja aquí." "No colour variables yet. Create them in Local variables." = "Aún no hay variables de color. Créalas en Variables locales." "No comments yet" = "Aún no hay comentarios" +"No compatibility warnings reported" = "No se han registrado advertencias de compatibilidad" "No components yet. Select layers and press {keys}." = "Aún no hay componentes. Selecciona capas y pulsa {keys}." "No conflicts." = "Sin conflictos." "No contributor data was built into this copy." = "Esta copia no incluye datos de colaboradores." @@ -904,6 +910,7 @@ status = "draft — needs native review" "Open overlay" = "Abrir superposición" "Open prototype preview" = "Abrir vista previa del prototipo" "Open variables ({count})" = "Abrir variables ({count})" +"Opened as a new document" = "Abierto como un documento nuevo" "Opened “{name}” in a new tab" = "“{name}” abierta en una pestaña nueva" "Opening the file…" = "Abriendo el archivo…" "Opening {n} files as new files" = { one = "Abriendo como archivo nuevo", other = "Abriendo {n} archivos como archivos nuevos" } @@ -930,6 +937,8 @@ status = "draft — needs native review" "Padding" = "Margen interno" "Page" = "Página" "Pages" = "Páginas" +"Pages added to the document" = "Páginas añadidas al documento" +"Placed on the current page" = "Colocado en la página actual" "Pages: {pages}" = "Páginas: {pages}" "Paint bucket" = "Bote de pintura" "Pan the canvas" = "Desplazar el lienzo" @@ -1507,26 +1516,88 @@ status = "draft — needs native review" "call any command its access allows" = "llamar a cualquier comando que su acceso permita" "call only: {commands}" = "llamar solo a: {commands}" "can edit the document" = "puede editar el documento" +"conditional prototype actions nested too deeply were skipped" = "se omitieron las acciones condicionales de prototipo anidadas a demasiada profundidad" +"conditional prototype actions with conditions that can't be evaluated were skipped" = "se omitieron las acciones condicionales de prototipo con condiciones que no se pueden evaluar" +"connector labels were skipped" = "se omitieron las etiquetas de los conectores" +"connectors whose ends can't be placed are empty" = "los conectores cuyos extremos no se pueden colocar están vacíos" "crop marks" = "marcas de corte" +"damaged vector geometry was skipped" = "se omitió la geometría vectorial dañada" +"damaged vector networks import as outlines" = "las redes vectoriales dañadas se importan como contornos" "development: {path}" = "desarrollo: {path}" +"document children other than pages were skipped" = "se omitieron los elementos del documento que no son páginas" "e.g. count + 1" = "p. ej. count + 1" "e.g. count >= 3 and not done" = "p. ej. count >= 3 and not done" +"elbowed and curved connectors without a saved path import as straight lines" = "los conectores angulares y curvos sin trazado guardado se importan como líneas rectas" +"entries without a node type were skipped" = "se omitieron las entradas sin tipo de nodo" "figcraft:// links from a browser, chat or email open this app and join the session." = "Los enlaces figcraft:// de un navegador, chat o correo abren esta app y se unen a la sesión." +"filled areas of a vector with their own fill use the layer's fill" = "las áreas rellenas de un vector con relleno propio usan el relleno de la capa" "from {server}" = "de {server}" +"gamepad triggers without exactly one known controller button were skipped" = "se omitieron los desencadenantes de mando sin exactamente un botón de mando conocido" +"grid tracks of fixed or content size import as equal tracks" = "las pistas de cuadrícula de tamaño fijo o según el contenido se importan como pistas iguales" "hidden" = "oculta" "iOS" = "iOS" +"instance swap properties whose default component isn't in the file were skipped" = "se omitieron las propiedades de intercambio de instancia cuyo componente predeterminado no está en el archivo" +"instances inside their own main component were imported as frames" = "las instancias dentro de su propio componente principal se importaron como marcos" +"instances of components that aren't in the file show no layers" = "las instancias de componentes que no están en el archivo no muestran capas" +"instances of components that aren't in the file were imported as frames" = "las instancias de componentes que no están en el archivo se importaron como marcos" +"instances that would nest a component inside itself were imported as frames" = "las instancias que anidarían un componente dentro de sí mismo se importaron como marcos" +"instances without a main component were imported as empty frames" = "las instancias sin componente principal se importaron como marcos vacíos" +"invalid or oversized vector networks import as outlines" = "las redes vectoriales no válidas o demasiado grandes se importan como contornos" +"invalid or oversized vector path data was skipped" = "se omitieron los datos de trazados vectoriales no válidos o demasiado grandes" "just now" = "justo ahora" +"key triggers without a supported key were skipped" = "se omitieron los desencadenantes de tecla sin una tecla compatible" +"key up triggers import as key presses" = "los desencadenantes al soltar una tecla se importan como pulsaciones de tecla" +"layers without a position were placed at their parent's origin" = "las capas sin posición se colocaron en el origen de su elemento padre" +"layers without a type were skipped" = "se omitieron las capas sin tipo" "locked" = "bloqueada" "no bleed" = "sin sangrado" "nobody else here yet" = "aún no hay nadie más" "now" = "ahora" "or" = "o" +"points with their own stroke join use the layer's" = "los puntos con su propia unión de trazo usan la de la capa" +"prototype actions of unknown types were skipped" = "se omitieron las acciones de prototipo de tipos desconocidos" +"prototype actions without a destination in the file were skipped" = "se omitieron las acciones de prototipo sin destino en el archivo" +"prototype expressions on variables that aren't in the variables JSON were skipped" = "se omitieron las expresiones de prototipo con variables que no están en el JSON de variables" +"prototype expressions with unsupported functions were skipped" = "se omitieron las expresiones de prototipo con funciones no compatibles" +"prototype interactions without supported actions were skipped" = "se omitieron las interacciones de prototipo sin acciones compatibles" +"prototype navigations of unknown types were skipped" = "se omitieron las navegaciones de prototipo de tipos desconocidos" +"prototype transitions of unknown types play instantly" = "las transiciones de prototipo de tipos desconocidos se reproducen al instante" +"prototype triggers of unknown types were skipped" = "se omitieron los desencadenantes de prototipo de tipos desconocidos" "put a widget on the canvas" = "poner un widget en el lienzo" "read and edit the document" = "leer y editar el documento" "read the document" = "leer el documento" "reads the document" = "lee el documento" +"set-variable actions on variables that aren't in the variables JSON were skipped" = "se omitieron las acciones «Establecer variable» sobre variables que no están en el JSON de variables" +"set-variable actions with values that can't be evaluated were skipped" = "se omitieron las acciones «Establecer variable» con valores que no se pueden evaluar" +"set-variable actions with values that don't fit the variable were skipped" = "se omitieron las acciones «Establecer variable» con valores que no encajan con la variable" +"set-variable actions without a value were skipped" = "se omitieron las acciones «Establecer variable» sin valor" +"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "se omitieron las acciones «Establecer modo de variable» sobre colecciones o modos que no están en el JSON de variables" +"stars and polygons without point counts or geometry use the default point count" = "las estrellas y los polígonos sin número de puntos ni geometría usan el número de puntos predeterminado" +"text style overrides refer to missing table entries" = "las anulaciones de estilo de texto hacen referencia a entradas de tabla que faltan" +"the file has no pages" = "el archivo no tiene páginas" "the plugin" = "el plugin" +"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "el JSON de variables no tiene `variableCollections` (se esperaba la respuesta de variables locales) y se ignoró" "this session only" = "solo esta sesión" +"unknown component property types were skipped" = "se omitieron los tipos de propiedad de componente desconocidos" +"variable aliases to variables that aren't in the variables JSON (or of another type) were skipped" = "se omitieron los alias a variables que no están en el JSON de variables (o son de otro tipo)" +"variable bindings to properties the layer doesn't have were skipped" = "se omitieron las vinculaciones de variables a propiedades que la capa no tiene" +"variable bindings to variables that aren't in the variables JSON (library variables) were skipped" = "se omitieron las vinculaciones a variables que no están en el JSON de variables (variables de biblioteca)" +"variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)" = "se omitieron las vinculaciones de variables: el JSON del archivo no tiene definiciones de variables (pasa el JSON de variables locales como `variables` o incrusta su `meta` junto al JSON del archivo)" +"variable bindings whose variable type doesn't fit the property were skipped" = "se omitieron las vinculaciones de variables cuyo tipo no encaja con la propiedad" +"variable collections without modes were skipped" = "se omitieron las colecciones de variables sin modos" +"variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped" = "se omitieron las consultas de modo de variable en expresiones de prototipo que no son una variable y uno de sus modos" +"variable modes of collections that aren't in the file were skipped" = "se omitieron los modos de variable de colecciones que no están en el archivo" +"variable modes of collections that aren't in the variables JSON were skipped" = "se omitieron los modos de variable de colecciones que no están en el JSON de variables" +"variable values that don't match their type (or composed colours) were skipped" = "se omitieron los valores de variable que no coinciden con su tipo (o colores compuestos)" +"variables of unknown types were skipped" = "se omitieron las variables de tipos desconocidos" +"variables whose collection isn't in the variables JSON were skipped" = "se omitieron las variables cuya colección no está en el JSON de variables" +"variables without readable values were skipped" = "se omitieron las variables sin valores legibles" +"vectors without geometry are empty" = "los vectores sin geometría están vacíos" +"vectors without geometry are empty (export the file JSON with geometry=paths)" = "los vectores sin geometría están vacíos (exporta el JSON del archivo con geometry=paths)" +"very long text was cut to 1,048,576 characters" = "el texto muy largo se recortó a 1.048.576 caracteres" +"very long vector paths were skipped" = "se omitieron los trazados vectoriales muy largos" +"video actions are not supported and were skipped" = "las acciones de vídeo no son compatibles y se omitieron" +"video triggers are not supported and were skipped" = "los desencadenantes de vídeo no son compatibles y se omitieron" "yesterday" = "ayer" "{instances} in {files}" = "{instances} en {files}" "{language}: draft translation, waiting for a native speaker's review." = "{language}: traducción en borrador, pendiente de revisión por un hablante nativo." @@ -1571,6 +1642,7 @@ status = "draft — needs native review" "{n} matches" = { one = "{n} coincidencia", other = "{n} coincidencias" } "{n} minutes ago" = { one = "hace {n} minuto", other = "hace {n} minutos" } "{n} months ago" = { one = "hace {n} mes", other = "hace {n} meses" } +"{n} more notes omitted from this report" = { one = "{n} nota más omitida de este informe", other = "{n} notas más omitidas de este informe" } "{n} of {total}" = "{n} de {total}" "{n} people joined" = { one = "{n} persona se ha unido", other = "{n} personas se han unido" } "{n} preferred components" = { one = "{n} componente preferido", other = "{n} componentes preferidos" } diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 236f8e0d..2990cd3f 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -139,6 +139,7 @@ pub fn keys(app: &mut FigApp, ctx: &Context) { let open = u.color_profile.is_some() || u.find.is_some() || u.batch_rename.is_some() + || u.import_report.is_some() || u.confirm_close || u.export_preview || u.shortcuts_panel @@ -147,7 +148,7 @@ pub fn keys(app: &mut FigApp, ctx: &Context) { if !open || !ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { return; } - if u.color_profile.take().is_some() || u.find.take().is_some() || u.batch_rename.take().is_some() { + if u.color_profile.take().is_some() || u.find.take().is_some() || u.batch_rename.take().is_some() || u.import_report.take().is_some() { return; } for flag in [&mut u.confirm_close, &mut u.export_preview, &mut u.shortcuts_panel, &mut u.preferences, &mut u.about] { @@ -160,6 +161,7 @@ pub fn keys(app: &mut FigApp, ctx: &Context) { pub fn show(app: &mut FigApp, ctx: &Context) { // Dropped files waiting to open: their card, then (once it has shown) the files. crate::pending_open::pending_opens(app, ctx); + crate::import_report::show(app, ctx); let t = Tokens::get(ctx); if app.ui.about { let mut open = true; diff --git a/crates/ui-egui/src/home_recents.rs b/crates/ui-egui/src/home_recents.rs index d633f776..6d9794c2 100644 --- a/crates/ui-egui/src/home_recents.rs +++ b/crates/ui-egui/src/home_recents.rs @@ -568,7 +568,7 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, t: &Tokens) { action = action.or(chosen); match action { Some(Action::Open(path)) => { - if app.run("file.open", json!({"path": path})).is_some() { + if app.run_user("file.open", json!({"path": path})).is_some() { app.ui.home = false; } } diff --git a/crates/ui-egui/src/import_report.rs b/crates/ui-egui/src/import_report.rs new file mode 100644 index 00000000..b756e871 --- /dev/null +++ b/crates/ui-egui/src/import_report.rs @@ -0,0 +1,246 @@ +//! The import report: what a successful `.fig`, REST JSON or SVG import left out or changed, shown +//! in a dialog window after the file opens, so the notes don't vanish with a toast. + +use egui::RichText; +use serde_json::{Value, json}; + +use crate::FigApp; +use crate::i18n::{tr, tr_noop, trn}; +use crate::theme::{Tokens, regular}; + +/// A report stays small even when a damaged input produces many or very long notes. +pub const MAX_NOTES: usize = 500; +pub const MAX_NOTE_CHARS: usize = 2_000; + +/// The fixed notes of the `.fig` and REST importers, translated in the report. `.fig` imports name +/// "the file" where REST imports name the JSON they read. Other notes (SVG's, say) are shown as they are. +const IMPORT_NOTES: &[&str] = &[ + tr_noop("conditional prototype actions nested too deeply were skipped"), + tr_noop("conditional prototype actions with conditions that can't be evaluated were skipped"), + tr_noop("connector labels were skipped"), + tr_noop("connectors whose ends can't be placed are empty"), + tr_noop("damaged vector geometry was skipped"), + tr_noop("damaged vector networks import as outlines"), + tr_noop("document children other than pages were skipped"), + tr_noop("elbowed and curved connectors without a saved path import as straight lines"), + tr_noop("entries without a node type were skipped"), + tr_noop("filled areas of a vector with their own fill use the layer's fill"), + tr_noop("gamepad triggers without exactly one known controller button were skipped"), + tr_noop("grid tracks of fixed or content size import as equal tracks"), + tr_noop("instance swap properties whose default component isn't in the file were skipped"), + tr_noop("instances inside their own main component were imported as frames"), + tr_noop("instances of components that aren't in the file show no layers"), + tr_noop("instances of components that aren't in the file were imported as frames"), + tr_noop("instances that would nest a component inside itself were imported as frames"), + tr_noop("instances without a main component were imported as empty frames"), + tr_noop("invalid or oversized vector networks import as outlines"), + tr_noop("invalid or oversized vector path data was skipped"), + tr_noop("key triggers without a supported key were skipped"), + tr_noop("key up triggers import as key presses"), + tr_noop("layers without a position were placed at their parent's origin"), + tr_noop("layers without a type were skipped"), + tr_noop("points with their own stroke join use the layer's"), + tr_noop("prototype actions of unknown types were skipped"), + tr_noop("prototype actions without a destination in the file were skipped"), + tr_noop("prototype expressions on variables that aren't in the variables JSON were skipped"), + tr_noop("prototype expressions with unsupported functions were skipped"), + tr_noop("prototype interactions without supported actions were skipped"), + tr_noop("prototype navigations of unknown types were skipped"), + tr_noop("prototype transitions of unknown types play instantly"), + tr_noop("prototype triggers of unknown types were skipped"), + tr_noop("set-variable actions on variables that aren't in the variables JSON were skipped"), + tr_noop("set-variable actions with values that can't be evaluated were skipped"), + tr_noop("set-variable actions with values that don't fit the variable were skipped"), + tr_noop("set-variable actions without a value were skipped"), + tr_noop("set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped"), + tr_noop("stars and polygons without point counts or geometry use the default point count"), + tr_noop("text style overrides refer to missing table entries"), + tr_noop("the file has no pages"), + tr_noop("the variables JSON has no `variableCollections` (expected the local variables response) and was ignored"), + tr_noop("unknown component property types were skipped"), + tr_noop("variable aliases to variables that aren't in the variables JSON (or of another type) were skipped"), + tr_noop("variable bindings to properties the layer doesn't have were skipped"), + tr_noop("variable bindings to variables that aren't in the variables JSON (library variables) were skipped"), + tr_noop( + "variable bindings were skipped: the file JSON has no variable definitions (pass the local variables JSON as `variables`, or embed its `meta` next to the file JSON)", + ), + tr_noop("variable bindings whose variable type doesn't fit the property were skipped"), + tr_noop("variable collections without modes were skipped"), + tr_noop("variable mode lookups in prototype expressions that aren't a variable and one of its modes were skipped"), + tr_noop("variable modes of collections that aren't in the file were skipped"), + tr_noop("variable modes of collections that aren't in the variables JSON were skipped"), + tr_noop("variable values that don't match their type (or composed colours) were skipped"), + tr_noop("variables of unknown types were skipped"), + tr_noop("variables whose collection isn't in the variables JSON were skipped"), + tr_noop("variables without readable values were skipped"), + tr_noop("vectors without geometry are empty"), + tr_noop("vectors without geometry are empty (export the file JSON with geometry=paths)"), + tr_noop("very long text was cut to 1,048,576 characters"), + tr_noop("very long vector paths were skipped"), + tr_noop("video actions are not supported and were skipped"), + tr_noop("video triggers are not supported and were skipped"), +]; + +#[derive(Clone, Debug)] +pub struct Report { + pub name: String, + pub pages: Option, + pub layers: Option, + pub images: Option, + /// The pages of an imported file were added to the open document. + pub appended: bool, + /// An SVG was placed on the current page of the open document. + pub placed: bool, + /// The document the report belongs to: it goes when that document closes. + pub uid: u64, + pub notes: Vec, + pub note_count: usize, +} + +impl Report { + /// Only successful `file.open` and `file.import` results with import `warnings` create a report; + /// a native file's `repairs` are shown by the engine's own message, and an SVG opened without + /// notes has nothing to report. `params` are the command's, for the file's name. + pub fn from_result(command: &str, params: &Value, result: &Value) -> Option { + if !matches!(command, "file.open" | "file.openRecent" | "file.import") { + return None; + } + let warnings = result.get("warnings").and_then(Value::as_array)?; + let file_name = params.get("path").and_then(Value::as_str).and_then(|p| std::path::Path::new(p).file_name()).and_then(|n| n.to_str()); + if command != "file.import" && warnings.is_empty() && file_name.is_some_and(|n| n.to_ascii_lowercase().ends_with(".svg")) { + return None; + } + // `file.import` of an SVG places a frame on the current page (no `pages`): its notes + // are reported, but no pages were appended. + let appended = command == "file.import" && result.get("pages").is_some(); + if command == "file.import" && !appended && warnings.is_empty() { + return None; + } + let summary = result.get("summary"); + let count = |key: &str| summary.and_then(|s| s.get(key)).and_then(Value::as_u64); + let name = summary + .and_then(|s| s.get("name")) + .and_then(Value::as_str) + .or(file_name) + .or_else(|| params.get("name").and_then(Value::as_str)) + .unwrap_or(tr("Imported document")); + let mut report = Self { + name: name.chars().take(512).collect(), + pages: count("pages"), + layers: count("layers"), + images: count("images"), + appended, + placed: command == "file.import" && !appended, + uid: 0, + notes: Vec::new(), + note_count: 0, + }; + for note in warnings.iter().filter_map(Value::as_str) { + report.add_note(note); + } + Some(report) + } + + fn add_note(&mut self, note: &str) { + self.note_count = self.note_count.saturating_add(1); + if self.notes.len() < MAX_NOTES { + let mut text: String = note.chars().take(MAX_NOTE_CHARS).collect(); + if note.chars().nth(MAX_NOTE_CHARS).is_some() { + text.push('…'); + } + self.notes.push(text); + } + } + + pub fn inspect(&self) -> Value { + json!({"name": self.name, "pages": self.pages, "layers": self.layers, "images": self.images, + "appended": self.appended, "placed": self.placed, "warnings": self.notes, "warningCount": self.note_count}) + } +} + +/// `note` in the UI language when it is a known import note; a repeat count (` (×N)`) is kept. The +/// stored notes are lower-case fragments; a known note is shown as a sentence, so its first letter +/// is capitalised here (unknown notes are left as they are). +pub(crate) fn note_text(note: &str) -> String { + let translated = |base: &str| { + if !IMPORT_NOTES.contains(&base) { + return base.to_owned(); + } + let text = tr(base); + let mut chars = text.chars(); + chars.next().map_or_else(String::new, |first| first.to_uppercase().chain(chars).collect()) + }; + match note.rsplit_once(" (×") { + Some((base, count)) if count.strip_suffix(')').is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) => { + format!("{} (×{count}", translated(base)) + } + _ => translated(note), + } +} + +/// Show the report for a file the user opened or imported (menus, drops, pastes, recents, the +/// system's open event); commands from the control channel, MCP or scripts never call this. +pub fn capture(app: &mut FigApp, command: &str, params: &Value, result: &Value) { + if let Some(mut report) = Report::from_result(command, params, result) { + report.uid = app.session.ds().map_or(0, |d| d.uid); + app.ui.import_report = Some(report); + } +} + +pub fn show(app: &mut FigApp, ctx: &egui::Context) { + // The report goes with its document. + if app.ui.import_report.as_ref().is_some_and(|r| !app.session.docs.iter().any(|d| d.uid == r.uid)) { + app.ui.import_report = None; + } + let Some(report) = &app.ui.import_report else { return }; + let t = Tokens::get(ctx); + let mut open = true; + let mut close_clicked = false; + crate::dialogs::window(tr("Import report"), ctx).open(&mut open).show(ctx, |ui| { + ui.set_width(420.0); + ui.label(RichText::new(&report.name).strong()); + ui.label(if report.appended { + tr("Pages added to the document") + } else if report.placed { + tr("Placed on the current page") + } else { + tr("Opened as a new document") + }); + let counts: Vec = [(tr("Pages"), report.pages), (tr("Layers"), report.layers), (tr("Images"), report.images)] + .into_iter() + .filter_map(|(label, count)| count.map(|n| format!("{label}: {n}"))) + .collect(); + if !counts.is_empty() { + ui.label(counts.join(" · ")); + } + ui.add_space(8.0); + if report.notes.is_empty() { + ui.label(tr("No compatibility warnings reported")); + } else { + ui.label(RichText::new(tr("Compatibility notes")).strong()); + ui.label( + RichText::new(tr( + "FigCraft doesn't support every feature of these files yet. The notes list what was left out or changed in the imported copy; the original file is unchanged.", + )) + .color(t.text_secondary) + .font(regular(11.0)), + ); + ui.add_space(4.0); + egui::ScrollArea::vertical().id_salt("import_report_notes").max_height(260.0).show(ui, |ui| { + for note in &report.notes { + ui.label(note_text(note)); + ui.add_space(4.0); + } + }); + } + let omitted = report.note_count.saturating_sub(report.notes.len()); + if omitted > 0 { + ui.label(trn("{n} more notes omitted from this report", omitted, &[])); + } + ui.add_space(12.0); + close_clicked = crate::widgets::button(ui, tr("Close"), true, true).clicked(); + }); + if !open || close_clicked { + app.ui.import_report = None; + } +} diff --git a/crates/ui-egui/src/import_report_tests.rs b/crates/ui-egui/src/import_report_tests.rs new file mode 100644 index 00000000..cc493e4b --- /dev/null +++ b/crates/ui-egui/src/import_report_tests.rs @@ -0,0 +1,174 @@ +use figcraft_engine::Session; +use serde_json::json; + +use crate::import_report::{MAX_NOTE_CHARS, MAX_NOTES, Report, note_text}; +use crate::{FigApp, NoServices}; + +#[test] +fn import_results_with_warnings_become_reports_with_counts() { + let result = + json!({"warnings": ["layers without a type were skipped"], "summary": {"name": "Synthetic design", "pages": 2, "layers": 12, "images": 3}}); + let report = Report::from_result("file.import", &json!({}), &result).unwrap(); + assert_eq!(report.name, "Synthetic design"); + assert_eq!((report.pages, report.layers, report.images), (Some(2), Some(12), Some(3))); + assert_eq!(report.notes, ["layers without a type were skipped"]); + assert_eq!(report.note_count, 1); +} + +#[test] +fn only_import_commands_with_warnings_create_reports() { + // A warning-free `.fig` or REST open still has a summary to show. + let report = Report::from_result("file.open", &json!({}), &json!({"warnings": [], "summary": {"name": "Local", "pages": 1}})).unwrap(); + assert!(!report.appended && report.notes.is_empty()); + // A native file's repairs have their own message; an SVG without notes has no `warnings`. + assert!(Report::from_result("file.open", &json!({}), &json!({"repairs": ["Repaired parent"]})).is_none()); + assert!(Report::from_result("file.open", &json!({}), &json!({"index": 0})).is_none()); + assert!(Report::from_result("file.export", &json!({}), &json!({"warnings": ["Export warning"]})).is_none()); + assert!(Report::from_result("shape.rectangle", &json!({}), &json!({})).is_none()); + // File ▸ Open recent opens by path and reports like File ▸ Open. + let recent = json!({"warnings": ["layers without a type were skipped"], "summary": {"name": "Recent", "pages": 1}}); + assert!(Report::from_result("file.openRecent", &json!({"path": "Recent.json"}), &recent).is_some()); + assert!(Report::from_result("file.openRecent", &json!({"path": "Clean.svg"}), &json!({"warnings": []})).is_none()); +} + +#[test] +fn svg_placements_are_not_reported_as_appended_pages() { + assert!(Report::from_result("file.import", &json!({}), &json!({"id": 7})).is_none()); + let report = Report::from_result("file.import", &json!({}), &json!({"id": 7, "warnings": ["Filter skipped"]})).unwrap(); + assert!(!report.appended); + assert!(Report::from_result("file.import", &json!({}), &json!({"pages": [1], "summary": {"pages": 1}, "warnings": []})).unwrap().appended); +} + +#[test] +fn report_retention_is_bounded_and_truncates_only_at_character_boundaries() { + let long_note = "🦎".repeat(MAX_NOTE_CHARS + 20); + let notes: Vec<_> = std::iter::repeat_n(long_note, MAX_NOTES + 37).collect(); + let report = Report::from_result("file.open", &json!({}), &json!({"warnings": notes})).unwrap(); + assert_eq!(report.note_count, MAX_NOTES + 37); + assert_eq!(report.notes.len(), MAX_NOTES); + assert!(report.notes.iter().all(|note| note.chars().count() == MAX_NOTE_CHARS + 1 && note.ends_with('…'))); +} + +#[test] +fn note_text_keeps_the_repeat_suffix_and_unknown_notes() { + let base = "instances of components that aren't in the file were imported as frames"; + let repeated = note_text(&format!("{base} (×122)")); + assert_eq!(repeated, "Instances of components that aren't in the file were imported as frames (×122)"); + assert_eq!(note_text("some unknown note"), "some unknown note"); + assert_eq!(note_text("weird (×x)"), "weird (×x)"); + assert_eq!(note_text("weird (×12) tail"), "weird (×12) tail"); +} + +#[test] +fn opening_a_rest_json_file_reports_what_was_skipped() { + let path = std::env::temp_dir().join(format!("figcraft-import-report-{}.json", std::process::id())); + let file = json!({"document": {"id": "0:0", "name": "Synthetic", "type": "DOCUMENT", "children": [ + {"id": "0:1", "name": "Page 1", "type": "CANVAS", "children": [ + {"id": "1:1", "name": "Card", "type": "FRAME", "size": {"x": 200, "y": 100}, "relativeTransform": [[1, 0, 10], [0, 1, 20]], + "children": [{"id": "1:2", "name": "No type"}]}]}]}}); + std::fs::write(&path, file.to_string()).unwrap(); + let mut app = FigApp::new(Session::new(), Box::new(NoServices)); + // A programmatic open (control channel, MCP) never opens the window. + assert!(app.run("file.open", json!({"path": path.to_string_lossy()})).is_some()); + assert!(app.ui.import_report.is_none()); + let opened = app.run_user("file.open", json!({"path": path.to_string_lossy()})); + std::fs::remove_file(&path).unwrap(); + assert!(opened.is_some()); + let report = app.ui.import_report.as_ref().expect("a report for the REST import"); + assert!(!report.appended); + assert_eq!(report.pages, Some(1)); + assert!(report.layers.is_some_and(|n| n >= 1)); + assert!(report.notes.iter().any(|n| n == "layers without a type were skipped"), "{:?}", report.notes); + assert_eq!(app.inspect()["importReport"]["warningCount"], report.note_count); +} + +#[test] +fn escape_closes_the_import_report_like_other_simple_dialogs() { + let mut u = crate::layers_drag_tests::Ui::new(Session::new()); + let mut report = Report::from_result("file.open", &json!({}), &json!({"warnings": ["Synthetic note"], "summary": {"name": "Synthetic design"}})); + report.as_mut().unwrap().uid = u.app.session.ds().unwrap().uid; + u.app.ui.import_report = report; + u.frame(vec![]); + let escape = + vec![egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }]; + u.frame(escape); + assert!(u.app.ui.import_report.is_none(), "Escape closes the report"); +} + +fn shown_texts(shapes: Vec) -> Vec { + fn walk(shape: &egui::Shape, out: &mut Vec) { + match shape { + egui::Shape::Text(t) => out.push(t.galley.text().to_string()), + egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, out)), + _ => {} + } + } + let mut out = Vec::new(); + shapes.iter().for_each(|c| walk(&c.shape, &mut out)); + out +} + +#[test] +fn a_clean_svg_open_has_no_report_and_a_placed_svg_says_where_it_went() { + let svg_open = Report::from_result("file.open", &json!({"path": "icons/Logo.SVG"}), &json!({"warnings": []})); + assert!(svg_open.is_none(), "a clean SVG opened with file.open has nothing to report"); + let noted = Report::from_result("file.open", &json!({"path": "Logo.svg"}), &json!({"warnings": ["Filter skipped"]})).unwrap(); + assert_eq!(noted.name, "Logo.svg"); + + let placed = Report::from_result("file.import", &json!({"path": "dir/Logo.svg"}), &json!({"id": 7, "warnings": ["Filter skipped"]})).unwrap(); + assert!(placed.placed && !placed.appended); + assert_eq!(placed.name, "Logo.svg"); + let mut u = crate::layers_drag_tests::Ui::new(Session::new()); + let mut placed = placed; + placed.uid = u.app.session.ds().unwrap().uid; + u.app.ui.import_report = Some(placed); + let texts = shown_texts({ + u.frame(vec![]); + u.frame_output(vec![]).shapes + }); + assert!(texts.iter().any(|t| t == "Placed on the current page"), "{texts:?}"); + assert!(!texts.iter().any(|t| t == "Opened as a new document"), "{texts:?}"); + assert!(texts.iter().any(|t| t == "Logo.svg"), "{texts:?}"); +} + +#[test] +fn the_report_goes_when_its_document_closes_and_counts_omitted_notes_in_the_singular() { + let mut u = crate::layers_drag_tests::Ui::new(Session::new()); + let mut report = Report::from_result("file.open", &json!({}), &json!({"warnings": ["One note"], "summary": {"name": "Synthetic"}})).unwrap(); + report.uid = u.app.session.ds().unwrap().uid; + report.note_count = 2; + u.app.ui.import_report = Some(report); + let texts = shown_texts({ + u.frame(vec![]); + u.frame_output(vec![]).shapes + }); + assert!(texts.iter().any(|t| t == "1 more note omitted from this report"), "{texts:?}"); + + u.app.session.execute("file.new", &json!({"name": "Other"})).unwrap(); + u.frame(vec![]); + assert!(u.app.ui.import_report.is_some(), "another tab was added; the report's document is still open"); + u.app.session.execute("file.close", &json!({"index": 0, "force": true})).unwrap(); + u.frame(vec![]); + assert!(u.app.ui.import_report.is_none(), "the report left with its document"); +} + +#[test] +fn a_dropped_file_shows_its_report_once_it_opens() { + let path = std::env::temp_dir().join(format!("figcraft-import-report-drop-{}.json", std::process::id())); + let file = json!({"document": {"id": "0:0", "name": "Dropped", "type": "DOCUMENT", "children": [ + {"id": "0:1", "name": "Page 1", "type": "CANVAS", "children": [{"id": "1:1", "name": "No type"}]}]}}); + std::fs::write(&path, file.to_string()).unwrap(); + let mut u = crate::layers_drag_tests::Ui::new(Session::new()); + u.app.ui.pending_opens.push(crate::pending_open::PendingOpen { + label: "Dropped.json".into(), + params: json!({"path": path.to_string_lossy()}), + command: "file.open", + target: None, + }); + for _ in 0..6 { + u.frame(vec![]); + } + std::fs::remove_file(&path).unwrap(); + let report = u.app.ui.import_report.as_ref().expect("the dropped file's report"); + assert_eq!(report.uid, u.app.session.ds().unwrap().uid); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 20a8a77c..3d64ecae 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -32,6 +32,7 @@ pub mod home; pub mod home_recents; pub mod i18n; pub mod icons; +pub mod import_report; pub mod layers; pub mod layout_panel; pub mod list_drag; @@ -108,6 +109,8 @@ mod home_tests; #[cfg(test)] mod idle_tests; #[cfg(test)] +mod import_report_tests; +#[cfg(test)] mod layers_behaviour_tests; #[cfg(test)] mod layers_drag_tests; @@ -456,6 +459,8 @@ pub struct UiState { /// ([`pending_open::pending_opens`]), and the frames it has been drawn. pub pending_opens: Vec, pub pending_open_frames: u8, + /// The latest successful file import's summary and compatibility notes. + pub import_report: Option, pub screenshot_requests: Vec<(u64, String)>, pub frame_ms: f64, pub render_ms: f64, @@ -564,6 +569,7 @@ impl Default for UiState { last_error: None, pending_opens: Vec::new(), pending_open_frames: 0, + import_report: None, screenshot_requests: Vec::new(), frame_ms: 0.0, render_ms: 0.0, @@ -676,6 +682,16 @@ impl FigApp { r.map_err(|e| self.failed(id, &e)).ok() } + /// [`run`](Self::run) for something the user did (a dialog, a drop, a paste, a recent file): + /// a file it imports shows its import report. Programmatic calls use `run` and never open one. + pub fn run_user(&mut self, id: &str, params: Value) -> Option { + let result = self.run(id, params.clone()); + if let Some(result) = &result { + import_report::capture(self, id, ¶ms, result); + } + result + } + /// Show command `id`'s error. fn failed(&mut self, id: &str, e: &figcraft_engine::EngineError) { let msg = e.to_string(); @@ -966,7 +982,7 @@ impl FigApp { false, |app, p| { let ps = p.to_string_lossy().to_string(); - if app.run("file.open", json!({"path": ps})).is_some() { + if app.run_user("file.open", json!({"path": ps})).is_some() { app.ui.home = false; } }, @@ -982,7 +998,7 @@ impl FigApp { let title = if place { i18n::tr("Place image") } else { i18n::tr("Import") }; self.pick_open(title, exts, move |app, p| { let ps = p.to_string_lossy().to_string(); - app.run("file.import", json!({"path": ps})); + app.run_user("file.import", json!({"path": ps})); }); } @@ -1527,6 +1543,7 @@ impl FigApp { "panels": {"left": self.ui.left_width, "right": self.ui.right_width, "leftCollapsed": self.ui.left_collapsed}, "status": self.session.status(), "lastError": self.ui.last_error, + "importReport": self.ui.import_report.as_ref().map(import_report::Report::inspect), }); state["pendingOpens"] = json!(self.ui.pending_opens.len()); state diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index b9d3299a..2c365d9f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -54,7 +54,7 @@ pub fn paste(app: &mut FigApp, event_text: Option<&str>) { } else if figcraft_engine::looks_like_svg(&text) { use base64::Engine as _; let b64 = base64::engine::general_purpose::STANDARD.encode(text.as_bytes()); - app.run("file.import", json!({"data": b64, "name": "Pasted SVG.svg", "x": cx, "y": cy, "center": true})); + app.run_user("file.import", json!({"data": b64, "name": "Pasted SVG.svg", "x": cx, "y": cy, "center": true})); } else { app.run("text.create", json!({"text": text, "x": cx, "y": cy})); } @@ -234,24 +234,25 @@ fn recent_menu(app: &mut FigApp, ui: &mut Ui, prefix: &str, key: &str, label: &s let picked = submenu(ui, prefix, &format!("{prefix}/{key}"), label, t, |ui| { let recents = &app.session.prefs.recent_files; let mut picked = None; - for (i, p) in recents.iter().enumerate() { + for p in recents.iter() { let path = std::path::Path::new(p); let name = path.file_name().map_or_else(|| p.clone(), |f| f.to_string_lossy().into_owned()); let exists = path.exists(); if item(ui, &name, "", exists, false, t) && exists { - picked = Some(json!({"index": i})); + picked = Some(("file.openRecent", json!({"path": p}))); } } if !recents.is_empty() { separator(ui); } if item(ui, tr("Clear recent"), "", !recents.is_empty(), false, t) && !recents.is_empty() { - picked = Some(json!({"clear": true})); + picked = Some(("file.openRecent", json!({"clear": true}))); } picked }); - let Some(params) = picked else { return false }; - if app.run("file.openRecent", params).is_some_and(|r| r.get("index").is_some()) { + let Some((command, params)) = picked else { return false }; + // Through the user path, so an import report shows; `file.openRecent` also drops a file that is gone. + if app.run_user(command, params).is_some_and(|r| r.get("index").is_some()) { app.ui.home = false; } true diff --git a/crates/ui-egui/src/pending_open.rs b/crates/ui-egui/src/pending_open.rs index 63c213ff..9d19b942 100644 --- a/crates/ui-egui/src/pending_open.rs +++ b/crates/ui-egui/src/pending_open.rs @@ -29,7 +29,7 @@ pub struct ImportTarget { fn run_pending(app: &mut FigApp, pending: PendingOpen) -> Option { let target = match pending.target { Some(target) => target, - None if pending.command != "file.import" => return app.run(pending.command, pending.params), + None if pending.command != "file.import" => return app.run_user(pending.command, pending.params), None => { cancel(app, tr("The destination document is unavailable. Nothing was imported.").to_string()); return None; @@ -49,7 +49,7 @@ fn run_pending(app: &mut FigApp, pending: PendingOpen) -> Option { let previous_selection = app.session.docs[index].selection.clone(); app.session.active = index; app.session.docs[index].page = target.page; - let result = app.run(pending.command, pending.params); + let result = app.run_user(pending.command, pending.params); if previous_page != target.page && let Some(doc) = app.session.docs.iter_mut().find(|doc| doc.uid == target.uid) && doc.doc.pages.contains(&previous_page) diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 397aed5b..80e8e488 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -117,7 +117,13 @@ zoom preview or the refined image (`canvas.quality`: `preview` or `refined`) and asking for a render to its preview and its refined image being handed to egui as the canvas texture (`canvas.previewLatencyMs`, `canvas.refineLatencyMs`, on a monotonic clock, rendering included in the browser too; the GPU upload and presenting the frame come after; `null` until -there was one). While a prototype plays, `presenter` +there was one). `importReport` holds the Import report window's content (`{name, pages, layers, images, appended, placed, warnings, warningCount}`, +`null` while none is open). The window opens only for a file the user opened or imported in the app +(File ▸ Open or Import, Open recent, a recent file on the home screen, a drop, a paste, the system's open event). +Emulated user input counts as the user's action: `ui.input` and `ui.key` (for example a Cmd+V paste or a click +on a menu item) can open it. Commands sent through this channel with `engine.execute`, MCP, `window.new {path}` +and files named on the command line never open it. +`file.open` and `file.import` return the counts as `summary`. While a prototype plays, `presenter` (full window) and `preview` (the preview pane) hold the player's state: the `current` frame and its `name`, `start`, `history`, open `overlays`, how many delayed mouse triggers are `waiting`, the screen's `scroll` offset and `scrollRange`, the `device` (and whether it is turned to `landscape`, its `deviceColor`) and the `viewport` of the frame diff --git a/docs/mcp.md b/docs/mcp.md index 4efee72a..9cc428b0 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -227,7 +227,7 @@ Or in a project's `.mcp.json`: | `select {ids}` | Replace the selection (`[]` deselects). | | `export {ids, format, scale \| size, path?, bleed?, cropMarks?, bleedMarks?, cmyk?, contentsOnly?, includeBoundingBox?, svgOutlineText?, svgIdAttribute?, svgSimplifyStroke?}` | png, jpg, svg, webp, pdf, or `fig`: the whole file as `.fig`, experimental (see Exporting `.fig` files below); `size` is `2x`, `512w` or `512h`. The image covers what the layers paint (shadows towards their offset, strokes, unclipped children, a mask group's mask; hidden layers take no room; a slice is its area with everything under it); `contentsOnly: false` also draws every other layer showing in that area, `includeBoundingBox` exports the layers' own boxes; SVG: `svgOutlineText: false` keeps ``, `svgIdAttribute` writes unique ids from layer names, `svgSimplifyStroke: false` writes every stroke as an outline. The same options go in export settings (`export.add` / `export.update`). PDF: frames one page each (sized in points), anything else together on one page; print options: `bleed` (points or a length like `"3mm"`, `"0.125in"`), crop and bleed marks, CMYK colour (see Exports below), also taken by `file.export`, `file.exportLayers`, `file.exportFramesPdf` and stored per export setting by `export.add` / `export.update`; `run_command` `file.exportFramesPdf {page?, path?}` writes every top-level frame of a page. Without `path`, images come back as MCP image content, SVG as text. Layers' own export settings: `run_command` with `export.add` / `export.update {index, size?, suffix?, format?}` / `export.remove` and `file.exportLayers {ids?, dir?, assets?}` (every layer once per setting, named `.`, a slash in a layer name making a folder; `assets: true` exports the Inspect tab's assets instead: icons as SVG, images as PNG). | | `screenshot {ids?, scale?, maxSize?, window?}` | PNG of the page (with background) or of layers, scaled to fit `maxSize` (default 1600). `window: true` captures the app window in front when attached, a window id (see Windows below) that window. | -| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. | +| `open_file {path, variables?}` / `save_file {path?}` | Open `.figcraft`, `.json` (FigCraft JSON or REST API file JSON with its variables, see below), `.fig` (best effort, see below), `.svg` (see Importing SVG below) or an image; save as `.figcraft`. `open_file` returns `opened` (for a file opened anew, a `summary` of `name`, `pages`, `layers` and `images`, and `warnings` or `repairs`; a file that was already open just gives its `index`) and the document's `document` outline. | | `undo` / `redo` | Step through history. | | `pointer {kind, x, y, to?, steps?, tool?, shift, alt, cmd, pressure?, pressureTo?}` | Mouse gestures in page coordinates with the current tool: `click`, `double`, `drag`, `down`, `move`, `up`, `hover`. A dragged layer drops into the frame or auto layout slot under the pointer, except one inside a group, a boolean group or an instance (outside its slots), which only moves. `pressure` (0–1) is pen pressure; a drag goes from `pressure` to `pressureTo` (see Brushes and pen pressure). | | `key {key, shift, alt, cmd}` | A shortcut or key as if typed on the canvas (every default shortcut, tool keys, Escape, Delete, arrows, number keys for opacity, arrows that pan the view when nothing is selected; while editing text, the text shortcuts and then text editing keys). Keys are names like `A`, `0`, `Enter`, `ArrowLeft`, `=`, `/`, or egui's (`Plus`, `Questionmark`); `cmd` is Cmd on macOS and Ctrl elsewhere. |