Skip to content

Commit

Permalink
Support user data with USINGZ
Browse files Browse the repository at this point in the history
  • Loading branch information
msvbg committed Aug 28, 2024
1 parent 8e4c86f commit ae75b6a
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 7 deletions.
2 changes: 0 additions & 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 = { path = "../clipper2c-sys" }
158 changes: 154 additions & 4 deletions src/clipper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ 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_clipper64_add_subject, clipper_clipper64_execute, clipper_clipper64_set_z_callback,
clipper_clipper64_size, 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 @@ -27,12 +28,29 @@ pub struct WithClips {}
impl ClipperState for WithClips {}

/// The Clipper struct used as a builder for applying boolean operations to paths.
#[derive(Debug)]
pub struct Clipper<S: ClipperState = NoSubjects, P: PointScaler = Centi> {
ptr: *mut ClipperClipper64,
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<S: ClipperState + std::fmt::Debug, P: PointScaler + std::fmt::Debug> std::fmt::Debug
for Clipper<S, P>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Clipper")
.field("ptr", &self.ptr)
.field("keep_ptr_on_drop", &self.keep_ptr_on_drop)
.field("_marker", &self._marker)
.field("_state", &self._state)
.field("raw_z_callback", &self.raw_z_callback.is_some())
.finish()
}
}

impl<P: PointScaler> Clipper<NoSubjects, P> {
Expand All @@ -48,6 +66,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 +93,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 +122,84 @@ 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 {
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 +267,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 +420,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 +438,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

0 comments on commit ae75b6a

Please sign in to comment.