fix some on properties and signal connection

This commit is contained in:
AndreaRigoni
2026-03-26 09:50:52 +00:00
parent 2c5d6842c3
commit e0ffeff5b7
22 changed files with 578 additions and 62 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
*.vtk filter=lfs diff=lfs merge=lfs -text
*.vti filter=lfs diff=lfs merge=lfs -text

90
CLAUDE.md Normal file
View File

@@ -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<T>` 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<T>` 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<T>` 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.

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -20,6 +20,8 @@ public:
~ContextPanel();
void setContext(uLib::ObjectsContext* context);
void selectObject(uLib::Object* obj);
void clearSelection();
signals:
void objectSelected(uLib::Object* obj);

View File

@@ -14,7 +14,10 @@
#include <QMenu>
#include <QAction>
#include <QApplication>
#include <QFileDialog>
#include <QFileInfo>
#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<ViewportPane*>();
@@ -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<uLib::Abstract::VoxImage*>(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() {

View File

@@ -2,6 +2,8 @@
#include <QSignalBlocker>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QComboBox>
#include <QCheckBox>
#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<double>* 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<double>::PropertyChanged, [this](){
m_Connection = uLib::Object::connect(m_Prop, &Property<double>::PropertyChanged, [this](){
m_Edit->setValue(m_Prop->Get());
});
}
@@ -118,10 +146,16 @@ DoublePropertyWidget::DoublePropertyWidget(Property<double>* prop, QWidget* pare
FloatPropertyWidget::FloatPropertyWidget(Property<float>* 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<float>::PropertyChanged, [this](){
m_Connection = uLib::Object::connect(m_Prop, &Property<float>::PropertyChanged, [this](){
m_Edit->setValue((double)m_Prop->Get());
});
}
@@ -130,10 +164,16 @@ IntPropertyWidget::IntPropertyWidget(Property<int>* 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<int>::PropertyChanged, [this](){
m_Connection = uLib::Object::connect(m_Prop, &Property<int>::PropertyChanged, [this](){
m_Edit->setValue((double)m_Prop->Get());
});
}
@@ -144,7 +184,7 @@ BoolPropertyWidget::BoolPropertyWidget(Property<bool>* 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<bool>::PropertyChanged, [this](){
m_Connection = uLib::Object::connect(m_Prop, &Property<bool>::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<std::string>* 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<std::string>::PropertyChanged, [this](){
m_Connection = uLib::Object::connect(m_Prop, &Property<std::string>::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<std::string>* 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<Property<int>*>(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<int>::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<std::string>([](PropertyBase* p, QWidget* parent){
return new StringPropertyWidget(static_cast<Property<std::string>*>(p), parent);
});
// Register EnumProperty specifically (needs to check type since it holds Property<int> but is EnumProperty)
m_Factories[std::type_index(typeid(EnumProperty))] = [](PropertyBase* p, QWidget* parent) {
return new EnumPropertyWidget(p, parent);
};
// Vector Registration
registerFactory<Vector2i>([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget<Vector2i, 2>(static_cast<Property<Vector2i>*>(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);

View File

@@ -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<VecT>* 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<typename VecT::Scalar>::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<VecT>::PropertyChanged, [this](){
m_Connection = uLib::Object::connect(m_Prop, &Property<VecT>::PropertyChanged, [this](){
updateEdits();
});
}
~VectorPropertyWidget() { m_Connection.disconnect(); }
private:
void updateEdits() {

Binary file not shown.

View File

@@ -88,6 +88,16 @@ const std::vector<PropertyBase*>& 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.

View File

@@ -93,6 +93,7 @@ public:
void RegisterProperty(PropertyBase* prop);
void RegisterDynamicProperty(PropertyBase* prop);
const std::vector<PropertyBase*>& GetProperties() const;
PropertyBase* GetProperty(const std::string& name) const;
////////////////////////////////////////////////////////////////////////////
// PARAMETERS //

View File

@@ -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<std::string>& GetEnumLabels() const {
static std::vector<std::string> empty;
return empty;
}
// Signal support
signals:
@@ -45,16 +51,16 @@ template <typename T>
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<float> FloatProperty;
typedef Property<double> DoubleProperty;
typedef Property<Bool_t> BoolProperty;
/**
* @brief Property specialized for enumerations, providing labels for GUI representations.
*/
class EnumProperty : public Property<int> {
public:
EnumProperty(Object* owner, const std::string& name, int* valuePtr, const std::vector<std::string>& labels, const std::string& units = "")
: Property<int>(owner, name, valuePtr, units), m_Labels(labels) {}
const std::vector<std::string>& 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<std::string> m_Labels;
};
/**
* @brief Macro to simplify property declaration within a class.
* Usage: ULIB_PROPERTY(float, Width, 1.0f)
@@ -188,10 +213,16 @@ public:
template<class T>
void save_override(const boost::serialization::hrp<T> &t) {
if (m_Object) {
// We use const_cast because we are just creating a proxy to the member
m_Object->RegisterDynamicProperty(
new Property<T>(m_Object, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value())
);
Property<T>* p = new Property<T>(m_Object, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value(), t.units() ? t.units() : "");
m_Object->RegisterDynamicProperty(p);
}
}
template<class T>
void save_override(const boost::serialization::hrp_enum<T> &t) {
if (m_Object) {
EnumProperty* p = new EnumProperty(m_Object, t.name(), (int*)&const_cast<boost::serialization::hrp_enum<T>&>(t).value(), t.labels(), t.units() ? t.units() : "");
m_Object->RegisterDynamicProperty(p);
}
}

View File

@@ -75,12 +75,14 @@ template <class T> struct access2 {};
template <class T>
class hrp : public boost::serialization::wrapper_traits<hrp<T>> {
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 <class T>
inline hrp<T> make_hrp(const char *name, T &t) {
return hrp<T>(name, t);
inline hrp<T> make_hrp(const char *name, T &t, const char* units = nullptr) {
return hrp<T>(name, t, units);
}
template <class T>
class hrp_enum : public boost::serialization::wrapper_traits<hrp_enum<T>> {
const char *m_name;
const char *m_units;
T &m_value;
std::vector<std::string> m_labels;
public:
explicit hrp_enum(const char *name_, T &t, const std::vector<std::string>& 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<std::string>& labels() const { return m_labels; }
BOOST_SERIALIZATION_SPLIT_MEMBER()
template <class Archivex>
void save(Archivex &ar, const unsigned int /* version */) const {
ar << boost::serialization::make_nvp(m_name, m_value);
}
template <class Archivex>
void load(Archivex &ar, const unsigned int /* version */) {
ar >> boost::serialization::make_nvp(m_name, m_value);
}
};
template <class T>
inline hrp_enum<T> make_hrp_enum(const char *name, T &t, const std::vector<std::string>& labels, const char* units = nullptr) {
return hrp_enum<T>(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

View File

@@ -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();

View File

@@ -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

View File

@@ -57,6 +57,7 @@
#include <vtkPolyData.h>
#include <vtkFeatureEdges.h>
#include <vtkTransform.h>
#include <vtkRenderWindow.h>
#include "uLibVtkInterface.h"
#include "vtkHandlerWidget.h"
@@ -87,12 +88,17 @@ public:
m_Assembly(vtkSmartPointer<vtkAssembly>::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<int>(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) { }

View File

@@ -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<class T>
void save_override(const boost::serialization::hrp<T> &t) {
if (m_Puppet) {
m_Puppet->RegisterDisplayProperty(
new uLib::Property<T>(m_Puppet, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value())
);
uLib::Property<T>* p = new uLib::Property<T>(m_Puppet, t.name(), &const_cast<boost::serialization::hrp<T>&>(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<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() : "");
m_Puppet->RegisterDisplayProperty(p);
Vtk::Puppet* puppet = m_Puppet;
uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); });
}
}

View File

@@ -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 <vtkAssembly.h>
@@ -116,6 +117,8 @@ Puppet* vtkObjectsContext::CreatePuppet(uLib::Object* obj) {
return new vtkDetectorChamber(static_cast<uLib::DetectorChamber*>(obj));
} else if (std::strcmp(className, "Cylinder") == 0) {
return new vtkCylinder(static_cast<uLib::Cylinder*>(obj));
} else if (std::strcmp(className, "VoxImage") == 0) {
return new vtkVoxImage(*static_cast<uLib::Abstract::VoxImage*>(obj));
} else if (std::strcmp(className, "Assembly") == 0) {
return new Assembly(static_cast<uLib::Assembly*>(obj));
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -208,6 +208,7 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
auto* self = static_cast<Viewport*>(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)

View File

@@ -62,6 +62,7 @@ public:
vtkCornerAnnotation* GetAnnotation();
vtkCameraOrientationWidget* GetCameraWidget();
void DisableHandler();
virtual void OnSelectionChanged(Puppet* p) {}
const std::vector<Puppet*>& getPuppets() const { return m_Puppets; }
// Grid control