Merge pull request #1037 from storytold/fix/983-supersampled-export
#983: supersample Art Optimized edges in exports
This commit is contained in:
commit
3416bf49cd
|
|
@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
|||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2) Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). |
|
||||
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2) Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). |
|
||||
| 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) |
|
||||
| 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day |
|
||||
| 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections |
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ fn render(doc: &Document, nodes: Vec<Node>, region: Rect, scale: f64, anti_alias
|
|||
tmp.layers = vec![Arc::new(layer)];
|
||||
let mut r = vectorcraft_render::Renderer::new();
|
||||
r.threads = 0;
|
||||
r.render_region_with(&tmp, region, scale, &RenderOptions { skip_templates: true, anti_alias, ..Default::default() })
|
||||
// Art Optimized is supersampled, as in exports: shapes meeting edge to edge leave no seam.
|
||||
r.render_region_with(&tmp, region, scale, &RenderOptions { skip_templates: true, anti_alias, supersample: true, ..Default::default() })
|
||||
}
|
||||
|
||||
/// Pixels per point of raster effects rendered as images (the document's raster effects
|
||||
|
|
|
|||
|
|
@ -582,8 +582,14 @@ pub fn render(doc: &Document, s: &WebSettings) -> std::result::Result<WebRender,
|
|||
let doc = doc.without_edit_modes();
|
||||
let (region, scale) = region(&doc, s)?;
|
||||
let page = (doc.setup.background == vectorcraft_doc::Background::White).then_some([255; 4]);
|
||||
let opts =
|
||||
vectorcraft_render::RenderOptions { background: page, skip_templates: true, anti_alias: s.anti_alias, precise: true, ..Default::default() };
|
||||
let opts = vectorcraft_render::RenderOptions {
|
||||
background: page,
|
||||
skip_templates: true,
|
||||
anti_alias: s.anti_alias,
|
||||
precise: true,
|
||||
supersample: true,
|
||||
..Default::default()
|
||||
};
|
||||
let img = vectorcraft_render::Renderer::new().render_region_with(&doc, region, scale, &opts);
|
||||
Ok(WebRender { rgba: img.to_straight(), width: img.width, height: img.height, region, scale })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,8 @@ impl RasterExportOptions {
|
|||
anti_alias: self.anti_alias,
|
||||
// Opaque art stays exactly opaque where translucent edges cross it (#787).
|
||||
precise: true,
|
||||
// Shapes meeting edge to edge leave no seam (#983).
|
||||
supersample: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,11 @@ pub struct RenderOptions {
|
|||
/// opaque, where 8-bit compositing leaves alpha 254 on some anti-aliased edges (#787). Off,
|
||||
/// the faster 8-bit pipeline (the canvas).
|
||||
pub precise: bool,
|
||||
/// Exports: Art Optimized edges are supersampled ([`supersample_factor`]): the art is drawn
|
||||
/// with hard edges at several times the size and averaged down, so shapes that meet edge to
|
||||
/// edge leave no seam of the background between them (#983). Off, edges are smoothed shape by
|
||||
/// shape (the canvas).
|
||||
pub supersample: bool,
|
||||
}
|
||||
|
||||
/// The opacity of the art around an isolated group or layer.
|
||||
|
|
@ -186,10 +191,48 @@ impl Default for RenderOptions {
|
|||
smooth_images: true,
|
||||
isolated: None,
|
||||
precise: false,
|
||||
supersample: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The most pixels a supersampled render draws (the frame and its compositing buffers hold a few
|
||||
/// times this many bytes).
|
||||
const MAX_SUPERSAMPLED_PIXELS: u64 = 40_000_000;
|
||||
|
||||
/// How many times wider and taller a `w`×`h` render is drawn when supersampled: 4, or less where
|
||||
/// that would pass [`MAX_SUPERSAMPLED_PIXELS`] or the renderer's 65,535-pixel side; `None` when not
|
||||
/// even twice fits (the edges are then smoothed shape by shape).
|
||||
pub fn supersample_factor(w: u32, h: u32) -> Option<u32> {
|
||||
(2..=4u32).rev().find(|k| {
|
||||
let (sw, sh) = (u64::from(w) * u64::from(*k), u64::from(h) * u64::from(*k));
|
||||
sw <= u64::from(u16::MAX) && sh <= u64::from(u16::MAX) && sw * sh <= MAX_SUPERSAMPLED_PIXELS
|
||||
})
|
||||
}
|
||||
|
||||
/// Average each `k`×`k` block of `src` (`w`×`h` pixels of 4 premultiplied bytes) into one pixel.
|
||||
fn downsample(src: &[u8], w: usize, h: usize, k: usize) -> Vec<u8> {
|
||||
let (ow, oh) = (w / k.max(1), h / k.max(1));
|
||||
let n = (k * k).max(1) as u32;
|
||||
let mut out = vec![0u8; ow * oh * 4];
|
||||
for (o, px) in out.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let (ox, oy) = (o % ow.max(1), o / ow.max(1));
|
||||
let mut acc = [0u32; 4];
|
||||
for y in oy * k..(oy + 1) * k {
|
||||
let row = y * w + ox * k;
|
||||
for p in src.get(row * 4..(row + k) * 4).unwrap_or_default().as_chunks::<4>().0 {
|
||||
for (a, v) in acc.iter_mut().zip(p) {
|
||||
*a += u32::from(*v);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (d, a) in px.iter_mut().zip(acc) {
|
||||
*d = ((a + n / 2) / n).min(255) as u8;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A rendered image (premultiplied RGBA8, row-major).
|
||||
pub struct Rendered {
|
||||
pub width: u32,
|
||||
|
|
@ -476,6 +519,16 @@ impl Renderer {
|
|||
/// screen colours (4 bytes a pixel, see [`Self::render_region_inks`]).
|
||||
fn render_as(&mut self, doc: &Document, width: u32, height: u32, view: Affine, opts: &RenderOptions, inks: bool) -> Rendered {
|
||||
let start = now();
|
||||
if opts.supersample && opts.anti_alias == AntiAlias::Art && opts.mask_view.is_none() {
|
||||
let (w, h) = (width.clamp(1, u16::MAX as u32), height.clamp(1, u16::MAX as u32));
|
||||
if let Some(k) = supersample_factor(w, h) {
|
||||
let hard = RenderOptions { anti_alias: AntiAlias::None, supersample: false, ..opts.clone() };
|
||||
let big = self.render_as(doc, w * k, h * k, Affine::scale(f64::from(k)) * view, &hard, inks);
|
||||
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 };
|
||||
}
|
||||
}
|
||||
let prepared = proof::prepare(doc, opts);
|
||||
let doc: &Document = &prepared;
|
||||
let w = width.clamp(1, u16::MAX as u32) as u16;
|
||||
|
|
|
|||
|
|
@ -385,3 +385,41 @@ fn images_take_their_nearest_pixel_without_smoothing() {
|
|||
assert!(greys(true) >= 4, "smooth: a ramp from black to white");
|
||||
assert_eq!(greys(false), 0, "nearest: black, then white");
|
||||
}
|
||||
|
||||
/// Art Optimized exports are supersampled (#983): two red shapes meeting along a diagonal leave no
|
||||
/// seam of the white background between them, where shape-by-shape smoothing shows one; the outer
|
||||
/// edges stay smooth.
|
||||
#[test]
|
||||
fn supersampled_art_leaves_no_seam_between_shapes_that_meet() {
|
||||
let red = || Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let layer = d.layers[0].id;
|
||||
// Two triangles sharing the diagonal of the square (10, 10)–(90, 90).
|
||||
for pts in [[(10.0, 10.0), (90.0, 10.0), (90.0, 90.0)], [(10.0, 10.0), (90.0, 90.0), (10.0, 90.0)]] {
|
||||
let mut b = vectorcraft_geom::BezPath::new();
|
||||
b.move_to(pts[0]);
|
||||
b.line_to(pts[1]);
|
||||
b.line_to(pts[2]);
|
||||
b.close_path();
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(layer), 0, Node::path(id, vectorcraft_geom::PathData::from_bezpath(&b), Appearance::basic(red(), Paint::None, 0.0))).unwrap();
|
||||
}
|
||||
let draw = |supersample: bool| {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), precise: true, supersample, ..Default::default() };
|
||||
Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts)
|
||||
};
|
||||
// The palest pixel on the shared diagonal, away from the corners.
|
||||
let palest = |r: &Rendered| (20..80).map(|i| r.pixel(i, i)[1].max(r.pixel(i + 1, i)[1])).max().unwrap();
|
||||
assert!(palest(&draw(false)) > 20, "shape-by-shape smoothing shows the seam");
|
||||
assert_eq!(palest(&draw(true)), 0, "supersampled: no seam");
|
||||
// The outer edge is still smoothed: a pixel half covered is between white and red.
|
||||
let ss = draw(true);
|
||||
let edge = ss.pixel(50, 9);
|
||||
assert!(edge == [255, 255, 255, 255] || (edge[1] > 0 && edge[1] < 255) || ss.pixel(50, 10)[1] < 255, "{edge:?}");
|
||||
// The factor fits the size and the renderer's limits.
|
||||
assert_eq!(supersample_factor(100, 100), Some(4));
|
||||
assert_eq!(supersample_factor(2000, 2000), Some(3));
|
||||
assert_eq!(supersample_factor(3000, 3000), Some(2));
|
||||
assert_eq!(supersample_factor(5000, 5000), None);
|
||||
assert_eq!(supersample_factor(40_000, 10), None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
|||
|
||||
## G11 Correctness bugs in core tools
|
||||
|
||||
- **Missing:** Shape Builder misses planar regions and targets whole shapes (#937, #893); Asset Export
|
||||
anti-aliasing ghost borders (#983); Transform effect dialog's invisible checkboxes and missing options (#885);
|
||||
- **Missing:** Shape Builder misses planar regions and targets whole shapes (#937, #893); Transform effect dialog's invisible checkboxes and missing options (#885);
|
||||
PDF text boxes moving on open (#722); SVG units reverting to points (#864); bezier drag preview freezing (#834).
|
||||
- **Impact:** wrong output in everyday work.
|
||||
- **Estimate:** 8–15 h.
|
||||
|
|
|
|||
Loading…
Reference in New Issue