mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user