From e0ffeff5b75b3c946ad5cb57ea8bdd5754df1a17 Mon Sep 17 00:00:00 2001 From: AndreaRigoni Date: Thu, 26 Mar 2026 09:50:52 +0000 Subject: [PATCH] fix some on properties and signal connection --- .gitattributes | 2 + CLAUDE.md | 90 +++++++++++ app/gcompose/src/ContextModel.cpp | 17 +- app/gcompose/src/ContextPanel.cpp | 24 +++ app/gcompose/src/ContextPanel.h | 2 + app/gcompose/src/MainPanel.cpp | 44 +++++- app/gcompose/src/PropertyWidgets.cpp | 119 ++++++++++++-- app/gcompose/src/PropertyWidgets.h | 18 ++- ...st_img_40_trim55505_scale1.00_sigma1.0.vtk | 3 + src/Core/Object.cpp | 10 ++ src/Core/Object.h | 1 + src/Core/Property.h | 47 +++++- src/Core/Serializable.h | 43 ++++- src/Vtk/Math/vtkVoxImage.cpp | 22 ++- src/Vtk/Math/vtkVoxImage.h | 4 +- src/Vtk/uLibVtkInterface.cxx | 148 +++++++++++++++--- src/Vtk/uLibVtkInterface.h | 31 +++- src/Vtk/vtkObjectsContext.cpp | 3 + src/Vtk/vtkQViewport.cpp | 5 + src/Vtk/vtkQViewport.h | 4 + src/Vtk/vtkViewport.cpp | 2 + src/Vtk/vtkViewport.h | 1 + 22 files changed, 578 insertions(+), 62 deletions(-) create mode 100644 .gitattributes create mode 100644 CLAUDE.md create mode 100644 assets/exmaples/vtk/2026_03_24_C1_Prod11_test_img_40_trim55505_scale1.00_sigma1.0.vtk diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..90ea56e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.vtk filter=lfs diff=lfs merge=lfs -text +*.vti filter=lfs diff=lfs merge=lfs -text diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..576bfeb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ + +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +```bash +# Activate the conda environment (required before any build/run) +export MAMBA_EXE="/home/share/micromamba/bin/micromamba" +export MAMBA_ROOT_PREFIX="/home/share/micromamba" +eval "$(/home/share/micromamba/bin/micromamba shell hook --shell bash)" +micromamba activate mutom + +# Configure (from repo root, using Conan preset) +cmake --preset conan-release + +# Build everything +cmake --build build -j$(nproc) + +# Build a specific target +cmake --build build --target gcompose -j$(nproc) + +# Run tests +cmake --build build --target test +# or +ctest --test-dir build + +# Run a single test binary (example) +./build/src/Core/testing/CoreTest + +# Run the gcompose GUI app +./build/app/gcompose/gcompose +``` + +First-time setup (if `build/` does not exist): +```bash +conan profile detect +conan install . --output-folder=build --build=missing +cmake --preset conan-release +``` + +## Architecture + +**uLib** is a C++ framework for Cosmic Muon Tomography (CMT), structured as layered shared libraries: + +``` +mutomCore → mutomMath → mutomDetectors → mutomGeant + ↘ + mutomVtk → gcompose (Qt6 GUI app) + mutomRoot +``` + +### Core Object Model (`src/Core/`) +- All framework objects inherit from `uLib::Object` +- **Property system**: `Property` wraps member pointers with change notification via `PropertyChanged` signal +- **Signal/slot**: `uLib::Object::connect(sender, &Sender::Signal, callback)` — resembles Qt but works for non-QObject classes +- **Serialization**: Boost archives (`xml_oarchive`, `text_oarchive`, `hrt_oarchive`); `hrp` marks fields as "human-readable properties" +- `ObjectsContext` is a container owning a list of `Object*` pointers; signals `ObjectAdded`/`ObjectRemoved` + +### VTK Layer (`src/Vtk/`) +- `Puppet` (inherits `uLib::Object`): wraps a VTK `vtkProp` for rendering. Has `GetContent()` returning the underlying domain object. Display-only properties are registered via `ULIB_ACTIVATE_DISPLAY_PROPERTIES` macro. +- `Viewport`: base class managing the VTK renderer, picking, selection logic. Maintains `m_Puppets` vector and `m_ObjectToPuppet` map. +- `QViewport` (inherits `QWidget` + `Viewport`): Qt-embedded VTK widget. Emits Qt signal `puppetSelected(Puppet*)` on click-selection via `OnSelectionChanged`. +- `vtkObjectsContext`: wraps `ObjectsContext`, creating/destroying `Puppet`s as objects come/go. Emits `PuppetAdded`/`PuppetRemoved`. +- Display properties: `serialize_display()` + `display_properties_archive` registers selected `hrp` fields as `PropertyBase*` in the puppet's `m_DisplayProperties`. `PropertyEditor::setObject(obj, displayOnly=true)` shows only those. + +### gcompose GUI App (`app/gcompose/src/`) +- `MainPanel`: top-level widget. Owns `ContextPanel` (left) and `ViewportPane` (right). Wires together viewport↔context selection via signals. +- `ContextPanel`: tree view of `ObjectsContext`. Emits `objectSelected(Object*)`. Contains an embedded `PropertiesPanel`. +- `PropertiesPanel`: shows `uLib::Object` properties via `PropertyEditor`. +- `ViewportPane`: embeds `QViewport` + a slide-out "Display Properties" panel (`PropertyEditor` in display-only mode). +- `PropertyEditor`: populates widgets from `Object::GetProperties()` (all) or `Puppet::GetDisplayProperties()` (display-only mode). + +### Selection Sync Flow +``` +Viewport click → Viewport::SelectPuppet() → QViewport::OnSelectionChanged() + → emit puppetSelected(p) + → MainPanel: contextPanel->selectObject(p->GetContent()) [updates tree + PropertiesPanel] + → MainPanel: firstPane->setObject(p) [updates Display Properties panel] + +ContextPanel tree click → emit objectSelected(obj) + → MainPanel: viewport->SelectPuppet(puppet) [visual selection in VTK] + → MainPanel: firstPane->setObject(puppet) [updates Display Properties panel] +``` + +### Key Patterns +- **Two signal systems coexist**: Qt signals (`Q_OBJECT`, `connect(...)`) for GUI; `uLib::Object::connect(...)` for domain signals. +- **Display properties** flow: `Puppet::serialize_display()` → `display_properties_archive` → `RegisterDisplayProperty()` → `PropertyEditor(displayOnly=true)`. Must call `ULIB_ACTIVATE_DISPLAY_PROPERTIES` in the puppet constructor. +- **Puppet ↔ Object map**: `Viewport::m_ObjectToPuppet` allows lookup by domain object; `vtkObjectsContext::GetPuppet(obj)` does the same. diff --git a/app/gcompose/src/ContextModel.cpp b/app/gcompose/src/ContextModel.cpp index d88d6ec..dd1d42c 100644 --- a/app/gcompose/src/ContextModel.cpp +++ b/app/gcompose/src/ContextModel.cpp @@ -14,10 +14,25 @@ void ContextModel::setContext(uLib::ObjectsContext* context) { beginResetModel(); m_rootContext = context; if (m_rootContext) { - uLib::Object::connect(m_rootContext, &uLib::Object::Updated, [this]() { + auto refresh = [this]() { this->beginResetModel(); this->endResetModel(); + }; + + uLib::Object::connect(m_rootContext, &uLib::Object::Updated, refresh); + uLib::Object::connect(m_rootContext, &uLib::ObjectsContext::ObjectAdded, [this, refresh](uLib::Object* obj) { + uLib::Object::connect(obj, &uLib::Object::Updated, refresh); + refresh(); }); + uLib::Object::connect(m_rootContext, &uLib::ObjectsContext::ObjectRemoved, [this, refresh](uLib::Object* obj) { + // Disconnect would be good here but not strictly required if refresh handles it + refresh(); + }); + + // Connect existing objects + for (auto* obj : m_rootContext->GetObjects()) { + uLib::Object::connect(obj, &uLib::Object::Updated, refresh); + } } endResetModel(); } diff --git a/app/gcompose/src/ContextPanel.cpp b/app/gcompose/src/ContextPanel.cpp index 6f92c9d..24b7567 100644 --- a/app/gcompose/src/ContextPanel.cpp +++ b/app/gcompose/src/ContextPanel.cpp @@ -92,3 +92,27 @@ void ContextPanel::onSelectionChanged(const QItemSelection& selected, const QIte emit objectSelected(target); m_propertiesPanel->setObject(target); } + +void ContextPanel::selectObject(uLib::Object* obj) { + if (!obj) { + clearSelection(); + return; + } + + for (int i = 0; i < m_model->rowCount(); ++i) { + QModelIndex idx = m_model->index(i, 0); + if (idx.internalPointer() == obj) { + QSignalBlocker blocker(m_treeView->selectionModel()); + m_treeView->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + m_treeView->scrollTo(idx); + m_propertiesPanel->setObject(obj); // Explicitly update properties too + return; + } + } +} + +void ContextPanel::clearSelection() { + QSignalBlocker blocker(m_treeView->selectionModel()); + m_treeView->selectionModel()->clearSelection(); + m_propertiesPanel->setObject(nullptr); +} diff --git a/app/gcompose/src/ContextPanel.h b/app/gcompose/src/ContextPanel.h index dcb8cdd..dd0bca6 100644 --- a/app/gcompose/src/ContextPanel.h +++ b/app/gcompose/src/ContextPanel.h @@ -20,6 +20,8 @@ public: ~ContextPanel(); void setContext(uLib::ObjectsContext* context); + void selectObject(uLib::Object* obj); + void clearSelection(); signals: void objectSelected(uLib::Object* obj); diff --git a/app/gcompose/src/MainPanel.cpp b/app/gcompose/src/MainPanel.cpp index 13ceaa2..c1f2f79 100644 --- a/app/gcompose/src/MainPanel.cpp +++ b/app/gcompose/src/MainPanel.cpp @@ -14,7 +14,10 @@ #include #include #include +#include +#include #include "StyleManager.h" +#include "Math/VoxImage.h" MainPanel::MainPanel(QWidget* parent) : QWidget(parent), m_context(nullptr), m_mainVtkContext(nullptr) { this->setObjectName("MainPanel"); @@ -124,6 +127,17 @@ void MainPanel::setContext(uLib::ObjectsContext* context) { m_mainVtkContext = new uLib::Vtk::vtkObjectsContext(context); // viewport->AddPuppet(*m_mainVtkContext); // redundant + auto syncSelection = [this](uLib::Vtk::Puppet* p) { + if (!p) { + m_contextPanel->clearSelection(); + m_firstPane->setObject(nullptr); + } else { + m_contextPanel->selectObject(p->GetContent()); + m_firstPane->setObject(p); + } + }; + connect(viewport, &uLib::Vtk::QViewport::puppetSelected, syncSelection); + uLib::Object::connect(m_mainVtkContext, &uLib::Vtk::vtkObjectsContext::PuppetAdded, [this](uLib::Vtk::Puppet* p) { if (p) { auto panes = this->findChildren(); @@ -179,7 +193,35 @@ void MainPanel::onCreateObject(const std::string& className) { } void MainPanel::onOpen() { - // Placeholder for open logic + QString fileName = QFileDialog::getOpenFileName(this, "Open File", "", + "VTK/VTI Images (*.vtk *.vti);;All Files (*.*)"); + + if (fileName.isEmpty()) return; + + QFileInfo info(fileName); + QString ext = info.suffix().toLower(); + + if (ext == "vti" || ext == "vtk") { + auto* obj = uLib::ObjectFactory::Instance().Create("VoxImage"); + auto* vox = dynamic_cast(obj); + if (vox) { + bool success = false; + if (ext == "vti") { + success = vox->ImportFromVti(fileName.toStdString().c_str()); + } else { + success = vox->ImportFromVtk(fileName.toStdString().c_str()); + } + + if (success) { + obj->SetInstanceName(info.fileName().toStdString()); + m_context->AddObject(obj); + } else { + delete obj; + } + } else { + delete obj; + } + } } void MainPanel::onSave() { diff --git a/app/gcompose/src/PropertyWidgets.cpp b/app/gcompose/src/PropertyWidgets.cpp index 7542585..83ef094 100644 --- a/app/gcompose/src/PropertyWidgets.cpp +++ b/app/gcompose/src/PropertyWidgets.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include "Vtk/uLibVtkInterface.h" #include "Math/Units.h" #include "Math/Dense.h" @@ -17,10 +19,12 @@ PropertyWidgetBase::PropertyWidgetBase(PropertyBase* prop, QWidget* parent) m_Label->setMinimumWidth(100); m_Layout->addWidget(m_Label); } -PropertyWidgetBase::~PropertyWidgetBase() {} +PropertyWidgetBase::~PropertyWidgetBase() { + m_Connection.disconnect(); +} // Helper for unit parsing -static double parseWithUnits(const QString& text, double* factorOut = nullptr, QString* suffixOut = nullptr) { +double parseWithUnits(const QString& text, double* factorOut, QString* suffixOut) { static QRegularExpression re("^\\s*([-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)\\s*(_?[a-zA-Z]+)?\\s*$"); QRegularExpressionMatch match = re.match(text); if (!match.hasMatch()) return 0.0; @@ -57,14 +61,23 @@ static double parseWithUnits(const QString& text, double* factorOut = nullptr, Q } // UnitLineEdit implementation -UnitLineEdit::UnitLineEdit(QWidget* parent) : QLineEdit(parent), m_Value(0), m_Factor(1.0), m_Suffix("mm"), m_IsInteger(false) { +UnitLineEdit::UnitLineEdit(QWidget* parent) : QLineEdit(parent), m_Value(0), m_Factor(1.0), m_Suffix(""), m_IsInteger(false) { connect(this, &QLineEdit::editingFinished, this, &UnitLineEdit::onEditingFinished); } +void UnitLineEdit::setUnits(const QString& suffix, double factor) { + m_Suffix = suffix; + m_Factor = factor; + updateText(); +} + void UnitLineEdit::setValue(double val) { if (m_Value != val) { m_Value = val; - // Initial heuristic for unit if it was mm and value becomes large + // Suffix heuristic ONLY if it was mm and no explicit unit was given? + // Actually, if m_Suffix is empty or we have a specific one, we should respect it. + // The original code had a heuristic, but it's better to let property decide. + // Let's keep it ONLY if m_Suffix was mm (legacy behavior) if (!m_IsInteger && m_Suffix == "mm" && std::abs(val) >= 1000.0) { m_Suffix = "m"; m_Factor = CLHEP::meter; } updateText(); } @@ -91,12 +104,21 @@ void UnitLineEdit::onEditingFinished() { void UnitLineEdit::updateText() { QSignalBlocker blocker(this); + QString s; if (m_IsInteger) { - setText(QString::number((int)m_Value)); + s = QString::number((int)m_Value); + if (s.isEmpty()) s = "0"; } else { double displayVal = m_Value / m_Factor; - setText(QString::number(displayVal, 'g', 6) + " " + m_Suffix); + s = QString::number(displayVal, 'g', 6); + if (!s.contains('.') && !s.contains('e')) { + s += ".0"; + } } + if (!m_Suffix.isEmpty()) { + s += " " + m_Suffix; + } + setText(s); } void UnitLineEdit::setIntegerOnly(bool integerOnly) { @@ -107,10 +129,16 @@ void UnitLineEdit::setIntegerOnly(bool integerOnly) { DoublePropertyWidget::DoublePropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Edit = new UnitLineEdit(this); - m_Layout->addWidget(m_Edit, 1); + QString units = QString::fromStdString(prop->GetUnits()); + if (!units.isEmpty()) { + double factor = 1.0; + parseWithUnits("1 " + units, &factor); + m_Edit->setUnits(units, factor); + } m_Edit->setValue(prop->Get()); + m_Layout->addWidget(m_Edit, 1); connect(m_Edit, &UnitLineEdit::valueManualChanged, [this](double val){ m_Prop->Set(val); }); - uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ + m_Connection = uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ m_Edit->setValue(m_Prop->Get()); }); } @@ -118,10 +146,16 @@ DoublePropertyWidget::DoublePropertyWidget(Property* prop, QWidget* pare FloatPropertyWidget::FloatPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Edit = new UnitLineEdit(this); - m_Layout->addWidget(m_Edit, 1); + QString units = QString::fromStdString(prop->GetUnits()); + if (!units.isEmpty()) { + double factor = 1.0; + parseWithUnits("1 " + units, &factor); + m_Edit->setUnits(units, factor); + } m_Edit->setValue(prop->Get()); + m_Layout->addWidget(m_Edit, 1); connect(m_Edit, &UnitLineEdit::valueManualChanged, [this](double val){ m_Prop->Set((float)val); }); - uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ + m_Connection = uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ m_Edit->setValue((double)m_Prop->Get()); }); } @@ -130,10 +164,16 @@ IntPropertyWidget::IntPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Edit = new UnitLineEdit(this); m_Edit->setIntegerOnly(true); - m_Layout->addWidget(m_Edit, 1); + QString units = QString::fromStdString(prop->GetUnits()); + if (!units.isEmpty()) { + double factor = 1.0; + parseWithUnits("1 " + units, &factor); + m_Edit->setUnits(units, factor); + } m_Edit->setValue(prop->Get()); + m_Layout->addWidget(m_Edit, 1); connect(m_Edit, &UnitLineEdit::valueManualChanged, [this](double val){ m_Prop->Set((int)val); }); - uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ + m_Connection = uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ m_Edit->setValue((double)m_Prop->Get()); }); } @@ -144,7 +184,7 @@ BoolPropertyWidget::BoolPropertyWidget(Property* prop, QWidget* parent) m_CheckBox->setChecked(prop->Get()); m_Layout->addWidget(m_CheckBox, 1); connect(m_CheckBox, &QCheckBox::toggled, [this](bool val){ if (m_Prop->Get() != val) m_Prop->Set(val); }); - uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ + m_Connection = uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ if (m_CheckBox->isChecked() != m_Prop->Get()) { QSignalBlocker blocker(m_CheckBox); m_CheckBox->setChecked(m_Prop->Get()); @@ -158,11 +198,11 @@ StringPropertyWidget::StringPropertyWidget(Property* prop, QWidget* m_LineEdit = new QLineEdit(this); m_LineEdit->setText(QString::fromStdString(prop->Get())); m_Layout->addWidget(m_LineEdit, 1); - connect(m_LineEdit, &QLineEdit::editingFinished, [this](){ + connect(m_LineEdit, &QLineEdit::editingFinished, [this](){ std::string val = m_LineEdit->text().toStdString(); - if (m_Prop->Get() != val) m_Prop->Set(val); + if (m_Prop->Get() != val) m_Prop->Set(val); }); - uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ + m_Connection = uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ if (m_LineEdit->text().toStdString() != m_Prop->Get()) { QSignalBlocker blocker(m_LineEdit); m_LineEdit->setText(QString::fromStdString(m_Prop->Get())); @@ -171,6 +211,37 @@ StringPropertyWidget::StringPropertyWidget(Property* prop, QWidget* } StringPropertyWidget::~StringPropertyWidget() {} +class EnumPropertyWidget : public PropertyWidgetBase { + PropertyBase* m_Prop; + QComboBox* m_Combo; +public: + EnumPropertyWidget(PropertyBase* prop, QWidget* parent) + : PropertyWidgetBase(prop, parent), m_Prop(prop) { + m_Combo = new QComboBox(this); + const auto& labels = prop->GetEnumLabels(); + for (const auto& label : labels) { + m_Combo->addItem(QString::fromStdString(label)); + } + + // Get initial value + if (auto* p = dynamic_cast*>(prop)) { + m_Combo->setCurrentIndex(p->Get()); + connect(m_Combo, &QComboBox::currentIndexChanged, [p](int index){ + p->Set(index); + }); + // Store connection in base m_Connection so it's auto-disconnected on destruction. + m_Connection = uLib::Object::connect(p, &Property::PropertyChanged, [this, p](){ + if (m_Combo->currentIndex() != p->Get()) { + QSignalBlocker blocker(m_Combo); + m_Combo->setCurrentIndex(p->Get()); + } + }); + } + + m_Layout->addWidget(m_Combo, 1); + } +}; + PropertyEditor::PropertyEditor(QWidget* parent) : QWidget(parent), m_Object(nullptr) { m_MainLayout = new QVBoxLayout(this); m_MainLayout->setContentsMargins(0, 0, 0, 0); @@ -197,6 +268,11 @@ PropertyEditor::PropertyEditor(QWidget* parent) : QWidget(parent), m_Object(null registerFactory([](PropertyBase* p, QWidget* parent){ return new StringPropertyWidget(static_cast*>(p), parent); }); + + // Register EnumProperty specifically (needs to check type since it holds Property but is EnumProperty) + m_Factories[std::type_index(typeid(EnumProperty))] = [](PropertyBase* p, QWidget* parent) { + return new EnumPropertyWidget(p, parent); + }; // Vector Registration registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); @@ -230,11 +306,22 @@ void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) { } for (auto* prop : *props) { + // Priority 1: Check if it provides enum labels + if (!prop->GetEnumLabels().empty()) { + m_ContainerLayout->addWidget(new EnumPropertyWidget(prop, m_Container)); + continue; + } + + // Priority 2: Standard factory lookup auto it = m_Factories.find(prop->GetTypeIndex()); if (it != m_Factories.end()) { QWidget* widget = it->second(prop, m_Container); m_ContainerLayout->addWidget(widget); } else { + // Debug info for unknown types + std::cout << "PropertyEditor: No factory for " << prop->GetName() + << " (Type: " << prop->GetTypeName() << ")" << std::endl; + QWidget* fallback = new PropertyWidgetBase(prop, m_Container); fallback->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")")); m_ContainerLayout->addWidget(fallback); diff --git a/app/gcompose/src/PropertyWidgets.h b/app/gcompose/src/PropertyWidgets.h index 15f5046..43ee623 100644 --- a/app/gcompose/src/PropertyWidgets.h +++ b/app/gcompose/src/PropertyWidgets.h @@ -14,11 +14,14 @@ #include "Core/Property.h" #include "Core/Object.h" +#include "Core/Signal.h" #include "Math/Dense.h" namespace uLib { namespace Qt { +double parseWithUnits(const QString& text, double* factorOut = nullptr, QString* suffixOut = nullptr); + class PropertyWidgetBase : public QWidget { Q_OBJECT public: @@ -30,6 +33,9 @@ protected: PropertyBase* m_BaseProperty; QHBoxLayout* m_Layout; QLabel* m_Label; + // Stores the uLib signal connection so it can be disconnected on destruction, + // preventing use-after-free when PropertyEditor::clear() deletes widgets. + Connection m_Connection; }; class UnitLineEdit : public QLineEdit { @@ -37,6 +43,7 @@ class UnitLineEdit : public QLineEdit { public: UnitLineEdit(QWidget* parent = nullptr); void setValue(double val); + void setUnits(const QString& suffix, double factor = 1.0); double getValue() const { return m_Value; } void setIntegerOnly(bool b); @@ -86,11 +93,19 @@ class VectorPropertyWidget : public PropertyWidgetBase { public: VectorPropertyWidget(Property* prop, QWidget* parent = nullptr) : PropertyWidgetBase(prop, parent), m_Prop(prop) { + QString units = QString::fromStdString(prop->GetUnits()); + double factor = 1.0; + if (!units.isEmpty()) { + parseWithUnits("1 " + units, &factor); + } for (int i = 0; i < Size; ++i) { m_Edits[i] = new UnitLineEdit(this); if (std::is_integral::value) { m_Edits[i]->setIntegerOnly(true); } + if (!units.isEmpty()) { + m_Edits[i]->setUnits(units, factor); + } m_Layout->addWidget(m_Edits[i], 1); connect(m_Edits[i], &UnitLineEdit::valueManualChanged, [this, i](double val){ @@ -100,10 +115,11 @@ public: }); } updateEdits(); - uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ + m_Connection = uLib::Object::connect(m_Prop, &Property::PropertyChanged, [this](){ updateEdits(); }); } + ~VectorPropertyWidget() { m_Connection.disconnect(); } private: void updateEdits() { diff --git a/assets/exmaples/vtk/2026_03_24_C1_Prod11_test_img_40_trim55505_scale1.00_sigma1.0.vtk b/assets/exmaples/vtk/2026_03_24_C1_Prod11_test_img_40_trim55505_scale1.00_sigma1.0.vtk new file mode 100644 index 0000000..38ac22d --- /dev/null +++ b/assets/exmaples/vtk/2026_03_24_C1_Prod11_test_img_40_trim55505_scale1.00_sigma1.0.vtk @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5640d13437a7632d55396c12aec33fac859744bf93f13007ca154ecd4b7ef68e +size 75283840 diff --git a/src/Core/Object.cpp b/src/Core/Object.cpp index c65a86f..0cf37ce 100644 --- a/src/Core/Object.cpp +++ b/src/Core/Object.cpp @@ -88,6 +88,16 @@ const std::vector& Object::GetProperties() const { return d->m_Properties; } +PropertyBase* Object::GetProperty(const std::string& name) const { + for (auto* p : d->m_Properties) { + if (p->GetName() == name) return p; + } + for (auto* p : d->m_DynamicProperties) { + if (p->GetName() == name) return p; + } + return nullptr; +} + // In Object.h, the template serialize needs to be updated to call property serialization. // However, since Object::serialize is a template in the header, we might need a helper here. diff --git a/src/Core/Object.h b/src/Core/Object.h index 260a9df..9eb31e1 100644 --- a/src/Core/Object.h +++ b/src/Core/Object.h @@ -93,6 +93,7 @@ public: void RegisterProperty(PropertyBase* prop); void RegisterDynamicProperty(PropertyBase* prop); const std::vector& GetProperties() const; + PropertyBase* GetProperty(const std::string& name) const; //////////////////////////////////////////////////////////////////////////// // PARAMETERS // diff --git a/src/Core/Property.h b/src/Core/Property.h index 91d817f..274f43a 100644 --- a/src/Core/Property.h +++ b/src/Core/Property.h @@ -23,6 +23,12 @@ public: virtual const char* GetTypeName() const = 0; virtual std::string GetValueAsString() const = 0; virtual std::type_index GetTypeIndex() const = 0; // Added + virtual const std::string& GetUnits() const = 0; + virtual void SetUnits(const std::string& units) = 0; + virtual const std::vector& GetEnumLabels() const { + static std::vector empty; + return empty; + } // Signal support signals: @@ -45,16 +51,16 @@ template class Property : public PropertyBase { public: // PROXY: Use an existing variable as back-end storage - Property(Object* owner, const std::string& name, T* valuePtr) - : m_owner(owner), m_name(name), m_value(valuePtr), m_own(false) { + Property(Object* owner, const std::string& name, T* valuePtr, const std::string& units = "") + : m_owner(owner), m_name(name), m_units(units), m_value(valuePtr), m_own(false) { if (m_owner) { m_owner->RegisterProperty(this); } } // MANAGED: Create and own internal storage - Property(Object* owner, const std::string& name, const T& defaultValue = T()) - : m_owner(owner), m_name(name), m_value(new T(defaultValue)), m_own(true) { + Property(Object* owner, const std::string& name, const T& defaultValue = T(), const std::string& units = "") + : m_owner(owner), m_name(name), m_units(units), m_value(new T(defaultValue)), m_own(true) { if (m_owner) { m_owner->RegisterProperty(this); } @@ -68,6 +74,8 @@ public: virtual const std::string& GetName() const override { return m_name; } virtual const char* GetTypeName() const override { return typeid(T).name(); } virtual std::type_index GetTypeIndex() const override { return std::type_index(typeid(T)); } + virtual const std::string& GetUnits() const override { return m_units; } + virtual void SetUnits(const std::string& units) override { m_units = units; } std::string GetValueAsString() const override { @@ -118,6 +126,7 @@ public: private: std::string m_name; + std::string m_units; T* m_value; bool m_own; Object* m_owner; @@ -135,6 +144,22 @@ typedef Property FloatProperty; typedef Property DoubleProperty; typedef Property BoolProperty; +/** + * @brief Property specialized for enumerations, providing labels for GUI representations. + */ +class EnumProperty : public Property { +public: + EnumProperty(Object* owner, const std::string& name, int* valuePtr, const std::vector& labels, const std::string& units = "") + : Property(owner, name, valuePtr, units), m_Labels(labels) {} + + const std::vector& GetEnumLabels() const override { return m_Labels; } + const char* GetTypeName() const override { return "Enum"; } + virtual std::type_index GetTypeIndex() const override { return std::type_index(typeid(EnumProperty)); } + +private: + std::vector m_Labels; +}; + /** * @brief Macro to simplify property declaration within a class. * Usage: ULIB_PROPERTY(float, Width, 1.0f) @@ -188,10 +213,16 @@ public: template void save_override(const boost::serialization::hrp &t) { if (m_Object) { - // We use const_cast because we are just creating a proxy to the member - m_Object->RegisterDynamicProperty( - new Property(m_Object, t.name(), &const_cast&>(t).value()) - ); + Property* p = new Property(m_Object, t.name(), &const_cast&>(t).value(), t.units() ? t.units() : ""); + m_Object->RegisterDynamicProperty(p); + } + } + + template + void save_override(const boost::serialization::hrp_enum &t) { + if (m_Object) { + EnumProperty* p = new EnumProperty(m_Object, t.name(), (int*)&const_cast&>(t).value(), t.labels(), t.units() ? t.units() : ""); + m_Object->RegisterDynamicProperty(p); } } diff --git a/src/Core/Serializable.h b/src/Core/Serializable.h index 1a6fb7d..e5e620d 100644 --- a/src/Core/Serializable.h +++ b/src/Core/Serializable.h @@ -75,12 +75,14 @@ template struct access2 {}; template class hrp : public boost::serialization::wrapper_traits> { const char *m_name; + const char *m_units; T &m_value; public: - explicit hrp(const char *name_, T &t) : m_name(name_), m_value(t) {} + explicit hrp(const char *name_, T &t, const char* units_ = nullptr) : m_name(name_), m_units(units_), m_value(t) {} const char *name() const { return this->m_name; } + const char *units() const { return this->m_units; } T &value() { return this->m_value; } const T &const_value() const { return this->m_value; } @@ -98,11 +100,46 @@ public: }; template -inline hrp make_hrp(const char *name, T &t) { - return hrp(name, t); +inline hrp make_hrp(const char *name, T &t, const char* units = nullptr) { + return hrp(name, t, units); +} + +template +class hrp_enum : public boost::serialization::wrapper_traits> { + const char *m_name; + const char *m_units; + T &m_value; + std::vector m_labels; + +public: + explicit hrp_enum(const char *name_, T &t, const std::vector& labels, const char* units_ = nullptr) + : m_name(name_), m_units(units_), m_value(t), m_labels(labels) {} + + const char *name() const { return this->m_name; } + const char *units() const { return this->m_units; } + T &value() { return this->m_value; } + const std::vector& labels() const { return m_labels; } + + BOOST_SERIALIZATION_SPLIT_MEMBER() + + template + void save(Archivex &ar, const unsigned int /* version */) const { + ar << boost::serialization::make_nvp(m_name, m_value); + } + + template + void load(Archivex &ar, const unsigned int /* version */) { + ar >> boost::serialization::make_nvp(m_name, m_value); + } +}; + +template +inline hrp_enum make_hrp_enum(const char *name, T &t, const std::vector& labels, const char* units = nullptr) { + return hrp_enum(name, t, labels, units); } #define HRP(name) boost::serialization::make_hrp(BOOST_PP_STRINGIZE(name), name) +#define HRPU(name, units) boost::serialization::make_hrp(BOOST_PP_STRINGIZE(name), name, units) } // namespace serialization } // namespace boost diff --git a/src/Vtk/Math/vtkVoxImage.cpp b/src/Vtk/Math/vtkVoxImage.cpp index c0b29d8..da14a8c 100644 --- a/src/Vtk/Math/vtkVoxImage.cpp +++ b/src/Vtk/Math/vtkVoxImage.cpp @@ -126,9 +126,11 @@ vtkVoxImage::vtkVoxImage(Content &content) : m_Content(content), m_Actor(vtkVolume::New()), m_Image(vtkImageData::New()), m_Outline(vtkCubeSource::New()), m_OutlineActor(vtkActor::New()), - m_Reader(NULL), m_Writer(NULL), writer_factor(1.E6) { + m_Reader(NULL), m_Writer(NULL), writer_factor(1.E6), + m_Window(40/1.E6), m_Level(20/1.E6), m_ShadingPreset(0) { GetContent(); InstallPipe(); + ULIB_ACTIVATE_DISPLAY_PROPERTIES; } vtkVoxImage::~vtkVoxImage() { @@ -199,14 +201,15 @@ void vtkVoxImage::ReadFromXMLFile(const char *fname) { } void vtkVoxImage::setShadingPreset(int blendType) { + m_ShadingPreset = blendType; vtkSmartVolumeMapper *mapper = (vtkSmartVolumeMapper *)m_Actor->GetMapper(); vtkVolumeProperty *property = m_Actor->GetProperty(); static vtkColorTransferFunction *colorFun = vtkColorTransferFunction::New(); static vtkPiecewiseFunction *opacityFun = vtkPiecewiseFunction::New(); - float window = 40 / writer_factor; - float level = 20 / writer_factor; + float window = m_Window; + float level = m_Level; property->SetColor(colorFun); property->SetScalarOpacity(opacityFun); @@ -281,13 +284,24 @@ void vtkVoxImage::SetRepresentation(Representation mode) { m_OutlineActor->SetVisibility(1); } else if (mode == Surface) { m_Actor->SetVisibility(1); - m_OutlineActor->SetVisibility(0); + m_OutlineActor->SetVisibility(1); // Keep outline visible as boundary } else { Puppet::SetRepresentation(mode); } } +void vtkVoxImage::serialize_display(uLib::Archive::display_properties_archive & ar, const unsigned int version) { + // Call base class if it has display properties + Puppet::serialize_display(ar, version); + + // Use the member variables if they are available + ar & boost::serialization::make_hrp("Window", m_Window); + ar & boost::serialization::make_hrp("Level", m_Level); + ar & boost::serialization::make_hrp_enum("Shading", m_ShadingPreset, {"MIP", "Composite", "Composite Shaded", "MIP Bone", "MIP Hot", "Additive"}); +} + void vtkVoxImage::Update() { + setShadingPreset(m_ShadingPreset); m_Actor->Update(); m_Outline->SetBounds(m_Image->GetBounds()); m_Outline->Update(); diff --git a/src/Vtk/Math/vtkVoxImage.h b/src/Vtk/Math/vtkVoxImage.h index 7eb9ba4..647445d 100644 --- a/src/Vtk/Math/vtkVoxImage.h +++ b/src/Vtk/Math/vtkVoxImage.h @@ -65,7 +65,8 @@ public: void setShadingPreset(int blendType = 2); void SetRepresentation(Representation mode); - void Update(); + void Update() override; + void serialize_display(uLib::Archive::display_properties_archive & ar, const unsigned int version = 0) override; protected: void InstallPipe(); @@ -84,6 +85,7 @@ private: float m_Window; float m_Level; + int m_ShadingPreset; }; } // namespace Vtk diff --git a/src/Vtk/uLibVtkInterface.cxx b/src/Vtk/uLibVtkInterface.cxx index 8ce675d..5e9157a 100644 --- a/src/Vtk/uLibVtkInterface.cxx +++ b/src/Vtk/uLibVtkInterface.cxx @@ -57,6 +57,7 @@ #include #include #include +#include #include "uLibVtkInterface.h" #include "vtkHandlerWidget.h" @@ -87,12 +88,17 @@ public: m_Assembly(vtkSmartPointer::New()), m_ShowBoundingBox(false), m_ShowScaleMeasures(false), - m_Representation(-1), + m_Representation(Puppet::Surface), m_Opacity(-1.0), m_Selectable(true), - m_Selected(false) + m_Selected(false), + m_Visibility(true), + m_Dragable(true) { m_Color[0] = m_Color[1] = m_Color[2] = -1.0; + m_Position[0] = m_Position[1] = m_Position[2] = 0.0; + m_Orientation[0] = m_Orientation[1] = m_Orientation[2] = 0.0; + m_Scale[0] = m_Scale[1] = m_Scale[2] = 1.0; } ~PuppetData() { @@ -110,25 +116,49 @@ public: bool m_ShowBoundingBox; bool m_ShowScaleMeasures; - int m_Representation; double m_Color[3]; double m_Opacity; + bool m_Selectable; + bool m_Selected; + bool m_Visibility; + bool m_Dragable; + double m_Position[3]; + double m_Orientation[3]; + double m_Scale[3]; + void ApplyAppearance(vtkProp *p) { + p->SetVisibility(m_Visibility); + p->SetPickable(m_Selectable); + p->SetDragable(m_Dragable); + vtkActor *actor = vtkActor::SafeDownCast(p); - if (!actor) return; + if (actor) { + if (m_Representation != -1) { + if (m_Representation == Puppet::SurfaceWithEdges) { + actor->GetProperty()->SetRepresentation(VTK_SURFACE); + actor->GetProperty()->SetEdgeVisibility(1); + } else { + actor->GetProperty()->SetRepresentation(m_Representation); + actor->GetProperty()->SetEdgeVisibility(0); + } + } + if (m_Color[0] != -1.0) { + actor->GetProperty()->SetColor(m_Color); + } - if (m_Representation != -1) { - actor->GetProperty()->SetRepresentation(m_Representation); + if (m_Opacity != -1.0) { + actor->GetProperty()->SetOpacity(m_Opacity); + } } - if (m_Color[0] != -1.0) { - actor->GetProperty()->SetColor(m_Color); - } - - if (m_Opacity != -1.0) { - actor->GetProperty()->SetOpacity(m_Opacity); + // Handle transformation if it's a Prop3D + if (auto* p3d = vtkProp3D::SafeDownCast(p)) { + // NOTE: Usually managed by Puppet::Update from model, but here for direct prop manipulation + // p3d->SetPosition(m_Position); + // p3d->SetOrientation(m_Orientation); + // p3d->SetScale(m_Scale); } } @@ -188,9 +218,6 @@ public: } } } - - bool m_Selectable; - bool m_Selected; }; // -------------------------------------------------------------------------- // @@ -226,6 +253,21 @@ void Puppet::SetProp(vtkProp *prop) pd->m_Assembly->AddPart(p3d); } 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[0] < 0) + vp->GetColor(pd->m_Color); + } + } } } @@ -370,13 +412,7 @@ void Puppet::ShowScaleMeasures(bool show) void Puppet::SetRepresentation(Representation mode) { - int rep = VTK_SURFACE; - switch (mode) { - case Points: rep = VTK_POINTS; break; - case Wireframe: rep = VTK_WIREFRAME; break; - case Surface: rep = VTK_SURFACE; break; - } - pd->m_Representation = rep; + pd->m_Representation = static_cast(mode); vtkProp3DCollection *props = pd->m_Assembly->GetParts(); props->InitTraversal(); @@ -391,6 +427,10 @@ void Puppet::SetRepresentation(const char *mode) if (s == "points") SetRepresentation(Points); else if (s == "wireframe") SetRepresentation(Wireframe); else if (s == "shaded" || s == "surface") SetRepresentation(Surface); + else if (s == "edges" || s == "surface+edges" || s == "surfacewithedges") SetRepresentation(SurfaceWithEdges); + else if (s == "volume") SetRepresentation(Volume); + else if (s == "outline") SetRepresentation(Outline); + else if (s == "slice") SetRepresentation(Slice); } void Puppet::SetColor(double r, double g, double b) @@ -453,6 +493,18 @@ bool Puppet::IsSelected() const void Puppet::Update() { + vtkProp* root = this->GetProp(); + if (root) { + pd->ApplyAppearance(root); + + // Apply transformation if it's a Prop3D + if (auto* p3d = vtkProp3D::SafeDownCast(root)) { + p3d->SetPosition(pd->m_Position); + p3d->SetOrientation(pd->m_Orientation); + p3d->SetScale(pd->m_Scale); + } + } + vtkProp3DCollection *props = pd->m_Assembly->GetParts(); props->InitTraversal(); for (int i = 0; i < props->GetNumberOfItems(); ++i) { @@ -473,6 +525,42 @@ void Puppet::Update() double* bounds = pd->m_Assembly->GetBounds(); pd->m_CubeAxesActor->SetBounds(bounds); } + + // Notify that the object has been updated (important for UI refresh) + this->Object::Updated(); + + // Trigger immediate re-render of all connected viewports + pd->m_Renderers->InitTraversal(); + for (int i = 0; i < pd->m_Renderers->GetNumberOfItems(); ++i) { + if (auto* ren = pd->m_Renderers->GetNextItem()) { + if (ren->GetRenderWindow()) ren->GetRenderWindow()->Render(); + } + } +} + +void Puppet::SyncFromVtk() +{ + vtkProp* root = this->GetProp(); + if (auto* p3d = vtkProp3D::SafeDownCast(root)) { + double pos[3], ori[3], scale[3]; + p3d->GetPosition(pos); + p3d->GetOrientation(ori); + p3d->GetScale(scale); + + // Update properties + for (int i=0; i<3; ++i) { + pd->m_Position[i] = pos[i]; + pd->m_Orientation[i] = ori[i]; + pd->m_Scale[i] = scale[i]; + } + + // Get the properties from the object + if (auto* propPos = this->GetProperty("Position")) propPos->Updated(); + if (auto* propOri = this->GetProperty("Orientation")) propOri->Updated(); + if (auto* propScale = this->GetProperty("Scale")) propScale->Updated(); + + this->Object::Updated(); + } } void Puppet::ConnectInteractor(vtkRenderWindowInteractor *interactor) @@ -484,7 +572,21 @@ void Puppet::serialize_display(Archive::display_properties_archive & ar, const u ar & boost::serialization::make_hrp("ColorG", pd->m_Color[1]); ar & boost::serialization::make_hrp("ColorB", pd->m_Color[2]); ar & boost::serialization::make_hrp("Opacity", pd->m_Opacity); - ar & boost::serialization::make_hrp("Representation", pd->m_Representation); + 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); + + // Geometry knobs (caution: these might be overridden by internal matrices) + ar & boost::serialization::make_hrp("PosX", pd->m_Position[0], "mm"); + ar & boost::serialization::make_hrp("PosY", pd->m_Position[1], "mm"); + ar & boost::serialization::make_hrp("PosZ", pd->m_Position[2], "mm"); + ar & boost::serialization::make_hrp("OriX", pd->m_Orientation[0], "deg"); + ar & boost::serialization::make_hrp("OriY", pd->m_Orientation[1], "deg"); + ar & boost::serialization::make_hrp("OriZ", pd->m_Orientation[2], "deg"); + ar & boost::serialization::make_hrp("ScaleX", pd->m_Scale[0]); + ar & boost::serialization::make_hrp("ScaleY", pd->m_Scale[1]); + ar & boost::serialization::make_hrp("ScaleZ", pd->m_Scale[2]); } void Puppet::serialize(Archive::xml_oarchive & ar, const unsigned int v) { } diff --git a/src/Vtk/uLibVtkInterface.h b/src/Vtk/uLibVtkInterface.h index 7110f2f..2b086bb 100644 --- a/src/Vtk/uLibVtkInterface.h +++ b/src/Vtk/uLibVtkInterface.h @@ -82,8 +82,9 @@ public: bool IsSelected() const; virtual void Update(); + virtual void SyncFromVtk(); - enum Representation { Points, Wireframe, Surface }; + 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); @@ -131,6 +132,17 @@ private: } // namespace Vtk } // namespace uLib + + + + + + + + + +// -------------------------------------------------------------------------- // +// DISPLAY PROPERTIES SERIALIZE // -------------------------------------------------------------------------- // namespace uLib { @@ -148,9 +160,20 @@ public: template void save_override(const boost::serialization::hrp &t) { if (m_Puppet) { - m_Puppet->RegisterDisplayProperty( - new uLib::Property(m_Puppet, t.name(), &const_cast&>(t).value()) - ); + uLib::Property* p = new uLib::Property(m_Puppet, t.name(), &const_cast&>(t).value(), t.units() ? t.units() : ""); + m_Puppet->RegisterDisplayProperty(p); + Vtk::Puppet* puppet = m_Puppet; + uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); }); + } + } + + template + void save_override(const boost::serialization::hrp_enum &t) { + if (m_Puppet) { + uLib::EnumProperty* p = new uLib::EnumProperty(m_Puppet, t.name(), (int*)&const_cast&>(t).value(), t.labels(), t.units() ? t.units() : ""); + m_Puppet->RegisterDisplayProperty(p); + Vtk::Puppet* puppet = m_Puppet; + uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); }); } } diff --git a/src/Vtk/vtkObjectsContext.cpp b/src/Vtk/vtkObjectsContext.cpp index 6f157c5..13121d9 100644 --- a/src/Vtk/vtkObjectsContext.cpp +++ b/src/Vtk/vtkObjectsContext.cpp @@ -2,6 +2,7 @@ #include "Vtk/Math/vtkContainerBox.h" #include "Vtk/Math/vtkCylinder.h" #include "Vtk/Math/vtkAssembly.h" +#include "Vtk/Math/vtkVoxImage.h" #include "HEP/Detectors/vtkDetectorChamber.h" #include @@ -116,6 +117,8 @@ Puppet* vtkObjectsContext::CreatePuppet(uLib::Object* obj) { return new vtkDetectorChamber(static_cast(obj)); } else if (std::strcmp(className, "Cylinder") == 0) { return new vtkCylinder(static_cast(obj)); + } else if (std::strcmp(className, "VoxImage") == 0) { + return new vtkVoxImage(*static_cast(obj)); } else if (std::strcmp(className, "Assembly") == 0) { return new Assembly(static_cast(obj)); } diff --git a/src/Vtk/vtkQViewport.cpp b/src/Vtk/vtkQViewport.cpp index 7cd392a..f890bd4 100644 --- a/src/Vtk/vtkQViewport.cpp +++ b/src/Vtk/vtkQViewport.cpp @@ -101,6 +101,11 @@ void QViewport::onGridButtonClicked() SetGridVisible(m_GridButton->isChecked()); } +void QViewport::OnSelectionChanged(Puppet* p) +{ + emit puppetSelected(p); +} + void QViewport::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); diff --git a/src/Vtk/vtkQViewport.h b/src/Vtk/vtkQViewport.h index c311d66..168a926 100644 --- a/src/Vtk/vtkQViewport.h +++ b/src/Vtk/vtkQViewport.h @@ -30,6 +30,8 @@ namespace Vtk { */ class QViewport : public QWidget, public Viewport { Q_OBJECT +signals: + void puppetSelected(uLib::Vtk::Puppet* p); public: explicit QViewport(QWidget* parent = nullptr); virtual ~QViewport(); @@ -42,6 +44,8 @@ public: virtual vtkRenderWindowInteractor* GetInteractor() override; QVTKOpenGLNativeWidget* GetWidget() { return m_VtkWidget; } + virtual void OnSelectionChanged(Puppet* p) override; + protected: virtual void resizeEvent(QResizeEvent* event) override; diff --git a/src/Vtk/vtkViewport.cpp b/src/Vtk/vtkViewport.cpp index d05522b..d7b610b 100644 --- a/src/Vtk/vtkViewport.cpp +++ b/src/Vtk/vtkViewport.cpp @@ -208,6 +208,7 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren) auto* self = static_cast(clientdata); for (auto* p : self->m_Puppets) { if (p->IsSelected()) { + p->SyncFromVtk(); p->Update(); } } @@ -518,6 +519,7 @@ void Viewport::SelectPuppet(Puppet* prop) } Render(); + OnSelectionChanged(prop); } void Viewport::SetGridVisible(bool visible) diff --git a/src/Vtk/vtkViewport.h b/src/Vtk/vtkViewport.h index 173ce3f..a97d53f 100644 --- a/src/Vtk/vtkViewport.h +++ b/src/Vtk/vtkViewport.h @@ -62,6 +62,7 @@ public: vtkCornerAnnotation* GetAnnotation(); vtkCameraOrientationWidget* GetCameraWidget(); void DisableHandler(); + virtual void OnSelectionChanged(Puppet* p) {} const std::vector& getPuppets() const { return m_Puppets; } // Grid control