Render: custom geometry sub-paths fill even-odd, so inner sub-paths cut holes
A freeform whose path holds a sub-path inside another (an icon, outlined text, a frame drawn as two rectangles) showed the inner one filled when both wound the same way: DeckCraft filled custom paths non-zero. PowerPoint fills every custom path alternately (even-odd), so the inner sub-path is a hole whichever way it winds. Wild synthetic set: holes slide 7.60 -> 2.84 mean diff. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
525ec7b1a9
commit
39adfcd62a
|
|
@ -435,7 +435,9 @@ pub fn shape_geometry(shape: &Shape, w: f64, h: f64) -> Geometry {
|
|||
let (pw, ph) = (if p.w > 0.0 { p.w } else { w.max(1e-9) }, if p.h > 0.0 { p.h } else { h.max(1e-9) });
|
||||
let mut bp = parse_path(&p.d);
|
||||
bp.apply_affine(Affine::scale_non_uniform(w / pw, h / ph));
|
||||
out.push(SubPath { path: bp, fill: p.fill, stroke: p.stroke, even_odd: false });
|
||||
// PowerPoint fills each custom path's sub-paths alternately (even-odd): a sub-path
|
||||
// inside another cuts a hole whichever way it winds, as in icons and outlined text.
|
||||
out.push(SubPath { path: bp, fill: p.fill, stroke: p.stroke, even_odd: true });
|
||||
}
|
||||
Geometry { paths: out, text_rect: Rect::new(0.0, 0.0, w, h), handles: vec![], sites: vec![] }
|
||||
}
|
||||
|
|
@ -1614,6 +1616,9 @@ pub fn render_shape(pres: &Presentation, slide: &Slide, id: ShapeId, scale: f64)
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_wild;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mt_tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
//! Render tests for shape, line, fill and effect cases checked against PowerPoint's own exports
|
||||
//! of synthetic "wild" slides (observed black-box; see `plan/powerpoint/tools/gen_wild.py`).
|
||||
|
||||
use super::*;
|
||||
use deckcraft_color::Rgba;
|
||||
use deckcraft_model::style::ColorRef;
|
||||
use deckcraft_model::{CustomPath, Geom, Presentation, Shape, ShapeId, Xfrm};
|
||||
|
||||
fn deck_with(shapes: Vec<Shape>) -> Presentation {
|
||||
let mut p = Presentation::default();
|
||||
let s = std::sync::Arc::make_mut(&mut p.slides[0]);
|
||||
s.shapes = shapes;
|
||||
p
|
||||
}
|
||||
|
||||
fn render(shapes: Vec<Shape>) -> Image {
|
||||
render_slide(&deck_with(shapes), 0, &RenderOpts { scale: 1.0, ..Default::default() })
|
||||
}
|
||||
|
||||
fn solid(c: Rgba) -> Option<Fill> {
|
||||
Some(Fill::solid(ColorRef::rgb(c)))
|
||||
}
|
||||
|
||||
fn no_line() -> Option<Line> {
|
||||
Some(Line { fill: Some(Fill::None), ..Default::default() })
|
||||
}
|
||||
|
||||
fn close(px: [u8; 4], c: Rgba, tol: i32) -> bool {
|
||||
(px[0] as i32 - c.r as i32).abs() <= tol && (px[1] as i32 - c.g as i32).abs() <= tol && (px[2] as i32 - c.b as i32).abs() <= tol
|
||||
}
|
||||
|
||||
const WHITE: Rgba = Rgba::WHITE;
|
||||
const RED: Rgba = Rgba::rgb(255, 0, 0);
|
||||
|
||||
#[test]
|
||||
fn custom_geometry_sub_paths_cut_holes_whichever_way_they_wind() {
|
||||
// Outer square clockwise, inner square also clockwise: PowerPoint still shows a hole.
|
||||
let d = "M 0 0 L 100 0 L 100 100 L 0 100 Z M 25 25 L 75 25 L 75 75 L 25 75 Z";
|
||||
let sh = Shape {
|
||||
id: ShapeId(10),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 200.0)),
|
||||
geom: Geom::Custom { paths: vec![CustomPath { w: 100.0, h: 100.0, d: d.into(), ..Default::default() }] },
|
||||
fill: solid(RED),
|
||||
line: no_line(),
|
||||
..Default::default()
|
||||
};
|
||||
let img = render(vec![sh]);
|
||||
assert!(close(img.pixel(120, 120), RED, 2), "outer ring filled");
|
||||
assert!(close(img.pixel(200, 200), WHITE, 2), "inner square is a hole: {:?}", img.pixel(200, 200));
|
||||
}
|
||||
Loading…
Reference in New Issue