Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ categories = ["algorithms"]
default = ["doc-images"]
doc-images = []
serde = ["dep:serde", "clipper2c-sys/serde"]
usingz = ["clipper2c-sys/usingz"]

[dependencies]
libc = "0.2"
Expand All @@ -31,3 +32,6 @@ serde_json = "1"
# docs.rs uses a nightly compiler, so by instructing it to use our `doc-images` feature we
# ensure that it will render any images that we may have in inner attribute documentation.
features = ["doc-images"]

[patch.crates-io]
clipper2c-sys = { rev = "210878394637f94bcd3a476bdf24ddf4ba8a39fd", git = "https://github.com/tirithen/clipper2c-sys" }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: fix this before merging

144 changes: 142 additions & 2 deletions src/clipper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ use std::marker::PhantomData;
use clipper2c_sys::{
clipper_clipper64, clipper_clipper64_add_clip, clipper_clipper64_add_open_subject,
clipper_clipper64_add_subject, clipper_clipper64_execute, clipper_clipper64_size,
clipper_delete_clipper64, clipper_delete_paths64, ClipperClipper64,
clipper_delete_clipper64, clipper_delete_paths64, ClipperClipper64, ClipperPoint64,
};

use crate::{malloc, Centi, ClipType, FillRule, Paths, PointScaler};
use crate::{malloc, Centi, ClipType, FillRule, Paths, Point, PointScaler};

/// The state of the Clipper struct.
pub trait ClipperState {}
Expand All @@ -33,6 +33,10 @@ pub struct Clipper<S: ClipperState = NoSubjects, P: PointScaler = Centi> {
keep_ptr_on_drop: bool,
_marker: PhantomData<P>,
_state: S,

/// We only hold on to this in order to avoid leaking memory when Clipper is dropped
#[cfg(feature = "usingz")]
raw_z_callback: Option<*mut libc::c_void>,
}

impl<P: PointScaler> Clipper<NoSubjects, P> {
Expand All @@ -48,6 +52,9 @@ impl<P: PointScaler> Clipper<NoSubjects, P> {
keep_ptr_on_drop: false,
_marker: PhantomData,
_state: NoSubjects {},

#[cfg(feature = "usingz")]
raw_z_callback: None,
}
}
}
Expand All @@ -72,6 +79,9 @@ impl<P: PointScaler> Clipper<NoSubjects, P> {
keep_ptr_on_drop: false,
_marker: PhantomData,
_state: WithSubjects {},

#[cfg(feature = "usingz")]
raw_z_callback: None,
};

drop(self);
Expand All @@ -98,12 +108,88 @@ impl<P: PointScaler> Clipper<NoSubjects, P> {
keep_ptr_on_drop: false,
_marker: PhantomData,
_state: WithSubjects {},

#[cfg(feature = "usingz")]
raw_z_callback: None,
};

drop(self);

clipper.add_open_subject(subject)
}

#[cfg(feature = "usingz")]
/// Allows specifying a callback that will be called each time a new vertex
/// is created by Clipper, in order to set user data on such points. New
/// points are created at the intersections between two edges, and the
/// callback will be called with the four neighboring points from those two
/// edges. The last argument is the new point itself.
///
/// # Examples
///
/// ```rust
/// use clipper2::*;
///
/// let mut clipper = Clipper::<NoSubjects, Centi>::new();
/// clipper.set_z_callback(|_: Point<_>, _: Point<_>, _: Point<_>, _: Point<_>, p: &mut Point<_>| {
/// p.set_z(1);
/// });
/// ```
pub fn set_z_callback(
&mut self,
callback: impl Fn(Point<P>, Point<P>, Point<P>, Point<P>, &mut Point<P>),
) {
// The closure will be represented by a trait object behind a fat
// pointer. Since fat pointers are larger than thin pointers, they
// cannot be passed through a thin-pointer c_void type. We must
// therefore wrap the fat pointer in another box, leading to this double
// indirection.
let cb: Box<Box<dyn Fn(Point<P>, Point<P>, Point<P>, Point<P>, &mut Point<P>)>> =
Box::new(Box::new(callback));
let raw_cb = Box::into_raw(cb) as *mut _;

// It there is an old callback stored, drop it before replacing it
if let Some(old_raw_cb) = self.raw_z_callback {
drop(unsafe { Box::from_raw(old_raw_cb as *mut _) });
}
self.raw_z_callback = Some(raw_cb);

unsafe {
clipper2c_sys::clipper_clipper64_set_z_callback(
self.ptr,
raw_cb,
Some(handle_set_z_callback::<P>),
);
}
}
}

extern "C" fn handle_set_z_callback<P: PointScaler>(
user_data: *mut ::std::os::raw::c_void,
e1bot: *const ClipperPoint64,
e1top: *const ClipperPoint64,
e2bot: *const ClipperPoint64,
e2top: *const ClipperPoint64,
pt: *mut ClipperPoint64,
) {
// SAFETY: user_data was set in set_z_callback, and is valid for as long as
// the Clipper2 instance exists.
let callback: &mut &mut dyn Fn(Point<P>, Point<P>, Point<P>, Point<P>, &mut Point<P>) =
unsafe { std::mem::transmute(user_data) };

// SAFETY: Clipper2 should produce valid pointers
let mut new_point = unsafe { Point::<P>::from(*pt) };
let e1bot = unsafe { Point::<P>::from(*e1bot) };
let e1top = unsafe { Point::<P>::from(*e1top) };
let e2bot = unsafe { Point::<P>::from(*e2bot) };
let e2top = unsafe { Point::<P>::from(*e2top) };

callback(e1bot, e1top, e2bot, e2top, &mut new_point);

// SAFETY: pt is a valid pointer and new_point is not null
unsafe {
*pt = *new_point.as_clipperpoint64();
}
}

