mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 11:16:56 -04:00
Fix #1724: 3D effects on a nonzero compound path keep the area its contours fill
Extrude & Bevel, Inflate and Rotate built their solid from the outline by nesting alone: a contour inside an odd number of others was a hole. Under the nonzero rule a contour inside another that runs the same way encloses filled art, so the effect cut a hole the object doesn't have. The fill rule now reaches the outline builder, which leaves out contours with filled art on both sides before pairing outlines and holes.
This commit is contained in:
1 parent
779120cb5b
commit
a6d5c461a1
3 files changed
+145
-83
No files matched your search
@@ -113,19 +113,19 @@ fn profile(n: &Node) -> Option<(PathData, FillRule)> {
|
||||
}
|
||||
}
|
||||
|
||||
fn facets(id: &str, path: &PathData, bounds: Rect, p: &Value) -> Result<Vec<Facet>, String> {
|
||||
fn facets(id: &str, path: &PathData, rule: FillRule, bounds: Rect, p: &Value) -> Result<Vec<Facet>, String> {
|
||||
let view = view_options(p);
|
||||
match id {
|
||||
EXTRUDE => vectorcraft_three_d::extrude(path, bounds, extrude_options(p), view),
|
||||
INFLATE => vectorcraft_three_d::inflate(path, bounds, inflate_options(p), view),
|
||||
_ => vectorcraft_three_d::plane(path, bounds, view),
|
||||
EXTRUDE => vectorcraft_three_d::extrude(path, rule, bounds, extrude_options(p), view),
|
||||
INFLATE => vectorcraft_three_d::inflate(path, rule, bounds, inflate_options(p), view),
|
||||
_ => vectorcraft_three_d::plane(path, rule, bounds, view),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate before a command changes the document; the evaluator bounds loaded files too.
|
||||
pub fn validate_solid(n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
let label = crate::effect_info(id).map_or("This effect", |e| e.label.trim_end_matches('…'));
|
||||
let (path, _) =
|
||||
let (path, rule) =
|
||||
profile(n).ok_or_else(|| format!("{label} currently requires a path or compound path. Convert type, images and groups to paths first."))?;
|
||||
let b = path.bounds().ok_or_else(|| format!("{label} requires a shape with at least two anchors"))?;
|
||||
if let Some(v) = p.get("bevel")
|
||||
@@ -134,7 +134,7 @@ pub fn validate_solid(n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
let names: Vec<_> = BevelShape::ALL.iter().map(|b| b.id()).collect();
|
||||
return Err(format!("bevel must be none or one of {}", names.join(", ")));
|
||||
}
|
||||
facets(id, &path, b, p).map(|_| ())
|
||||
facets(id, &path, rule, b, p).map(|_| ())
|
||||
}
|
||||
|
||||
/// One surface: each facet a path shaded from `paint`, as a group with the paint's opacity and
|
||||
@@ -185,11 +185,12 @@ fn paint_of(item: &AppearanceItem) -> (Paint, f32, BlendMode) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The area `item` paints on `path`: the path itself for a fill, the stroke's outline for a stroke.
|
||||
fn region(path: &PathData, rule: FillRule, item: &AppearanceItem) -> PathData {
|
||||
/// The area `item` paints on `path`, and the rule that fills it: the path itself for a fill, the
|
||||
/// stroke's outline (whose contours nest as its holes do) for a stroke.
|
||||
fn region(path: &PathData, rule: FillRule, item: &AppearanceItem) -> (PathData, FillRule) {
|
||||
match item {
|
||||
AppearanceItem::Stroke(s) if s.width > 0.0 && s.width.is_finite() => crate::stroke::outline_region(path, rule, s),
|
||||
_ => path.clone(),
|
||||
AppearanceItem::Stroke(s) if s.width > 0.0 && s.width.is_finite() => (crate::stroke::outline_region(path, rule, s), FillRule::EvenOdd),
|
||||
_ => (path.clone(), rule),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +205,8 @@ fn art_for(n: &Node, fx: &Effect, path: &PathData, rule: FillRule, items: &[&App
|
||||
let mut out = vec![];
|
||||
for item in items {
|
||||
let (paint, opacity, blend) = paint_of(item);
|
||||
let f = facets(ROTATE, ®ion(path, rule, item), b, &p).ok()?;
|
||||
let (area, rule) = region(path, rule, item);
|
||||
let f = facets(ROTATE, &area, rule, b, &p).ok()?;
|
||||
out.push(Arc::new(surface(n, f, &paint, opacity, blend)));
|
||||
}
|
||||
return Some(out);
|
||||
@@ -213,7 +215,7 @@ fn art_for(n: &Node, fx: &Effect, path: &PathData, rule: FillRule, items: &[&App
|
||||
let (paint, opacity, blend) = paint_of(item);
|
||||
// A loaded file or a shape edited past the limits keeps drawing its source path instead of
|
||||
// failing the frame; commands validate the same evaluator and report its errors.
|
||||
let f = facets(&fx.id, path, b, &p).ok()?;
|
||||
let f = facets(&fx.id, path, rule, b, &p).ok()?;
|
||||
Some(vec![Arc::new(surface(n, f, &paint, opacity, blend))])
|
||||
}
|
||||
|
||||
|
||||
+27
-11
@@ -9,7 +9,7 @@
|
||||
//! Visibility is a painter's algorithm, as for Revolve: faces of a closed solid that face away
|
||||
//! from the viewer are left out, the rest are drawn back to front.
|
||||
|
||||
use vectorcraft_geom::{BezPath, ParamCurve, PathData, Point, Rect};
|
||||
use vectorcraft_geom::{BezPath, FillRule, ParamCurve, PathData, Point, Rect};
|
||||
|
||||
use crate::{MAX_FACES, Revolve, V3, eye_distance, perspective_scale};
|
||||
|
||||
@@ -290,7 +290,11 @@ fn pieces(c: vectorcraft_geom::CubicBez, tolerance: f64) -> u32 {
|
||||
/// outlines run one way and holes the other, so nonzero winding fills them as the art is. Curves
|
||||
/// are followed within a tolerance of 1/2000 of `size` (at least 0.05 pt), so small curves take
|
||||
/// few points and large ones more; more than `limit` points in all is refused.
|
||||
fn rings(path: &PathData, size: f64, limit: usize) -> Result<Vec<Vec<Point>>, String> {
|
||||
///
|
||||
/// Under the nonzero `rule` an outline with filled art on both sides is no edge of the shape and
|
||||
/// is left out: a contour inside another that runs the same way doesn't cut a hole, as it would
|
||||
/// if nesting alone decided (#1724).
|
||||
fn rings(path: &PathData, rule: FillRule, size: f64, limit: usize) -> Result<Vec<Vec<Point>>, String> {
|
||||
let tolerance = (size / 2000.0).max(0.05);
|
||||
let mut count = 0;
|
||||
let mut out = vec![];
|
||||
@@ -325,6 +329,18 @@ fn rings(path: &PathData, size: f64, limit: usize) -> Result<Vec<Vec<Point>>, St
|
||||
out.push(ring);
|
||||
}
|
||||
}
|
||||
if rule == FillRule::NonZero {
|
||||
let turn = |r: &[Point]| if signed_area(r) > 0.0 { 1 } else { -1 };
|
||||
let mut edge = Vec::with_capacity(out.len());
|
||||
for (i, r) in out.iter().enumerate() {
|
||||
let p = r.first().copied().unwrap_or_default();
|
||||
// The winding number just outside the ring, and just inside it.
|
||||
let outside: i32 = out.iter().enumerate().filter(|(j, o)| *j != i && contains(o, p)).map(|(_, o)| turn(o)).sum();
|
||||
edge.push((outside != 0) != (outside + turn(r) != 0));
|
||||
}
|
||||
let mut edge = edge.into_iter();
|
||||
out.retain(|_| edge.next().unwrap_or(true));
|
||||
}
|
||||
// A ring inside an odd number of others is a hole.
|
||||
let holes: Vec<bool> = out
|
||||
.iter()
|
||||
@@ -634,14 +650,14 @@ fn steps(o: &Extrude) -> Vec<Step> {
|
||||
|
||||
/// Extrude & Bevel: the shape's outline extruded backwards, with optional twist, taper, bevels
|
||||
/// and caps.
|
||||
pub fn extrude(path: &PathData, bounds: Rect, options: Extrude, view: View) -> Result<Vec<Facet>, String> {
|
||||
pub fn extrude(path: &PathData, rule: FillRule, bounds: Rect, options: Extrude, view: View) -> Result<Vec<Facet>, String> {
|
||||
check_bounds(bounds)?;
|
||||
let o = options.sanitized();
|
||||
// No depth: the flat shape, seen from either side.
|
||||
if o.depth <= 0.0 {
|
||||
return plane(path, bounds, view);
|
||||
return plane(path, rule, bounds, view);
|
||||
}
|
||||
let rings = rings(path, bounds.width().max(bounds.height()), MAX_SOLID_POINTS)?;
|
||||
let rings = rings(path, rule, bounds.width().max(bounds.height()), MAX_SOLID_POINTS)?;
|
||||
let camera = Camera::new(view, bounds, o.depth);
|
||||
let mut b = Builder { camera: &camera, closed: o.cap, facets: vec![] };
|
||||
let steps = steps(&o);
|
||||
@@ -675,9 +691,9 @@ pub fn extrude(path: &PathData, bounds: Rect, options: Extrude, view: View) -> R
|
||||
|
||||
/// Rotate: the flat art turned in 3D about the centre of `bounds` (which may be larger than the
|
||||
/// shape's own, so that every part of one object turns about the same point).
|
||||
pub fn plane(path: &PathData, bounds: Rect, view: View) -> Result<Vec<Facet>, String> {
|
||||
pub fn plane(path: &PathData, rule: FillRule, bounds: Rect, view: View) -> Result<Vec<Facet>, String> {
|
||||
check_bounds(bounds)?;
|
||||
let rings = rings(path, bounds.width().max(bounds.height()), MAX_PLANE_POINTS)?;
|
||||
let rings = rings(path, rule, bounds.width().max(bounds.height()), MAX_PLANE_POINTS)?;
|
||||
let camera = Camera::new(view, bounds, 0.0);
|
||||
let mut b = Builder { camera: &camera, closed: false, facets: vec![] };
|
||||
let contours: Vec<Vec<V3>> = rings.iter().map(|r| r.iter().map(|p| camera.place(*p, 0.0)).collect()).collect();
|
||||
@@ -997,18 +1013,18 @@ fn material(outline: &PathData, cell: Rect) -> Option<Vec<Vec<Point>>> {
|
||||
&[Point::new(cell.x0, cell.y0), Point::new(cell.x1, cell.y0), Point::new(cell.x1, cell.y1), Point::new(cell.x0, cell.y1)],
|
||||
true,
|
||||
));
|
||||
let rule = vectorcraft_geom::FillRule::NonZero;
|
||||
let rule = FillRule::NonZero;
|
||||
let part =
|
||||
vectorcraft_pathops::try_boolean(&r, rule, outline, rule, vectorcraft_pathops::BoolOp::Intersect, vectorcraft_pathops::DEFAULT_PRECISION)
|
||||
.ok()?;
|
||||
rings(&part, cell.width(), MAX_PLANE_POINTS).ok()
|
||||
rings(&part, rule, cell.width(), MAX_PLANE_POINTS).ok()
|
||||
}
|
||||
|
||||
/// Inflate: the shape puffed out like a cushion, its edge kept where the art's outline is.
|
||||
pub fn inflate(path: &PathData, bounds: Rect, options: Inflate, view: View) -> Result<Vec<Facet>, String> {
|
||||
pub fn inflate(path: &PathData, rule: FillRule, bounds: Rect, options: Inflate, view: View) -> Result<Vec<Facet>, String> {
|
||||
check_bounds(bounds)?;
|
||||
let o = options.sanitized();
|
||||
let rings = rings(path, bounds.width().max(bounds.height()), MAX_SOLID_POINTS)?;
|
||||
let rings = rings(path, rule, bounds.width().max(bounds.height()), MAX_SOLID_POINTS)?;
|
||||
if rings.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use vectorcraft_geom::{BezPath, PathData, Point, Rect};
|
||||
use vectorcraft_geom::{BezPath, FillRule, PathData, Point, Rect};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -41,7 +41,7 @@ fn close(a: f64, b: f64, tol: f64) -> bool {
|
||||
#[test]
|
||||
fn seen_straight_on_an_extrusion_is_its_front_face_exactly_where_the_art_is() {
|
||||
let (p, b) = shape(&square(10.0, 20.0, 100.0));
|
||||
let f = extrude(&p, b, Extrude::default(), front()).unwrap();
|
||||
let f = extrude(&p, FillRule::EvenOdd, b, Extrude::default(), front()).unwrap();
|
||||
assert_eq!(f.len(), 1, "the walls and back face away: only the front cap shows");
|
||||
assert_eq!(f[0].contours.len(), 1);
|
||||
assert_eq!(extent(&f), Rect::new(10.0, 20.0, 110.0, 120.0));
|
||||
@@ -51,7 +51,7 @@ fn seen_straight_on_an_extrusion_is_its_front_face_exactly_where_the_art_is() {
|
||||
fn turned_about_y_the_front_foreshortens_and_one_side_wall_shows() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let o = Extrude { depth: 40.0, ..Extrude::default() };
|
||||
let f = extrude(&p, b, o, View { rotation_y: 30.0, ..front() }).unwrap();
|
||||
let f = extrude(&p, FillRule::EvenOdd, b, o, View { rotation_y: 30.0, ..front() }).unwrap();
|
||||
// Orthographic: the silhouette is the front's width times cos 30° plus the depth times sin 30°.
|
||||
let w = extent(&f).width();
|
||||
assert!(close(w, 100.0 * 30f64.to_radians().cos() + 40.0 * 30f64.to_radians().sin(), 1e-6), "{w}");
|
||||
@@ -66,10 +66,10 @@ fn turned_about_y_the_front_foreshortens_and_one_side_wall_shows() {
|
||||
fn no_depth_is_the_flat_shape_and_no_cap_shows_the_inside() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let v = View { rotation_x: 20.0, rotation_y: 30.0, ..front() };
|
||||
let flat = extrude(&p, b, Extrude { depth: 0.0, ..Extrude::default() }, v).unwrap();
|
||||
assert_eq!(flat, plane(&p, b, v).unwrap());
|
||||
let solid = extrude(&p, b, Extrude::default(), v).unwrap();
|
||||
let hollow = extrude(&p, b, Extrude { cap: false, ..Extrude::default() }, v).unwrap();
|
||||
let flat = extrude(&p, FillRule::EvenOdd, b, Extrude { depth: 0.0, ..Extrude::default() }, v).unwrap();
|
||||
assert_eq!(flat, plane(&p, FillRule::EvenOdd, b, v).unwrap());
|
||||
let solid = extrude(&p, FillRule::EvenOdd, b, Extrude::default(), v).unwrap();
|
||||
let hollow = extrude(&p, FillRule::EvenOdd, b, Extrude { cap: false, ..Extrude::default() }, v).unwrap();
|
||||
// Solid: front, right and top faces. Hollow: all four walls (two seen from inside), no caps.
|
||||
assert_eq!(solid.len(), 3);
|
||||
assert_eq!(hollow.len(), 4);
|
||||
@@ -81,10 +81,10 @@ fn taper_shrinks_the_back_and_twist_turns_it() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let side = View { rotation_y: 90.0, ..front() };
|
||||
let o = Extrude { depth: 60.0, cap: false, ..Extrude::default() };
|
||||
let straight = extent(&extrude(&p, b, o, side).unwrap());
|
||||
let straight = extent(&extrude(&p, FillRule::EvenOdd, b, o, side).unwrap());
|
||||
assert!(close(straight.height(), 100.0, 1e-6) && close(straight.width(), 60.0, 1e-6));
|
||||
// Seen from the side, a 50 % taper keeps the front's full height and halves the back's.
|
||||
let tapered = extrude(&p, b, Extrude { taper: 0.5, ..o }, side).unwrap();
|
||||
let tapered = extrude(&p, FillRule::EvenOdd, b, Extrude { taper: 0.5, ..o }, side).unwrap();
|
||||
let back: Vec<Point> = tapered.iter().flat_map(|f| f.contours.iter().flatten().copied()).filter(|q| close(q.x, straight.x0, 1e-6)).collect();
|
||||
let (lo, hi) = back.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), q| (lo.min(q.y), hi.max(q.y)));
|
||||
let at_front: Vec<Point> = tapered.iter().flat_map(|f| f.contours.iter().flatten().copied()).filter(|q| close(q.x, straight.x1, 1e-6)).collect();
|
||||
@@ -93,11 +93,11 @@ fn taper_shrinks_the_back_and_twist_turns_it() {
|
||||
assert!(close(front_h, 100.0, 1e-6) && close(back_h, 50.0, 1e-6), "front {front_h}, back {back_h}");
|
||||
// Seen from behind, a 45° twist shows the back cap turned into a diamond.
|
||||
let back = View { rotation_y: 180.0, ..front() };
|
||||
let twisted = extrude(&p, b, Extrude { twist: 45.0, cap: true, ..o }, back).unwrap();
|
||||
let twisted = extrude(&p, FillRule::EvenOdd, b, Extrude { twist: 45.0, cap: true, ..o }, back).unwrap();
|
||||
assert_eq!(twisted.len(), 1, "only the back cap faces the viewer");
|
||||
assert!(close(extent(&twisted).width(), 100.0 * 2f64.sqrt(), 1e-6), "{:?}", extent(&twisted));
|
||||
// Its walls are cut into slices that follow the turn.
|
||||
let open = extrude(&p, b, Extrude { twist: 45.0, ..o }, View { rotation_x: -20.0, rotation_y: 30.0, ..front() }).unwrap();
|
||||
let open = extrude(&p, FillRule::EvenOdd, b, Extrude { twist: 45.0, ..o }, View { rotation_x: -20.0, rotation_y: 30.0, ..front() }).unwrap();
|
||||
assert!(open.len() > 2 * 8, "{}", open.len());
|
||||
}
|
||||
|
||||
@@ -105,13 +105,13 @@ fn taper_shrinks_the_back_and_twist_turns_it() {
|
||||
fn a_bevel_inside_shrinks_the_cap_and_outside_grows_the_walls() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let bevel = Bevel { width: 10.0, height: 10.0, ..Bevel::default() };
|
||||
let inside = extrude(&p, b, Extrude { bevel: Some(Bevel { inside: true, ..bevel }), ..Extrude::default() }, front()).unwrap();
|
||||
let inside = extrude(&p, FillRule::EvenOdd, b, Extrude { bevel: Some(Bevel { inside: true, ..bevel }), ..Extrude::default() }, front()).unwrap();
|
||||
// Straight on: the cap inset by the width, and the chamfer around it filling the outline.
|
||||
let cap = inside.iter().find(|f| close(signed_area(&f.contours[0]).abs(), 80.0 * 80.0, 1e-6));
|
||||
assert!(cap.is_some(), "an 80 × 80 cap");
|
||||
assert_eq!(extent(&inside), Rect::new(0.0, 0.0, 100.0, 100.0));
|
||||
assert!(close(area(&inside), 100.0 * 100.0, 1e-6));
|
||||
let outside = extrude(&p, b, Extrude { bevel: Some(bevel), ..Extrude::default() }, front()).unwrap();
|
||||
let outside = extrude(&p, FillRule::EvenOdd, b, Extrude { bevel: Some(bevel), ..Extrude::default() }, front()).unwrap();
|
||||
assert_eq!(extent(&outside), Rect::new(-10.0, -10.0, 110.0, 110.0), "the walls stand 10 pt outside the art");
|
||||
assert!(outside.iter().any(|f| close(signed_area(&f.contours[0]).abs(), 100.0 * 100.0, 1e-6)), "the cap is the art");
|
||||
// The chamfer's faces lean towards the light differently from the cap.
|
||||
@@ -123,12 +123,16 @@ fn bevel_shapes_repeats_and_both_sides_change_the_surface() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let v = View { rotation_x: -20.0, rotation_y: 30.0, ..front() };
|
||||
let base = Bevel { width: 12.0, height: 12.0, ..Bevel::default() };
|
||||
let classic = extrude(&p, b, Extrude { bevel: Some(base), ..Extrude::default() }, v).unwrap();
|
||||
let classic = extrude(&p, FillRule::EvenOdd, b, Extrude { bevel: Some(base), ..Extrude::default() }, v).unwrap();
|
||||
for shape in BevelShape::ALL.into_iter().skip(1) {
|
||||
assert_ne!(classic, extrude(&p, b, Extrude { bevel: Some(Bevel { shape, ..base }), ..Extrude::default() }, v).unwrap(), "{shape:?}");
|
||||
assert_ne!(
|
||||
classic,
|
||||
extrude(&p, FillRule::EvenOdd, b, Extrude { bevel: Some(Bevel { shape, ..base }), ..Extrude::default() }, v).unwrap(),
|
||||
"{shape:?}"
|
||||
);
|
||||
}
|
||||
for bevel in [Bevel { repeat: 3, ..base }, Bevel { repeat: 3, space: 0.3, ..base }, Bevel { both_sides: true, ..base }] {
|
||||
assert_ne!(classic, extrude(&p, b, Extrude { bevel: Some(bevel), ..Extrude::default() }, v).unwrap(), "{bevel:?}");
|
||||
assert_ne!(classic, extrude(&p, FillRule::EvenOdd, b, Extrude { bevel: Some(bevel), ..Extrude::default() }, v).unwrap(), "{bevel:?}");
|
||||
}
|
||||
// The step shape keeps its corners: half the width at half the height.
|
||||
assert_eq!(BevelShape::Step.at(0.5), (0.5, 0.5));
|
||||
@@ -147,11 +151,11 @@ fn holes_stay_holes_in_the_caps_and_get_walls_of_their_own() {
|
||||
// The hole drawn the same way round as the outline: even-odd nesting still makes it a hole.
|
||||
b.extend(square(30.0, 30.0, 40.0));
|
||||
let (p, r) = shape(&b);
|
||||
let f = extrude(&p, r, Extrude::default(), front()).unwrap();
|
||||
let f = extrude(&p, FillRule::EvenOdd, r, Extrude::default(), front()).unwrap();
|
||||
assert_eq!(f.len(), 1);
|
||||
assert_eq!(f[0].contours.len(), 2);
|
||||
assert!(close(area(&f), 100.0 * 100.0 - 40.0 * 40.0, 1e-6));
|
||||
let turned = extrude(&p, r, Extrude::default(), View { rotation_y: 40.0, ..front() }).unwrap();
|
||||
let turned = extrude(&p, FillRule::EvenOdd, r, Extrude::default(), View { rotation_y: 40.0, ..front() }).unwrap();
|
||||
// Front cap, the outline's right wall and the hole's left wall (seen through the hole).
|
||||
assert_eq!(turned.len(), 3);
|
||||
}
|
||||
@@ -159,15 +163,15 @@ fn holes_stay_holes_in_the_caps_and_get_walls_of_their_own() {
|
||||
#[test]
|
||||
fn a_plane_turns_about_the_given_centre() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let f = plane(&p, b, View { rotation_y: 60.0, ..front() }).unwrap();
|
||||
let f = plane(&p, FillRule::EvenOdd, b, View { rotation_y: 60.0, ..front() }).unwrap();
|
||||
assert_eq!(f.len(), 1);
|
||||
let e = extent(&f);
|
||||
assert!(close(e.width(), 50.0, 1e-6) && close(e.center().x, 50.0, 1e-9));
|
||||
// Turned about the centre of a larger box, the square moves as well as narrowing.
|
||||
let wide = plane(&p, Rect::new(0.0, 0.0, 300.0, 100.0), View { rotation_y: 60.0, ..front() }).unwrap();
|
||||
let wide = plane(&p, FillRule::EvenOdd, Rect::new(0.0, 0.0, 300.0, 100.0), View { rotation_y: 60.0, ..front() }).unwrap();
|
||||
assert!(close(extent(&wide).x0, 150.0 - 150.0 * 0.5, 1e-6));
|
||||
// Its back shows when turned past 90°: a plane is two-sided.
|
||||
assert_eq!(plane(&p, b, View { rotation_y: 150.0, ..front() }).unwrap().len(), 1);
|
||||
assert_eq!(plane(&p, FillRule::EvenOdd, b, View { rotation_y: 150.0, ..front() }).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -175,7 +179,7 @@ fn inflate_covers_the_shape_and_puffs_by_the_volume() {
|
||||
let mut c = BezPath::new();
|
||||
c.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((100.0, 100.0), 50.0), 0.01));
|
||||
let (p, b) = shape(&c);
|
||||
let f = inflate(&p, b, Inflate::default(), front()).unwrap();
|
||||
let f = inflate(&p, FillRule::EvenOdd, b, Inflate::default(), front()).unwrap();
|
||||
// Straight on, the cushion covers the circle (sampled as a polygon) and nothing else.
|
||||
assert!(close(area(&f), std::f64::consts::PI * 50.0 * 50.0, 60.0), "{}", area(&f));
|
||||
assert!(extent(&f).x0 >= 50.0 - 1e-6 && extent(&f).x1 <= 150.0 + 1e-6);
|
||||
@@ -184,7 +188,7 @@ fn inflate_covers_the_shape_and_puffs_by_the_volume() {
|
||||
assert!(hi - lo > 0.3, "{lo}..{hi}");
|
||||
// From the side: 50 % volume rounds a disc into a half sphere on a flat back.
|
||||
let side = View { rotation_y: 90.0, ..front() };
|
||||
let thick = |o: Inflate| extent(&inflate(&p, b, o, side).unwrap()).width();
|
||||
let thick = |o: Inflate| extent(&inflate(&p, FillRule::EvenOdd, b, o, side).unwrap()).width();
|
||||
assert!(close(thick(Inflate::default()), 50.0, 2.0), "{}", thick(Inflate::default()));
|
||||
assert!(close(thick(Inflate { both_sides: true, ..Inflate::default() }), 100.0, 4.0));
|
||||
assert!(close(thick(Inflate { depth: 30.0, ..Inflate::default() }), 80.0, 2.0));
|
||||
@@ -197,7 +201,7 @@ fn inflate_leaves_holes_open() {
|
||||
let mut b = square(0.0, 0.0, 100.0);
|
||||
b.extend(square(30.0, 30.0, 40.0));
|
||||
let (p, r) = shape(&b);
|
||||
let f = inflate(&p, r, Inflate::default(), front()).unwrap();
|
||||
let f = inflate(&p, FillRule::EvenOdd, r, Inflate::default(), front()).unwrap();
|
||||
assert!(close(area(&f), 100.0 * 100.0 - 40.0 * 40.0, 1e-3), "{}", area(&f));
|
||||
}
|
||||
|
||||
@@ -205,9 +209,9 @@ fn inflate_leaves_holes_open() {
|
||||
fn malformed_and_degenerate_input_is_bounded() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
for r in [Rect::new(f64::NAN, 0.0, 1.0, 1.0), Rect::new(0.0, 0.0, 1e9, 1.0)] {
|
||||
assert!(extrude(&p, r, Extrude::default(), front()).is_err());
|
||||
assert!(inflate(&p, r, Inflate::default(), front()).is_err());
|
||||
assert!(plane(&p, r, front()).is_err());
|
||||
assert!(extrude(&p, FillRule::EvenOdd, r, Extrude::default(), front()).is_err());
|
||||
assert!(inflate(&p, FillRule::EvenOdd, r, Inflate::default(), front()).is_err());
|
||||
assert!(plane(&p, FillRule::EvenOdd, r, front()).is_err());
|
||||
}
|
||||
let wild = Extrude {
|
||||
depth: f64::INFINITY,
|
||||
@@ -216,18 +220,24 @@ fn malformed_and_degenerate_input_is_bounded() {
|
||||
cap: true,
|
||||
bevel: Some(Bevel { width: f64::NAN, height: 1e12, repeat: usize::MAX, space: 7.0, ..Bevel::default() }),
|
||||
};
|
||||
let f = extrude(&p, b, wild, View { rotation_x: f64::NAN, perspective: 900.0, ..front() }).unwrap();
|
||||
let f = extrude(&p, FillRule::EvenOdd, b, wild, View { rotation_x: f64::NAN, perspective: 900.0, ..front() }).unwrap();
|
||||
assert!(f.iter().flat_map(|f| f.contours.iter().flatten()).all(|q| q.x.is_finite() && q.y.is_finite()));
|
||||
assert!(
|
||||
inflate(&p, b, Inflate { depth: -5.0, volume: f64::NAN, both_sides: true, twist: f64::INFINITY, taper: f64::NAN, cap: true }, front())
|
||||
.is_ok()
|
||||
inflate(
|
||||
&p,
|
||||
FillRule::EvenOdd,
|
||||
b,
|
||||
Inflate { depth: -5.0, volume: f64::NAN, both_sides: true, twist: f64::INFINITY, taper: f64::NAN, cap: true },
|
||||
front()
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(extrude(&PathData::default(), b, Extrude::default(), front()).unwrap().is_empty());
|
||||
assert!(extrude(&PathData::default(), FillRule::EvenOdd, b, Extrude::default(), front()).unwrap().is_empty());
|
||||
// A line has no area to extrude.
|
||||
let mut line = BezPath::new();
|
||||
line.move_to((0.0, 0.0));
|
||||
line.line_to((100.0, 0.0));
|
||||
assert!(extrude(&PathData::from_bezpath(&line), b, Extrude::default(), front()).unwrap().is_empty());
|
||||
assert!(extrude(&PathData::from_bezpath(&line), FillRule::EvenOdd, b, Extrude::default(), front()).unwrap().is_empty());
|
||||
// Too many anchors: refused, not truncated.
|
||||
let mut many = BezPath::new();
|
||||
many.move_to((0.0, 0.0));
|
||||
@@ -235,15 +245,15 @@ fn malformed_and_degenerate_input_is_bounded() {
|
||||
many.line_to((f64::from(i), f64::from(i % 7)));
|
||||
}
|
||||
many.close_path();
|
||||
assert!(extrude(&PathData::from_bezpath(&many), b, Extrude::default(), front()).is_err());
|
||||
assert!(extrude(&PathData::from_bezpath(&many), FillRule::EvenOdd, b, Extrude::default(), front()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perspective_makes_the_near_face_larger() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let o = Extrude { depth: 100.0, ..Extrude::default() };
|
||||
let flat = extent(&extrude(&p, b, o, front()).unwrap());
|
||||
let deep = extent(&extrude(&p, b, o, View { perspective: 30.0, ..front() }).unwrap());
|
||||
let flat = extent(&extrude(&p, FillRule::EvenOdd, b, o, front()).unwrap());
|
||||
let deep = extent(&extrude(&p, FillRule::EvenOdd, b, o, View { perspective: 30.0, ..front() }).unwrap());
|
||||
assert!(deep.width() > flat.width() + 1.0);
|
||||
assert!(close(deep.center().x, flat.center().x, 1e-6));
|
||||
}
|
||||
@@ -257,7 +267,7 @@ fn a_wider_perspective_brings_the_eye_nearer() {
|
||||
let mut last = 100.0;
|
||||
for angle in [20.0, 60.0, 110.0, 160.0] {
|
||||
// The front face alone shows: its width is the cube's on the page.
|
||||
let width = extent(&extrude(&p, b, o, View { perspective: angle, ..front() }).unwrap()).width();
|
||||
let width = extent(&extrude(&p, FillRule::EvenOdd, b, o, View { perspective: angle, ..front() }).unwrap()).width();
|
||||
assert!(width > last, "{angle}°: {width} after {last}");
|
||||
last = width;
|
||||
}
|
||||
@@ -274,10 +284,10 @@ fn the_inflate_membrane_matches_known_solutions() {
|
||||
let mut c = BezPath::new();
|
||||
c.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((100.0, 100.0), 50.0), 0.01));
|
||||
let (p, b) = shape(&c);
|
||||
let disc = Membrane::solve(&rings(&p, 100.0, MAX_SOLID_POINTS).unwrap(), b.origin(), b.width() / 48.0, 49, 49);
|
||||
let disc = Membrane::solve(&rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap(), b.origin(), b.width() / 48.0, 49, 49);
|
||||
assert!(close(disc.peak, 50.0 * 50.0 / 4.0, 625.0 * 0.02), "{}", disc.peak);
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let sq = Membrane::solve(&rings(&p, 100.0, MAX_SOLID_POINTS).unwrap(), b.origin(), 100.0 / 48.0, 49, 49);
|
||||
let sq = Membrane::solve(&rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap(), b.origin(), 100.0 / 48.0, 49, 49);
|
||||
assert!(close(sq.peak, 0.07367 * 100.0 * 100.0, 736.7 * 0.02), "{}", sq.peak);
|
||||
// Smooth across the middle: no crease where a distance-to-the-edge height would have one
|
||||
// (the diagonals). The second difference along a diagonal stays small and even.
|
||||
@@ -303,10 +313,10 @@ fn inflate_cells_across_a_hole_edge_are_cut_to_the_shape() {
|
||||
b.close_path();
|
||||
let (p, r) = shape(&b);
|
||||
let v = View { rotation_x: -21.88, rotation_y: -23.4, rotation_z: 9.06, ..front() };
|
||||
let f = inflate(&p, r, Inflate::default(), v).unwrap();
|
||||
let f = inflate(&p, FillRule::EvenOdd, r, Inflate::default(), v).unwrap();
|
||||
assert!(f.iter().all(|f| f.contours.len() == 1), "every facet is one piece of the surface");
|
||||
// Straight on, the facets still cover exactly the shape.
|
||||
let flat = inflate(&p, r, Inflate::default(), front()).unwrap();
|
||||
let flat = inflate(&p, FillRule::EvenOdd, r, Inflate::default(), front()).unwrap();
|
||||
assert!(close(area(&flat), 220.0 * 220.0 - 100.0 * 90.0, 1e-3), "{}", area(&flat));
|
||||
}
|
||||
|
||||
@@ -332,19 +342,19 @@ fn bumps(count: usize) -> (PathData, Rect) {
|
||||
fn small_curves_take_few_points_and_rotate_takes_large_outlines() {
|
||||
// 300 curves were 4,800 points at sixteen each, over the old limit of 1,024.
|
||||
let (p, r) = bumps(300);
|
||||
assert!(extrude(&p, r, Extrude::default(), front()).is_ok());
|
||||
assert!(inflate(&p, r, Inflate { volume: 20.0, ..Inflate::default() }, front()).is_ok());
|
||||
assert!(plane(&p, r, front()).is_ok());
|
||||
assert!(extrude(&p, FillRule::EvenOdd, r, Extrude::default(), front()).is_ok());
|
||||
assert!(inflate(&p, FillRule::EvenOdd, r, Inflate { volume: 20.0, ..Inflate::default() }, front()).is_ok());
|
||||
assert!(plane(&p, FillRule::EvenOdd, r, front()).is_ok());
|
||||
// 4,000 curves: too many for a solid, not for Rotate.
|
||||
let (p, r) = bumps(4000);
|
||||
assert!(extrude(&p, r, Extrude::default(), front()).unwrap_err().contains("too complex"));
|
||||
assert_eq!(plane(&p, r, front()).unwrap().len(), 1);
|
||||
assert!(extrude(&p, FillRule::EvenOdd, r, Extrude::default(), front()).unwrap_err().contains("too complex"));
|
||||
assert_eq!(plane(&p, FillRule::EvenOdd, r, front()).unwrap().len(), 1);
|
||||
// Each piece stays within the tolerance of the curve: a circle's chords sag at most 0.05 pt
|
||||
// here (1/2000 of its 100 pt size is less).
|
||||
let mut c = BezPath::new();
|
||||
c.extend(vectorcraft_geom::kurbo::Shape::path_elements(&vectorcraft_geom::kurbo::Circle::new((50.0, 50.0), 50.0), 0.01));
|
||||
let (p, r) = shape(&c);
|
||||
let ring = &rings(&p, 100.0, MAX_SOLID_POINTS).unwrap()[0];
|
||||
let ring = &rings(&p, FillRule::EvenOdd, 100.0, MAX_SOLID_POINTS).unwrap()[0];
|
||||
for (a, b) in ring.iter().zip(ring.iter().cycle().skip(1)) {
|
||||
let mid = a.midpoint(*b);
|
||||
assert!(50.0 - mid.distance(Point::new(50.0, 50.0)) <= 0.05 + 1e-3, "{a:?} {b:?}");
|
||||
@@ -352,7 +362,7 @@ fn small_curves_take_few_points_and_rotate_takes_large_outlines() {
|
||||
assert!(r.width() > 0.0);
|
||||
// A small bump takes a few points; a large curve more than sixteen where it needs them.
|
||||
let (p, r) = bumps(300);
|
||||
let small = rings(&p, r.width().max(r.height()), MAX_SOLID_POINTS).unwrap()[0].len();
|
||||
let small = rings(&p, FillRule::EvenOdd, r.width().max(r.height()), MAX_SOLID_POINTS).unwrap()[0].len();
|
||||
assert!(small <= 300 * 4 + 3, "{small} points for 300 bumps");
|
||||
assert!(ring.len() > 4 * 16, "a 100 pt circle within 0.05 pt: {}", ring.len());
|
||||
}
|
||||
@@ -362,7 +372,7 @@ fn small_curves_take_few_points_and_rotate_takes_large_outlines() {
|
||||
#[test]
|
||||
fn the_edge_index_finds_the_nearest_edge() {
|
||||
let (p, r) = bumps(200);
|
||||
let rings = rings(&p, r.width(), MAX_SOLID_POINTS).unwrap();
|
||||
let rings = rings(&p, FillRule::EvenOdd, r.width(), MAX_SOLID_POINTS).unwrap();
|
||||
let edges = Edges::new(&rings);
|
||||
for k in 0..400 {
|
||||
let t = f64::from(k);
|
||||
@@ -376,7 +386,7 @@ fn the_edge_index_finds_the_nearest_edge() {
|
||||
/// straight ahead: the search doesn't stop before it has looked there.
|
||||
#[test]
|
||||
fn the_edge_index_looks_along_the_side_from_outside() {
|
||||
let ring = |b: BezPath| rings(&shape(&b).0, 64.0, MAX_SOLID_POINTS).unwrap().remove(0);
|
||||
let ring = |b: BezPath| rings(&shape(&b).0, FillRule::EvenOdd, 64.0, MAX_SOLID_POINTS).unwrap().remove(0);
|
||||
// Straight ahead of the point, a little further up its side, and the far corner (a 64 pt grid).
|
||||
let rings = vec![ring(square(2.5, 0.0, 1.0)), ring(square(0.0, 4.2, 0.8)), ring(square(63.0, 63.0, 1.0))];
|
||||
let edges = Edges::new(&rings);
|
||||
@@ -390,7 +400,7 @@ fn the_edge_index_looks_along_the_side_from_outside() {
|
||||
fn twist_holds_between_0_and_360_degrees() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
let v = View { rotation_x: -20.0, rotation_y: 30.0, ..front() };
|
||||
let at = |twist: f64| extrude(&p, b, Extrude { twist, ..Extrude::default() }, v).unwrap();
|
||||
let at = |twist: f64| extrude(&p, FillRule::EvenOdd, b, Extrude { twist, ..Extrude::default() }, v).unwrap();
|
||||
assert_eq!(at(-45.0), at(0.0));
|
||||
assert_eq!(at(500.0), at(360.0));
|
||||
assert_ne!(at(45.0), at(0.0));
|
||||
@@ -408,11 +418,11 @@ fn twisted_walls_face_the_way_they_turn() {
|
||||
for (x, y) in [(-20.0, 30.0), (0.0, 90.0), (30.0, -60.0), (-45.0, 150.0)] {
|
||||
let v = View { rotation_x: x, rotation_y: y, ..front() };
|
||||
let o = Extrude { twist, ..Extrude::default() };
|
||||
let got = extrude(&p, b, o, v).unwrap();
|
||||
let got = extrude(&p, FillRule::EvenOdd, b, o, v).unwrap();
|
||||
// Expected: walls (and caps) whose outward normal faces the viewer.
|
||||
let o = o.sanitized();
|
||||
let camera = Camera::new(v, b, o.depth);
|
||||
let ring = &rings(&p, b.width().max(b.height()), MAX_SOLID_POINTS).unwrap()[0];
|
||||
let ring = &rings(&p, FillRule::EvenOdd, b.width().max(b.height()), MAX_SOLID_POINTS).unwrap()[0];
|
||||
let slices = ((twist / 6.0).ceil() as usize).clamp(8, 64);
|
||||
let z = |t: f64| o.depth / 2.0 - o.depth * t;
|
||||
let mut expected = 0;
|
||||
@@ -456,7 +466,7 @@ fn inflate_twists_tapers_and_opens_its_back() {
|
||||
let o = Inflate { depth: 60.0, volume: 0.0, ..Inflate::default() };
|
||||
// From the side, a flat (0 % volume) cushion is the extrusion: front 100 high, the back 50
|
||||
// at a 50 % taper.
|
||||
let tapered = inflate(&p, b, Inflate { taper: 0.5, ..o }, side).unwrap();
|
||||
let tapered = inflate(&p, FillRule::EvenOdd, b, Inflate { taper: 0.5, ..o }, side).unwrap();
|
||||
let e = extent(&tapered);
|
||||
assert!(close(e.width(), 60.0, 1e-6) && close(e.height(), 100.0, 1e-6), "{e:?}");
|
||||
let back: Vec<Point> = tapered.iter().flat_map(|f| f.contours.iter().flatten().copied()).filter(|q| close(q.x, e.x0, 1e-6)).collect();
|
||||
@@ -464,18 +474,19 @@ fn inflate_twists_tapers_and_opens_its_back() {
|
||||
assert!(close(hi - lo, 50.0, 1e-6), "back {}", hi - lo);
|
||||
// From behind, a 45° twist shows the flat back as a diamond.
|
||||
let behind = View { rotation_y: 180.0, ..front() };
|
||||
let twisted = inflate(&p, b, Inflate { twist: 45.0, ..o }, behind).unwrap();
|
||||
let twisted = inflate(&p, FillRule::EvenOdd, b, Inflate { twist: 45.0, ..o }, behind).unwrap();
|
||||
assert!(close(extent(&twisted).width(), 100.0 * 2f64.sqrt(), 1e-6), "{:?}", extent(&twisted));
|
||||
// Hollow: no back; from behind the inside of the inflated front shows instead of a flat face.
|
||||
let solid = inflate(&p, b, Inflate { volume: 50.0, ..o }, behind).unwrap();
|
||||
let solid = inflate(&p, FillRule::EvenOdd, b, Inflate { volume: 50.0, ..o }, behind).unwrap();
|
||||
assert_eq!(solid.len(), 1, "the flat back alone");
|
||||
let hollow = inflate(&p, b, Inflate { volume: 50.0, cap: false, ..o }, behind).unwrap();
|
||||
let hollow = inflate(&p, FillRule::EvenOdd, b, Inflate { volume: 50.0, cap: false, ..o }, behind).unwrap();
|
||||
assert!(hollow.len() > 100 && hollow.iter().all(|f| f.contours.len() == 1 && f.contours[0].len() < 12));
|
||||
// With both sides puffed, the back puff is scaled with the taper: lower and smaller.
|
||||
let both = |taper: f64| extent(&inflate(&p, b, Inflate { both_sides: true, volume: 50.0, taper, depth: 0.0, ..o }, side).unwrap());
|
||||
let both =
|
||||
|taper: f64| extent(&inflate(&p, FillRule::EvenOdd, b, Inflate { both_sides: true, volume: 50.0, taper, depth: 0.0, ..o }, side).unwrap());
|
||||
assert!(both(0.5).width() < both(1.0).width() && both(0.5).height() <= both(1.0).height() + 1e-6);
|
||||
// Out of range values hold at the documented ends.
|
||||
let at = |i: Inflate| inflate(&p, b, i, View { rotation_x: -20.0, rotation_y: 30.0, ..front() }).unwrap();
|
||||
let at = |i: Inflate| inflate(&p, FillRule::EvenOdd, b, i, View { rotation_x: -20.0, rotation_y: 30.0, ..front() }).unwrap();
|
||||
assert_eq!(at(Inflate { twist: -30.0, ..o }), at(o));
|
||||
assert_eq!(at(Inflate { taper: 0.0, ..o }), at(Inflate { taper: 0.01, ..o }));
|
||||
}
|
||||
@@ -487,10 +498,43 @@ fn a_flat_twisted_inflate_matches_the_extrusion() {
|
||||
let (p, b) = shape(&square(0.0, 0.0, 100.0));
|
||||
for (x, y) in [(-20.0, 30.0), (0.0, 90.0), (30.0, -60.0), (-45.0, 150.0), (60.0, 200.0)] {
|
||||
let v = View { rotation_x: x, rotation_y: y, ..front() };
|
||||
let e = extent(&extrude(&p, b, Extrude { depth: 60.0, twist: 120.0, taper: 0.6, ..Extrude::default() }, v).unwrap());
|
||||
let i = extent(&inflate(&p, b, Inflate { depth: 60.0, twist: 120.0, taper: 0.6, volume: 0.0, ..Inflate::default() }, v).unwrap());
|
||||
let e = extent(&extrude(&p, FillRule::EvenOdd, b, Extrude { depth: 60.0, twist: 120.0, taper: 0.6, ..Extrude::default() }, v).unwrap());
|
||||
let i = extent(
|
||||
&inflate(&p, FillRule::EvenOdd, b, Inflate { depth: 60.0, twist: 120.0, taper: 0.6, volume: 0.0, ..Inflate::default() }, v).unwrap(),
|
||||
);
|
||||
for (a, c) in [(e.x0, i.x0), (e.y0, i.y0), (e.x1, i.x1), (e.y1, i.y1)] {
|
||||
assert!(close(a, c, 1e-6), "view ({x}, {y}): extrusion {e:?}, inflate {i:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #1724: under the nonzero rule a contour inside another that runs the same way cuts no hole
|
||||
/// (the middle is filled, as the art is drawn); one running the other way does, and so does
|
||||
/// either under the even-odd rule.
|
||||
#[test]
|
||||
fn nonzero_shapes_keep_the_middle_a_same_way_contour_encloses() {
|
||||
let rect = |r: Rect, clockwise: bool| {
|
||||
let mut pts = vec![Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)];
|
||||
if clockwise {
|
||||
pts.reverse();
|
||||
}
|
||||
vectorcraft_geom::SubPath::polyline(&pts, true)
|
||||
};
|
||||
let (outer, inner) = (Rect::new(0.0, 0.0, 100.0, 100.0), Rect::new(25.0, 25.0, 75.0, 75.0));
|
||||
let nested = |inner_clockwise: bool| PathData::new(vec![rect(outer, false), rect(inner, inner_clockwise)]);
|
||||
let flat = |p: &PathData, rule: FillRule| area(&plane(p, rule, outer, front()).unwrap());
|
||||
let (solid, holed) = (100.0 * 100.0, 100.0 * 100.0 - 50.0 * 50.0);
|
||||
assert!(close(flat(&nested(false), FillRule::NonZero), solid, 1e-6), "the same way round: filled");
|
||||
assert!(close(flat(&nested(true), FillRule::NonZero), holed, 1e-6), "the other way round: a hole");
|
||||
assert!(close(flat(&nested(false), FillRule::EvenOdd), holed, 1e-6));
|
||||
assert!(close(flat(&nested(true), FillRule::EvenOdd), holed, 1e-6));
|
||||
// A third contour inside the hole is an island again, whichever way it runs.
|
||||
let island = |clockwise: bool| PathData::new(vec![rect(outer, false), rect(inner, true), rect(Rect::new(40.0, 40.0, 60.0, 60.0), clockwise)]);
|
||||
for clockwise in [false, true] {
|
||||
assert!(close(flat(&island(clockwise), FillRule::NonZero), holed + 20.0 * 20.0, 1e-6), "{clockwise}");
|
||||
}
|
||||
// Extrude's caps and Inflate follow the same outline.
|
||||
let caps = |p: &PathData| area(&extrude(p, FillRule::NonZero, outer, Extrude::default(), front()).unwrap());
|
||||
assert!(caps(&nested(false)) > caps(&nested(true)) + 1000.0);
|
||||
assert!(inflate(&nested(false), FillRule::NonZero, outer, Inflate::default(), front()).is_ok());
|
||||
}
|
||||
Reference in new issue
Block a user