From d0894d02236e6d408df5ca4272e0338c2ec5cbcc Mon Sep 17 00:00:00 2001 From: Greg Peairs Date: Tue, 28 Oct 2025 16:58:38 +0000 Subject: [PATCH] Add xor2d --- CHANGELOG.md | 4 ++++ docs/src/polygons.md | 3 ++- src/DeviceLayout.jl | 6 ++++-- src/polygons.jl | 31 ++++++++++++++++++++++++++++++- test/test_clipping.jl | 5 +++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b694581..ea37054a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format of this changelog is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## Upcoming + + - Added `xor2d` for polygon XOR + ## 1.6.0 (2025-10-16) - Improved metadata handling for `LayoutTarget` and `SolidModelTarget` diff --git a/docs/src/polygons.md b/docs/src/polygons.md index bdab26a4..fe446b4a 100644 --- a/docs/src/polygons.md +++ b/docs/src/polygons.md @@ -14,7 +14,7 @@ For example, if you ask for the bounding box of a path node (which could define ## Clipping -Geometric Boolean operations on polygons are called "clipping" operations. For 2D geometry, these—`union2d`, `difference2d`, and `intersection2d`—are the only geometric Booleans available. Other geometry types are first converted to polygons using `to_polygons` to perform clipping. +Geometric Boolean operations on polygons are called "clipping" operations. For 2D geometry, these—`union2d`, `difference2d`, `intersection2d`, and `xor2d`—are the only geometric Booleans available. Other geometry types are first converted to polygons using `to_polygons` to perform clipping. !!! info @@ -27,6 +27,7 @@ More general operations may be accomplished using the `clip` function. union2d difference2d intersect2d + xor2d clip cliptree ``` diff --git a/src/DeviceLayout.jl b/src/DeviceLayout.jl index d1234b05..34f91086 100644 --- a/src/DeviceLayout.jl +++ b/src/DeviceLayout.jl @@ -385,7 +385,8 @@ import .Polygons: rounded_corner, sweep_poly, unfold, - union2d + union2d, + xor2d export Polygons, Polygon, Ellipse, @@ -409,7 +410,8 @@ export Polygons, rounded_corner, sweep_poly, unfold, - union2d + union2d, + xor2d include("align.jl") using .Align diff --git a/src/polygons.jl b/src/polygons.jl index 1d9c530c..3dcfd0f4 100644 --- a/src/polygons.jl +++ b/src/polygons.jl @@ -80,7 +80,8 @@ export circle, rounded_corner, sweep_poly, unfold, - union2d + union2d, + xor2d const USCALE = 1.0 * Unitful.fm const SCALE = 10.0^9 @@ -1156,6 +1157,34 @@ function intersect2d(plus, minus) ) end +""" + xor2d(p1, p2) + +Return the symmetric difference (XOR) of `p1` and `p2` as a `ClippedPolygon`. + +The XOR operation returns regions that are in either `p1` or `p2`, but not in both. +This is useful for finding non-overlapping regions between two sets of polygons. + +Each of `p1` and `p2` may be a `GeometryEntity` or array of `GeometryEntity`. All entities +are first converted to polygons using [`to_polygons`](@ref). + +Each of `p1` and `p2` can also be a `GeometryStructure` or `GeometryReference`, in which case +`elements(flatten(p))` will be converted to polygons. + +Each can also be a pair `geom => layer`, where `geom` is a +`GeometryStructure` or `GeometryReference`, while `layer` is a `DeviceLayout.Meta`, a layer name `Symbol`, and/or a collection +of either, in which case only the elements in those layers will be used. +""" +function xor2d(p1, p2) + return clip( + Clipper.ClipTypeXor, + p1, + p2, + pfs=Clipper.PolyFillTypePositive, + pfc=Clipper.PolyFillTypePositive + ) +end + function add_path!( c::Clipper.Clip, path::Vector{Point{T}}, diff --git a/test/test_clipping.jl b/test/test_clipping.jl index e3e80c83..4fa4f77a 100644 --- a/test/test_clipping.jl +++ b/test/test_clipping.jl @@ -114,6 +114,11 @@ @test to_polygons( union2d(Rectangle(2, 2) + Point(1, 1), [Rectangle(2, 2), Rectangle(10, 10)]) )[1] == Polygon(Point{Int}[(10, 10), (0, 10), (0, 0), (10, 0)]) + + # XOR + r1 = Rectangle(2.0, 2.0) + r2 = Rectangle(2.0, 2.0) + Point(1.0, 1.0) + @test xor2d(r1, r2) == union2d(difference2d(r2, r1), difference2d(r1, r2)) end @testset "> ClippedPolygon operations w/o units" begin