refactor: update transformation system, improve template readability, and reorganize VTK assembly management

This commit is contained in:
AndreaRigoni
2026-03-31 16:04:03 +00:00
parent 22d0041942
commit d4fd2d3914
30 changed files with 568 additions and 501 deletions

View File

@@ -11,6 +11,12 @@
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}"
}
},
{
"name": "mutom",
"description": "",
"displayName": "",
"inherits": []
}
]
}

View File

@@ -1,73 +0,0 @@
# Transformation Flow and Synchronization System
This document describes how transformations are applied and synchronized between the interactive 3D viewport, the visualization puppets, and the underlying mathematical models within the `uLib` framework.
## Architecture Overview
The system follows a Model-View-Controller (MVC) like pattern where:
- **Model**: `uLib::AffineTransform` (or derived classes like `ContainerBox`).
- **View/Puppet**: `uLib::Vtk::Puppet` (and specialized derivations like `Vtk::Assembly`).
- **Controller/Interaction**: `vtkHandlerWidget` (the transformation gizmo).
---
## 1. Interaction Flow (Gizmo -> Model)
When a user interacts with the `vtkHandlerWidget` (dragging arrows, rings, or cubes), the following chain of events occurs:
```mermaid
sequenceDiagram
participant User
participant HW as vtkHandlerWidget
participant VP as vtkViewport
participant P as vtkPuppet
participant M as uLib Model
User->>HW: Drag handle (MouseMove)
HW->>HW: Calculate Delta Matrix (op)
HW->>HW: Total = StartState * op
HW->>HW: Decompose Total into P, O, S
HW->>P: SetPosition, SetOrientation, SetScale
HW-->>VP: Invoke InteractionEvent
VP->>P: SyncFromVtk()
P->>P: Get local matrix from VTK Prop
P->>M: SetMatrix(matrix)
M-->>M: Update local properties (P, O, S)
M-->>P: Emit Updated signal
P->>P: Puppet::Update()
P->>P: (Redundant sanity write to Prop)
```
### Key Principles:
- **Single Source of Truth**: The `uLib::AffineTransform` is the owner of the transformation state.
- **Internal TRS vs UserMatrix**: We apply transformations directly to VTK's internal `Position`, `Orientation`, and `Scale` properties. This ensures the data is "visible" to VTK actors and simplifies decomposition.
- **Cumulative Bias Avoidance**: The `HandlerWidget` calculates transformations relative to the state at the start of the click, preventing numerical drift during a single drag operation.
---
## 2. Synchronization Loop Resolution
To prevent infinite loops and "double-transformation" artifacts (especially in assemblies), the following protections are in place:
1. **Hierarchy Isolation**: The `Puppet` base class distinguishes between the **Root Property** (which receives the puppet's master transformation) and **Sub-Parts** (which only receive appearance updates like color/visibility). This prevents parts from inheriting the same displacement twice.
2. **Re-entrancy Guards**: Puppets use an `m_InUpdate` flag to prevent a feedback loop where `SyncFromVtk` triggers a Model Update, which then re-triggers the Puppet Update.
3. **Signal Blocking**: In specialized cases (like `vtkAssembly`), `m_BlockUpdate` is used to prevent the model-to-puppet push during a puppet-to-model sync.
---
## 3. Undo System (Ctrl-Z)
### Current Implementation (Delta Chain)
Currently, the system maintains a `m_TransformChain` of delta matrices.
- **Record**: After every drag, a delta matrix ($M_{delta} = M_{end} \cdot M_{start}^{-1}$) is appended to the chain.
- **Undo**: The last delta is removed, and the prop is reconstructed by reapplying the remaining chain from a `BaseMatrix`.
### Planned Improvement (TRS Snapshots)
We are migrating to a `uLib::TRS` snapshot system for Undo.
- **Record**: At the start of a drag, the current `TRS` state of the object is pushed to the `m_UndoStack`.
- **Undo**: The top `TRS` is popped and applied directly to the model.
This approach is more robust because:
- It eliminates matrix multiplication error accumulation.
- It bypasses rotation convention/order issues (Gimbal lock in deltas).
- It returns the object to exactly its previous property values.

21
docs/update_properties.md Normal file
View File

@@ -0,0 +1,21 @@
# Properties and the vtk-gui representation
This is the rationale behind the connection between TRS properties and Puppet Transformation.
The properties from model get propoagated via Object signalling system (the Update signal) to the vtkRepresentation and to the Qt widgets so that the overall transformation of the model reflects into a modification of its representation in vtk and in the gui.
In addition the properties need to be adjusted also from vtk, for example if user uses handlerwidget to change the transformation this is eventually applied to Puppet and Puppet should propagate the transformation change to the vtk representation object (for instance vtkContainerBox) and the latter eventually propagates the change into the model.
the Puppet or the vtk representation wrapper ( vtkContainerBox for instance is the wrapper od ContainerBox ) should not directly show the transformation of the handlerwidget but it should show the transformation of the model once applied so we are always seeing the actual aspect of the model reflected to the vtk representation and not the other way around.
So in syntesis the model is the master and the vtk representation and the gui are the slaves of any modification, but the vtkHandlerWidget is able to apply a transform that should be applied to the model and then the model should propagate the transformation change to the vtk representation and to the gui.
## The Puppet
The puppet is the proxy of the spatial placement of objects in the scene. Puppets should have an internal ContainerBox that is shown in the scene around the content to be able to pick Puppet from vtkViewport using the handler widget. The HandlerWidget moves the Puppet ContainerBox (the red Highlight element whe selected) to reflect the handler current transformation, but the transformation is propagated to the derived Puppet classes like vtkContainerBox.
The vtkHandlerWidget should handle the transformation of the puppet internal ContainerBox. The changes of the ContainerBox will be propagated to the derived classes and eventually to the model.

View File

@@ -38,6 +38,76 @@
#include <boost/any.hpp>
/**
* @file Debug.h
* @brief Unified Debugging and Monitoring System for uLib.
*
* # Debug System Documentation
*
* The `Debug` system provides a flexible, adapter-based mechanism for monitoring
* and outputting internal variables and states without hardcoding output logic
* into core classes.
*
* ## Architecture
*
* The system follows an adapter pattern:
*
* - **`DebugAdapterInterface`**: The base interface for all output targets.
* Subclasses define how primitive types and strings are handled (e.g., printing
* to `std::cout`, writing to a log file, or updating a real-time UI widget).
*
* - **`Debug` Class**: The central registry. It stores:
* 1. A list of "Adapters" (`DebugAdapterInterface`).
* 2. A list of "Items" to monitor. Each item is a reference to a variable
* associated with a name.
*
* - **Type Safety**: Although variables are stored using `boost::any` (type erasure),
* the system automatically preserves the original type information via
* internal template adapters (`AnyCastAdapter`), ensuring that the correct
* overload of the adapter interface is called.
*
* ## Core Components
*
* | Component | Description |
* | :--- | :--- |
* | `Debug` | The main controller used to add adapters and register variables. |
* | `DebugAdapterInterface` | Virtual base for creating new output methods. |
* | `DebugAdapterText` | A simple built-in adapter for `std::ostream` output. |
*
* ## How to Use
*
* ### 1. Initialize the Debug Object
* ```cpp
* uLib::Debug dbg;
* ```
*
* ### 2. Add an Adapter
* ```cpp
* uLib::DebugAdapterText console(std::cout);
* dbg.AddAdapter(console);
* ```
*
* ### 3. Register Variables to Monitor
* Use the `operator()` to bind a variable by reference.
* ```cpp
* int frameCount = 0;
* std::string state = "Initializing";
* dbg("Frames", frameCount);
* dbg("Status", state);
* ```
*
* ### 4. Update
* Call `Update()` periodically (e.g., once per frame) to push the current
* values of all registered variables to all connected adapters.
* ```cpp
* while(running) {
* frameCount++;
* dbg.Update(); // This triggers the output
* }
* ```
*/
namespace uLib {
@@ -107,7 +177,6 @@ public:
class Debug {
typedef detail::DebugAdapterInterface AdapterInterface;
typedef SmartPointer<detail::DebugAdapterInterface> Adapter;

View File

@@ -95,6 +95,8 @@ namespace uLib {
typedef boost::signals2::signal_base SignalBase;
typedef boost::signals2::connection Connection;
typedef boost::signals2::shared_connection_block ConnectionBlock;
template <typename T> struct Signal {
typedef boost::signals2::signal<T> type;

View File

@@ -96,10 +96,13 @@ public:
signals:
virtual void Updated() override {
if (m_InUpdated) return; // break signal recursion
if (m_InUpdated) return;
m_InUpdated = true;
// Synchronize TRS part
this->TRS::Updated();
this->ComputeBoundingBox();
ULIB_SIGNAL_EMIT(Object::Updated);
m_InUpdated = false;
}

View File

@@ -33,6 +33,8 @@
#include "Math/Transform.h"
#include <utility>
#include <iostream>
namespace uLib {
/**
@@ -99,9 +101,9 @@ public:
*/
template <class ArchiveT>
void serialize(ArchiveT & ar, const unsigned int version) {
ar & boost::serialization::make_nvp("TRS", boost::serialization::base_object<TRS>(*this));
ar & HRP(Size);
ar & HRP(Origin);
ar & boost::serialization::make_nvp("TRS", boost::serialization::base_object<TRS>(*this));
}
/**
@@ -216,8 +218,13 @@ signals:
/** Signal emitted when properties change */
virtual void Updated() override {
// 1. Synchronize local box part (Size/Origin -> m_LocalT)
this->Sync();
ULIB_SIGNAL_EMIT(Object::Updated);
// 2. Synchronize TRS part (position/rotation/scaling -> m_T) and emit signal
this->TRS::Updated();
// std::cout << "ContainerBox::Updated()" << std::endl;
}
private:

View File

@@ -177,8 +177,11 @@ public:
signals:
/** Signal emitted when properties change */
virtual void Updated() override {
// 1. Synchronize local cylinder part (Radius/Height/Axis -> m_LocalT)
this->Sync();
ULIB_SIGNAL_EMIT(Object::Updated);
// 2. Synchronize TRS part (position/rotation/scaling -> m_T) and emit signal
this->TRS::Updated();
}
private:

View File

@@ -246,6 +246,11 @@ public:
this->GetTransform() = GetAffineMatrix();
}
void Updated() override {
this->SyncMatrix();
this->AffineTransform::Updated();
}
template <class ArchiveT>
void serialize(ArchiveT & ar, const unsigned int version) {
ar & HRPU(position, "mm");

View File

@@ -98,15 +98,14 @@ template <typename T> void Kernel<T>::PrintSelf(std::ostream &o) const {
////////////////////////////////////////////////////////////////////////////////
#define _TPL_ template <typename VoxelT, typename AlgorithmT>
#define _TPLT_ VoxelT, AlgorithmT
_TPL_
VoxImageFilter<_TPLT_>::VoxImageFilter(const Vector3i &size)
template <typename VoxelT, typename AlgorithmT>
VoxImageFilter<VoxelT, AlgorithmT>::VoxImageFilter(const Vector3i &size)
: m_KernelData(size), t_Algoritm(static_cast<AlgorithmT *>(this)) {}
_TPL_
void VoxImageFilter<_TPLT_>::Run() {
template <typename VoxelT, typename AlgorithmT>
void VoxImageFilter<VoxelT, AlgorithmT>::Run() {
VoxImage<VoxelT> buffer = *m_Image;
#pragma omp parallel for
for (int i = 0; i < m_Image->Data().size(); ++i)
@@ -114,8 +113,8 @@ void VoxImageFilter<_TPLT_>::Run() {
#pragma omp barrier
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelOffset() {
template <typename VoxelT, typename AlgorithmT>
void VoxImageFilter<VoxelT, AlgorithmT>::SetKernelOffset() {
Vector3i id(0, 0, 0);
for (int z = 0; z < m_KernelData.GetDims()(2); ++z) {
for (int x = 0; x < m_KernelData.GetDims()(0); ++x) {
@@ -127,8 +126,8 @@ void VoxImageFilter<_TPLT_>::SetKernelOffset() {
}
}
_TPL_
float VoxImageFilter<_TPLT_>::Distance2(const Vector3i &v) {
template <typename VoxelT, typename AlgorithmT>
float VoxImageFilter<VoxelT, AlgorithmT>::Distance2(const Vector3i &v) {
Vector3i tmp = v;
const Vector3i &dim = this->m_KernelData.GetDims();
Vector3i center = dim / 2;
@@ -140,8 +139,8 @@ float VoxImageFilter<_TPLT_>::Distance2(const Vector3i &v) {
0.25 * (3 - (dim(0) % 2) - (dim(1) % 2) - (dim(2) % 2)));
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelNumericXZY(
template <typename VoxelT, typename AlgorithmT>
void VoxImageFilter<VoxelT, AlgorithmT>::SetKernelNumericXZY(
const std::vector<float> &numeric) {
// set data order //
StructuredData::Order order = m_KernelData.GetDataOrder();
@@ -159,8 +158,8 @@ void VoxImageFilter<_TPLT_>::SetKernelNumericXZY(
// m_KernelData.SetDataOrder(order);
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelSpherical(float (*shape)(float)) {
template <typename VoxelT, typename AlgorithmT>
void VoxImageFilter<VoxelT, AlgorithmT>::SetKernelSpherical(float (*shape)(float)) {
Vector3i id;
for (int y = 0; y < m_KernelData.GetDims()(1); ++y) {
for (int z = 0; z < m_KernelData.GetDims()(2); ++z) {
@@ -172,8 +171,8 @@ void VoxImageFilter<_TPLT_>::SetKernelSpherical(float (*shape)(float)) {
}
}
_TPL_ template <class ShapeT>
void VoxImageFilter<_TPLT_>::SetKernelSpherical(ShapeT shape) {
template <typename VoxelT, typename AlgorithmT> template <class ShapeT>
void VoxImageFilter<VoxelT, AlgorithmT>::SetKernelSpherical(ShapeT shape) {
Interface::IsA<ShapeT, Interface::VoxImageFilterShape>();
Vector3i id;
for (int y = 0; y < m_KernelData.GetDims()(1); ++y) {
@@ -186,8 +185,8 @@ void VoxImageFilter<_TPLT_>::SetKernelSpherical(ShapeT shape) {
}
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(
template <typename VoxelT, typename AlgorithmT>
void VoxImageFilter<VoxelT, AlgorithmT>::SetKernelWeightFunction(
float (*shape)(const Vector3f &)) {
const Vector3i &dim = m_KernelData.GetDims();
Vector3i id;
@@ -207,8 +206,8 @@ void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(
}
}
_TPL_ template <class ShapeT>
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(ShapeT shape) {
template <typename VoxelT, typename AlgorithmT> template <class ShapeT>
void VoxImageFilter<VoxelT, AlgorithmT>::SetKernelWeightFunction(ShapeT shape) {
Interface::IsA<ShapeT, Interface::VoxImageFilterShape>();
const Vector3i &dim = m_KernelData.GetDims();
Vector3i id;
@@ -228,14 +227,14 @@ void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(ShapeT shape) {
}
}
_TPL_
void VoxImageFilter<_TPLT_>::SetImage(Abstract::VoxImage *image) {
template <typename VoxelT, typename AlgorithmT>
void VoxImageFilter<VoxelT, AlgorithmT>::SetImage(Abstract::VoxImage *image) {
this->m_Image = reinterpret_cast<VoxImage<VoxelT> *>(image);
this->SetKernelOffset();
}
_TPL_
float VoxImageFilter<_TPLT_>::Convolve(const VoxImage<VoxelT> &buffer,
template <typename VoxelT, typename AlgorithmT>
float VoxImageFilter<VoxelT, AlgorithmT>::Convolve(const VoxImage<VoxelT> &buffer,
int index) {
const DataAllocator<VoxelT> &vbuf = buffer.ConstData();
const DataAllocator<VoxelT> &vker = m_KernelData.ConstData();
@@ -252,8 +251,8 @@ float VoxImageFilter<_TPLT_>::Convolve(const VoxImage<VoxelT> &buffer,
return conv / ksum;
}
#undef _TPLT_
#undef _TPL_
} // namespace uLib

View File

@@ -64,7 +64,7 @@ vtkDetectorChamber::vtkDetectorChamber(DetectorChamber *content)
this->SetProp(m_PlaneActor);
this->contentUpdate();
this->Update();
}
vtkDetectorChamber::~vtkDetectorChamber() {
@@ -76,8 +76,8 @@ DetectorChamber *vtkDetectorChamber::GetContent() {
return static_cast<DetectorChamber *>(m_Content);
}
void vtkDetectorChamber::contentUpdate() {
this->BaseClass::contentUpdate();
void vtkDetectorChamber::Update() {
this->BaseClass::Update();
if (!m_Content) return;
DetectorChamber *c = this->GetContent();

View File

@@ -56,7 +56,7 @@ public:
Content *GetContent();
virtual void contentUpdate() override;
virtual void Update() override;
protected:
vtkActor *m_PlaneActor;

View File

@@ -40,20 +40,21 @@ namespace Vtk {
vtkMuonScatter::vtkMuonScatter(MuonScatter &content)
: m_Content(&content), m_LineIn(vtkLineSource::New()),
m_LineOut(vtkLineSource::New()), m_PolyData(vtkPolyData::New()),
m_SpherePoca(NULL) {
m_SpherePoca(NULL), m_Asm(vtkAssembly::New()) {
InstallPipe();
}
vtkMuonScatter::vtkMuonScatter(const MuonScatter &content)
: m_Content(const_cast<MuonScatter *>(&content)),
m_LineIn(vtkLineSource::New()), m_LineOut(vtkLineSource::New()),
m_PolyData(vtkPolyData::New()), m_SpherePoca(NULL) {
m_PolyData(vtkPolyData::New()), m_SpherePoca(NULL), m_Asm(vtkAssembly::New()) {
InstallPipe();
}
vtkMuonScatter::~vtkMuonScatter() {
m_LineIn->Delete();
m_LineOut->Delete();
m_Asm->Delete();
if (m_SpherePoca)
m_SpherePoca->Delete();
}
@@ -87,13 +88,15 @@ void vtkMuonScatter::InstallPipe() {
mapper->SetInputConnection(m_LineIn->GetOutputPort());
vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
this->SetProp(actor);
m_Asm->AddPart(actor);
mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(m_LineOut->GetOutputPort());
actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
this->SetProp(actor);
m_Asm->AddPart(actor);
this->SetProp(m_Asm);
}
vtkPolyData *vtkMuonScatter::GetPolyData() const {
@@ -123,7 +126,7 @@ void vtkMuonScatter::AddPocaPoint(HPoint3f poca) {
mapper->SetInputConnection(m_SpherePoca->GetOutputPort());
vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
this->SetProp(actor);
m_Asm->AddPart(actor);
}
HPoint3f vtkMuonScatter::GetPocaPoint() {

View File

@@ -27,6 +27,7 @@
#define VTKMUONSCATTER_H
#include <vtkActor.h>
#include <vtkAssembly.h>
#include <vtkAppendPolyData.h>
#include <vtkLineSource.h>
#include <vtkPolyDataMapper.h>
@@ -85,6 +86,7 @@ private:
vtkLineSource *m_LineOut;
vtkSphereSource *m_SpherePoca;
vtkPolyData *m_PolyData;
vtkAssembly *m_Asm;
};
} // namespace Vtk

View File

@@ -50,6 +50,7 @@ vtkVoxRaytracerRepresentation::vtkVoxRaytracerRepresentation(Content &content)
m_RayRepresentation(vtkAppendPolyData::New()),
m_RayRepresentationActor(vtkActor::New()),
m_Transform(vtkTransform::New()),
m_Asm(vtkAssembly::New()),
m_HasMuon(false), m_HasPoca(false) {
default_radius = content.GetImage()->GetSpacing()(0) / 4;
m_Sphere1->SetRadius(default_radius);
@@ -313,20 +314,19 @@ void vtkVoxRaytracerRepresentation::InstallPipe() {
vtkSmartPointer<vtkPolyDataMapper> mapper =
vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(append->GetOutputPort());
vtkSmartPointer<vtkActor> actor = vtkActor::New();
actor->SetMapper(mapper);
actor->GetProperty()->SetColor(0.6, 0.6, 1);
this->SetProp(actor);
m_Asm->AddPart(actor);
mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(m_RayLine->GetOutputPort());
m_RayLineActor->SetMapper(mapper);
m_RayLineActor->GetProperty()->SetColor(1, 0, 0);
this->SetProp(m_RayLineActor);
m_Asm->AddPart(m_RayLineActor);
vtkSmartPointer<vtkTransformPolyDataFilter> polyfilter =
vtkSmartPointer<vtkTransformPolyDataFilter>::New();
@@ -343,7 +343,9 @@ void vtkVoxRaytracerRepresentation::InstallPipe() {
vra->GetProperty()->SetEdgeVisibility(true);
vra->GetProperty()->SetColor(0.5, 0.5, 0.5);
this->SetProp(vra);
m_Asm->AddPart(vra);
this->SetProp(m_Asm);
}
} // namespace Vtk

View File

@@ -107,6 +107,7 @@ private:
bool m_HasPoca;
Scalarf default_radius;
vtkSmartPointer<vtkAssembly> m_Asm;
vtkAppendPolyData *m_RayLine;
vtkActor *m_RayLineActor;
vtkActor *m_RayRepresentationActor;

View File

@@ -39,7 +39,7 @@ Assembly::Assembly(uLib::Assembly *content)
this->InstallPipe();
if (m_Content) {
Object::connect(m_Content, &uLib::Assembly::Updated,
this, &Assembly::contentUpdate);
this, &Assembly::Update);
}
}
@@ -84,29 +84,27 @@ void Assembly::InstallPipe() {
}
}
// 4. Apply initial transform
this->UpdateTransform();
this->UpdateBoundingBox();
}
// ------------------------------------------------------------------ //
void Assembly::contentUpdate() {
if (m_InUpdate) return;
m_InUpdate = true;
this->UpdateTransform();
this->UpdateBoundingBox();
if (m_ChildContext)
m_ChildContext->Update();
Puppet::Update();
m_InUpdate = false;
// 4. Force initial visual sync
this->Update();
}
// ------------------------------------------------------------------ //
void Assembly::Update() {
if (m_InUpdate) return;
m_InUpdate = true;
this->contentUpdate();
if (m_Content && m_VtkAsm) {
// Apply world matrix from the assembly content
vtkNew<vtkMatrix4x4> m;
Matrix4fToVtk(m_Content->GetMatrix(), m);
m_VtkAsm->SetUserMatrix(m);
m_VtkAsm->Modified();
}
this->Puppet::Update();
this->UpdateBoundingBox();
if (m_ChildContext)
m_ChildContext->Update();
m_InUpdate = false;
}
@@ -116,14 +114,11 @@ void Assembly::SyncFromVtk() {
m_InUpdate = true;
double pos[3], ori[3], scale[3];
m_VtkAsm->GetPosition(pos);
m_VtkAsm->GetOrientation(ori);
m_VtkAsm->GetScale(scale);
m_Content->SetPosition(Vector3f(pos[0], pos[1], pos[2]));
m_Content->SetOrientation(Vector3f(ori[0], ori[1], ori[2]) * CLHEP::degree);
m_Content->SetScale(Vector3f(scale[0], scale[1], scale[2]));
// VTK -> Model: Update world matrix (accounting for model parents)
if (vtkProp3D* proxy = this->GetProxyProp()) {
m_Content->SetWorldMatrix(VtkToMatrix4f(proxy->GetUserMatrix()));
m_Content->FromMatrix(m_Content->GetMatrix());
}
this->UpdateBoundingBox();
if (m_ChildContext)
@@ -134,14 +129,6 @@ void Assembly::SyncFromVtk() {
m_InUpdate = false;
}
// ------------------------------------------------------------------ //
void Assembly::UpdateTransform() {
if (!m_Content || !m_VtkAsm) return;
this->ApplyTransform(m_VtkAsm);
m_VtkAsm->Modified();
}
// ------------------------------------------------------------------ //
void Assembly::UpdateBoundingBox() {
if (!m_Content || !m_BBoxActor) return;

View File

@@ -50,14 +50,14 @@ public:
virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; }
virtual uLib::ObjectsContext* GetChildren() override { return (uLib::ObjectsContext*)m_Content; }
/** @brief Called when the model signals an update (model→VTK push). */
void contentUpdate();
/**
* @brief Returns the puppet managing child objects.
*/
/** @brief Returns the puppet managing child objects. */
vtkObjectsContext *GetChildrenContext() const;
private:
void UpdateTransform();
void UpdateBoundingBox();
void InstallPipe();

View File

@@ -48,18 +48,28 @@ namespace uLib {
namespace Vtk {
struct ContainerBoxData {
vtkSmartPointer<vtkActor> m_Cube;
vtkSmartPointer<vtkActor> m_Axes;
vtkSmartPointer<vtkActor> m_Cube;
vtkSmartPointer<vtkActor> m_Axes;
vtkSmartPointer<vtkAssembly> m_VtkAsm;
vtkSmartPointer<vtkMatrix4x4> m_Affine;
uLib::Connection m_UpdateSignal;
ContainerBoxData() : m_Cube(vtkSmartPointer<vtkActor>::New()), m_Axes(vtkSmartPointer<vtkActor>::New()) {}
ContainerBoxData() : m_Cube(vtkSmartPointer<vtkActor>::New()),
m_Axes(vtkSmartPointer<vtkActor>::New()),
m_VtkAsm(vtkSmartPointer<vtkAssembly>::New()),
m_Affine(vtkSmartPointer<vtkMatrix4x4>::New()) {}
~ContainerBoxData() {
}
};
vtkContainerBox::vtkContainerBox(vtkContainerBox::Content *content)
: d(new ContainerBoxData()), m_Content(content) {
this->InstallPipe();
Object::connect(m_Content, &Content::Updated, this, &vtkContainerBox::contentUpdate);
d->m_UpdateSignal = Object::connect(m_Content, &uLib::Object::Updated, this, &vtkContainerBox::Update);
}
vtkContainerBox::~vtkContainerBox() {
@@ -72,47 +82,50 @@ vtkPolyData *vtkContainerBox::GetPolyData() const {
}
void vtkContainerBox::contentUpdate() {
void vtkContainerBox::Update() {
RecursiveMutex::ScopedLock lock(this->m_UpdateMutex);
if (!m_Content)
return;
if (!m_Content) return;
vtkProp3D* root = vtkProp3D::SafeDownCast(this->GetProp());
if (!root) return;
if (root) {
// Apply local full matrix (TRS * LocalBox) so that nested assemblies work correctly
Matrix4f fullLocal = m_Content->GetMatrix() * m_Content->GetLocalMatrix();
vtkNew<vtkMatrix4x4> m;
Matrix4fToVtk(fullLocal, m);
root->SetUserMatrix(m);
root->Modified();
}
d->m_Cube->SetUserMatrix(nullptr);
d->m_Axes->SetUserMatrix(nullptr);
TRS trs(*m_Content);
this->ApplyTransform(root);
root->Modified();
m_BlockUpdate = false;
Puppet::Update();
// Delegate rest of update (appearance, render, etc)
ConnectionBlock blocker(d->m_UpdateSignal);
this->Puppet::Update();
}
void vtkContainerBox::Update() {
this->contentUpdate();
}
void vtkContainerBox::SyncFromVtk() {
RecursiveMutex::ScopedLock lock(this->m_UpdateMutex);
if (!m_Content) return;
vtkProp3D* assembly = vtkProp3D::SafeDownCast(this->GetProp());
if (!assembly) return;
double pos[3], ori[3], scale[3];
assembly->GetPosition(pos);
assembly->GetOrientation(ori);
assembly->GetScale(scale);
vtkProp3D* root = this->GetProxyProp();
if (!root) return;
// VTK -> Model: Extract new world TRS from proxy, which matches the model's TRS center
vtkMatrix4x4* rootMat = root->GetUserMatrix();
if (rootMat) {
std::cout << "[vtkContainerBox::SyncFromVtk] Read Proxy UserMatrix:" << std::endl;
rootMat->Print(std::cout);
}
m_Content->SetPosition(Vector3f(pos[0], pos[1], pos[2]));
m_Content->SetOrientation(Vector3f(ori[0], ori[1], ori[2]) * CLHEP::degree);
m_Content->SetScale(Vector3f(scale[0], scale[1], scale[2]));
Matrix4f vtkWorld = VtkToMatrix4f(rootMat);
m_Content->Updated(); // Notify change
// Synchronize TRS property members from the updated local matrix
m_Content->FromMatrix(vtkWorld);
std::cout << "[vtkContainerBox::SyncFromVtk] New Model WorldMatrix:" << std::endl << m_Content->GetWorldMatrix() << std::endl;
// Since we modified the model, notify observers, but block the loop back to VTK
// ConnectionBlock blocker(d->m_UpdateSignal);
m_Content->Updated();
}
@@ -154,17 +167,16 @@ void vtkContainerBox::InstallPipe() {
mapper->SetInputConnection(axes->GetOutputPort());
mapper->Update();
this->SetProp(d->m_Cube);
this->SetProp(d->m_Axes);
d->m_VtkAsm->AddPart(d->m_Cube);
d->m_VtkAsm->AddPart(d->m_Axes);
this->SetProp(d->m_VtkAsm);
vtkProp3D* root = vtkProp3D::SafeDownCast(this->GetProp());
vtkProp3D* root = d->m_VtkAsm;
if (root) {
TRS trs(*c);
root->SetPosition(trs.position.x(), trs.position.y(), trs.position.z());
root->SetOrientation(trs.rotation.x(), trs.rotation.y(), trs.rotation.z());
root->SetScale(trs.scaling.x(), trs.scaling.y(), trs.scaling.z());
root->SetUserMatrix(nullptr);
d->m_Affine = Matrix4fToVtk(m_Content->GetMatrix());
root->SetUserMatrix(d->m_Affine);
}
this->Update();
}
} // namespace Vtk

View File

@@ -46,9 +46,14 @@ public:
virtual class vtkPolyData *GetPolyData() const;
virtual void contentUpdate();
/**
* @brief Updates the VTK representation from the internal state.
*/
virtual void Update() override;
/**
* @brief Synchronizes the model from the VTK representation (VTK→model).
*/
virtual void SyncFromVtk() override;
virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; }
@@ -59,6 +64,7 @@ protected:
struct ContainerBoxData *d;
Content *m_Content;
bool m_BlockUpdate = false;
};
} // namespace Vtk

View File

@@ -40,7 +40,7 @@ namespace Vtk {
vtkCylinder::vtkCylinder(vtkCylinder::Content *content)
: m_Content(content), m_Actor(nullptr), m_VtkAsm(nullptr) {
this->InstallPipe();
Object::connect(m_Content, &Content::Updated, this, &vtkCylinder::contentUpdate);
Object::connect(m_Content, &uLib::Object::Updated, this, &vtkCylinder::Update);
}
vtkCylinder::~vtkCylinder() {
@@ -48,59 +48,56 @@ vtkCylinder::~vtkCylinder() {
if (m_VtkAsm) m_VtkAsm->Delete();
}
void vtkCylinder::contentUpdate() {
void vtkCylinder::Update() {
if (!m_Content)
return;
vtkProp3D* root = vtkProp3D::SafeDownCast(this->GetProp());
if (!root) return;
if (root) {
// 1. Placement handled specifically from content (use TRS GetMatrix, not World)
vtkNew<vtkMatrix4x4> m;
Matrix4fToVtk(m_Content->GetMatrix(), m);
root->SetUserMatrix(m);
// 1. Placement handled by base Puppet class via Sync / Update logic
// Update internal pd->m_Transform from content
Puppet::Update();
// 2. Shape-local properties (Radius, Height, Axis alignment) go to the internal actor
// These are relative to the root assembly
vtkTransform* alignment = vtkTransform::SafeDownCast(m_Actor->GetUserTransform());
if (alignment) {
alignment->Identity();
alignment->PostMultiply();
// Initial source is centered Y-cylinder (Radial XZ [-1,1], Height Y [-0.5, 0.5])
// Apply Radius and Height scaling
alignment->Scale(m_Content->GetRadius(), m_Content->GetHeight(), m_Content->GetRadius());
// 2. Shape-local properties (Radius, Height, Axis alignment) go to the internal actor
// These are relative to the root assembly
vtkTransform* alignment = vtkTransform::SafeDownCast(m_Actor->GetUserTransform());
if (alignment) {
alignment->Identity();
alignment->PostMultiply();
// Initial source is centered Y-cylinder (Radial XZ [-1,1], Height Y [-0.5, 0.5])
// Apply Radius and Height scaling
alignment->Scale(m_Content->GetRadius(), m_Content->GetHeight(), m_Content->GetRadius());
// Apply Axis alignment
int axis = m_Content->GetAxis();
if (axis == 0) alignment->RotateZ(-90); // Y -> X
else if (axis == 1) ; // Y -> Y (identity)
else if (axis == 2) alignment->RotateX(90); // Y -> Z
}
// Apply Axis alignment
int axis = m_Content->GetAxis();
if (axis == 0) alignment->RotateZ(-90); // Y -> X
else if (axis == 1) ; // Y -> Y (identity)
else if (axis == 2) alignment->RotateX(90); // Y -> Z
root->Modified();
}
root->Modified();
}
void vtkCylinder::Update() {
this->contentUpdate();
// Use base class sync, which handles appearance and children
this->Puppet::Update();
}
void vtkCylinder::SyncFromVtk() {
if (!m_Content) return;
vtkProp3D* assembly = vtkProp3D::SafeDownCast(this->GetProp());
vtkProp3D* assembly = this->GetProxyProp();
if (!assembly) return;
double pos[3], ori[3], scale[3];
assembly->GetPosition(pos);
assembly->GetOrientation(ori);
assembly->GetScale(scale);
// VTK -> Model: Update TRS properties from VTK matrix via world transform
m_Content->SetWorldMatrix(VtkToMatrix4f(assembly->GetUserMatrix()));
m_Content->SetPosition(Vector3f(pos[0], pos[1], pos[2]));
// Convert VTK degrees to model radians
m_Content->SetOrientation(Vector3f(ori[0], ori[1], ori[2]) * CLHEP::degree);
m_Content->SetScale(Vector3f(scale[0], scale[1], scale[2]));
// Resync TRS property members (pos/rot/scale) from the newly set local matrix
m_Content->FromMatrix(m_Content->GetMatrix());
m_Content->Updated(); // Notify change
// Since we modified the model, notify observers, but block the loop back to VTK
m_Content->Updated();
}
void vtkCylinder::InstallPipe() {
@@ -127,7 +124,7 @@ void vtkCylinder::InstallPipe() {
m_VtkAsm->AddPart(m_Actor);
this->contentUpdate();
this->Update();
}
} // namespace Vtk

View File

@@ -48,10 +48,7 @@ public:
vtkCylinder(Content *content);
virtual ~vtkCylinder();
/** Synchronizes the VTK actor with the uLib model matrix */
virtual void contentUpdate();
/** Synchronizes the uLib model matrix with the VTK actor (e.g., after UI manipulation) */
/** Synchronizes the VTK actor with the uLib model matrix and vice-versa */
virtual void Update() override;
/** Synchronizes the uLib model matrix with the VTK actor specifically for gizmo interactions */

View File

@@ -124,6 +124,7 @@ void vtkVoxImage::SetContent() {
vtkVoxImage::vtkVoxImage(Content &content)
: m_Content(content), m_Actor(vtkVolume::New()),
m_Asm(vtkAssembly::New()),
m_Image(vtkImageData::New()), m_Outline(vtkCubeSource::New()),
m_OutlineActor(vtkActor::New()),
m_Reader(NULL), m_Writer(NULL), writer_factor(1.E6),
@@ -136,6 +137,7 @@ vtkVoxImage::vtkVoxImage(Content &content)
vtkVoxImage::~vtkVoxImage() {
m_Image->Delete();
m_Actor->Delete();
m_Asm->Delete();
m_Outline->Delete();
m_OutlineActor->Delete();
}
@@ -330,8 +332,9 @@ void vtkVoxImage::InstallPipe() {
m_OutlineActor->GetProperty()->SetRepresentationToWireframe();
m_OutlineActor->GetProperty()->SetAmbient(0.7);
this->SetProp(m_Actor);
this->SetProp(m_OutlineActor);
m_Asm->AddPart(m_Actor);
m_Asm->AddPart(m_OutlineActor);
this->SetProp(m_Asm);
// Default look
this->SetRepresentation(Surface);

View File

@@ -31,6 +31,7 @@
#include <vtkVolume.h>
#include <vtkXMLImageDataReader.h>
#include <vtkXMLImageDataWriter.h>
#include <vtkAssembly.h>
#include <Math/VoxImage.h>
@@ -77,6 +78,7 @@ private:
vtkImageData *m_Image;
vtkCubeSource *m_Outline;
vtkActor *m_OutlineActor;
vtkAssembly *m_Asm;
vtkXMLImageDataReader *m_Reader;
vtkXMLImageDataWriter *m_Writer;

View File

@@ -58,6 +58,7 @@
#include <vtkPolyData.h>
#include <vtkFeatureEdges.h>
#include <vtkTransform.h>
#include <vtkCubeSource.h>
#include <vtkRenderWindow.h>
#include "uLibVtkInterface.h"
@@ -65,6 +66,7 @@
#include "Math/Dense.h"
#include "Vtk/Math/vtkDense.h"
#include "Core/Property.h"
#include "Math/Transform.h"
@@ -83,7 +85,7 @@ public:
PuppetData(Puppet* owner) :
m_Puppet(owner),
m_Renderers(vtkSmartPointer<vtkRendererCollection>::New()),
m_Assembly(vtkSmartPointer<vtkAssembly>::New()),
m_Prop(nullptr),
m_ShowBoundingBox(false),
m_ShowScaleMeasures(false),
m_Representation(Puppet::Surface),
@@ -103,7 +105,7 @@ public:
Puppet *m_Puppet;
// members //
vtkSmartPointer<vtkRendererCollection> m_Renderers;
vtkSmartPointer<vtkAssembly> m_Assembly;
vtkSmartPointer<vtkProp3D> m_Prop;
vtkSmartPointer<vtkOutlineSource> m_OutlineSource;
vtkSmartPointer<vtkActor> m_OutlineActor;
@@ -152,30 +154,26 @@ public:
} else if (vtkAssembly *asm_p = vtkAssembly::SafeDownCast(p)) {
// Recursively apply to parts of the assembly
vtkProp3DCollection *parts = asm_p->GetParts();
parts->InitTraversal();
for (int i = 0; i < parts->GetNumberOfItems(); ++i) {
this->ApplyAppearance(parts->GetNextProp3D());
if (parts) {
parts->InitTraversal();
for (int i = 0; i < parts->GetNumberOfItems(); ++i) {
this->ApplyAppearance(parts->GetNextProp3D());
}
}
}
}
void ApplyTransform(vtkProp3D* p3d) {
if (p3d) {
if (auto* content = dynamic_cast<uLib::AffineTransform*>(m_Puppet->GetContent())) {
vtkNew<vtkMatrix4x4> m;
Matrix4fToVtk(content->GetWorldMatrix(), m);
p3d->SetUserMatrix(m);
} else {
p3d->SetUserMatrix(nullptr);
p3d->SetPosition(m_Transform.position.x(), m_Transform.position.y(), m_Transform.position.z());
// Convert Model Radians to VTK Degrees
p3d->SetOrientation(m_Transform.rotation.x() / CLHEP::degree,
m_Transform.rotation.y() / CLHEP::degree,
m_Transform.rotation.z() / CLHEP::degree);
p3d->SetScale(m_Transform.scaling.x(), m_Transform.scaling.y(), m_Transform.scaling.z());
}
p3d->SetUserMatrix(nullptr);
p3d->SetPosition(m_Transform.position.x(), m_Transform.position.y(), m_Transform.position.z());
// Convert Model Radians to VTK Degrees
p3d->SetOrientation(m_Transform.rotation.x() / CLHEP::degree,
m_Transform.rotation.y() / CLHEP::degree,
m_Transform.rotation.z() / CLHEP::degree);
p3d->SetScale(m_Transform.scaling.x(), m_Transform.scaling.y(), m_Transform.scaling.z());
}
}
@@ -183,13 +181,21 @@ public:
if (m_Selected) {
// Find first polydata in assembly to highlight
vtkPolyData* polydata = nullptr;
vtkPropCollection *parts = m_Assembly->GetParts();
parts->InitTraversal();
for (int i = 0; i < parts->GetNumberOfItems(); ++i) {
vtkActor *actor = vtkActor::SafeDownCast(parts->GetNextProp());
if (actor && actor->GetMapper()) {
if (vtkActor *actor = vtkActor::SafeDownCast(m_Prop)) {
if (actor->GetMapper()) {
polydata = vtkPolyData::SafeDownCast(actor->GetMapper()->GetDataSetInput());
if (polydata) break;
}
} else if (vtkAssembly *asm_p = vtkAssembly::SafeDownCast(m_Prop)) {
vtkPropCollection *parts = asm_p->GetParts();
if (parts) {
parts->InitTraversal();
for (int i = 0; i < parts->GetNumberOfItems(); ++i) {
vtkActor *a = vtkActor::SafeDownCast(parts->GetNextProp());
if (a && a->GetMapper()) {
polydata = vtkPolyData::SafeDownCast(a->GetMapper()->GetDataSetInput());
if (polydata) break;
}
}
}
}
@@ -205,40 +211,50 @@ public:
}
if (!m_HighlightActor) {
vtkSmartPointer<vtkFeatureEdges> edges = vtkSmartPointer<vtkFeatureEdges>::New();
edges->BoundaryEdgesOn();
edges->FeatureEdgesOn();
edges->SetFeatureAngle(30);
edges->NonManifoldEdgesOn();
edges->ManifoldEdgesOff();
edges->SetInputData(polydata);
vtkSmartPointer<vtkCubeSource> cube = vtkSmartPointer<vtkCubeSource>::New();
double bounds[6];
polydata->GetBounds(bounds);
// Add a small padding to prevent z-fighting
double maxDim = std::max({bounds[1]-bounds[0], bounds[3]-bounds[2], bounds[5]-bounds[4]});
double pad = maxDim * 0.02;
if(pad < 1e-4) pad = 0.05;
cube->SetBounds(bounds[0]-pad, bounds[1]+pad,
bounds[2]-pad, bounds[3]+pad,
bounds[4]-pad, bounds[5]+pad);
m_HighlightActor = vtkSmartPointer<vtkActor>::New();
vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(edges->GetOutputPort());
mapper->SetInputConnection(cube->GetOutputPort());
m_HighlightActor->SetMapper(mapper);
m_HighlightActor->GetProperty()->SetColor(1.0, 0.5, 0.0); // Orange
m_HighlightActor->GetProperty()->SetRepresentationToWireframe();
m_HighlightActor->GetProperty()->SetColor(1.0, 0.0, 0.0); // Red
m_HighlightActor->GetProperty()->SetLineWidth(2.0);
m_HighlightActor->GetProperty()->SetLighting(0);
} else {
// Update input data (safe even if same)
if (auto* mapper = vtkPolyDataMapper::SafeDownCast(m_HighlightActor->GetMapper())) {
if (auto* edges = vtkFeatureEdges::SafeDownCast(mapper->GetInputAlgorithm())) {
edges->SetInputData(polydata);
if (auto* cube = vtkCubeSource::SafeDownCast(mapper->GetInputAlgorithm())) {
double bounds[6];
polydata->GetBounds(bounds);
double maxDim = std::max({bounds[1]-bounds[0], bounds[3]-bounds[2], bounds[5]-bounds[4]});
double pad = maxDim * 0.02;
if(pad < 1e-4) pad = 0.05;
cube->SetBounds(bounds[0]-pad, bounds[1]+pad,
bounds[2]-pad, bounds[3]+pad,
bounds[4]-pad, bounds[5]+pad);
cube->Modified();
}
}
}
// Update highlight matrix from the root prop
vtkProp3D* root = nullptr;
if (m_Assembly->GetParts()->GetNumberOfItems() == 1) {
root = vtkProp3D::SafeDownCast(m_Assembly->GetParts()->GetLastProp());
} else {
root = m_Assembly;
}
if (root) {
m_HighlightActor->SetUserMatrix(root->GetMatrix());
// Update highlight matrix from the model world matrix
if (m_Puppet) {
if (auto* content = m_Puppet->GetContent()) {
if (auto* tr = dynamic_cast<uLib::TRS*>(content)) {
vtkNew<vtkMatrix4x4> vwm;
Matrix4fToVtk(tr->GetWorldMatrix(), vwm);
m_HighlightActor->SetUserMatrix(vwm);
}
}
}
m_Renderers->InitTraversal();
@@ -285,36 +301,38 @@ Puppet::~Puppet()
vtkProp *Puppet::GetProp()
{
if (pd->m_Assembly->GetParts()->GetNumberOfItems() == 1)
return pd->m_Assembly->GetParts()->GetLastProp();
else
return pd->m_Assembly;
return pd->m_Prop;
}
vtkProp3D *Puppet::GetProxyProp()
{
// The handler should manipulate the highlight actor if it exists
if (pd->m_HighlightActor) {
return pd->m_HighlightActor;
}
return vtkProp3D::SafeDownCast(this->GetProp());
}
void Puppet::SetProp(vtkProp *prop)
{
if(prop) {
prop->SetPickable(pd->m_Selectable);
if (auto* p3d = vtkProp3D::SafeDownCast(prop)) {
pd->m_Assembly->AddPart(p3d);
}
pd->m_Prop = vtkProp3D::SafeDownCast(prop);
pd->ApplyAppearance(prop);
// For the first actor added, seed the tracked display values from the VTK
// actor's current state so the display properties panel shows meaningful
// initial values instead of the -1 "not-overriding" sentinels.
if (pd->m_Assembly->GetParts()->GetNumberOfItems() == 1) {
if (auto* actor = vtkActor::SafeDownCast(prop)) {
vtkProperty* vp = actor->GetProperty();
if (pd->m_Representation < 0)
pd->m_Representation = vp->GetRepresentation();
if (pd->m_Opacity < 0)
pd->m_Opacity = vp->GetOpacity();
if (pd->m_Color.x() < 0) {
double c[3];
vp->GetColor(c);
pd->m_Color = Vector3d(c[0], c[1], c[2]);
}
if (auto* actor = vtkActor::SafeDownCast(prop)) {
vtkProperty* vp = actor->GetProperty();
if (pd->m_Representation < 0)
pd->m_Representation = vp->GetRepresentation();
if (pd->m_Opacity < 0)
pd->m_Opacity = vp->GetOpacity();
if (pd->m_Color.x() < 0) {
double c[3];
vp->GetColor(c);
pd->m_Color = Vector3d(c[0], c[1], c[2]);
}
}
}
@@ -338,12 +356,18 @@ void Puppet::ApplyTransform(vtkProp3D* p3d)
vtkPropCollection *Puppet::GetParts()
{
return pd->m_Assembly->GetParts();
if (auto* asm_p = vtkAssembly::SafeDownCast(pd->m_Prop)) {
return asm_p->GetParts();
}
return nullptr;
}
vtkPropCollection *Puppet::GetProps()
{
return pd->m_Assembly->GetParts();
if (auto* asm_p = vtkAssembly::SafeDownCast(pd->m_Prop)) {
return asm_p->GetParts();
}
return nullptr;
}
void Puppet::ConnectRenderer(vtkRenderer *renderer)
@@ -397,7 +421,8 @@ vtkRendererCollection *Puppet::GetRenderers() const
void Puppet::PrintSelf(std::ostream &o) const
{
o << "Props Assembly: \n";
pd->m_Assembly->PrintSelf(o,vtkIndent(1));
if (pd->m_Prop)
pd->m_Prop->PrintSelf(o,vtkIndent(1));
o << "Connected Renderers: \n";
pd->m_Renderers->PrintSelf(o,vtkIndent(1));
@@ -417,9 +442,11 @@ void Puppet::ShowBoundingBox(bool show)
pd->m_OutlineActor->GetProperty()->SetColor(1.0, 1.0, 1.0);
}
double* bounds = pd->m_Assembly->GetBounds();
pd->m_OutlineSource->SetBounds(bounds);
pd->m_OutlineSource->Update();
if (pd->m_Prop) {
double* bounds = pd->m_Prop->GetBounds();
pd->m_OutlineSource->SetBounds(bounds);
pd->m_OutlineSource->Update();
}
pd->m_Renderers->InitTraversal();
for (int i = 0; i < pd->m_Renderers->GetNumberOfItems(); ++i) {
@@ -449,8 +476,10 @@ void Puppet::ShowScaleMeasures(bool show)
pd->m_CubeAxesActor->GetProperty()->SetColor(1.0, 1.0, 1.0);
}
double* bounds = pd->m_Assembly->GetBounds();
pd->m_CubeAxesActor->SetBounds(bounds);
if (pd->m_Prop) {
double* bounds = pd->m_Prop->GetBounds();
pd->m_CubeAxesActor->SetBounds(bounds);
}
pd->m_Renderers->InitTraversal();
for (int i = 0; i < pd->m_Renderers->GetNumberOfItems(); ++i) {
@@ -472,12 +501,7 @@ void Puppet::ShowScaleMeasures(bool show)
void Puppet::SetRepresentation(Representation mode)
{
pd->m_Representation = static_cast<int>(mode);
vtkProp3DCollection *props = pd->m_Assembly->GetParts();
props->InitTraversal();
for (int i = 0; i < props->GetNumberOfItems(); ++i) {
pd->ApplyAppearance(props->GetNextProp3D());
}
pd->ApplyAppearance(pd->m_Prop);
}
void Puppet::SetRepresentation(const char *mode)
@@ -497,23 +521,13 @@ void Puppet::SetColor(double r, double g, double b)
pd->m_Color[0] = r;
pd->m_Color[1] = g;
pd->m_Color[2] = b;
vtkProp3DCollection *props = pd->m_Assembly->GetParts();
props->InitTraversal();
for (int i = 0; i < props->GetNumberOfItems(); ++i) {
pd->ApplyAppearance(props->GetNextProp3D());
}
pd->ApplyAppearance(pd->m_Prop);
}
void Puppet::SetOpacity(double alpha)
{
pd->m_Opacity = alpha;
vtkProp3DCollection *props = pd->m_Assembly->GetParts();
props->InitTraversal();
for (int i = 0; i < props->GetNumberOfItems(); ++i) {
pd->ApplyAppearance(props->GetNextProp3D());
}
pd->ApplyAppearance(pd->m_Prop);
}
@@ -525,11 +539,7 @@ void Puppet::SetOpacity(double alpha)
void Puppet::SetSelectable(bool selectable)
{
pd->m_Selectable = selectable;
vtkProp3DCollection *props = pd->m_Assembly->GetParts();
props->InitTraversal();
for (int i = 0; i < props->GetNumberOfItems(); ++i) {
props->GetNextProp3D()->SetPickable(selectable);
}
pd->ApplyAppearance(pd->m_Prop);
}
bool Puppet::IsSelectable() const
@@ -552,38 +562,27 @@ bool Puppet::IsSelected() const
void Puppet::Update()
{
vtkProp* root = this->GetProp();
if (root) {
// Handle transformation synchronization from content
if (auto* content = dynamic_cast<uLib::AffineTransform*>(GetContent())) {
pd->m_Transform = *content; // Uses TRS(const AffineTransform&)
}
// Derived classes should have updated the transform if they override Update()
// or we can apply base transform if it's default:
// pd->ApplyTransform(pd->m_Prop);
if (auto* p3d = vtkProp3D::SafeDownCast(root)) {
pd->ApplyTransform(p3d);
}
pd->ApplyAppearance(root);
}
vtkProp3DCollection *props = pd->m_Assembly->GetParts();
props->InitTraversal();
for (int i = 0; i < props->GetNumberOfItems(); ++i) {
pd->ApplyAppearance(props->GetNextProp3D());
}
pd->ApplyAppearance(pd->m_Prop);
if (pd->m_Selected) {
pd->UpdateHighlight();
}
if (pd->m_ShowBoundingBox) {
double* bounds = pd->m_Assembly->GetBounds();
pd->m_OutlineSource->SetBounds(bounds);
pd->m_OutlineSource->Update();
}
if (pd->m_ShowScaleMeasures) {
double* bounds = pd->m_Assembly->GetBounds();
pd->m_CubeAxesActor->SetBounds(bounds);
if (pd->m_Prop) {
if (pd->m_ShowBoundingBox) {
double* bounds = pd->m_Prop->GetBounds();
pd->m_OutlineSource->SetBounds(bounds);
pd->m_OutlineSource->Update();
}
if (pd->m_ShowScaleMeasures) {
double* bounds = pd->m_Prop->GetBounds();
pd->m_CubeAxesActor->SetBounds(bounds);
}
}
// Notify that the object has been updated (important for UI refresh)
@@ -599,37 +598,6 @@ void Puppet::Update()
}
void Puppet::SyncFromVtk()
{
vtkProp* root = this->GetProp();
if (auto* p3d = vtkProp3D::SafeDownCast(root)) {
// Handle content synchronization if it's an AffineTransform
if (auto* content = dynamic_cast<uLib::AffineTransform*>(GetContent())) {
// Apply current VTK world transform to model.
// When 'sinking the chain', p3d's matrix represents the world matrix
// because it has no parent in VTK (nesting was removed).
vtkNew<vtkMatrix4x4> m;
p3d->GetMatrix(m);
content->SetWorldMatrix(VtkToMatrix4f(m));
// Sync local TRS from the newly updated model
pd->m_Transform = *content;
}
else {
// Fallback for simple props
double pos[3], ori[3], scale[3];
p3d->GetPosition(pos);
p3d->GetOrientation(ori);
p3d->GetScale(scale);
pd->m_Transform.position = Vector3f(pos[0], pos[1], pos[2]);
pd->m_Transform.rotation = Vector3f(ori[0], ori[1], ori[2]) * CLHEP::degree;
pd->m_Transform.scaling = Vector3f(scale[0], scale[1], scale[2]);
}
this->Object::Updated();
}
}
void Puppet::ConnectInteractor(vtkRenderWindowInteractor *interactor)
{
}
@@ -653,7 +621,8 @@ struct AppearanceProxy {
void serialize(Archive & ar, const unsigned int version) {
ar & boost::serialization::make_hrp("Color", pd->m_Color, "color");
ar & boost::serialization::make_hrp("Opacity", pd->m_Opacity).range(0.0, 1.0).set_default(1.0);
ar & boost::serialization::make_hrp_enum("Representation", pd->m_Representation, {"Points", "Wireframe", "Surface", "SurfaceWithEdges", "Volume", "Outline", "Slice"});
ar & boost::serialization::make_hrp_enum("Representation",
pd->m_Representation, {"Points", "Wireframe", "Surface", "SurfaceWithEdges", "Volume", "Outline", "Slice"});
ar & boost::serialization::make_hrp("Visibility", pd->m_Visibility);
ar & boost::serialization::make_hrp("Pickable", pd->m_Selectable);
ar & boost::serialization::make_hrp("Dragable", pd->m_Dragable);

View File

@@ -26,15 +26,15 @@
#ifndef ULIBVTKINTERFACE_H
#define ULIBVTKINTERFACE_H
#include "Core/Monitor.h"
#include "Core/Object.h"
#include "Core/Property.h"
#include <boost/mpl/bool.hpp>
#include <boost/serialization/serialization.hpp>
#include <boost/type_traits/is_class.hpp>
#include <iomanip>
#include <ostream>
#include <vector>
#include <boost/type_traits/is_class.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/serialization/serialization.hpp>
#include "Core/Object.h"
#include "Core/Property.h"
#include "Core/Monitor.h"
// vtk classes forward declaration //
class vtkProp;
@@ -46,22 +46,27 @@ class vtkRendererCollection;
class vtkRenderWindowInteractor;
namespace uLib {
namespace Archive { class display_properties_archive; }
namespace Vtk { class Puppet; class Viewer; }
namespace Archive {
class display_properties_archive;
}
namespace Vtk {
class Puppet;
class Viewer;
} // namespace Vtk
} // namespace uLib
namespace uLib {
namespace Vtk {
class Puppet : public uLib::Object {
uLibTypeMacro(Puppet, uLib::Object)
uLibTypeMacro(Puppet, uLib::Object)
public:
Puppet();
public : Puppet();
virtual ~Puppet();
virtual vtkProp *GetProp();
virtual vtkProp3D *GetProxyProp();
virtual vtkPropCollection *GetParts();
@@ -87,10 +92,31 @@ public:
void SetSelected(bool selected = true);
bool IsSelected() const;
/**
* @brief Synchronizes the VTK representation with the internal state and properties.
*
* This method should be called whenever the underlying model or display properties
* are modified to ensure the visual representation in VTK is consistent.
*/
virtual void Update();
virtual void SyncFromVtk();
enum Representation { Points = 0, Wireframe = 1, Surface = 2, SurfaceWithEdges = 3, Volume = 4, Outline = 5, Slice = 6 };
/**
* @brief Synchronizes the internal state and properties from the VTK representation.
*
* This method should be called when the VTK representation has been modified
* (e.g., via a gizmo) and the changes need to be pushed back to the model.
*/
virtual void SyncFromVtk() {}
enum Representation {
Points = 0,
Wireframe = 1,
Surface = 2,
SurfaceWithEdges = 3,
Volume = 4,
Outline = 5,
Slice = 6
};
void SetRepresentation(Representation mode);
void SetRepresentation(const char *mode);
@@ -101,10 +127,15 @@ public:
vtkRendererCollection *GetRenderers() const;
const std::vector<uLib::PropertyBase*>& GetDisplayProperties() const { return m_DisplayProperties; }
void RegisterDisplayProperty(uLib::PropertyBase* prop) { m_DisplayProperties.push_back(prop); }
const std::vector<uLib::PropertyBase *> &GetDisplayProperties() const {
return m_DisplayProperties;
}
void RegisterDisplayProperty(uLib::PropertyBase *prop) {
m_DisplayProperties.push_back(prop);
}
virtual void serialize_display(uLib::Archive::display_properties_archive & ar, const unsigned int version = 0);
virtual void serialize_display(uLib::Archive::display_properties_archive &ar,
const unsigned int version = 0);
virtual void ConnectInteractor(class vtkRenderWindowInteractor *interactor);
@@ -116,15 +147,15 @@ protected:
void RemoveProp(vtkProp *prop);
void ApplyAppearance(vtkProp* prop);
void ApplyTransform(vtkProp3D* p3d);
void ApplyAppearance(vtkProp *prop);
void ApplyTransform(vtkProp3D *p3d);
std::vector<uLib::PropertyBase*> m_DisplayProperties;
std::vector<uLib::PropertyBase *> m_DisplayProperties;
mutable uLib::RecursiveMutex m_UpdateMutex;
private:
Puppet(const Puppet&) = delete;
Puppet& operator=(const Puppet&) = delete;
Puppet(const Puppet &) = delete;
Puppet &operator=(const Puppet &) = delete;
friend class PuppetData;
class PuppetData *pd;
@@ -133,104 +164,110 @@ private:
} // namespace Vtk
} // namespace uLib
// -------------------------------------------------------------------------- //
// DISPLAY PROPERTIES SERIALIZE
// DISPLAY PROPERTIES SERIALIZE
// -------------------------------------------------------------------------- //
namespace uLib {
namespace Archive {
/**
* @brief Specialized archive for registering display-only properties in Puppets.
* @brief Specialized archive for registering display-only properties in
* Puppets.
*/
class display_properties_archive : public boost::archive::detail::common_oarchive<display_properties_archive> {
class display_properties_archive
: public boost::archive::detail::common_oarchive<
display_properties_archive> {
public:
display_properties_archive(Vtk::Puppet* puppet) :
boost::archive::detail::common_oarchive<display_properties_archive>(boost::archive::no_header),
display_properties_archive(Vtk::Puppet *puppet)
: boost::archive::detail::common_oarchive<display_properties_archive>(
boost::archive::no_header),
m_Puppet(puppet) {}
std::string GetCurrentGroup() const {
std::string group;
for (const auto& g : m_GroupStack) {
if (!group.empty()) group += ".";
group += g;
}
return group;
std::string GetCurrentGroup() const {
std::string group;
for (const auto &g : m_GroupStack) {
if (!group.empty())
group += ".";
group += g;
}
return group;
}
template<class T>
void save_override(const boost::serialization::hrp<T> &t) {
if (m_Puppet) {
uLib::Property<T>* p = new uLib::Property<T>(m_Puppet, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value(), t.units() ? t.units() : "", GetCurrentGroup());
if (t.has_range()) p->SetRange(t.min_val(), t.max_val());
if (t.has_default()) p->SetDefault(t.default_val());
m_Puppet->RegisterDisplayProperty(p);
Vtk::Puppet* puppet = m_Puppet;
uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); });
}
template <class T> void save_override(const boost::serialization::hrp<T> &t) {
if (m_Puppet) {
uLib::Property<T> *p = new uLib::Property<T>(
m_Puppet, t.name(),
&const_cast<boost::serialization::hrp<T> &>(t).value(),
t.units() ? t.units() : "", GetCurrentGroup());
if (t.has_range())
p->SetRange(t.min_val(), t.max_val());
if (t.has_default())
p->SetDefault(t.default_val());
m_Puppet->RegisterDisplayProperty(p);
Vtk::Puppet *puppet = m_Puppet;
uLib::Object::connect(p, &uLib::PropertyBase::Updated,
[puppet]() { puppet->Update(); });
}
}
template<class T>
void save_override(const boost::serialization::hrp_enum<T> &t) {
if (m_Puppet) {
uLib::EnumProperty* p = new uLib::EnumProperty(m_Puppet, t.name(), (int*)&const_cast<boost::serialization::hrp_enum<T>&>(t).value(), t.labels(), t.units() ? t.units() : "", GetCurrentGroup());
m_Puppet->RegisterDisplayProperty(p);
Vtk::Puppet* puppet = m_Puppet;
uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); });
}
template <class T>
void save_override(const boost::serialization::hrp_enum<T> &t) {
if (m_Puppet) {
uLib::EnumProperty *p = new uLib::EnumProperty(
m_Puppet, t.name(),
(int *)&const_cast<boost::serialization::hrp_enum<T> &>(t).value(),
t.labels(), t.units() ? t.units() : "", GetCurrentGroup());
m_Puppet->RegisterDisplayProperty(p);
Vtk::Puppet *puppet = m_Puppet;
uLib::Object::connect(p, &uLib::PropertyBase::Updated,
[puppet]() { puppet->Update(); });
}
}
template<class T> void save_override(const boost::serialization::nvp<T> &t) {
if (t.name()) m_GroupStack.push_back(t.name());
this->save_helper(t.const_value(), typename boost::is_class<T>::type());
if (t.name()) m_GroupStack.pop_back();
}
template <class T> void save_override(const boost::serialization::nvp<T> &t) {
if (t.name())
m_GroupStack.push_back(t.name());
this->save_helper(t.const_value(), typename boost::is_class<T>::type());
if (t.name())
m_GroupStack.pop_back();
}
// Recursion for nested classes, ignore primitives
template<class T> void save_override(const T &t) {
this->save_helper(t, typename boost::is_class<T>::type());
}
// Recursion for nested classes, ignore primitives
template <class T> void save_override(const T &t) {
this->save_helper(t, typename boost::is_class<T>::type());
}
template<class T>
void save_helper(const T &t, boost::mpl::true_) {
boost::serialization::serialize_adl(*this, const_cast<T&>(t), 0);
}
template <class T> void save_helper(const T &t, boost::mpl::true_) {
boost::serialization::serialize_adl(*this, const_cast<T &>(t), 0);
}
template<class T>
void save_helper(const T &t, boost::mpl::false_) {}
template <class T> void save_helper(const T &t, boost::mpl::false_) {}
void save_override(const boost::archive::object_id_type & t) {}
void save_override(const boost::archive::object_reference_type & t) {}
void save_override(const boost::archive::version_type & t) {}
void save_override(const boost::archive::class_id_type & t) {}
void save_override(const boost::archive::class_id_optional_type & t) {}
void save_override(const boost::archive::class_id_reference_type & t) {}
void save_override(const boost::archive::class_name_type & t) {}
void save_override(const boost::archive::tracking_type & t) {}
void save_override(const boost::archive::object_id_type &t) {}
void save_override(const boost::archive::object_reference_type &t) {}
void save_override(const boost::archive::version_type &t) {}
void save_override(const boost::archive::class_id_type &t) {}
void save_override(const boost::archive::class_id_optional_type &t) {}
void save_override(const boost::archive::class_id_reference_type &t) {}
void save_override(const boost::archive::class_name_type &t) {}
void save_override(const boost::archive::tracking_type &t) {}
private:
Vtk::Puppet* m_Puppet;
std::vector<std::string> m_GroupStack;
Vtk::Puppet *m_Puppet;
std::vector<std::string> m_GroupStack;
};
} // namespace Archive
// This macro MUST be defined after both Puppet and display_properties_archive are fully defined.
#define ULIB_ACTIVATE_DISPLAY_PROPERTIES \
{ \
uLib::Archive::display_properties_archive dar(this); \
this->serialize_display(dar, 0); \
}
// This macro MUST be defined after both Puppet and display_properties_archive
// are fully defined.
#define ULIB_ACTIVATE_DISPLAY_PROPERTIES \
{ \
uLib::Archive::display_properties_archive dar(this); \
this->serialize_display(dar, 0); \
}
} // namespace uLib

View File

@@ -547,16 +547,14 @@ void vtkHandlerWidget::OnMouseMove() {
total->Concatenate(op);
if (this->Prop3D) {
double p[3], r[3], s[3];
total->GetPosition(p);
total->GetOrientation(r);
total->GetScale(s);
this->Prop3D->SetPosition(p);
// VTK GetOrientation already returned degrees, so r is in degrees.
// We apply it directly back to VTK.
this->Prop3D->SetOrientation(r);
this->Prop3D->SetScale(s);
this->Prop3D->SetUserMatrix(nullptr);
vtkNew<vtkMatrix4x4> result;
total->GetMatrix(result);
this->Prop3D->SetUserMatrix(result);
// Reset individual TRS components so UserMatrix is the single source of truth
this->Prop3D->SetPosition(0, 0, 0);
this->Prop3D->SetOrientation(0, 0, 0);
this->Prop3D->SetScale(1, 1, 1);
}
this->Prop3D->Modified();

View File

@@ -107,6 +107,12 @@ void vtkObjectsContext::Update() {
}
}
void vtkObjectsContext::SyncFromVtk() {
for (auto const& [obj, puppet] : m_Puppets) {
puppet->SyncFromVtk();
}
}
Puppet* vtkObjectsContext::CreatePuppet(uLib::Object* obj) {
if (!obj) return nullptr;

View File

@@ -30,6 +30,9 @@ public:
/** @brief Updates all managed puppets. */
virtual void Update() override;
/** @brief Synchronizes all managed puppets back to their models. */
virtual void SyncFromVtk() override;
public:
virtual void PuppetAdded(Puppet* puppet);
virtual void PuppetRemoved(Puppet* puppet);

View File

@@ -505,11 +505,11 @@ void Viewport::SelectPuppet(Puppet* prop)
if (pv->m_HandlerWidget) {
if (prop) {
vtkProp3D* prop3d = vtkProp3D::SafeDownCast(prop->GetProp());
vtkProp3D* prop3d = prop->GetProxyProp();
if (prop3d) {
pv->m_HandlerWidget->SetProp3D(prop3d);
pv->m_HandlerWidget->SetEnabled(1);
pv->m_HandlerWidget->PlaceWidget(prop3d->GetBounds());
pv->m_HandlerWidget->PlaceWidget(prop3d->GetBounds()); //TODO: FIX !
}
} else {
pv->m_HandlerWidget->SetEnabled(0);