#1197: Make Compound Path cuts holes where paths overlap

Make Compound Path kept each path's direction under the non-zero fill
rule, so a shape drawn inside or across another, running the same way,
left the overlap filled instead of making a hole. The paths in front of
the backmost one now run against it, so where they overlap it they cut
holes, as users expect from a compound path.
This commit is contained in:
bflatastic
2026-10-11 21:32:23 +07:00
parent 3b60d0ceb6
commit 7f35522b15
3 changed files with 27 additions and 3 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+10 -2
View File
@@ -652,11 +652,19 @@ fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
d.remove(*id)?;
}
let ch = d.node_mut(cid).and_then(|n| n.children_mut()).ok_or(EngineError::NoNode(cid))?;
for mut p in paths {
// The paths in front run against the backmost one, so where they overlap it they cut holes
// under the non-zero fill rule (#1197).
let direction = |n: &Node| n.path_data().and_then(|pd| pd.subpaths.first()).map_or(0.0, |sp| sp.signed_area().signum());
let back = paths.first().map_or(0.0, |p| direction(p));
for (i, mut p) in paths.into_iter().enumerate() {
let pn = Arc::make_mut(&mut p);
pn.appearance = Appearance::default();
if let NodeKind::Path { live, .. } = &mut pn.kind {
let flip = i > 0 && back != 0.0 && direction(pn) == back;
if let NodeKind::Path { path, live, .. } = &mut pn.kind {
*live = None;
if flip {
path.reverse();
}
}
ch.push(p);
}
+16
View File
@@ -1699,3 +1699,19 @@ fn file_list_lists_a_folder_within_the_read_root() {
assert!(s.execute("file.list", &json!({})).is_err());
let _ = std::fs::remove_dir_all(base);
}
/// #1197: Make Compound Path cuts holes where the paths in front overlap the backmost one, also
/// when they were drawn running the same way.
#[test]
fn a_compound_path_has_holes_where_its_paths_overlap() {
let mut s = session();
let square = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
let diamond = NodeId(s.execute("path.create", &json!({"d": "M50 -20 L120 50 L50 120 L-20 50 Z"})).unwrap()["id"].as_u64().unwrap());
s.execute("select.set", &json!({"ids": [square.0, diamond.0]})).unwrap();
let c = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap());
let hit =
|x: f64, y: f64| vectorcraft_doc::hit::hit_test(&s.doc().unwrap().doc, Point::new(x, y), Default::default()).map(|h| h.top_object(None));
assert_eq!(hit(50.0, 50.0), None, "the overlap is a hole");
assert_eq!(hit(3.0, 3.0), Some(c), "the square's corner is filled");
assert_eq!(hit(110.0, 50.0), Some(c), "the diamond's point outside the square is filled");
}