diff --git a/Cargo.lock b/Cargo.lock index 0c89d6f..0fe1b9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,8 +84,6 @@ dependencies = [ [[package]] name = "clipper2c-sys" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2995248c63ec82bf22c921cfd26fe407a4402b7039e045d286f7470cd83fe9e" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 946a566..d73aab5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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" } diff --git a/src/clipper.rs b/src/clipper.rs index d6f317b..f52ca13 100644 --- a/src/clipper.rs +++ b/src/clipper.rs @@ -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 {} @@ -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 { ptr: *mut ClipperClipper64, keep_ptr_on_drop: bool, _marker: PhantomData

, _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 std::fmt::Debug + for Clipper +{ + 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 Clipper { @@ -48,6 +66,9 @@ impl Clipper { keep_ptr_on_drop: false, _marker: PhantomData, _state: NoSubjects {}, + + #[cfg(feature = "usingz")] + raw_z_callback: None, } } } @@ -72,6 +93,9 @@ impl Clipper { keep_ptr_on_drop: false, _marker: PhantomData, _state: WithSubjects {}, + + #[cfg(feature = "usingz")] + raw_z_callback: None, }; drop(self); @@ -98,12 +122,84 @@ impl Clipper { 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::::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

, Point

, Point

, Point

, &mut 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, Point

, Point

, Point

, &mut Point

)>> = + 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::

)); + } + } +} + +extern "C" fn handle_set_z_callback( + 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

, Point

, Point

, Point

, &mut Point

) = + unsafe { std::mem::transmute(user_data) }; + + // SAFETY: Clipper2 should produce valid pointers + let mut new_point = unsafe { Point::

::from(*pt) }; + let e1bot = unsafe { Point::

::from(*e1bot) }; + let e1top = unsafe { Point::

::from(*e1top) }; + let e2bot = unsafe { Point::

::from(*e2bot) }; + let e2top = unsafe { Point::

::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 Clipper { @@ -171,6 +267,9 @@ impl Clipper { keep_ptr_on_drop: false, _marker: PhantomData, _state: WithClips {}, + + #[cfg(feature = "usingz")] + raw_z_callback: None, }; drop(self); @@ -321,6 +420,13 @@ impl Drop for Clipper { 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 +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::::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); + } +} diff --git a/src/path.rs b/src/path.rs index c659d0e..13aa847 100644 --- a/src/path.rs +++ b/src/path.rs @@ -322,6 +322,7 @@ impl Path

{ .map(|point: Point

| ClipperPoint64 { x: point.x_scaled(), y: point.y_scaled(), + ..Default::default() }) .collect::>() .as_mut_ptr(), diff --git a/src/point.rs b/src/point.rs index 747b647..266d218 100644 --- a/src/point.rs +++ b/src/point.rs @@ -97,9 +97,15 @@ pub struct Point( ); impl Point

{ + #[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 { @@ -109,6 +115,18 @@ impl Point

{ 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 { @@ -118,12 +136,24 @@ impl Point

{ 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, ) @@ -132,7 +162,14 @@ impl Point

{ /// 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 +195,18 @@ impl Point

{ 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 Default for Point

{