Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user data with USINGZ #11

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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.

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

[dependencies]
libc = "0.2"
clipper2c-sys = "0.1.4"
clipper2c-sys = { rev = "210878394637f94bcd3a476bdf24ddf4ba8a39fd", git = "https://github.com/tirithen/clipper2c-sys" }
thiserror = "1.0.59"
serde = { version = "1", features = ["derive"], optional = true }

Expand All @@ -31,3 +32,5 @@ 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]
152 changes: 152 additions & 0 deletions src/clipper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,99 @@ 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 = Point::new_with_z(p.x(), p.y(), 1);
/// });
/// ```
pub fn set_z_callback(
&mut self,
callback: impl Fn(
crate::Point<P>,
crate::Point<P>,
crate::Point<P>,
crate::Point<P>,
&mut crate::Point<P>,
),
) {
use crate::Point;

// 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>),
);
}
}
}

#[cfg(feature = "usingz")]
extern "C" fn handle_set_z_callback<P: PointScaler>(
user_data: *mut ::std::os::raw::c_void,
e1bot: *const clipper2c_sys::ClipperPoint64,
e1top: *const clipper2c_sys::ClipperPoint64,
e2bot: *const clipper2c_sys::ClipperPoint64,
e2top: *const clipper2c_sys::ClipperPoint64,
pt: *mut clipper2c_sys::ClipperPoint64,
) {
use crate::Point;

// 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 crate::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 +268,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 +421,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 +439,48 @@ pub enum ClipperError {
#[error("Failed boolean operation")]
FailedBooleanOperation,
}

#[cfg(test)]
mod test {
#[cfg(feature = "usingz")]
#[test]
fn test_set_z_callback() {
use std::{cell::Cell, rc::Rc};

use super::*;
use crate::Point;

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 = Point::new_with_z(p.x(), p.y(), 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);
}
}
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -472,7 +473,7 @@ mod test {
assert_eq!(area, -1200.0);
}

#[cfg(feature = "serde")]
#[cfg(all(feature = "serde", not(feature = "usingz")))]
#[test]
fn test_serde() {
let path = Path::<Centi>::from(vec![(0.0, 0.0), (1.0, 1.0)]);
Expand Down
2 changes: 1 addition & 1 deletion src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ mod test {
assert_eq!(paths.len(), 2);
}

#[cfg(feature = "serde")]
#[cfg(all(feature = "serde", not(feature = "usingz")))]
#[test]
fn test_serde() {
let paths = Paths::<Centi>::from(vec![(0.4, 0.0), (5.0, 1.0)]);
Expand Down
60 changes: 58 additions & 2 deletions src/point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,23 +97,52 @@ 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 {
x: i64::MIN,
y: i64::MIN,
},
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 {
x: i64::MAX,
y: i64::MAX,
},
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,
);
Expand All @@ -124,6 +153,20 @@ impl<P: PointScaler> Point<P> {
ClipperPoint64 {
x: P::scale(x) as i64,
y: P::scale(y) as i64,
..Default::default()
},
PhantomData,
)
}

#[cfg(feature = "usingz")]
/// Create a new point with user data.
pub fn new_with_z(x: f64, y: f64, z: i64) -> Self {
Self(
ClipperPoint64 {
x: P::scale(x) as i64,
y: P::scale(y) as i64,
z,
},
PhantomData,
)
Expand All @@ -132,7 +175,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 +208,12 @@ 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
}
}

impl<P: PointScaler> Default for Point<P> {
Expand Down Expand Up @@ -243,7 +299,7 @@ mod test {
assert_eq!(point.y_scaled(), 4000);
}

#[cfg(feature = "serde")]
#[cfg(all(feature = "serde", not(feature = "usingz")))]
#[test]
fn test_serde() {
let point = Point::<Centi>::new(1.0, 2.0);
Expand Down