mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:40:14 -06:00
Fix #3797: documents open in Preview, and Outline is a setting of each document
View > Outline was one app-wide flag saved in the interface preferences, so after Cmd+Y every later launch drew every opened or new document as outlines, with no fills. The flag is now part of each document's view: a document opens in Preview, each tab shows its own Outline or Preview, and the setting is not saved at launch. An older preferences file that holds the flag still loads, and its value is ignored. The Outline command and ui.set's outline act on the active document and are an error without one.
This commit is contained in:
@@ -72,6 +72,11 @@ impl VectorcraftApp {
|
||||
self.session.active_index().and_then(|i| self.views.get(i))
|
||||
}
|
||||
|
||||
/// Whether the active document is in Outline view (View › Outline); false without one.
|
||||
pub fn outline_view(&self) -> bool {
|
||||
self.view().is_some_and(|v| v.outline)
|
||||
}
|
||||
|
||||
/// The zero point shown by rulers and position fields: the document's with global rulers, the
|
||||
/// active artboard's with artboard rulers. Command coordinates stay absolute.
|
||||
pub(crate) fn ruler_origin(&self) -> vectorcraft_geom::Point {
|
||||
@@ -88,7 +93,7 @@ impl VectorcraftApp {
|
||||
pub fn view_info(&self) -> ViewInfo {
|
||||
ViewInfo {
|
||||
zoom: self.view().map(|v| v.zoom).unwrap_or(1.0),
|
||||
outline: self.ui.view.outline,
|
||||
outline: self.outline_view(),
|
||||
smart_guides: self.ui.view.smart_guides,
|
||||
guides: self.ui.view.guides,
|
||||
snap_to_grid: self.ui.view.snap_to_grid,
|
||||
|
||||
@@ -492,7 +492,7 @@ fn cache_key(
|
||||
cy: v.center.y,
|
||||
w,
|
||||
h,
|
||||
outline: app.ui.view.outline,
|
||||
outline: app.outline_view(),
|
||||
trim: app.ui.view.trim_view,
|
||||
templates: app.ui.view.templates,
|
||||
guides: app.ui.view.guides,
|
||||
@@ -560,7 +560,7 @@ fn frame_options(
|
||||
mask_view: Option<vectorcraft_doc::NodeId>,
|
||||
smooth_images: bool,
|
||||
) -> vectorcraft_render::RenderOptions {
|
||||
let opts = vectorcraft_render::RenderOptions { outline: app.ui.view.outline, background: None, artboards: false, ..Default::default() };
|
||||
let opts = vectorcraft_render::RenderOptions { outline: app.outline_view(), background: None, artboards: false, ..Default::default() };
|
||||
vectorcraft_render::RenderOptions {
|
||||
proof: vectorcraft_render::proof::active_proof(),
|
||||
overprint_preview: vectorcraft_render::proof::overprint_preview_on(),
|
||||
@@ -669,7 +669,7 @@ fn paint_paper(app: &VectorcraftApp, ui: &Ui, painter: &egui::Painter, xf: &Xf,
|
||||
// View › Hide Artboards: no edges and no pasteboard, the whole canvas paper (#4038).
|
||||
let setup = &doc.setup;
|
||||
painter.rect_filled(rect, 0.0, if setup.simulate_paper { crate::panels::c32(&setup.paper()) } else { Color32::WHITE });
|
||||
} else if !app.ui.view.outline {
|
||||
} else if !app.outline_view() {
|
||||
let setup = &doc.setup;
|
||||
let grid = (st.transparency_grid && setup.background == vectorcraft_doc::Background::Transparent).then(|| checker_texture(ui.ctx(), setup));
|
||||
let paper = if setup.simulate_paper { crate::panels::c32(&setup.paper()) } else { Color32::WHITE };
|
||||
@@ -687,7 +687,7 @@ fn paint_paper(app: &VectorcraftApp, ui: &Ui, painter: &egui::Painter, xf: &Xf,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if app.ui.view.outline {
|
||||
} else if app.outline_view() {
|
||||
for ab in &doc.artboards {
|
||||
painter.add(Shape::convex_polygon(xf.quad(ab.rect), Color32::WHITE, Stroke::NONE));
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ pub(super) fn hit_with<T>(
|
||||
let prefs = &app.session.prefs;
|
||||
let opt = vectorcraft_doc::hit::HitOptions {
|
||||
tol: prefs.selection_tolerance / zoom,
|
||||
outline: app.ui.view.outline,
|
||||
outline: app.outline_view(),
|
||||
path_only: prefs.object_selection_by_path_only,
|
||||
type_path_only: prefs.type_selection_by_path_only,
|
||||
scope: app.session.active().and_then(|st| st.isolation),
|
||||
|
||||
@@ -2588,7 +2588,7 @@ fn pan_fills_the_area_the_last_frame_leaves() {
|
||||
assert!(app.canvas.fill_worker.as_ref().unwrap().pending.is_none(), "the late fill was taken");
|
||||
// A pan with Outline turned on: the frame shown's options are not the ones asked for, and
|
||||
// no fill is asked for.
|
||||
app.ui.view.outline = true;
|
||||
app.view_mut().unwrap().outline = true;
|
||||
app.view_mut().unwrap().center = Point::new(400.0, 150.0);
|
||||
frame(&mut app);
|
||||
assert!(jobs.try_iter().last().is_some_and(|j| j.key.outline), "the outline frame is on the worker");
|
||||
@@ -3521,7 +3521,7 @@ fn scrolls_and_pans_move_the_view_by_whole_device_pixels() {
|
||||
/// another document, or when the device pixels a point changed.
|
||||
#[test]
|
||||
fn a_scrolls_leftover_pixel_part_carries_only_to_the_same_view() {
|
||||
let view = View { zoom: 1.0, center: Point::new(100.0, 100.0), fitted: true, rotation: 0.0, artboard: 0 };
|
||||
let view = View { zoom: 1.0, center: Point::new(100.0, 100.0), fitted: true, rotation: 0.0, artboard: 0, outline: false };
|
||||
let mut v = view;
|
||||
let rest = scroll_by(&mut v, 1, 1.0, vec2(0.4, -0.3), ScrollRest::default());
|
||||
assert_eq!(v.center, view.center, "less than half a pixel does not move the view");
|
||||
@@ -3765,6 +3765,64 @@ fn show_live_paint_gaps_draws_the_selected_groups_gaps_in_their_preview_color()
|
||||
assert!(!serde_json::from_value::<crate::UiState>(saved).unwrap().view.live_paint_gaps);
|
||||
}
|
||||
|
||||
/// #3797: View › Outline is a setting of each document's view. A document opened or made after
|
||||
/// another was put in Outline opens in Preview, where a click inside its fill picks it, and each
|
||||
/// document keeps its own setting.
|
||||
#[test]
|
||||
fn outline_is_per_document_and_new_documents_start_in_preview() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 100, "height": 100})).unwrap();
|
||||
app.sync_views();
|
||||
let inside = Point::new(70.0, 70.0);
|
||||
let picks_fill = |app: &VectorcraftApp| super::outline::hit_at(app, inside, 1.0).is_some();
|
||||
assert!(picks_fill(&app), "Preview picks the fill");
|
||||
app.run("view.outline", json!({})).unwrap();
|
||||
assert!(app.outline_view() && app.view_info().outline);
|
||||
assert!(!picks_fill(&app), "Outline picks only the path");
|
||||
// An SVG opened now opens in Preview, its fill drawn and picked.
|
||||
let svg =
|
||||
br##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect x="20" y="20" width="100" height="100" fill="#ff0000"/></svg>"##;
|
||||
crate::io::open_bytes(&mut app, "b.svg", svg, None).unwrap();
|
||||
assert_eq!(app.session.active_index(), Some(1));
|
||||
assert!(!app.outline_view() && !app.view_info().outline, "the opened document is in Preview");
|
||||
assert!(picks_fill(&app));
|
||||
assert_eq!(crate::menus::dynamic_label(&app, "view.outline", "Outline"), "Outline");
|
||||
assert_eq!(crate::menus::checked(&app, "view.outline", &json!({})), Some(false));
|
||||
// A new document opens in Preview too.
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
assert!(!app.outline_view());
|
||||
// The first document is still in Outline.
|
||||
app.session.execute("document.activate", &json!({"index": 0})).unwrap();
|
||||
assert!(app.outline_view() && !picks_fill(&app));
|
||||
assert_eq!(crate::menus::dynamic_label(&app, "view.outline", "Outline"), "Preview");
|
||||
assert_eq!(crate::menus::checked(&app, "view.outline", &json!({})), Some(true));
|
||||
// Without a document there is nothing to put in Outline.
|
||||
while !app.session.documents().is_empty() {
|
||||
app.session.execute("file.close", &json!({})).unwrap();
|
||||
}
|
||||
app.sync_views();
|
||||
assert!(!app.outline_view() && !crate::menus::enabled(&app, "view.outline"));
|
||||
assert!(app.run("view.outline", json!({})).is_err());
|
||||
}
|
||||
|
||||
/// #3797: a preferences file saved while View › Outline was an interface setting still loads, with
|
||||
/// its other view settings, and its Outline value does not put a document in Outline.
|
||||
#[test]
|
||||
fn old_outline_preference_loads_but_does_not_restore_outline() {
|
||||
let mut saved = serde_json::to_value(crate::UiState::default()).unwrap();
|
||||
assert!(saved["view"].get("outline").is_none(), "Outline is not saved with the interface");
|
||||
saved["view"]["outline"] = json!(true);
|
||||
saved["view"]["rulers"] = json!(true);
|
||||
let ui = serde_json::from_value::<crate::UiState>(saved).unwrap().sanitized();
|
||||
assert!(ui.view.rulers, "the other view settings load");
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.ui = ui;
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
app.sync_views();
|
||||
assert!(!app.outline_view(), "a new document is in Preview");
|
||||
}
|
||||
|
||||
/// #511: a star shows a widget in each of its ten corners with Direct Selection (none with
|
||||
/// the Selection tool: it isn't a live shape); View → Hide Corner Widget hides them.
|
||||
#[test]
|
||||
|
||||
@@ -12,6 +12,13 @@ pub(super) fn tab_title(d: &vectorcraft_engine::DocState, zoom: f64, outline: bo
|
||||
format!("{}{} @ {} ({mode})", d.title(), if d.is_dirty() { "*" } else { "" }, zoom_label(zoom, display_print_size).replace('%', " %"))
|
||||
}
|
||||
|
||||
/// The title of open document `i`'s tab, with its own view's zoom and Outline or Preview.
|
||||
pub(super) fn doc_tab_title(app: &VectorcraftApp, i: usize, d: &vectorcraft_engine::DocState) -> String {
|
||||
let view = app.views.get(i);
|
||||
let zoom = view.map_or(1.0, |v| v.zoom);
|
||||
tab_title(d, zoom, view.is_some_and(|v| v.outline), app.session.prefs.display_print_size)
|
||||
}
|
||||
|
||||
/// Whether a document tab's × comes before its title (macOS) rather than after it (Windows,
|
||||
/// Linux and the web).
|
||||
pub(super) const CLOSE_BEFORE_TITLE: bool = cfg!(target_os = "macos");
|
||||
@@ -46,8 +53,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| {
|
||||
let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0);
|
||||
let title = tab_title(d, zoom, app.ui.view.outline, app.session.prefs.display_print_size);
|
||||
let title = doc_tab_title(app, i, d);
|
||||
let color = if Some(i) == active { t.text_strong } else { t.text_dim };
|
||||
(title.clone(), ui.painter().layout_no_wrap(title, theme::semibold(title_size), color))
|
||||
})
|
||||
|
||||
@@ -866,6 +866,18 @@ fn the_tab_title_names_the_opacity_mask_while_it_is_edited() {
|
||||
assert!(title(&s).ends_with("(RGB/Preview)"));
|
||||
}
|
||||
|
||||
/// #3797: each document's tab names its own Outline or Preview.
|
||||
#[test]
|
||||
fn only_the_outlined_documents_tab_reads_outline() {
|
||||
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
app.run("view.outline", json!({})).unwrap();
|
||||
let titles: Vec<String> = app.session.documents().iter().enumerate().map(|(i, d)| super::tabs::doc_tab_title(&app, i, d)).collect();
|
||||
assert!(titles[0].ends_with("(RGB/Preview)"), "{titles:?}");
|
||||
assert!(titles[1].ends_with("(RGB/Outline)"), "{titles:?}");
|
||||
}
|
||||
|
||||
/// One headless frame of the Control bar; returns the isolation button's rectangle, if it shows.
|
||||
fn isolation_button(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Option<egui::Rect> {
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 300.0));
|
||||
|
||||
@@ -593,6 +593,10 @@ fn synthetic_click(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlR
|
||||
fn set_ui(app: &mut VectorcraftApp, req: &ControlRequest) -> Outcome {
|
||||
let p = &req.params;
|
||||
let s = |k: &str| p.get(k).and_then(Value::as_str);
|
||||
// Outline is a setting of the active document's view: without one, nothing is set.
|
||||
if p.get("outline").is_some_and(Value::is_boolean) && app.session.active().is_none() {
|
||||
return err("no document");
|
||||
}
|
||||
let mut r = Ok(Value::Null);
|
||||
if let Some(b) = s("brightness") {
|
||||
r = app.run("window.brightness", json!({"brightness": b}));
|
||||
@@ -613,7 +617,7 @@ fn set_ui(app: &mut VectorcraftApp, req: &ControlRequest) -> Outcome {
|
||||
if let Some(want) = p.get(k).and_then(Value::as_bool) {
|
||||
let cur = match k {
|
||||
"rulers" => app.ui.view.rulers,
|
||||
"outline" => app.ui.view.outline,
|
||||
"outline" => app.outline_view(),
|
||||
"grid" => app.ui.view.grid,
|
||||
"smartGuides" => app.ui.view.smart_guides,
|
||||
_ => app.ui.view.bounding_box,
|
||||
|
||||
@@ -22,6 +22,24 @@ fn ui_set_turns_the_image_trace_preview_off_and_on() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #3797: `ui.set` `outline` sets the active document's view; without a document it is an error
|
||||
/// and sets nothing else either.
|
||||
#[test]
|
||||
fn ui_set_outline_sets_the_active_documents_view() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
for params in [json!({"outline": false}), json!({"outline": true, "grid": true})] {
|
||||
let r = input_request(&mut app, "ui.set", params);
|
||||
assert_eq!(r["ok"], false, "{r}");
|
||||
}
|
||||
assert!(!app.ui.view.grid, "nothing is set");
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
|
||||
let r = input_request(&mut app, "ui.set", json!({"outline": true}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert!(app.outline_view());
|
||||
assert_eq!(app.views.iter().map(|v| v.outline).collect::<Vec<_>>(), [false, true], "the other document stays in Preview");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_image_trace_preview_is_a_command() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String {
|
||||
fn view_toggle_label(app: &VectorcraftApp, id: &str) -> Option<String> {
|
||||
let v = &app.ui.view;
|
||||
let text: String = match id {
|
||||
"view.outline" => if v.outline { "Preview" } else { "Outline" }.into(),
|
||||
"view.outline" => if app.outline_view() { "Preview" } else { "Outline" }.into(),
|
||||
"window.workspace.reset" => format!("Reset {}", app.ui.workspace),
|
||||
"view.edges" => if v.edges { "Hide Edges" } else { "Show Edges" }.into(),
|
||||
"view.template" => if v.templates { "Hide Template" } else { "Show Template" }.into(),
|
||||
|
||||
@@ -184,7 +184,11 @@ fn run_ui_part2(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
"edit.colorSettings" | "edit.assignProfile" if p.as_object().is_none_or(|o| o.is_empty()) => {
|
||||
crate::panel_docking::reveal_panel(app, "separations").and_then(|_| app.session.execute(id, p).map_err(|e| e.to_string()))
|
||||
}
|
||||
"view.outline" => flag(&mut app.ui.view.outline),
|
||||
// Outline or Preview is a setting of the active document's view.
|
||||
"view.outline" => match app.view_mut() {
|
||||
Some(v) => flag(&mut v.outline),
|
||||
None => Err("no document".into()),
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
Some(r)
|
||||
|
||||
@@ -41,7 +41,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
selected_text(false).is_some_and(|t| t.vertical == (id == "type.orientation.vertical"))
|
||||
}
|
||||
"type.opticalMarginAlignment" => crate::panels::character::shared_text_flag(app, |t| t.optical_margin) == Some(true),
|
||||
"view.outline" => v.outline,
|
||||
"view.outline" => app.outline_view(),
|
||||
"view.pixelPreview" => v.pixel_preview,
|
||||
"view.trimView" => v.trim_view,
|
||||
"view.snapToPixel" => v.snap_to_pixel,
|
||||
@@ -205,6 +205,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
||||
| "view.fitArtboard"
|
||||
| "view.fitAll"
|
||||
| "view.actualSize"
|
||||
| "view.outline"
|
||||
| "view.rotate" => app.session.active().is_some(),
|
||||
"view.rotateToSelection" | crate::addons::arrange::ZOOM => app.session.active().is_some_and(|d| !d.selection.objects.is_empty()),
|
||||
id if id.starts_with("file.openRecent") => recent_slot(app, id).is_some(),
|
||||
|
||||
@@ -21,11 +21,15 @@ pub struct View {
|
||||
/// Size show it.
|
||||
#[serde(default)]
|
||||
pub artboard: usize,
|
||||
/// View › Outline: the document's art is drawn as outlines. Each document has its own, and a
|
||||
/// document opens in Preview.
|
||||
#[serde(default)]
|
||||
pub outline: bool,
|
||||
}
|
||||
|
||||
impl Default for View {
|
||||
fn default() -> Self {
|
||||
Self { zoom: 1.0, center: Point::new(306.0, 396.0), fitted: false, rotation: 0.0, artboard: 0 }
|
||||
Self { zoom: 1.0, center: Point::new(306.0, 396.0), fitted: false, rotation: 0.0, artboard: 0, outline: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +49,7 @@ impl View {
|
||||
fitted: true,
|
||||
rotation: if v.rotation.is_finite() { v.rotation } else { 0.0 },
|
||||
artboard: 0,
|
||||
outline: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -232,7 +237,6 @@ pub struct ViewFlags {
|
||||
pub guides: bool,
|
||||
pub smart_guides: bool,
|
||||
pub bounding_box: bool,
|
||||
pub outline: bool,
|
||||
pub pixel_preview: bool,
|
||||
pub snap_to_grid: bool,
|
||||
pub snap_to_point: bool,
|
||||
@@ -271,7 +275,6 @@ impl Default for ViewFlags {
|
||||
guides: true,
|
||||
smart_guides: true,
|
||||
bounding_box: true,
|
||||
outline: false,
|
||||
pixel_preview: false,
|
||||
snap_to_grid: false,
|
||||
snap_to_point: true,
|
||||
|
||||
@@ -101,7 +101,7 @@ the two can still win that race.
|
||||
| `ui.move` | `{x, y}` | screen-point pointer move |
|
||||
| `ui.click` / `ui.drag` | `{x, y, button?, count?, shift?, alt?, ctrl?, cmd?}` / `{x, y, toX, toY, steps?, count?, button?, shift?, alt?, ctrl?, cmd?}` | egui pointer input; see synthetic input budgets above |
|
||||
| `ui.wheel` | `{x, y, dy?, dx?, unit?: "line"\|"point", shift?, alt?, cmd?}` | a mouse wheel turn over screen point (x, y): `dy` notches up (+) or down, `dx` sideways. Over the canvas the wheel scrolls and Cmd- or Alt-wheel (Option on the Mac) zooms about the pointer; with the `zoomWithMouseWheel` preference the wheel and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused numeric field (click it first) each notch steps its value as Up/Down do (Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything else in a panel the wheel scrolls it |
|
||||
| `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?, imageTracePreview?}` | `imageTracePreview`: the Image Trace panel's Preview (`ui.inspect` shows it as `ui.image_trace_preview`) |
|
||||
| `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?, imageTracePreview?}` | `outline`: View › Outline for the active document only (each document has its own, a document opens in Preview, and `ui.inspect` shows it as `view.outline`; an error without a document). `imageTracePreview`: the Image Trace panel's Preview (`ui.inspect` shows it as `ui.image_trace_preview`) |
|
||||
| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` or `{fields: [{field, value}…]}` | fill and submit the open dialog. `ui.dialog.set` takes the fields the dialog has (`ui.inspect`), `__` fields, `discard` on a dialog with a discard button, `preview` on one with a live preview, the fields that the dialog reads but adds only when a control changes (such as `searchFolder`, or `copy` in the Transform dialogs), and the fields it passes to the command it runs: every parameter of the `command` dialog's command (`ui.paramDialog`), and the `textureImage` that Load Texture… sets in the Glass and Texturizer effects; any other field is an error that lists the dialog's fields, and the dialog stays as it was `fields` sets the list (at most 256 items) in order (a missing `value` is `null`) and checks every item first: when one is refused, the call is an error and no field changes |
|
||||
| `ui.screenshot` | `{path?, data?}` | capture the window (PNG), written to `path`; `data: true` sends it back as `pngBase64` too. The inline PNG is capped at 64 MiB: with a `path` the file is still written and `pngBase64` is omitted when the image is larger, while a path-less `data: true` request fails if either the raw RGBA image or the encoded PNG exceeds the cap. Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error |
|
||||
| `ui.render` | `{path?, scale?, artboard?, data?, traceViews?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first; `traceViews: true`: Image Trace objects draw in their View, as on the canvas, instead of their tracing result): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
|
||||
|
||||
Reference in New Issue
Block a user