From a74f3ad1bbc3bf9124909a9b5cad1d0d6e79729d Mon Sep 17 00:00:00 2001 From: Ben-PH Date: Wed, 15 Apr 2026 22:38:56 +0200 Subject: [PATCH 1/4] Tdf first tests --- crates/opencascade-sys/include/wrapper.hxx | 20 +++++++ crates/opencascade-sys/src/lib.rs | 9 +++ crates/opencascade/src/lib.rs | 1 + crates/opencascade/src/tdf.rs | 69 ++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 crates/opencascade/src/tdf.rs diff --git a/crates/opencascade-sys/include/wrapper.hxx b/crates/opencascade-sys/include/wrapper.hxx index 01f462b66..5912b166b 100644 --- a/crates/opencascade-sys/include/wrapper.hxx +++ b/crates/opencascade-sys/include/wrapper.hxx @@ -79,6 +79,8 @@ #include #include #include +#include +#include #include #include #include @@ -88,6 +90,24 @@ #include #include +// BEGIN Tdf stuff +inline std::unique_ptr TDF_Data_new() { + return std::unique_ptr(new TDF_Data()); +} + +inline std::unique_ptr TDF_Data_root(const TDF_Data& data) { + return std::unique_ptr(new TDF_Label(data.Root())); +} + +inline std::unique_ptr TDF_Label_new_child(const TDF_Label& label) { + return std::unique_ptr(new TDF_Label(label.NewChild())); +} + +inline bool TDF_Label_is_null(const TDF_Label& label) { + return label.IsNull(); +} +// END Tdf stuff + // Generic template constructor template std::unique_ptr construct_unique(Args... args) { return std::unique_ptr(new T(args...)); diff --git a/crates/opencascade-sys/src/lib.rs b/crates/opencascade-sys/src/lib.rs index 77ca10d70..11f084021 100644 --- a/crates/opencascade-sys/src/lib.rs +++ b/crates/opencascade-sys/src/lib.rs @@ -74,6 +74,15 @@ pub mod ffi { #[cxx_name = "construct_unique"] pub fn Message_ProgressRange_ctor() -> UniquePtr; + // TDF_Data management + type TDF_Data; + type TDF_Label; + + pub fn TDF_Data_new() -> UniquePtr; + pub fn TDF_Data_root(data: &TDF_Data) -> UniquePtr; + pub fn TDF_Label_new_child(label: &TDF_Label) -> UniquePtr; + pub fn TDF_Label_is_null(label: &TDF_Label) -> bool; + // Handles type HandleStandardType; type HandleGeomCurve; diff --git a/crates/opencascade/src/lib.rs b/crates/opencascade/src/lib.rs index 9c7bb68a5..54f57a699 100644 --- a/crates/opencascade/src/lib.rs +++ b/crates/opencascade/src/lib.rs @@ -10,6 +10,7 @@ pub mod workplane; mod law_function; mod make_pipe_shell; +pub mod tdf; #[derive(Error, Debug)] pub enum Error { diff --git a/crates/opencascade/src/tdf.rs b/crates/opencascade/src/tdf.rs new file mode 100644 index 000000000..9457f8051 --- /dev/null +++ b/crates/opencascade/src/tdf.rs @@ -0,0 +1,69 @@ +use cxx::UniquePtr; +use opencascade_sys::ffi; +use std::marker::PhantomData; + +/// ```compile_fail +/// use opencascade::tdf::TdfData; +/// let root = { +/// let doc = TdfData::new(); +/// doc.root() +/// }; +/// ``` +pub struct TdfData { + inner: UniquePtr, +} + + +impl TdfData { + pub fn new() -> Self { + Self { + inner: ffi::TDF_Data_new(), + } + } + + pub fn root(&self) -> TdfLabel<'_> { + TdfLabel { + inner: ffi::TDF_Data_root(self.inner.as_ref().unwrap()), + _phantom: PhantomData, + } + } +} + +pub struct TdfLabel<'doc> { + inner: UniquePtr, + _phantom: PhantomData<&'doc TdfData>, +} + +impl<'doc> TdfLabel<'doc> { + pub fn new_child(&self) -> TdfLabel<'doc> { + TdfLabel { + inner: ffi::TDF_Label_new_child(self.inner.as_ref().unwrap()), + _phantom: PhantomData, + } + } + + pub fn is_null(&self) -> bool { + ffi::TDF_Label_is_null(self.inner.as_ref().unwrap()) + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tdf_data_root_is_not_null() { + let doc = TdfData::new(); + let root = doc.root(); + assert!(!root.is_null()); + } + + #[test] + fn test_new_child_is_not_null() { + let doc = TdfData::new(); + let root = doc.root(); + // let child = root.new_child(); + assert!(!root.is_null()); + // drop(child); + } + +} From 148e33ee4fc30bae7bf307b7f54cd1ec4ee27e43 Mon Sep 17 00:00:00 2001 From: Ben-PH Date: Thu, 16 Apr 2026 00:01:09 +0200 Subject: [PATCH 2/4] fix: HandleTdfData instead of TDF_Data --- crates/opencascade-sys/include/wrapper.hxx | 12 ++++++++---- crates/opencascade-sys/src/lib.rs | 6 +++--- crates/opencascade/src/tdf.rs | 18 ++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/crates/opencascade-sys/include/wrapper.hxx b/crates/opencascade-sys/include/wrapper.hxx index 5912b166b..dc8d46342 100644 --- a/crates/opencascade-sys/include/wrapper.hxx +++ b/crates/opencascade-sys/include/wrapper.hxx @@ -81,6 +81,7 @@ #include #include #include +#include #include #include #include @@ -91,12 +92,15 @@ #include // BEGIN Tdf stuff -inline std::unique_ptr TDF_Data_new() { - return std::unique_ptr(new TDF_Data()); +typedef opencascade::handle HandleTdfData; +typedef opencascade::handle HandleTdfDelta; + +inline std::unique_ptr TDF_Data_new() { + return std::unique_ptr(new opencascade::handle(new TDF_Data())); } -inline std::unique_ptr TDF_Data_root(const TDF_Data& data) { - return std::unique_ptr(new TDF_Label(data.Root())); +inline std::unique_ptr TDF_Data_root(const HandleTdfData& data) { + return std::unique_ptr(new TDF_Label(data->Root())); } inline std::unique_ptr TDF_Label_new_child(const TDF_Label& label) { diff --git a/crates/opencascade-sys/src/lib.rs b/crates/opencascade-sys/src/lib.rs index 11f084021..498ba7e98 100644 --- a/crates/opencascade-sys/src/lib.rs +++ b/crates/opencascade-sys/src/lib.rs @@ -75,11 +75,11 @@ pub mod ffi { pub fn Message_ProgressRange_ctor() -> UniquePtr; // TDF_Data management - type TDF_Data; + type HandleTdfData; type TDF_Label; - pub fn TDF_Data_new() -> UniquePtr; - pub fn TDF_Data_root(data: &TDF_Data) -> UniquePtr; + pub fn TDF_Data_new() -> UniquePtr; + pub fn TDF_Data_root(data: &HandleTdfData) -> UniquePtr; pub fn TDF_Label_new_child(label: &TDF_Label) -> UniquePtr; pub fn TDF_Label_is_null(label: &TDF_Label) -> bool; diff --git a/crates/opencascade/src/tdf.rs b/crates/opencascade/src/tdf.rs index 9457f8051..344e96615 100644 --- a/crates/opencascade/src/tdf.rs +++ b/crates/opencascade/src/tdf.rs @@ -10,22 +10,16 @@ use std::marker::PhantomData; /// }; /// ``` pub struct TdfData { - inner: UniquePtr, + inner: UniquePtr, } - impl TdfData { pub fn new() -> Self { - Self { - inner: ffi::TDF_Data_new(), - } + Self { inner: ffi::TDF_Data_new() } } pub fn root(&self) -> TdfLabel<'_> { - TdfLabel { - inner: ffi::TDF_Data_root(self.inner.as_ref().unwrap()), - _phantom: PhantomData, - } + TdfLabel { inner: ffi::TDF_Data_root(self.inner.as_ref().unwrap()), _phantom: PhantomData } } } @@ -50,6 +44,11 @@ impl<'doc> TdfLabel<'doc> { mod tests { use super::*; + #[test] + fn test_tdf_data_constructs() { + let doc = TdfData::new(); + drop(doc); + } #[test] fn test_tdf_data_root_is_not_null() { let doc = TdfData::new(); @@ -65,5 +64,4 @@ mod tests { assert!(!root.is_null()); // drop(child); } - } From 67da19566550cffc62768145183431ed6c8174fb Mon Sep 17 00:00:00 2001 From: Ben-PH Date: Thu, 16 Apr 2026 00:10:19 +0200 Subject: [PATCH 3/4] add: TdfTransaction --- crates/opencascade-sys/include/wrapper.hxx | 20 +++++++++++ crates/opencascade-sys/src/lib.rs | 7 ++++ crates/opencascade/src/tdf.rs | 40 ++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/crates/opencascade-sys/include/wrapper.hxx b/crates/opencascade-sys/include/wrapper.hxx index dc8d46342..c9d3ddba7 100644 --- a/crates/opencascade-sys/include/wrapper.hxx +++ b/crates/opencascade-sys/include/wrapper.hxx @@ -82,6 +82,7 @@ #include #include #include +#include #include #include #include @@ -110,6 +111,25 @@ inline std::unique_ptr TDF_Label_new_child(const TDF_Label& label) { inline bool TDF_Label_is_null(const TDF_Label& label) { return label.IsNull(); } +inline std::unique_ptr TDF_Transaction_new(const HandleTdfData& data) { + return std::unique_ptr(new TDF_Transaction(data)); +} + +inline Standard_Integer TDF_Transaction_open(TDF_Transaction& transaction) { + return transaction.Open(); +} + +inline std::unique_ptr TDF_Transaction_commit(TDF_Transaction& transaction) { + return std::unique_ptr(new opencascade::handle(transaction.Commit(true))); +} + +inline void TDF_Transaction_abort(TDF_Transaction& transaction) { + transaction.Abort(); +} + +inline bool TDF_Transaction_is_open(const TDF_Transaction& transaction) { + return transaction.IsOpen(); +} // END Tdf stuff // Generic template constructor diff --git a/crates/opencascade-sys/src/lib.rs b/crates/opencascade-sys/src/lib.rs index 498ba7e98..1acecdfae 100644 --- a/crates/opencascade-sys/src/lib.rs +++ b/crates/opencascade-sys/src/lib.rs @@ -77,11 +77,18 @@ pub mod ffi { // TDF_Data management type HandleTdfData; type TDF_Label; + type TDF_Transaction; + type HandleTdfDelta; pub fn TDF_Data_new() -> UniquePtr; pub fn TDF_Data_root(data: &HandleTdfData) -> UniquePtr; pub fn TDF_Label_new_child(label: &TDF_Label) -> UniquePtr; pub fn TDF_Label_is_null(label: &TDF_Label) -> bool; + pub fn TDF_Transaction_new(data: &HandleTdfData) -> UniquePtr; + pub fn TDF_Transaction_open(transaction: Pin<&mut TDF_Transaction>) -> i32; + pub fn TDF_Transaction_commit(transaction: Pin<&mut TDF_Transaction>) -> UniquePtr; + pub fn TDF_Transaction_abort(transaction: Pin<&mut TDF_Transaction>); + pub fn TDF_Transaction_is_open(transaction: &TDF_Transaction) -> bool; // Handles type HandleStandardType; diff --git a/crates/opencascade/src/tdf.rs b/crates/opencascade/src/tdf.rs index 344e96615..07077a6ee 100644 --- a/crates/opencascade/src/tdf.rs +++ b/crates/opencascade/src/tdf.rs @@ -21,6 +21,12 @@ impl TdfData { pub fn root(&self) -> TdfLabel<'_> { TdfLabel { inner: ffi::TDF_Data_root(self.inner.as_ref().unwrap()), _phantom: PhantomData } } + pub fn transaction(&self) -> TdfTransaction<'_> { + TdfTransaction { + inner: ffi::TDF_Transaction_new(self.inner.as_ref().unwrap()), + _phantom: PhantomData, + } + } } pub struct TdfLabel<'doc> { @@ -40,6 +46,30 @@ impl<'doc> TdfLabel<'doc> { ffi::TDF_Label_is_null(self.inner.as_ref().unwrap()) } } +pub struct TdfTransaction<'doc> { + inner: UniquePtr, + _phantom: PhantomData<&'doc TdfData>, +} + +impl<'doc> TdfTransaction<'doc> { + pub fn open(&mut self) -> i32 { + ffi::TDF_Transaction_open(self.inner.pin_mut()) + } + + // TODO: wrap HandleTdfDelta in a safe TdfDelta type before contributing upstream. + // Currently leaks the raw FFI type into the safe wrapper layer. + pub fn commit(mut self) -> UniquePtr { + ffi::TDF_Transaction_commit(self.inner.pin_mut()) + } + + pub fn abort(mut self) { + ffi::TDF_Transaction_abort(self.inner.pin_mut()) + } + + pub fn is_open(&self) -> bool { + ffi::TDF_Transaction_is_open(self.inner.as_ref().unwrap()) + } +} #[cfg(test)] mod tests { use super::*; @@ -64,4 +94,14 @@ mod tests { assert!(!root.is_null()); // drop(child); } + #[test] + fn test_new_child_with_transaction() { + let doc = TdfData::new(); + let mut tx = doc.transaction(); + tx.open(); + let root = doc.root(); + let child = root.new_child(); + assert!(!child.is_null()); + tx.commit(); + } } From 2800a8e29c99ddb5b7454c538ba2850bcbda7711 Mon Sep 17 00:00:00 2001 From: Ben-PH Date: Thu, 16 Apr 2026 14:38:03 +0200 Subject: [PATCH 4/4] feat: Surfacing of TdfData api and some of its friends --- crates/opencascade-sys/include/wrapper.hxx | 62 ++++ crates/opencascade-sys/src/lib.rs | 46 ++- crates/opencascade/src/primitives/shape.rs | 8 + crates/opencascade/src/tdf.rs | 312 +++++++++++++++++++-- flake.nix | 93 ++++++ 5 files changed, 501 insertions(+), 20 deletions(-) create mode 100644 flake.nix diff --git a/crates/opencascade-sys/include/wrapper.hxx b/crates/opencascade-sys/include/wrapper.hxx index c9d3ddba7..6c0715705 100644 --- a/crates/opencascade-sys/include/wrapper.hxx +++ b/crates/opencascade-sys/include/wrapper.hxx @@ -83,6 +83,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -130,6 +133,65 @@ inline void TDF_Transaction_abort(TDF_Transaction& transaction) { inline bool TDF_Transaction_is_open(const TDF_Transaction& transaction) { return transaction.IsOpen(); } +inline std::unique_ptr TDF_Data_undo( + HandleTdfData& data, + const HandleTdfDelta& delta +) { + return std::unique_ptr( + new HandleTdfDelta(data->Undo(delta, Standard_True)) + ); +} + +inline std::unique_ptr TNaming_NamedShape_Get( + const Handle_TNaming_NamedShape& ns +) { + return std::unique_ptr(new TopoDS_Shape(ns->Get())); +} + +// BEGIN TNaming section of Tdf stuff + + + +inline std::unique_ptr TNaming_Builder_ctor(const TDF_Label& label) { + return std::unique_ptr(new TNaming_Builder(label)); +} + +inline void TNaming_Builder_generated(TNaming_Builder& builder, const TopoDS_Shape& newShape) { + builder.Generated(newShape); +} + +inline void TNaming_Builder_generated_with_old(TNaming_Builder& builder, const TopoDS_Shape& oldShape, const TopoDS_Shape& newShape) { + builder.Generated(oldShape, newShape); +} + +inline void TNaming_Builder_modify(TNaming_Builder& builder, const TopoDS_Shape& oldShape, const TopoDS_Shape& newShape) { + builder.Modify(oldShape, newShape); +} + +inline void TNaming_Builder_delete(TNaming_Builder& builder, const TopoDS_Shape& oldShape) { + builder.Delete(oldShape); +} + +inline void TNaming_Builder_select(TNaming_Builder& builder, const TopoDS_Shape& shape, const TopoDS_Shape& inShape) { + builder.Select(shape, inShape); +} + +inline std::unique_ptr TNaming_Builder_named_shape( + const TNaming_Builder& builder +) { + return std::unique_ptr( + new Handle_TNaming_NamedShape(builder.NamedShape()) + ); +} + +inline std::unique_ptr TNaming_Tool_original_shape( + const Handle_TNaming_NamedShape& ns +) { + return std::unique_ptr( + new TopoDS_Shape(TNaming_Tool::OriginalShape(ns)) + ); +} +// END TNaming section of Tdf stuff // END Tdf stuff // Generic template constructor diff --git a/crates/opencascade-sys/src/lib.rs b/crates/opencascade-sys/src/lib.rs index 1acecdfae..f95af9d83 100644 --- a/crates/opencascade-sys/src/lib.rs +++ b/crates/opencascade-sys/src/lib.rs @@ -86,9 +86,50 @@ pub mod ffi { pub fn TDF_Label_is_null(label: &TDF_Label) -> bool; pub fn TDF_Transaction_new(data: &HandleTdfData) -> UniquePtr; pub fn TDF_Transaction_open(transaction: Pin<&mut TDF_Transaction>) -> i32; - pub fn TDF_Transaction_commit(transaction: Pin<&mut TDF_Transaction>) -> UniquePtr; + pub fn TDF_Transaction_commit( + transaction: Pin<&mut TDF_Transaction>, + ) -> UniquePtr; pub fn TDF_Transaction_abort(transaction: Pin<&mut TDF_Transaction>); pub fn TDF_Transaction_is_open(transaction: &TDF_Transaction) -> bool; + pub fn TDF_Data_undo( + data: Pin<&mut HandleTdfData>, + delta: &HandleTdfDelta, + ) -> UniquePtr; + + // TNaming + type TNaming_Builder; + + pub fn TNaming_Builder_ctor(label: &TDF_Label) -> UniquePtr; + pub fn TNaming_Builder_generated( + builder: Pin<&mut TNaming_Builder>, + new_shape: &TopoDS_Shape, + ); + pub fn TNaming_Builder_generated_with_old( + builder: Pin<&mut TNaming_Builder>, + old_shape: &TopoDS_Shape, + new_shape: &TopoDS_Shape, + ); + pub fn TNaming_Builder_modify( + builder: Pin<&mut TNaming_Builder>, + old_shape: &TopoDS_Shape, + new_shape: &TopoDS_Shape, + ); + pub fn TNaming_Builder_delete(builder: Pin<&mut TNaming_Builder>, old_shape: &TopoDS_Shape); + pub fn TNaming_Builder_select( + builder: Pin<&mut TNaming_Builder>, + shape: &TopoDS_Shape, + in_shape: &TopoDS_Shape, + ); + type Handle_TNaming_NamedShape; + + pub fn TNaming_Builder_named_shape( + builder: &TNaming_Builder, + ) -> UniquePtr; + + pub fn TNaming_Tool_original_shape( + ns: &Handle_TNaming_NamedShape, + ) -> UniquePtr; + pub fn TNaming_NamedShape_Get(ns: &Handle_TNaming_NamedShape) -> UniquePtr; // Handles type HandleStandardType; @@ -478,7 +519,9 @@ pub mod ffi { pub fn IsNull(self: &TopoDS_Shape) -> bool; pub fn IsEqual(self: &TopoDS_Shape, other: &TopoDS_Shape) -> bool; + pub fn IsSame(self: &TopoDS_Shape, other: &TopoDS_Shape) -> bool; pub fn ShapeType(self: &TopoDS_Shape) -> TopAbs_ShapeEnum; + pub fn Location(self: &TopoDS_Shape) -> &TopLoc_Location; type TopAbs_Orientation; pub fn Orientation(self: &TopoDS_Shape) -> TopAbs_Orientation; @@ -1262,6 +1305,7 @@ pub mod ffi { pub fn IsDone(self: &BRepMesh_IncrementalMesh) -> bool; type TopLoc_Location; + pub fn IsIdentity(self: &TopLoc_Location) -> bool; #[cxx_name = "construct_unique"] pub fn TopLoc_Location_ctor() -> UniquePtr; diff --git a/crates/opencascade/src/primitives/shape.rs b/crates/opencascade/src/primitives/shape.rs index 439cb0a68..07437b724 100644 --- a/crates/opencascade/src/primitives/shape.rs +++ b/crates/opencascade/src/primitives/shape.rs @@ -678,6 +678,14 @@ impl Shape { Self::from_shape(upgrader.Shape()) } + /// Reads the translation component of this shape's location transform. + /// For shapes with composed locations (e.g. nested assemblies), this + /// reflects the full composed transform, not just the outermost translation. + pub fn translation(&self) -> DVec3 { + let location = self.inner.as_ref().unwrap().Location(); + let trsf = ffi::TopLoc_Location_Transformation(&location); + DVec3::new(trsf.Value(1, 4), trsf.Value(2, 4), trsf.Value(3, 4)) + } pub fn set_global_translation(&mut self, translation: DVec3) { let mut transform = ffi::new_transform(); let translation_vec = make_vec(translation); diff --git a/crates/opencascade/src/tdf.rs b/crates/opencascade/src/tdf.rs index 07077a6ee..36796771c 100644 --- a/crates/opencascade/src/tdf.rs +++ b/crates/opencascade/src/tdf.rs @@ -1,7 +1,62 @@ +//! Bindings for OCCT's TDF (Topological Data Framework) and TNaming packages. +//! +//! [`TdfData`] is the top-level datastructure containing a tree of [`TdfLabel`]s +//! +//! The use of these labels within the tree is the OCCT way of capturing moments in the topological story of a +//! "document", and the tree describes how these moments come together. +//! +//! [`TdfTransaction`]s are used to scope changes in a document, and committed changes are captured +//! by [`TdfDelta`]. Feeding a [`TdfDelta`] into a document via [`TdfData::undo`] enables a +//! reversion of a transaction, with said [`TdfData::undo`] emitting an inverted of the delta to +//! allow for a "backwards-undo", i.e. "redo" +//! +//! That leaves us with [`TnamingBuilder`]: It is the write interface for TNaming: it records the +//! provenance of topological shapes into the document, establishing the +//! relationships that allow stable shape references to survive model rebuilds. +//! +//! # Example +//! +//! ``` +//! use opencascade::tdf::{TdfData, TnamingBuilder}; +//! use opencascade::primitives::Shape; +//! use glam::dvec3; +//! +//! // A document and two shapes: one before, and after a translation +//! let original = Shape::box_centered(10.0, 10.0, 10.0); +//! let mut translated = Shape::box_centered(10.0, 10.0, 10.0); +//! translated.set_global_translation(dvec3(5.0, 0.0, 0.0)); +//! +//! let mut doc = TdfData::new(); +//! +//! // Label hierarchy mirrors the operation tree +//! let mut tx = doc.transaction(); +//! tx.open(); +//! let label = doc.root().new_child(); +//! +//! // Record the translation as a shape evolution. +//! // The builder borrows the transaction and must be dropped before commit. +//! // Future API iterations will enforce this structurally via closure arguments. +//! { +//! let mut builder = TnamingBuilder::new(&tx, &label); +//! builder.modify( +//! &original, +//! &translated, +//! ); +//! } +//! +//! // Commit captures the delta; undo reverses it +//! let delta = tx.commit(); +//! let redo_delta = doc.undo(delta); +//! +//! // Redo reapplies +//! let _ = doc.undo(redo_delta); +//! ``` use cxx::UniquePtr; use opencascade_sys::ffi; use std::marker::PhantomData; +use crate::primitives::Shape; + /// ```compile_fail /// use opencascade::tdf::TdfData; /// let root = { @@ -9,9 +64,23 @@ use std::marker::PhantomData; /// doc.root() /// }; /// ``` +/// ```compile_fail +/// use opencascade::tdf::TdfData; +/// let tx = { +/// let doc = TdfData::new(); +/// doc.transaction() +/// }; +/// ``` pub struct TdfData { inner: UniquePtr, } +/// Owned record of what changed in a committed transaction. Passed back +/// into [`TdfData::undo`] to reverse those changes. The inverse delta +/// returned by undo enables redo, and deltas can be retained externally +/// for history traversal or version management. +pub struct TdfDelta { + pub(crate) inner: UniquePtr, +} impl TdfData { pub fn new() -> Self { @@ -27,6 +96,48 @@ impl TdfData { _phantom: PhantomData, } } + /// Consumes the passed in `delta`, reverting the [`TdfTransaction::commit`] (and friends) that + /// constructed it. + /// ``` + /// # use opencascade::tdf::{TdfData, TnamingBuilder, TnamingNamedShape}; + /// # use opencascade::primitives::Shape; + /// # use glam::{ dvec3, DVec3 }; + /// let original = Shape::box_centered(10.0, 10.0, 10.0); + /// let mut translated = Shape::box_centered(10.0, 10.0, 10.0); + /// translated.set_global_translation(dvec3(5.0, 0.0, 0.0)); + /// let mut doc = TdfData::new(); + /// let mut tx = doc.transaction(); + /// tx.open(); + /// let label = doc.root().new_child(); + /// // Record the translation as a shape evolution. + /// // The builder borrows the transaction and must be dropped before commit. + /// // Future API iterations will enforce this structurally via closure arguments. + /// let named_shape; + /// { + /// let mut builder = TnamingBuilder::new(&tx, &label); + /// builder.modify( + /// &original, + /// &translated, + /// ); + /// named_shape = builder.named_shape(); + /// } + /// + /// let delta = tx.commit(); + /// // after commit: active shape is the translated one + /// assert_ne!(named_shape.get().translation(), DVec3::ZERO); + /// let redo_delta = doc.undo(delta); + /// // after undo: active shape reverts + /// assert_eq!(named_shape.get().translation(), DVec3::ZERO); + /// let _ = doc.undo(redo_delta); + /// // after redo: translated again + /// assert_ne!(named_shape.get().translation(), DVec3::ZERO); + /// ``` + #[must_use] + pub fn undo(&mut self, delta: TdfDelta) -> TdfDelta { + // unwrap: A [`TdfDelta`] not being a valid handle implies it was not constructed through + // the use of [`TdfTransaction::commit`] at time of writing. + TdfDelta { inner: ffi::TDF_Data_undo(self.inner.pin_mut(), delta.inner.as_ref().unwrap()) } + } } pub struct TdfLabel<'doc> { @@ -46,39 +157,152 @@ impl<'doc> TdfLabel<'doc> { ffi::TDF_Label_is_null(self.inner.as_ref().unwrap()) } } +/// Brackets a set of mutations to the label tree. The record of what +/// changed is not retained here, as it is transfers to [`TdfDelta`] on commit. +/// +/// ```compile_fail +/// use opencascade::tdf::{TdfData, TnamingBuilder}; +/// use opencascade::primitives::Shape; +/// let original = Shape::box_centered(10.0, 10.0, 10.0); +/// let mut doc = TdfData::new(); +/// let mut tx = doc.transaction(); +/// tx.open(); +/// let label = doc.root().new_child(); +/// let mut builder = TnamingBuilder::new(&tx, &label); +/// let _ = tx.commit(); +/// builder.select(&original, &original); // use after commit attempt +/// ``` pub struct TdfTransaction<'doc> { inner: UniquePtr, _phantom: PhantomData<&'doc TdfData>, } impl<'doc> TdfTransaction<'doc> { + // TODO: this i32 return value needs to be new-typed and have an interface that encapsulates + // its nature. Refer to OCCT support for nested transactions and how this value indexes into + // them. For now, the nesting is always 1, but that will change... pub fn open(&mut self) -> i32 { ffi::TDF_Transaction_open(self.inner.pin_mut()) } - // TODO: wrap HandleTdfDelta in a safe TdfDelta type before contributing upstream. - // Currently leaks the raw FFI type into the safe wrapper layer. - pub fn commit(mut self) -> UniquePtr { - ffi::TDF_Transaction_commit(self.inner.pin_mut()) + pub fn is_open(&self) -> bool { + ffi::TDF_Transaction_is_open(self.inner.as_ref().unwrap()) + } + + /// "Closes the bracket" on this transaction, committing the change to the [`TdfData`] + /// Dropping the returned [`TdfDelta`] permanently discards the ability + /// to undo this transaction. + #[must_use] + pub fn commit(mut self) -> TdfDelta { + TdfDelta { inner: ffi::TDF_Transaction_commit(self.inner.pin_mut()) } } pub fn abort(mut self) { ffi::TDF_Transaction_abort(self.inner.pin_mut()) } +} +/// The write interface for topological naming. Records shape evolution, i.e. +/// the provenance relationships between pre- and post-operation shapes, +/// into a [`TdfLabel`]. These records are what allow stable references +/// to topological entities to survive model rebuilds. +/// +/// An open [`TdfTransaction`] is required for the builder's full lifetime +/// because the underlying attribute write must be captured in a delta. +/// The borrow checker enforces this: [`TdfTransaction::commit`] cannot +/// be called while any builder is live. +/// +/// # Correctness +/// [`TdfTransaction::open`] must be called before constructing a builder. +/// The borrow checker enforces that the transaction outlives the builder, +/// but cannot enforce that the transaction is open. Attempting to construct a builder +/// on an unopened transaction will panic at the OCCT level. +/// +/// Correct usage: +/// ``` +/// use opencascade::tdf::{TdfData, TnamingBuilder}; +/// let mut doc = TdfData::new(); +/// let mut tx = doc.transaction(); +/// tx.open(); // must precede builder construction +/// let label = doc.root().new_child(); +/// let mut builder = TnamingBuilder::new(&tx, &label); +/// ``` +/// +/// Future API iterations will enforce this structurally, e.g. via closure arguments, +/// making the open/build/commit sequence impossible to misorder. +pub struct TnamingBuilder<'tx, 'doc: 'tx> { + inner: UniquePtr, + _phantom: PhantomData<&'tx TdfTransaction<'doc>>, +} - pub fn is_open(&self) -> bool { - ffi::TDF_Transaction_is_open(self.inner.as_ref().unwrap()) +impl<'tx, 'doc: 'tx> TnamingBuilder<'tx, 'doc> { + /// The `_tx` argument is passed in to bind the lifetime of the [`Self`] to the scope of the + /// transaction it is used within + pub fn new(_tx: &'tx TdfTransaction<'doc>, label: &TdfLabel<'doc>) -> Self { + Self { + inner: ffi::TNaming_Builder_ctor(label.inner.as_ref().unwrap()), + _phantom: PhantomData, + } + } + /// Use `old_shape: None` for primitives with no predecessor. + pub fn generated(&mut self, old_shape: Option<&Shape>, new_shape: &Shape) { + match old_shape { + None => ffi::TNaming_Builder_generated( + self.inner.pin_mut(), + new_shape.inner.as_ref().unwrap(), + ), + Some(old) => ffi::TNaming_Builder_generated_with_old( + self.inner.pin_mut(), + old.inner.as_ref().unwrap(), + new_shape.inner.as_ref().unwrap(), + ), + } + } + pub fn modify(&mut self, old_shape: &Shape, new_shape: &Shape) { + ffi::TNaming_Builder_modify( + self.inner.pin_mut(), + old_shape.inner.as_ref().unwrap(), + new_shape.inner.as_ref().unwrap(), + ); + } + + pub fn delete(&mut self, old_shape: &Shape) { + ffi::TNaming_Builder_delete(self.inner.pin_mut(), old_shape.inner.as_ref().unwrap()); + } + + pub fn select(&mut self, shape: &Shape, in_shape: &Shape) { + ffi::TNaming_Builder_select( + self.inner.pin_mut(), + shape.inner.as_ref().unwrap(), + in_shape.inner.as_ref().unwrap(), + ); + } + + pub fn named_shape(&self) -> TnamingNamedShape { + TnamingNamedShape { inner: ffi::TNaming_Builder_named_shape(self.inner.as_ref().unwrap()) } + } +} + +/// A handle to the [`TNaming_NamedShape`] attribute written to a [`TdfLabel`] +/// by [`TnamingBuilder`]. Retaining this after the builder is dropped allows +/// querying the recorded shape evolution after commit and across undo/redo +/// boundaries. +pub struct TnamingNamedShape { + pub(crate) inner: UniquePtr, +} + +impl TnamingNamedShape { + pub fn original_shape(&self) -> Shape { + Shape { inner: ffi::TNaming_Tool_original_shape(self.inner.as_ref().unwrap()) } + } + /// The new shape from the recorded evolution pair. Reflects the current undo/redo state + pub fn get(&self) -> Shape { + Shape { inner: ffi::TNaming_NamedShape_Get(self.inner.as_ref().unwrap()) } } } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_tdf_data_constructs() { - let doc = TdfData::new(); - drop(doc); - } #[test] fn test_tdf_data_root_is_not_null() { let doc = TdfData::new(); @@ -87,21 +311,71 @@ mod tests { } #[test] - fn test_new_child_is_not_null() { + fn test_new_child_with_transaction() { let doc = TdfData::new(); + let mut tx = doc.transaction(); + tx.open(); let root = doc.root(); - // let child = root.new_child(); - assert!(!root.is_null()); - // drop(child); + let child = root.new_child(); + assert!(!child.is_null()); + let _ = tx.commit(); } #[test] - fn test_new_child_with_transaction() { + fn test_tnaming_builder_constructs() { let doc = TdfData::new(); let mut tx = doc.transaction(); tx.open(); let root = doc.root(); - let child = root.new_child(); - assert!(!child.is_null()); - tx.commit(); + let label = root.new_child(); + let _builder = TnamingBuilder::new(&tx, &label); + } + #[test] + fn test_tnaming_builder_generated() { + use crate::primitives::Shape; + let doc = TdfData::new(); + let mut tx = doc.transaction(); + tx.open(); + let root = doc.root(); + let label = root.new_child(); + let mut builder = TnamingBuilder::new(&tx, &label); + let shape = Shape::sphere(1.0).build(); + builder.generated(None, &shape); + } + #[test] + fn test_tnaming_undo_round_trip() { + use crate::primitives::Shape; + use glam::{dvec3, DVec3}; + + let original = Shape::box_centered(10.0, 10.0, 10.0); + let mut translated = Shape::box_centered(10.0, 10.0, 10.0); + translated.set_global_translation(dvec3(5.0, 0.0, 0.0)); + + let mut doc = TdfData::new(); + let mut tx = doc.transaction(); + tx.open(); + let root = doc.root(); + let label = root.new_child(); + + let mut builder = TnamingBuilder::new(&tx, &label); + let named_shape; + { + builder.modify(&original, &translated); + + named_shape = builder.named_shape(); + } + + let delta = tx.commit(); + + // After commit: active shape should be the translated one + assert_ne!(named_shape.get().translation(), DVec3::ZERO); + + let _redo = doc.undo(delta); + + // After undo: active shape should be back to original + assert_eq!(named_shape.get().translation(), DVec3::ZERO); + + // original_shape is always the recorded old shape regardless of undo state + let recovered = named_shape.original_shape(); + assert_eq!(recovered.translation(), DVec3::ZERO); } } diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..7e00dbab8 --- /dev/null +++ b/flake.nix @@ -0,0 +1,93 @@ +{ + description = "Rust development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ]; + targets = [ "wasm32-unknown-unknown" ]; + }; + + opencascade = pkgs.opencascade-occt; + + # Mesa drivers expose Vulkan and OpenGL ICDs. vulkan-loader is the + # runtime that wgpu uses to discover them via the ICD JSON files. + vulkanPkgs = [ + pkgs.vulkan-loader + pkgs.vulkan-validation-layers + pkgs.mesa + pkgs.mesa.drivers + ]; + in + { + devShells.default = pkgs.mkShell { + buildInputs = [ + # Rust + rustToolchain + pkgs.cargo-watch + + # C/C++ toolchain (fixes "linker `cc` not found") + pkgs.gcc + pkgs.gnumake + pkgs.cmake + pkgs.pkg-config + + # OpenCASCADE (for occt-sys) + opencascade + + # Common occt-sys dependencies + pkgs.freetype + pkgs.fontconfig + pkgs.libGL + pkgs.libGLU + pkgs.libX11 + pkgs.libXcursor + pkgs.libXrandr + pkgs.libXmu + pkgs.libXi + pkgs.libXext + pkgs.libxkbcommon + + # Vulkan / wgpu + ] ++ vulkanPkgs ++ [ + + pkgs.libpng + pkgs.libjpeg + pkgs.libtiff + pkgs.tbb + pkgs.vtk + ]; + + shellHook = '' + export OPENCASCADE_ROOT="${opencascade}" + export PKG_CONFIG_PATH="${opencascade}/lib/pkgconfig:$PKG_CONFIG_PATH" + export LD_LIBRARY_PATH="${opencascade}/lib:${pkgs.freetype}/lib:${pkgs.fontconfig}/lib:${pkgs.libX11}/lib:${pkgs.libXcursor}/lib:${pkgs.libXrandr}/lib:${pkgs.libXi}/lib:${pkgs.libxkbcommon}/lib:${pkgs.vulkan-loader}/lib:${pkgs.mesa}/lib:$LD_LIBRARY_PATH" + export LIBRARY_PATH="${opencascade}/lib:$LIBRARY_PATH" + + # Point the Vulkan loader at Mesa's ICD JSON so it can find the + # actual GPU driver. Without this wgpu sees no backends at all. + export VK_ICD_FILENAMES="${pkgs.mesa.drivers}/share/vulkan/icd.d/intel_icd.x86_64.json:${pkgs.mesa.drivers}/share/vulkan/icd.d/radeon_icd.x86_64.json" + + # Uncomment to force wgpu to use the GL backend via ANGLE/Mesa + # instead of Vulkan — useful if Vulkan still fails: + # export WGPU_BACKEND=gl + + # CMake 4.x removed compatibility with CMakeLists.txt files that + # declare a pre-3.5 minimum version. occt-sys vendors one such file. + # CMAKE_POLICY_VERSION_MINIMUM is read directly by CMake 4.x itself, + # so setting it in the environment is sufficient — no -D flag needed. + export CMAKE_POLICY_VERSION_MINIMUM=3.5 + ''; + }; + }); +}