Skip to content

Support user data with USINGZ #11

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
@@ -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 }

@@ -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
@@ -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> {
@@ -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,
}
}
}
@@ -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);
@@ -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> {
@@ -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);
@@ -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 _) });
}
}
}
}
}
@@ -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
@@ -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(),
@@ -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)]);
2 changes: 1 addition & 1 deletion src/paths.rs
Original file line number Diff line number Diff line change
@@ -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)]);
60 changes: 58 additions & 2 deletions src/point.rs
Original file line number Diff line number Diff line change
@@ -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,
);
@@ -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,
)
@@ -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.
@@ -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> {
@@ -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);