mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
MCP: screenshot and render_preview can draw the Image Trace view
After imageTrace.setView, screenshot and render_preview still drew the tracing result, because ui.render renders with the export options and only the canvas turns on trace views. Only the app window showed the view. ui.render now takes traceViews. With traceViews: true, Image Trace objects draw in the view set with imageTrace.setView, as on the canvas, both headless and in the running app. screenshot and render_preview take traceViews and pass it on; without it they draw the tracing result, as exports do. The trace-and-style prompt names the argument. A supersampled render drew Outline view's lines, type outlines and dotted guides as little as a quarter of an output pixel wide, so they came out pale gray. The renderer now scales those widths and the guides' dashes by the supersampling factor in the larger frame, so the lines stay one output pixel wide. Docs: docs/mcp.md (render_preview, screenshot), docs/control-protocol.md (ui.render).
This commit is contained in:
@@ -263,7 +263,9 @@ impl Headless {
|
||||
let scale = p.get("scale").and_then(Value::as_f64).unwrap_or(1.0).clamp(0.01, 16.0);
|
||||
vectorcraft_render::raster_size(r, scale)?;
|
||||
// Rendered with the options of a PNG export on white, so its pixels are the exported file's.
|
||||
let opts = RasterExportOptions { background: Some([255; 3]), ..Default::default() }.render_options(RasterFormat::Png);
|
||||
// `traceViews: true` draws Image Trace objects as their View shows them, as the canvas does.
|
||||
let mut opts = RasterExportOptions { background: Some([255; 3]), ..Default::default() }.render_options(RasterFormat::Png);
|
||||
opts.trace_views = p.get("traceViews").and_then(Value::as_bool) == Some(true);
|
||||
let img = self.renderer.render_region_with(&doc, r, scale, &opts);
|
||||
// A cancelled render stops drawing: the picture is not whole and is not written.
|
||||
vectorcraft_engine::cancel::check().map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -309,8 +309,8 @@ Method:
|
||||
imageTrace.presets to see every preset with its numbers.
|
||||
Lower paths and higher corners give the clean, logo-like result; high fidelity needs paths
|
||||
near 90. imageTrace.setView {view: \"outlinesWithSourceImage\"} draws the traced outlines
|
||||
over the image in the app's window to compare them (screenshot {window: true} captures it);
|
||||
render_preview and screenshot without `window` draw the tracing result, as exports do.
|
||||
over the image in the app's window to compare them; render_preview and screenshot draw it
|
||||
with traceViews: true, and the tracing result otherwise, as exports do.
|
||||
3. imageTrace.expand keeps only the traced paths and removes the source image.
|
||||
imageTrace.makeAndExpand traces and expands in one command, in place of steps 2 and 3.
|
||||
4. apply_effect {effect} on the traced shapes. Call apply_effect with no `effect` for the
|
||||
@@ -557,7 +557,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// trace-and-style selects the opened image before tracing it, expands the trace, recolors it
|
||||
/// instead of painting it one color, and says that screenshots draw the tracing result.
|
||||
/// instead of painting it one color, and says that screenshots draw the trace view only with
|
||||
/// traceViews.
|
||||
#[test]
|
||||
fn trace_and_style_selects_expands_and_recolors() {
|
||||
let v = get("trace-and-style", &json!({"path": "art.png"})).unwrap();
|
||||
@@ -568,7 +569,7 @@ mod tests {
|
||||
let at = |s: &str| text.find(s).unwrap_or(usize::MAX);
|
||||
assert!(at("select.all") < at("imageTrace.make") && at("imageTrace.make") < at("imageTrace.expand"), "{text}");
|
||||
assert!(text.contains("black and white"), "{text}");
|
||||
assert!(!text.contains("screenshots show it"), "{text}");
|
||||
assert!(!text.contains("screenshots show it") && text.contains("traceViews: true"), "{text}");
|
||||
assert!(!text.contains("own pixel size") && text.contains("resolution it declares"), "{text}");
|
||||
// No step of its own paints the trace with set_paint.
|
||||
let steps: Vec<&str> = text.lines().filter(|l| is_item(l)).filter_map(|l| l.split_once(". ").map(|(_, rest)| rest)).collect();
|
||||
|
||||
@@ -350,6 +350,58 @@ fn trace_and_style_prompt_steps_run_in_order() {
|
||||
assert!(fills.len() > 1 && fills.contains("#00aa00") && !fills.contains(&from), "{fills:?}");
|
||||
}
|
||||
|
||||
/// `screenshot` and `render_preview` with `traceViews: true` draw an Image Trace object in the
|
||||
/// view set with imageTrace.setView, as the app's window does; without it they draw the tracing
|
||||
/// result, as exports do. The image is a dark disc (r = 30 px) on blue, traced with Ignore White.
|
||||
#[test]
|
||||
fn screenshot_and_render_preview_draw_the_trace_view_with_trace_views() {
|
||||
let mut s = server();
|
||||
let png = tmp("trace-views.png");
|
||||
image::RgbImage::from_fn(100, 100, |x, y| {
|
||||
let (dx, dy) = (f64::from(x) + 0.5 - 50.0, f64::from(y) + 0.5 - 50.0);
|
||||
if dx * dx + dy * dy <= 900.0 { image::Rgb([0, 0, 0]) } else { image::Rgb([170, 210, 255]) }
|
||||
})
|
||||
.save(&png)
|
||||
.unwrap();
|
||||
let ok = |s: &mut Server, id: u64, name: &str, args: Value| {
|
||||
let r = call(s, id, name, args);
|
||||
assert_eq!(r["isError"], false, "{name}: {r}");
|
||||
r
|
||||
};
|
||||
ok(&mut s, 1, "open_file", json!({"path": png.to_str().unwrap()}));
|
||||
ok(&mut s, 2, "run_command", json!({"command": "select.all"}));
|
||||
ok(&mut s, 3, "run_command", json!({"command": "imageTrace.make", "params": {"params": {"ignoreWhite": true}}}));
|
||||
let pixels = |r: &Value| {
|
||||
let data = r["content"].as_array().unwrap().iter().find(|c| c["type"] == "image").unwrap()["data"].as_str().unwrap();
|
||||
image::load_from_memory(&vectorcraft_format::base64_decode(data).unwrap()).unwrap().to_rgba8()
|
||||
};
|
||||
let dark = |p: &image::Rgba<u8>| p[0] < 60 && p[1] < 60 && p[2] < 60;
|
||||
let white = |p: &image::Rgba<u8>| p[0] > 245 && p[1] > 245 && p[2] > 245;
|
||||
let blue = |p: &image::Rgba<u8>| p[0] < 200 && p[2] > 245;
|
||||
let (middle, beside) = ((50, 50), (10, 10));
|
||||
let renders = [("screenshot", json!({})), ("render_preview", json!({"max_side": 100}))];
|
||||
for (id, (name, args)) in (10..).zip(renders) {
|
||||
let with = |mut a: Value| {
|
||||
a["traceViews"] = json!(true);
|
||||
a
|
||||
};
|
||||
ok(&mut s, id, "run_command", json!({"command": "imageTrace.setView", "params": {"view": "sourceImage"}}));
|
||||
let (view, result) = (pixels(&ok(&mut s, id, name, with(args.clone()))), pixels(&ok(&mut s, id, name, args.clone())));
|
||||
assert_eq!((view.width(), result.width()), (100, 100), "{name}");
|
||||
assert!(blue(view.get_pixel(beside.0, beside.1)), "{name}: the source image beside the disc {:?}", view.get_pixel(beside.0, beside.1));
|
||||
assert!(white(result.get_pixel(beside.0, beside.1)), "{name}: the tracing result {:?}", result.get_pixel(beside.0, beside.1));
|
||||
assert!(dark(view.get_pixel(middle.0, middle.1)) && dark(result.get_pixel(middle.0, middle.1)), "{name}: the disc's middle");
|
||||
// Outlines: a line at the disc's right side (x = 80) and no fill in its middle.
|
||||
ok(&mut s, id, "run_command", json!({"command": "imageTrace.setView", "params": {"view": "outlines"}}));
|
||||
let (view, result) = (pixels(&ok(&mut s, id, name, with(args.clone()))), pixels(&ok(&mut s, id, name, args.clone())));
|
||||
let row: Vec<u8> = (75..=85).map(|x| view.get_pixel(x, 50)[0]).collect();
|
||||
assert!(row.iter().any(|&r| r < 128), "{name}: the disc's outline {row:?}");
|
||||
assert!(white(view.get_pixel(middle.0, middle.1)), "{name}: no fill in outlines {:?}", view.get_pixel(middle.0, middle.1));
|
||||
assert!(dark(result.get_pixel(middle.0, middle.1)), "{name}: the tracing result's fill");
|
||||
assert_eq!(call(&mut s, id, name, json!({"traceViews": "yes"}))["isError"], true, "{name}: a non-boolean traceViews");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_path_gesture_undo() {
|
||||
let mut s = server();
|
||||
|
||||
+22
-2
@@ -154,7 +154,7 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
"Render a preview",
|
||||
"Render an artboard (default the first) as inline PNG without changing the document. max_side defaults to 1024, limited to 1..4096.",
|
||||
obj(
|
||||
json!({"max_side":{"type":"integer","minimum":1,"maximum":4096},"artboard":{"type":"integer","minimum":0,"description":"0-based artboard (default 0)"}}),
|
||||
json!({"max_side":{"type":"integer","minimum":1,"maximum":4096},"artboard":{"type":"integer","minimum":0,"description":"0-based artboard (default 0)"},"traceViews":{"type":"boolean","description":TRACE_VIEWS}}),
|
||||
&[],
|
||||
),
|
||||
true,
|
||||
@@ -344,6 +344,7 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
"artboard": {"type": "integer", "minimum": 0, "description": "Artboard index (default 0)"},
|
||||
"window": {"type": "boolean", "description": "Capture the app window (desktop app only)"},
|
||||
"max_side": {"type": "integer", "minimum": 1, "maximum": 4096, "description": MAX_SIDE_WINDOW},
|
||||
"traceViews": {"type": "boolean", "description": TRACE_VIEWS},
|
||||
}),
|
||||
&[],
|
||||
),
|
||||
@@ -850,6 +851,7 @@ fn screenshot(b: &mut dyn Backend, a: &Args) -> Result<ToolResult, String> {
|
||||
if max.is_some() && !window {
|
||||
return Err("`max_side` applies to a window capture (`window: true`); size an artboard render with `scale`".into());
|
||||
}
|
||||
let trace_views = trace_views(a)?;
|
||||
// The backend writes the file, where its automation roots allow, and sends the image back too
|
||||
// (`data`): this process writes nothing.
|
||||
let mut p = json!({"data": true});
|
||||
@@ -865,6 +867,9 @@ fn screenshot(b: &mut dyn Backend, a: &Args) -> Result<ToolResult, String> {
|
||||
p[k] = v.clone();
|
||||
}
|
||||
}
|
||||
if trace_views {
|
||||
p["traceViews"] = json!(true);
|
||||
}
|
||||
b.call("ui.render", p)?
|
||||
};
|
||||
let png = match r.get("pngBase64").and_then(Value::as_str) {
|
||||
@@ -946,6 +951,16 @@ const BRIEF: &str = "Leave out each command's params description (the bulk of th
|
||||
/// The `max_side` argument of the window captures.
|
||||
const MAX_SIDE_WINDOW: &str =
|
||||
"Window capture only: longest side of the returned image in pixels; a larger capture is scaled down to it (default: the full capture)";
|
||||
/// The `traceViews` argument of the artboard renders.
|
||||
const TRACE_VIEWS: &str = "Draw Image Trace objects in the view set with imageTrace.setView (outlines, the source image…), as the app's window does; by default they draw their tracing result, as exports do";
|
||||
|
||||
/// `traceViews`: false when absent.
|
||||
fn trace_views(a: &Args) -> Result<bool, String> {
|
||||
match a.get("traceViews") {
|
||||
None => Ok(false),
|
||||
Some(v) => v.as_bool().ok_or_else(|| "`traceViews` must be a boolean".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The command catalogue narrowed by the listing tools' arguments: `filter` (or `query`) on the
|
||||
/// id, label and menu path, `menu` on the menu path, `enabledOnly`, and `brief` (no `params`
|
||||
@@ -1340,6 +1355,7 @@ fn render_preview(b: &mut dyn Backend, a: &Args) -> Result<ToolResult, String> {
|
||||
None => 0,
|
||||
Some(v) => v.as_u64().ok_or("`artboard` must be a 0-based artboard index")?,
|
||||
};
|
||||
let trace_views = trace_views(a)?;
|
||||
let doc = b.call("document.inspect", json!({"depth":0}))?;
|
||||
let board =
|
||||
doc.get("artboards").and_then(Value::as_array).and_then(|v| v.get(usize::try_from(index).ok()?)).ok_or(format!("no artboard {index}"))?;
|
||||
@@ -1354,7 +1370,11 @@ fn render_preview(b: &mut dyn Backend, a: &Args) -> Result<ToolResult, String> {
|
||||
if width.max(height) * scale > 4096.0 {
|
||||
return Err("artboard is too large for a bounded preview".into());
|
||||
}
|
||||
let r = b.call("ui.render", json!({"scale":scale, "artboard": index}))?;
|
||||
let mut p = json!({"scale":scale, "artboard": index});
|
||||
if trace_views {
|
||||
p["traceViews"] = json!(true);
|
||||
}
|
||||
let r = b.call("ui.render", p)?;
|
||||
let encoded = r.get("pngBase64").and_then(Value::as_str).ok_or("renderer returned no image")?;
|
||||
let png = vectorcraft_format::base64_decode(encoded).ok_or("renderer returned bad base64")?;
|
||||
let img = image::load_from_memory_with_format(&png, image::ImageFormat::Png).map_err(|e| format!("read preview: {e}"))?;
|
||||
|
||||
@@ -582,6 +582,10 @@ pub struct Renderer {
|
||||
/// The art the last frame drew with Overprint Preview or Separations Preview: the next frame
|
||||
/// draws it again for the objects that have not changed ([`proof::prepare`]).
|
||||
proof_art: Option<proof::Prepared>,
|
||||
/// Width of [`Self::hairline`] in the pixels being drawn: 1, or the supersampling factor while
|
||||
/// a supersampled render draws its larger frame, so outlines stay one output pixel wide. Type
|
||||
/// outlines and dotted guides scale by it too.
|
||||
hairline: f64,
|
||||
}
|
||||
|
||||
/// How deep inline graphics nest (text in a symbol shown inline in text…) before they draw nothing.
|
||||
@@ -697,6 +701,7 @@ impl Renderer {
|
||||
isolation: None,
|
||||
isolation_settled: false,
|
||||
proof_art: None,
|
||||
hairline: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,7 +718,9 @@ impl Renderer {
|
||||
let (w, h) = (width.clamp(1, MAX_CONTEXT_SIDE), height.clamp(1, MAX_CONTEXT_SIDE));
|
||||
if let Some(k) = supersample_factor(w, h) {
|
||||
let hard = RenderOptions { anti_alias: AntiAlias::None, supersample: false, ..opts.clone() };
|
||||
let hairline = std::mem::replace(&mut self.hairline, f64::from(k));
|
||||
let big = self.render_as(doc, w * k, h * k, Affine::scale(f64::from(k)) * view, &hard, inks);
|
||||
self.hairline = hairline;
|
||||
let pixels = downsample(&big.pixels, big.width as usize, big.height as usize, k as usize);
|
||||
self.stats.micros = now().saturating_sub(start);
|
||||
return Rendered { width: w, height: h, pixels };
|
||||
@@ -1371,9 +1378,10 @@ impl Renderer {
|
||||
// running far off screen (from a file) asked for millions of them. Past
|
||||
// [`MAX_GUIDE_DASHES`] (by its control polygon, which is at least as long) it
|
||||
// is drawn solid.
|
||||
if dots && polygon_length(&screen) / 4.0 <= MAX_GUIDE_DASHES {
|
||||
let h = self.hairline;
|
||||
if dots && polygon_length(&screen) / (4.0 * h) <= MAX_GUIDE_DASHES {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.set_stroke(kurbo::Stroke::new(1.5).with_dashes(0.0, [1.5, 2.5]));
|
||||
ctx.set_stroke(kurbo::Stroke::new(1.5 * h).with_dashes(0.0, [1.5 * h, 2.5 * h]));
|
||||
ctx.set_paint(f.ink.fixed([r, g, b, 255]));
|
||||
ctx.stroke_path(&screen);
|
||||
} else {
|
||||
@@ -1570,7 +1578,7 @@ impl Renderer {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
let mut screen = bp.clone();
|
||||
screen.apply_affine(f.view);
|
||||
ctx.set_stroke(kurbo::Stroke::new(1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(self.hairline));
|
||||
ctx.set_paint(f.ink.fixed(rgba));
|
||||
ctx.stroke_path(&screen);
|
||||
}
|
||||
@@ -1931,7 +1939,7 @@ impl Renderer {
|
||||
let mut p = g.all.clone();
|
||||
p.apply_affine(xf);
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.set_stroke(kurbo::Stroke::new(1.0));
|
||||
ctx.set_stroke(kurbo::Stroke::new(self.hairline));
|
||||
ctx.set_paint(peniko::Color::BLACK);
|
||||
ctx.stroke_path(&p);
|
||||
// Colour bitmap glyphs: their boxes, as images show in outline mode.
|
||||
|
||||
@@ -1001,6 +1001,46 @@ fn supersampled_art_leaves_no_seam_between_shapes_that_meet() {
|
||||
assert_eq!(supersample_factor(40_000, 10), None);
|
||||
}
|
||||
|
||||
/// A supersampled render draws Outline view's lines one output pixel wide, as an unsupersampled
|
||||
/// one does, instead of a quarter of a pixel: the line down x = 50.5 draws darker than 64 of 255
|
||||
/// either way. So do the outlines of type and a dotted guide along y = 90.5.
|
||||
#[test]
|
||||
fn supersampled_outlines_stay_one_pixel_wide() {
|
||||
let _fonts = crate::tests_fontchange::fonts_steady();
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let style = vectorcraft_doc::text::CharStyle { size: 30.0, ..Default::default() };
|
||||
let text = vectorcraft_doc::text::TextObject::point(vectorcraft_geom::Point::new(60.0, 80.0), "H", style);
|
||||
let t = Node::new(d.alloc_id(), NodeKind::Text(Box::new(text)));
|
||||
d.insert(Some(d.layers[0].id), 0, t).unwrap();
|
||||
let n = rect_node(&mut d, Rect::new(20.0, 20.0, 50.5, 80.0), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
let mut guide = Node::path(d.alloc_id(), shapes::line(Point::new(5.0, 90.5), Point::new(95.0, 90.5)), Appearance::default());
|
||||
if let NodeKind::Path { guide, .. } = &mut guide.kind {
|
||||
*guide = true;
|
||||
}
|
||||
d.insert(Some(d.layers[0].id), 1, guide).unwrap();
|
||||
let draw = |supersample: bool| {
|
||||
let opts = RenderOptions {
|
||||
background: Some([255, 255, 255, 255]),
|
||||
outline: true,
|
||||
guide: ([0, 0, 0], true),
|
||||
precise: true,
|
||||
supersample,
|
||||
..Default::default()
|
||||
};
|
||||
Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts)
|
||||
};
|
||||
let darkest = |r: &Rendered| (48..=52).map(|x| r.pixel(x, 50)[0]).min().unwrap();
|
||||
assert!(darkest(&draw(false)) < 64, "unsupersampled: {}", darkest(&draw(false)));
|
||||
assert!(darkest(&draw(true)) < 64, "supersampled: {}", darkest(&draw(true)));
|
||||
let type_darkest = |r: &Rendered| (56..=95).flat_map(|x| (45..=85).map(move |y| (x, y))).map(|(x, y)| r.pixel(x, y)[0]).min().unwrap();
|
||||
assert!(type_darkest(&draw(false)) < 64, "unsupersampled type: {}", type_darkest(&draw(false)));
|
||||
assert!(type_darkest(&draw(true)) < 64, "supersampled type: {}", type_darkest(&draw(true)));
|
||||
let guide_darkest = |r: &Rendered| (10..=90).flat_map(|x| (89..=92).map(move |y| (x, y))).map(|(x, y)| r.pixel(x, y)[0]).min().unwrap();
|
||||
assert!(guide_darkest(&draw(false)) < 64, "unsupersampled guide: {}", guide_darkest(&draw(false)));
|
||||
assert!(guide_darkest(&draw(true)) < 64, "supersampled guide: {}", guide_darkest(&draw(true)));
|
||||
}
|
||||
|
||||
/// #1613: a zero-length subpath (`m` then `L` to the same point, as `.ai` files write dots) is a
|
||||
/// dot as wide as the stroke: a disc under a round cap, a square under a projecting one, nothing
|
||||
/// under a butt cap (PDF 32000-1 8.5.3.2, SVG 2 "Path data" zero-length segments).
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
//! dialog does not read is an error that lists its fields: [`crate::dialogs::accepts_field`])
|
||||
//! - `ui.resize {width, height}`, `ui.focus`, `ui.screenshot {path?, data?}` (`data: true`: the PNG
|
||||
//! comes back as `pngBase64` as well)
|
||||
//! - `ui.render {path?, scale?, artboard?, data?}`: render an artboard headlessly (`artboard`: its
|
||||
//! 0-based index, default the first; PNG with the pixels of a PNG export on white; without a
|
||||
//! path, or with `data: true`, as `pngBase64`)
|
||||
//! - `ui.render {path?, scale?, artboard?, data?, traceViews?}`: render an artboard headlessly
|
||||
//! (`artboard`: its 0-based index, default the first; PNG with the pixels of a PNG export on
|
||||
//! white; `traceViews: true` draws Image Trace objects in their View, as the canvas does; without
|
||||
//! a path, or with `data: true`, as `pngBase64`)
|
||||
//! - `app.open {path}` (any readable format) / `app.save {path?, svg?: {…SVG options}}` / `app.quit`
|
||||
//! (`file.close`, `file.closeAll` and `app.quit` first open a `saveChanges` dialog for each
|
||||
//! modified document: `ui.dialog.confirm` saves, set `discard: true` then confirm to discard)
|
||||
@@ -456,7 +457,9 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
return err(e);
|
||||
}
|
||||
// Rendered with the options of a PNG export on white, so its pixels are the exported file's.
|
||||
let opts = RasterExportOptions { background: Some([255; 3]), ..Default::default() }.render_options(RasterFormat::Png);
|
||||
// `traceViews: true` draws Image Trace objects as their View shows them, as the canvas does.
|
||||
let mut opts = RasterExportOptions { background: Some([255; 3]), ..Default::default() }.render_options(RasterFormat::Png);
|
||||
opts.trace_views = p.get("traceViews").and_then(Value::as_bool) == Some(true);
|
||||
let img = app.canvas.renderer.render_region_with(&doc, r, scale, &opts);
|
||||
let png = match img.to_png() {
|
||||
Ok(png) => png,
|
||||
|
||||
@@ -180,6 +180,34 @@ fn control_render_matches_a_png_export_on_white() {
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
/// `ui.render {traceViews: true}` draws an Image Trace object in its view, as the canvas does:
|
||||
/// in Source Image view the image's blue shows beside the traced disc, where `ui.render` without
|
||||
/// it draws the tracing result on white.
|
||||
#[test]
|
||||
fn control_render_draws_the_trace_view_with_trace_views() {
|
||||
let (mut app, tx, base) = confined("traceviews");
|
||||
let png = base.join("in/disc.png");
|
||||
image::RgbImage::from_fn(100, 100, |x, y| {
|
||||
let (dx, dy) = (f64::from(x) + 0.5 - 50.0, f64::from(y) + 0.5 - 50.0);
|
||||
if dx * dx + dy * dy <= 900.0 { image::Rgb([0, 0, 0]) } else { image::Rgb([170, 210, 255]) }
|
||||
})
|
||||
.save(&png)
|
||||
.unwrap();
|
||||
let r = request(&mut app, &tx, "app.open", json!({"path": s(png)}));
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.run("imageTrace.make", json!({"params": {"ignoreWhite": true}})).unwrap();
|
||||
app.run("imageTrace.setView", json!({"view": "sourceImage"})).unwrap();
|
||||
let pixels = |v: &Value| image::load_from_memory(&vectorcraft_format::base64_decode(v.as_str().unwrap()).unwrap()).unwrap().to_rgba8();
|
||||
let view = pixels(&request(&mut app, &tx, "ui.render", json!({"traceViews": true}))["result"]["pngBase64"]);
|
||||
let result = pixels(&request(&mut app, &tx, "ui.render", json!({}))["result"]["pngBase64"]);
|
||||
let (beside, middle) = (view.get_pixel(10, 10).0, view.get_pixel(50, 50).0);
|
||||
assert!(beside[0] < 200 && beside[2] > 245, "the source image beside the disc: {beside:?}");
|
||||
assert!(middle[..3].iter().all(|&c| c < 60), "the disc: {middle:?}");
|
||||
assert_eq!(result.get_pixel(10, 10).0, [255; 4], "the tracing result on white");
|
||||
let _ = std::fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_with_injected_input_are_confined() {
|
||||
let (mut app, tx, base) = confined("synthetic");
|
||||
|
||||
Reference in New Issue
Block a user