impl<P: PointScaler> Clipper<WithSubjects, P> {
Expand Down Expand Up @@ -171,6 +257,9 @@ impl<P: PointScaler> Clipper<WithSubjects, P> {
keep_ptr_on_drop: false,
_marker: PhantomData,
_state: WithClips {},

#[cfg(feature = "usingz")]
raw_z_callback: None,
};

drop(self);
Expand Down Expand Up @@ -321,6 +410,13 @@ impl<S: ClipperState, P: PointScaler> Drop for Clipper<S, P> {
fn drop(&mut self) {
if !self.keep_ptr_on_drop {
unsafe { clipper_delete_clipper64(self.ptr) }

#[cfg(feature = "usingz")]
{
if let Some(raw_cb) = self.raw_z_callback {
drop(unsafe { Box::from_raw(raw_cb as *mut _) });
}
}
}
}
}
Expand All @@ -332,3 +428,47 @@ pub enum ClipperError {
#[error("Failed boolean operation")]
FailedBooleanOperation,
}

#[cfg(test)]
mod test {
use std::{cell::Cell, rc::Rc};

use super::*;

#[cfg(feature = "usingz")]
#[test]
fn test_set_z_callback() {
let mut clipper = Clipper::<NoSubjects, Centi>::new();
let success = Rc::new(Cell::new(false));
{
let success = success.clone();
clipper.set_z_callback(
move |_e1bot: Point<_>,
_e1top: Point<_>,
_e2bot: Point<_>,
_e2top: Point<_>,
p: &mut Point<_>| {
p.set_z(1);
success.set(true);
},
);
}
let e1: Paths = vec![(0.0, 0.0), (2.0, 2.0), (0.0, 2.0)].into();
let e2: Paths = vec![(1.0, 0.0), (2.0, 0.0), (1.0, 2.0)].into();
let paths = clipper
.add_subject(e1)
.add_clip(e2)
.union(FillRule::default())
.unwrap();

assert_eq!(success.get(), true);

let n_intersecting = paths
.iter()
.flatten()
.into_iter()
.filter(|v| v.z() == 1)
.count();
assert_eq!(n_intersecting, 3);
}
}
1 change: 1 addition & 0 deletions src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ impl<P: PointScaler> Path<P> {
.map(|point: Point<P>| ClipperPoint64 {
x: point.x_scaled(),
y: point.y_scaled(),
..Default::default()
})
.collect::<Vec<_>>()
.as_mut_ptr(),
Expand Down
51 changes: 50 additions & 1 deletion src/point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,15 @@ pub struct Point<P: PointScaler = Centi>(
);

impl<P: PointScaler> Point<P> {
#[cfg(not(feature = "usingz"))]
/// The zero point.
pub const ZERO: Self = Self(ClipperPoint64 { x: 0, y: 0 }, PhantomData);

#[cfg(feature = "usingz")]
/// The zero point.
pub const ZERO: Self = Self(ClipperPoint64 { x: 0, y: 0, z: 0 }, PhantomData);

#[cfg(not(feature = "usingz"))]
/// The minimum value for a point.
pub const MIN: Self = Self(
ClipperPoint64 {
Expand All @@ -109,6 +115,18 @@ impl<P: PointScaler> Point<P> {
PhantomData,
);

#[cfg(feature = "usingz")]
/// The minimum value for a point.
pub const MIN: Self = Self(
ClipperPoint64 {
x: i64::MIN,
y: i64::MIN,
z: 0,
},
PhantomData,
);

#[cfg(not(feature = "usingz"))]
/// The maximum value for a point.
pub const MAX: Self = Self(
ClipperPoint64 {
Expand All @@ -118,12 +136,24 @@ impl<P: PointScaler> Point<P> {
PhantomData,
);

#[cfg(feature = "usingz")]
/// The maximum value for a point.
pub const MAX: Self = Self(
ClipperPoint64 {
x: i64::MAX,
y: i64::MAX,
z: 0,
},
PhantomData,
);

/// Create a new point.
pub fn new(x: f64, y: f64) -> Self {
Self(
ClipperPoint64 {
x: P::scale(x) as i64,
y: P::scale(y) as i64,
..Default::default()
},
PhantomData,
)
Expand All @@ -132,7 +162,14 @@ impl<P: PointScaler> Point<P> {
/// Create a new point from scaled values, this means that point is
/// constructed as is without applying the scaling multiplier.
pub fn from_scaled(x: i64, y: i64) -> Self {
Self(ClipperPoint64 { x, y }, PhantomData)
Self(
ClipperPoint64 {
x,
y,
..Default::default()
},
PhantomData,
)
}

/// Returns the x coordinate of the point.
Expand All @@ -158,6 +195,18 @@ impl<P: PointScaler> Point<P> {
pub(crate) fn as_clipperpoint64(&self) -> *const ClipperPoint64 {
&self.0
}

#[cfg(feature = "usingz")]
/// Returns the user data of the point.
pub fn z(&self) -> i64 {
self.0.z
}

#[cfg(feature = "usingz")]
/// Sets the user data of the point.
pub fn set_z(&mut self, z: i64) {
self.0.z = z;
}
}

impl<P: PointScaler> Default for Point<P> {
Expand Down