Compare commits
7 Commits
e4a8499104
...
andrea-alg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
876b8f4592 | ||
|
|
ec2027e980 | ||
|
|
69b47623f8 | ||
|
|
f5c1e317e8 | ||
|
|
e0ffeff5b7 | ||
|
|
2c5d6842c3 | ||
|
|
422113a0e9 |
7
.agents/rules/micromamba.md
Normal file
7
.agents/rules/micromamba.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
build in build directory using always micromamba "mutom" env.
|
||||
build with make flag -j$(nproc).
|
||||
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal 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
90
CLAUDE.md
Normal 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.
|
||||
@@ -21,7 +21,7 @@ endif()
|
||||
project(uLib)
|
||||
|
||||
# CUDA Toolkit seems to be missing locally. Toggle ON if nvcc is made available.
|
||||
option(USE_CUDA "Enable CUDA support" ON)
|
||||
option(USE_CUDA "Enable CUDA support" OFF)
|
||||
if(USE_CUDA)
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -allow-unsupported-compiler")
|
||||
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
|
||||
@@ -115,7 +115,7 @@ set(Boost_USE_MULTITHREADED ON)
|
||||
set(Boost_USE_STATIC_RUNTIME OFF)
|
||||
message(STATUS "CMAKE_PREFIX_PATH is ${CMAKE_PREFIX_PATH}")
|
||||
|
||||
find_package(HDF5 REQUIRED CONFIG)
|
||||
find_package(HDF5 REQUIRED)
|
||||
|
||||
find_package(Boost 1.45.0 COMPONENTS program_options serialization unit_test_framework REQUIRED)
|
||||
include_directories(${Boost_INCLUDE_DIRS})
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ public:
|
||||
~ContextPanel();
|
||||
|
||||
void setContext(uLib::ObjectsContext* context);
|
||||
void selectObject(uLib::Object* obj);
|
||||
void clearSelection();
|
||||
|
||||
signals:
|
||||
void objectSelected(uLib::Object* obj);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
#include <QSignalBlocker>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include "Vtk/uLibVtkInterface.h"
|
||||
#include "Math/Units.h"
|
||||
#include "Math/Dense.h"
|
||||
#include "Settings.h"
|
||||
|
||||
namespace uLib {
|
||||
namespace Qt {
|
||||
@@ -13,14 +16,29 @@ PropertyWidgetBase::PropertyWidgetBase(PropertyBase* prop, QWidget* parent)
|
||||
: QWidget(parent), m_BaseProperty(prop) {
|
||||
m_Layout = new QHBoxLayout(this);
|
||||
m_Layout->setContentsMargins(4, 2, 4, 2);
|
||||
m_Label = new QLabel(QString::fromStdString(prop->GetName()), this);
|
||||
m_Label->setMinimumWidth(100);
|
||||
|
||||
std::string unit = prop->GetUnits();
|
||||
QString labelText = QString::fromStdString(prop->GetName());
|
||||
if (!unit.empty()) {
|
||||
auto dim = Settings::Instance().IdentifyDimension(unit);
|
||||
std::string pref = Settings::Instance().GetPreferredUnit(dim);
|
||||
if (!pref.empty()) {
|
||||
labelText += " [" + QString::fromStdString(pref) + "]";
|
||||
} else {
|
||||
labelText += " [" + QString::fromStdString(unit) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
m_Label = new QLabel(labelText, this);
|
||||
m_Label->setMinimumWidth(120);
|
||||
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 +75,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 +118,18 @@ 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";
|
||||
}
|
||||
}
|
||||
setText(s);
|
||||
}
|
||||
|
||||
void UnitLineEdit::setIntegerOnly(bool integerOnly) {
|
||||
@@ -107,10 +140,17 @@ 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);
|
||||
std::string unit = prop->GetUnits();
|
||||
if (!unit.empty()) {
|
||||
auto dim = Settings::Instance().IdentifyDimension(unit);
|
||||
std::string pref = Settings::Instance().GetPreferredUnit(dim);
|
||||
double factor = Settings::Instance().GetUnitFactor(pref);
|
||||
m_Edit->setUnits(QString::fromStdString(pref), 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 +158,17 @@ 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);
|
||||
std::string unit = prop->GetUnits();
|
||||
if (!unit.empty()) {
|
||||
auto dim = Settings::Instance().IdentifyDimension(unit);
|
||||
std::string pref = Settings::Instance().GetPreferredUnit(dim);
|
||||
double factor = Settings::Instance().GetUnitFactor(pref);
|
||||
m_Edit->setUnits(QString::fromStdString(pref), 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 +177,17 @@ 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);
|
||||
std::string unit = prop->GetUnits();
|
||||
if (!unit.empty()) {
|
||||
auto dim = Settings::Instance().IdentifyDimension(unit);
|
||||
std::string pref = Settings::Instance().GetPreferredUnit(dim);
|
||||
double factor = Settings::Instance().GetUnitFactor(pref);
|
||||
m_Edit->setUnits(QString::fromStdString(pref), 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 +198,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 +212,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 +225,57 @@ StringPropertyWidget::StringPropertyWidget(Property<std::string>* prop, QWidget*
|
||||
}
|
||||
StringPropertyWidget::~StringPropertyWidget() {}
|
||||
|
||||
class GroupHeaderWidget : public QWidget {
|
||||
public:
|
||||
GroupHeaderWidget(const QString& name, QWidget* parent = nullptr) : QWidget(parent) {
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 8, 0, 4);
|
||||
auto* line = new QFrame(this);
|
||||
line->setFrameShape(QFrame::HLine);
|
||||
line->setFrameShadow(QFrame::Sunken);
|
||||
line->setStyleSheet("color: #555;");
|
||||
layout->addWidget(line);
|
||||
auto* label = new QLabel(name, this);
|
||||
QFont font = label->font();
|
||||
font.setBold(true);
|
||||
font.setPointSize(font.pointSize() + 1);
|
||||
label->setFont(font);
|
||||
label->setStyleSheet("color: #aaa; text-transform: uppercase;");
|
||||
layout->addWidget(label);
|
||||
}
|
||||
};
|
||||
|
||||
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 +302,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); });
|
||||
@@ -229,15 +339,51 @@ void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) {
|
||||
}
|
||||
}
|
||||
|
||||
// Group properties by their group string
|
||||
std::map<std::string, std::vector<::uLib::PropertyBase*>> groupedProps;
|
||||
std::vector<std::string> groupOrder;
|
||||
|
||||
for (auto* prop : *props) {
|
||||
auto it = m_Factories.find(prop->GetTypeIndex());
|
||||
if (it != m_Factories.end()) {
|
||||
QWidget* widget = it->second(prop, m_Container);
|
||||
m_ContainerLayout->addWidget(widget);
|
||||
} else {
|
||||
QWidget* fallback = new PropertyWidgetBase(prop, m_Container);
|
||||
fallback->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")"));
|
||||
m_ContainerLayout->addWidget(fallback);
|
||||
std::string group = prop->GetGroup();
|
||||
if (groupedProps.find(group) == groupedProps.end()) {
|
||||
groupOrder.push_back(group);
|
||||
}
|
||||
groupedProps[group].push_back(prop);
|
||||
}
|
||||
|
||||
for (const auto& groupName : groupOrder) {
|
||||
if (!groupName.empty()) {
|
||||
m_ContainerLayout->addWidget(new GroupHeaderWidget(QString::fromStdString(groupName), m_Container));
|
||||
}
|
||||
|
||||
for (auto* prop : groupedProps[groupName]) {
|
||||
QWidget* widget = nullptr;
|
||||
|
||||
// Priority 1: Check if it provides enum labels
|
||||
if (!prop->GetEnumLabels().empty()) {
|
||||
widget = new EnumPropertyWidget(prop, m_Container);
|
||||
} else {
|
||||
// Priority 2: Standard factory lookup
|
||||
auto it = m_Factories.find(prop->GetTypeIndex());
|
||||
if (it != m_Factories.end()) {
|
||||
widget = it->second(prop, m_Container);
|
||||
} else {
|
||||
// Debug info for unknown types
|
||||
std::cout << "PropertyEditor: No factory for " << prop->GetQualifiedName()
|
||||
<< " (Type: " << prop->GetTypeName() << ")" << std::endl;
|
||||
|
||||
widget = new PropertyWidgetBase(prop, m_Container);
|
||||
widget->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")"));
|
||||
}
|
||||
}
|
||||
|
||||
if (widget) {
|
||||
if (!groupName.empty()) {
|
||||
// Indent grouped properties
|
||||
widget->setContentsMargins(16, 0, 0, 0);
|
||||
}
|
||||
m_ContainerLayout->addWidget(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_ContainerLayout->addStretch(1);
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
|
||||
#include "Core/Property.h"
|
||||
#include "Core/Object.h"
|
||||
#include "Core/Signal.h"
|
||||
#include "Math/Dense.h"
|
||||
#include "Settings.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 +34,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 +44,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 +94,25 @@ class VectorPropertyWidget : public PropertyWidgetBase {
|
||||
public:
|
||||
VectorPropertyWidget(Property<VecT>* prop, QWidget* parent = nullptr)
|
||||
: PropertyWidgetBase(prop, parent), m_Prop(prop) {
|
||||
|
||||
std::string unit = prop->GetUnits();
|
||||
double factor = 1.0;
|
||||
QString prefSuffix;
|
||||
if (!unit.empty()) {
|
||||
auto dim = Settings::Instance().IdentifyDimension(unit);
|
||||
std::string pref = Settings::Instance().GetPreferredUnit(dim);
|
||||
factor = Settings::Instance().GetUnitFactor(pref);
|
||||
prefSuffix = QString::fromStdString(pref);
|
||||
}
|
||||
|
||||
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 (!prefSuffix.isEmpty()) {
|
||||
m_Edits[i]->setUnits(prefSuffix, factor);
|
||||
}
|
||||
m_Layout->addWidget(m_Edits[i], 1);
|
||||
|
||||
connect(m_Edits[i], &UnitLineEdit::valueManualChanged, [this, i](double val){
|
||||
@@ -100,10 +122,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() {
|
||||
|
||||
75
app/gcompose/src/Settings.h
Normal file
75
app/gcompose/src/Settings.h
Normal file
@@ -0,0 +1,75 @@
|
||||
#ifndef GCOMPOSE_SETTINGS_H
|
||||
#define GCOMPOSE_SETTINGS_H
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include "Math/Units.h"
|
||||
|
||||
namespace uLib {
|
||||
namespace Qt {
|
||||
|
||||
class Settings {
|
||||
public:
|
||||
static Settings& Instance() {
|
||||
static Settings instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
enum Dimension {
|
||||
Length,
|
||||
Angle,
|
||||
Energy,
|
||||
Time,
|
||||
Dimensionless
|
||||
};
|
||||
|
||||
void SetPreferredUnit(Dimension dim, const std::string& unit) {
|
||||
m_PreferredUnits[dim] = unit;
|
||||
}
|
||||
|
||||
std::string GetPreferredUnit(Dimension dim) const {
|
||||
auto it = m_PreferredUnits.find(dim);
|
||||
if (it != m_PreferredUnits.end()) return it->second;
|
||||
|
||||
switch(dim) {
|
||||
case Length: return "mm";
|
||||
case Angle: return "deg";
|
||||
case Energy: return "MeV";
|
||||
case Time: return "ns";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
double GetUnitFactor(const std::string& unit) const {
|
||||
if (unit == "m") return CLHEP::meter;
|
||||
if (unit == "cm") return CLHEP::centimeter;
|
||||
if (unit == "mm") return CLHEP::millimeter;
|
||||
if (unit == "um") return CLHEP::micrometer;
|
||||
if (unit == "deg") return CLHEP::degree;
|
||||
if (unit == "rad") return CLHEP::radian;
|
||||
if (unit == "ns") return CLHEP::nanosecond;
|
||||
if (unit == "s") return CLHEP::second;
|
||||
if (unit == "ms") return CLHEP::millisecond;
|
||||
if (unit == "MeV") return CLHEP::megaelectronvolt;
|
||||
if (unit == "GeV") return CLHEP::gigaelectronvolt;
|
||||
if (unit == "eV") return CLHEP::electronvolt;
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Dimension IdentifyDimension(const std::string& unit) const {
|
||||
if (unit == "m" || unit == "cm" || unit == "mm" || unit == "um" || unit == "nm") return Length;
|
||||
if (unit == "deg" || unit == "rad") return Angle;
|
||||
if (unit == "MeV" || unit == "GeV" || unit == "eV" || unit == "keV" || unit == "TeV") return Energy;
|
||||
if (unit == "ns" || unit == "s" || unit == "ms" || unit == "us") return Time;
|
||||
return Dimensionless;
|
||||
}
|
||||
|
||||
private:
|
||||
Settings() {}
|
||||
std::map<Dimension, std::string> m_PreferredUnits;
|
||||
};
|
||||
|
||||
} // namespace Qt
|
||||
} // namespace uLib
|
||||
|
||||
#endif
|
||||
@@ -46,23 +46,25 @@ ViewportPane::ViewportPane(QWidget* parent) : QWidget(parent), m_viewport(nullpt
|
||||
|
||||
m_layout->addWidget(m_titleBar);
|
||||
|
||||
// Main horizontal container for viewport and display panel
|
||||
QWidget* mainArea = new QWidget(this);
|
||||
QHBoxLayout* hLayout = new QHBoxLayout(mainArea);
|
||||
hLayout->setContentsMargins(0, 0, 0, 0);
|
||||
hLayout->setSpacing(0);
|
||||
m_layout->addWidget(mainArea);
|
||||
// Main area with splitter for viewport and display panel
|
||||
m_areaSplitter = new QSplitter(Qt::Horizontal, this);
|
||||
m_areaSplitter->setObjectName("ViewportAreaSplitter");
|
||||
m_layout->addWidget(m_areaSplitter, 1);
|
||||
|
||||
// Viewport will be added here via setViewport
|
||||
m_viewport = new uLib::Vtk::QViewport(mainArea);
|
||||
hLayout->addWidget(m_viewport);
|
||||
m_viewport = new uLib::Vtk::QViewport(m_areaSplitter);
|
||||
m_areaSplitter->addWidget(m_viewport);
|
||||
|
||||
// Display Panel (Overlay/Slide-out)
|
||||
m_displayPanel = new QFrame(mainArea);
|
||||
m_displayPanel = new QFrame(m_areaSplitter);
|
||||
m_displayPanel->setObjectName("DisplayPropertiesPanel");
|
||||
m_displayPanel->setFixedWidth(250);
|
||||
m_displayPanel->setMinimumWidth(150);
|
||||
m_displayPanel->hide();
|
||||
|
||||
m_areaSplitter->addWidget(m_displayPanel);
|
||||
m_areaSplitter->setStretchFactor(0, 1);
|
||||
m_areaSplitter->setStretchFactor(1, 0);
|
||||
|
||||
QVBoxLayout* panelLayout = new QVBoxLayout(m_displayPanel);
|
||||
panelLayout->setContentsMargins(5, 5, 5, 5);
|
||||
|
||||
@@ -72,8 +74,6 @@ ViewportPane::ViewportPane(QWidget* parent) : QWidget(parent), m_viewport(nullpt
|
||||
|
||||
m_displayEditor = new uLib::Qt::PropertyEditor(m_displayPanel);
|
||||
panelLayout->addWidget(m_displayEditor);
|
||||
|
||||
hLayout->addWidget(m_displayPanel);
|
||||
|
||||
connect(m_toggleBtn, &QPushButton::toggled, this, &ViewportPane::toggleDisplayPanel);
|
||||
connect(m_titleBar, &QWidget::customContextMenuRequested, this, &ViewportPane::showContextMenu);
|
||||
@@ -85,7 +85,15 @@ ViewportPane::ViewportPane(QWidget* parent) : QWidget(parent), m_viewport(nullpt
|
||||
ViewportPane::~ViewportPane() {}
|
||||
|
||||
void ViewportPane::toggleDisplayPanel() {
|
||||
m_displayPanel->setVisible(m_toggleBtn->isChecked());
|
||||
bool visible = m_toggleBtn->isChecked();
|
||||
m_displayPanel->setVisible(visible);
|
||||
if (visible && m_areaSplitter->sizes().value(1, 0) == 0) {
|
||||
QList<int> sizes = m_areaSplitter->sizes();
|
||||
int total = sizes[0] + sizes[1];
|
||||
sizes[1] = 250;
|
||||
sizes[0] = total - 250;
|
||||
m_areaSplitter->setSizes(sizes);
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportPane::setObject(uLib::Object* obj) {
|
||||
@@ -107,15 +115,14 @@ void ViewportPane::setObject(uLib::Object* obj) {
|
||||
|
||||
void ViewportPane::setViewport(QWidget* viewport, const QString& title) {
|
||||
if (m_viewport) {
|
||||
m_viewport->parentWidget()->layout()->removeWidget(m_viewport);
|
||||
delete m_viewport;
|
||||
}
|
||||
m_viewport = viewport;
|
||||
m_titleLabel->setText(title);
|
||||
|
||||
m_viewport->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
auto* mainAreaLayout = static_cast<QHBoxLayout*>(m_displayPanel->parentWidget()->layout());
|
||||
mainAreaLayout->insertWidget(0, m_viewport);
|
||||
m_areaSplitter->insertWidget(0, m_viewport);
|
||||
m_areaSplitter->setStretchFactor(0, 1);
|
||||
}
|
||||
|
||||
void ViewportPane::addVtkViewport() {
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace uLib {
|
||||
namespace Qt { class PropertyEditor; }
|
||||
}
|
||||
|
||||
class QSplitter;
|
||||
class QVBoxLayout;
|
||||
class QLabel;
|
||||
|
||||
@@ -39,6 +40,7 @@ private:
|
||||
QVBoxLayout* m_layout;
|
||||
QWidget* m_titleBar;
|
||||
QLabel* m_titleLabel;
|
||||
QSplitter* m_areaSplitter;
|
||||
QWidget* m_viewport;
|
||||
|
||||
// Display Properties Overlay
|
||||
|
||||
BIN
assets/exmaples/vtk/2026_03_24_C1_Prod11_test_img_40_trim55505_scale1.00_sigma1.0.vtk
(Stored with Git LFS)
Normal file
BIN
assets/exmaples/vtk/2026_03_24_C1_Prod11_test_img_40_trim55505_scale1.00_sigma1.0.vtk
(Stored with Git LFS)
Normal file
Binary file not shown.
338
docs/algorithms/algoritm.md
Normal file
338
docs/algorithms/algoritm.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# Algorithm Infrastructure
|
||||
|
||||
## Overview
|
||||
|
||||
An algorithm in the uLib infrastructure is a class for containing a functional that can be dynamically loaded into memory as a plug-in.
|
||||
It derives from the base `Object` class (`Core/Object.h`) and therefore can contain properties that define the serialization of operating parameters or the implementation of widgets for interactive parameter manipulation.
|
||||
|
||||
The algorithm class is designed to be inserted into an `AlgorithmTask`, a class for managing the execution of scheduled operations. A task contains `Run` and `Stop` methods to start and stop execution. A task can be configured to work in two modes:
|
||||
|
||||
- **Cyclic mode**: the algorithm is executed periodically with a configurable cycle time.
|
||||
- **Asynchronous mode**: the task waits for a trigger before each execution. Triggers can come from the uLib signal-slot system (`Object::connect`) or from a condition variable as defined in the monitor pattern (`Core/Monitor.h`).
|
||||
|
||||
The algorithm is defined as a template class on two types `T_enc` and `T_dec`. The encoder is a type for data input or another algorithm that is chained with this one and outputs data in a compatible format. The decoder is the type of data output or a downstream algorithm compatible with it.
|
||||
|
||||
## Class Hierarchy
|
||||
|
||||
```
|
||||
Object (Core/Object.h)
|
||||
|
|
||||
+-- Algorithm<T_enc, T_dec> (Core/Algorithm.h)
|
||||
| |
|
||||
| +-- VoxImageFilter<VoxelT, CrtpImplT> (Math/VoxImageFilter.h)
|
||||
| |
|
||||
| +-- VoxFilterAlgorithmLinear (Math/VoxImageFilterLinear.hpp)
|
||||
| +-- VoxFilterAlgorithmMedian (Math/VoxImageFilterMedian.hpp)
|
||||
| +-- VoxFilterAlgorithmAbtrim (Math/VoxImageFilterABTrim.hpp)
|
||||
| +-- VoxFilterAlgorithmSPR (Math/VoxImageFilterABTrim.hpp)
|
||||
| +-- VoxFilterAlgorithmThreshold (Math/VoxImageFilterThreshold.hpp)
|
||||
| +-- VoxFilterAlgorithmBilateral (Math/VoxImageFilterBilateral.hpp)
|
||||
| +-- VoxFilterAlgorithmBilateralTrim(Math/VoxImageFilterBilateral.hpp)
|
||||
| +-- VoxFilterAlgorithm2ndStat (Math/VoxImageFilter2ndStat.hpp)
|
||||
| +-- VoxFilterAlgorithmCustom (Math/VoxImageFilterCustom.hpp)
|
||||
|
|
||||
+-- Thread (Core/Threads.h)
|
||||
|
|
||||
+-- AlgorithmTask<T_enc, T_dec> (Core/Algorithm.h)
|
||||
```
|
||||
|
||||
## Algorithm (`Core/Algorithm.h`)
|
||||
|
||||
### Template Parameters
|
||||
|
||||
```cpp
|
||||
template <typename T_enc, typename T_dec>
|
||||
class Algorithm : public Object;
|
||||
```
|
||||
|
||||
- **`T_enc`** (Encoder): the input data type. Can be a raw data type or a pointer to a data structure. When chaining algorithms, the upstream algorithm's `T_dec` must be compatible with this algorithm's `T_enc`.
|
||||
- **`T_dec`** (Decoder): the output data type. Produced by `Process()` and consumed by the next algorithm in the chain.
|
||||
|
||||
### Core Interface
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `virtual T_dec Process(const T_enc& input) = 0` | Pure virtual. Implement the algorithm logic here. |
|
||||
| `T_dec operator()(const T_enc& input)` | Calls `Process()`. Enables functional syntax: `result = alg(data)`. |
|
||||
|
||||
### Algorithm Chaining
|
||||
|
||||
Algorithms can be linked in processing pipelines via encoder/decoder pointers:
|
||||
|
||||
```cpp
|
||||
Algorithm* upstream; // SetEncoder() / GetEncoder()
|
||||
Algorithm* downstream; // SetDecoder() / GetDecoder()
|
||||
```
|
||||
|
||||
This allows building chains like:
|
||||
|
||||
```
|
||||
[RawData] --> AlgorithmA --> AlgorithmB --> [Result]
|
||||
encoder decoder
|
||||
```
|
||||
|
||||
### Signals
|
||||
|
||||
| Signal | Emitted when |
|
||||
|--------|-------------|
|
||||
| `Started()` | The algorithm begins processing (caller responsibility). |
|
||||
| `Finished()` | The algorithm completes processing (caller responsibility). |
|
||||
|
||||
### Device Preference (CUDA)
|
||||
|
||||
Algorithms report their preferred execution device via `GetPreferredDevice()`:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `virtual MemoryDevice GetPreferredDevice() const` | Returns `RAM` or `VRAM`. Subclasses override. |
|
||||
| `void SetPreferredDevice(MemoryDevice dev)` | Manually set the device preference. |
|
||||
| `bool IsGPU() const` | Shorthand for `GetPreferredDevice() == VRAM`. |
|
||||
|
||||
GPU-based algorithms are responsible for calling `cudaDeviceSynchronize()` inside their `Process()` implementation before returning, so that results are available to the caller or downstream algorithm.
|
||||
|
||||
### Example: Defining a Custom Algorithm
|
||||
|
||||
```cpp
|
||||
class MyFilter : public Algorithm<VoxImage<Voxel>*, VoxImage<Voxel>*> {
|
||||
public:
|
||||
const char* GetClassName() const override { return "MyFilter"; }
|
||||
|
||||
VoxImage<Voxel>* Process(VoxImage<Voxel>* const& image) override {
|
||||
// ... filter the image in-place ...
|
||||
return image;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## AlgorithmTask (`Core/Algorithm.h`)
|
||||
|
||||
`AlgorithmTask` manages the execution of an `Algorithm` within a scheduled, threaded context. It inherits from `Thread` (`Core/Threads.h`) and uses `Mutex` (`Core/Monitor.h`) for synchronization.
|
||||
|
||||
### Template Parameters
|
||||
|
||||
```cpp
|
||||
template <typename T_enc, typename T_dec>
|
||||
class AlgorithmTask : public Thread;
|
||||
```
|
||||
|
||||
Must match the `Algorithm<T_enc, T_dec>` it manages.
|
||||
|
||||
### Configuration
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `void SetAlgorithm(AlgorithmType* alg)` | Set the algorithm to execute. |
|
||||
| `void SetMode(Mode mode)` | `Cyclic` or `Async`. |
|
||||
| `void SetCycleTime(int ms)` | Period for cyclic mode (milliseconds). |
|
||||
|
||||
### Execution Modes
|
||||
|
||||
#### Cyclic Mode
|
||||
|
||||
The algorithm's `Process()` is called periodically. The cycle waits on a `condition_variable_any` with timeout, so `Stop()` can interrupt immediately without waiting for the full cycle.
|
||||
|
||||
```cpp
|
||||
AlgorithmTask<int, int> task;
|
||||
task.SetAlgorithm(&myAlgorithm);
|
||||
task.SetMode(AlgorithmTask<int, int>::Cyclic);
|
||||
task.SetCycleTime(100); // every 100ms
|
||||
task.Run(inputData);
|
||||
// ... later ...
|
||||
task.Stop();
|
||||
```
|
||||
|
||||
#### Asynchronous Mode
|
||||
|
||||
The task thread blocks on a condition variable until `Notify()` is called. Each notification triggers exactly one `Process()` invocation.
|
||||
|
||||
```cpp
|
||||
task.SetMode(AlgorithmTask<int, int>::Async);
|
||||
task.Run(inputData);
|
||||
|
||||
// Trigger manually:
|
||||
task.Notify();
|
||||
|
||||
// Or connect to a signal:
|
||||
task.ConnectTrigger(sender, &SenderClass::DataReady);
|
||||
// Now each emission of DataReady() triggers one Process() call.
|
||||
```
|
||||
|
||||
### Lifecycle
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `void Run(const T_enc& input)` | Starts the background thread with the given input. |
|
||||
| `void Stop()` | Requests stop and joins the thread. |
|
||||
| `bool IsRunning()` | Inherited from `Thread`. |
|
||||
|
||||
### Signals
|
||||
|
||||
| Signal | Emitted when |
|
||||
|--------|-------------|
|
||||
| `Stopped()` | The task thread has completed (after last `Process()` and before thread exit). |
|
||||
|
||||
### Signal-Slot Triggering
|
||||
|
||||
`ConnectTrigger()` connects any uLib `Object` signal to the task's `Notify()` method:
|
||||
|
||||
```cpp
|
||||
task.ConnectTrigger(detector, &Detector::EventReady);
|
||||
```
|
||||
|
||||
This uses the uLib signal system (`Core/Signal.h`), not Qt signals. The connection is type-safe and works with the `Object::connect` infrastructure.
|
||||
|
||||
## VoxImageFilter (`Math/VoxImageFilter.h`)
|
||||
|
||||
`VoxImageFilter` specializes `Algorithm` for kernel-based volumetric image filtering. It uses CRTP (Curiously Recurring Template Pattern) so that concrete filters provide their `Evaluate()` method without virtual dispatch overhead in the inner loop.
|
||||
|
||||
### Template Parameters
|
||||
|
||||
```cpp
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
class VoxImageFilter : public Abstract::VoxImageFilter,
|
||||
public Algorithm<VoxImage<VoxelT>*, VoxImage<VoxelT>*>;
|
||||
```
|
||||
|
||||
- **`VoxelT`**: the voxel data type (must satisfy `Interface::Voxel` — requires `.Value` and `.Count` fields).
|
||||
- **`CrtpImplT`**: the concrete filter subclass. Must implement:
|
||||
```cpp
|
||||
float Evaluate(const VoxImage<VoxelT>& buffer, int index);
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. `Process(image)` creates a read-only buffer copy of the input image.
|
||||
2. For each voxel in parallel (OpenMP), it calls `CrtpImplT::Evaluate(buffer, index)`.
|
||||
3. `Evaluate()` reads from the buffer using the kernel offsets and writes the result.
|
||||
4. The filtered image is returned (in-place modification).
|
||||
|
||||
```
|
||||
Process(image)
|
||||
|
|
||||
+-- buffer = copy of image (read-only snapshot)
|
||||
|
|
||||
+-- #pragma omp parallel for
|
||||
| for each voxel i:
|
||||
| image[i].Value = CrtpImplT::Evaluate(buffer, i)
|
||||
|
|
||||
+-- return image
|
||||
```
|
||||
|
||||
### Kernel System
|
||||
|
||||
The `Kernel<VoxelT>` class stores convolution weights and precomputed index offsets:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `SetKernelNumericXZY(values)` | Set kernel weights from a flat vector (XZY order). |
|
||||
| `SetKernelSpherical(shape)` | Set weights via a radial function `f(distance^2)`. |
|
||||
| `SetKernelWeightFunction(shape)` | Set weights via a 3D position function `f(Vector3f)`. |
|
||||
|
||||
### CUDA Support
|
||||
|
||||
Concrete filters can override `Process()` with a CUDA implementation:
|
||||
|
||||
```cpp
|
||||
#if defined(USE_CUDA) && defined(__CUDACC__)
|
||||
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
|
||||
if (this->GetPreferredDevice() == MemoryDevice::VRAM) {
|
||||
// Launch CUDA kernel, synchronize, return
|
||||
} else {
|
||||
return BaseClass::Process(image); // CPU fallback
|
||||
}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
The base class `GetPreferredDevice()` automatically returns `VRAM` when the image or kernel data resides on the GPU, enabling transparent device dispatch.
|
||||
|
||||
Filters with CUDA implementations: `VoxFilterAlgorithmLinear`, `VoxFilterAlgorithmAbtrim`, `VoxFilterAlgorithmSPR`.
|
||||
|
||||
### Concrete Filters
|
||||
|
||||
| Filter | File | Description |
|
||||
|--------|------|-------------|
|
||||
| `VoxFilterAlgorithmLinear` | `VoxImageFilterLinear.hpp` | Weighted linear convolution (FIR filter). CUDA-enabled. |
|
||||
| `VoxFilterAlgorithmMedian` | `VoxImageFilterMedian.hpp` | Median filter with kernel-weighted sorting. |
|
||||
| `VoxFilterAlgorithmAbtrim` | `VoxImageFilterABTrim.hpp` | Alpha-beta trimmed mean filter. CUDA-enabled. |
|
||||
| `VoxFilterAlgorithmSPR` | `VoxImageFilterABTrim.hpp` | Robespierre filter: trimmed mean applied only to outlier voxels. CUDA-enabled. |
|
||||
| `VoxFilterAlgorithmThreshold` | `VoxImageFilterThreshold.hpp` | Binary threshold filter. |
|
||||
| `VoxFilterAlgorithmBilateral` | `VoxImageFilterBilateral.hpp` | Edge-preserving bilateral filter (intensity-weighted Gaussian). |
|
||||
| `VoxFilterAlgorithmBilateralTrim` | `VoxImageFilterBilateral.hpp` | Bilateral filter with alpha-beta trimming. |
|
||||
| `VoxFilterAlgorithm2ndStat` | `VoxImageFilter2ndStat.hpp` | Local variance (second-order statistic). |
|
||||
| `VoxFilterAlgorithmCustom` | `VoxImageFilterCustom.hpp` | User-supplied evaluation function via function pointer. |
|
||||
|
||||
### Example: Using a Filter with AlgorithmTask
|
||||
|
||||
```cpp
|
||||
// Create filter and configure kernel
|
||||
VoxFilterAlgorithmLinear<Voxel> filter(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights(27, 1.0f); // uniform 3x3x3
|
||||
filter.SetKernelNumericXZY(weights);
|
||||
|
||||
// Direct use
|
||||
filter.SetImage(&image);
|
||||
filter.Run();
|
||||
|
||||
// Or via Algorithm interface
|
||||
VoxImage<Voxel>* result = filter.Process(&image);
|
||||
|
||||
// Or scheduled in a task
|
||||
AlgorithmTask<VoxImage<Voxel>*, VoxImage<Voxel>*> task;
|
||||
task.SetAlgorithm(&filter);
|
||||
task.SetMode(AlgorithmTask<VoxImage<Voxel>*, VoxImage<Voxel>*>::Cyclic);
|
||||
task.SetCycleTime(500);
|
||||
task.Run(&image);
|
||||
```
|
||||
|
||||
## Structural Benefits
|
||||
|
||||
### 1. Uniform Processing Interface
|
||||
|
||||
Every algorithm — from a simple threshold to a GPU-accelerated convolution — exposes the same `Process(input) -> output` interface. Client code does not need to know the concrete type:
|
||||
|
||||
```cpp
|
||||
Algorithm<VoxImage<Voxel>*, VoxImage<Voxel>*>* alg = &anyFilter;
|
||||
alg->Process(&image);
|
||||
```
|
||||
|
||||
### 2. Pipeline Composition
|
||||
|
||||
The encoder/decoder chaining allows building data processing pipelines where each stage transforms data and passes it to the next. Type safety is enforced at compile time through template parameters.
|
||||
|
||||
### 3. Scheduled and Event-Driven Execution
|
||||
|
||||
`AlgorithmTask` decouples the algorithm from its execution schedule. The same algorithm can be:
|
||||
- Called directly (`Process()`)
|
||||
- Run periodically (Cyclic mode for monitoring/acquisition)
|
||||
- Triggered by events (Async mode for reactive processing)
|
||||
|
||||
### 4. Transparent CPU/GPU Dispatch
|
||||
|
||||
The `MemoryDevice` preference and `GetPreferredDevice()` virtual allow the same algorithm interface to dispatch to CPU or GPU implementations. The `DataAllocator` transparently manages RAM/VRAM transfers, and concrete filters override `Process()` with CUDA kernels when data is on the GPU.
|
||||
|
||||
### 5. Integration with the Object System
|
||||
|
||||
Since `Algorithm` inherits from `Object`, algorithms gain:
|
||||
- **Properties**: serializable parameters via the `Property<T>` system, enabling persistent configuration and GUI widget generation.
|
||||
- **Signals**: `Started`/`Finished` notifications for connecting to monitoring or logging.
|
||||
- **Serialization**: save/load algorithm configuration via Boost archives.
|
||||
- **Instance naming**: `SetInstanceName()` for runtime identification in contexts.
|
||||
|
||||
### 6. CRTP Performance for Inner Loops
|
||||
|
||||
`VoxImageFilter` uses CRTP to dispatch to `Evaluate()` without virtual function overhead. The per-voxel evaluation runs at full speed inside OpenMP parallel loops, while the outer `Process()` method remains virtual for polymorphic use through the Algorithm interface.
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
Core/Object.h — base class, properties, signals, serialization
|
||||
Core/Signal.h — signal-slot connection infrastructure
|
||||
Core/Monitor.h — Mutex, condition variables, ULIB_MUTEX_LOCK
|
||||
Core/Threads.h — Thread base class for AlgorithmTask
|
||||
Core/DataAllocator.h — MemoryDevice enum, RAM/VRAM data management
|
||||
Math/VoxImage.h — volumetric image container
|
||||
Math/VoxImageFilter.h — kernel-based filter framework
|
||||
```
|
||||
|
||||
|
||||
263
src/Core/Algorithm.h
Normal file
263
src/Core/Algorithm.h
Normal file
@@ -0,0 +1,263 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
------------------------------------------------------------------
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3.0 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library.
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#ifndef U_CORE_ALGORITHM_H
|
||||
#define U_CORE_ALGORITHM_H
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
|
||||
#include "Core/Object.h"
|
||||
#include "Core/Monitor.h"
|
||||
#include "Core/Threads.h"
|
||||
#include "Core/DataAllocator.h"
|
||||
|
||||
namespace uLib {
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// ALGORITHM /////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @brief Algorithm is a template class for containing a functional that can be
|
||||
* dynamically loaded as a plug-in. It derives from Object and supports
|
||||
* properties for serialization and interactive parameter widgets.
|
||||
*
|
||||
* Algorithms are responsible for their own GPU synchronization: if Process()
|
||||
* launches CUDA kernels, it must call cudaDeviceSynchronize() before returning
|
||||
* so that the result is available to the caller or downstream algorithm.
|
||||
*
|
||||
* @tparam T_enc Encoder type: the input data type, or a chained algorithm
|
||||
* whose output is compatible with this algorithm's input.
|
||||
* @tparam T_dec Decoder type: the output data type, or a chained algorithm
|
||||
* whose input is compatible with this algorithm's output.
|
||||
*/
|
||||
template <typename T_enc, typename T_dec>
|
||||
class Algorithm : public Object {
|
||||
public:
|
||||
using EncoderType = T_enc;
|
||||
using DecoderType = T_dec;
|
||||
|
||||
Algorithm()
|
||||
: Object()
|
||||
, m_Encoder(nullptr)
|
||||
, m_Decoder(nullptr)
|
||||
, m_PreferredDevice(MemoryDevice::RAM)
|
||||
{}
|
||||
virtual ~Algorithm() = default;
|
||||
|
||||
virtual const char* GetClassName() const override { return "Algorithm"; }
|
||||
|
||||
// Processing ///////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @brief Process input data and produce output.
|
||||
* Override this in subclasses to implement the algorithm logic.
|
||||
* GPU-based implementations must synchronize before returning.
|
||||
*/
|
||||
virtual T_dec Process(const T_enc& input) = 0;
|
||||
|
||||
/** @brief Operator form of Process for functional chaining. */
|
||||
T_dec operator()(const T_enc& input) { return Process(input); }
|
||||
|
||||
// Chaining /////////////////////////////////////////////////////////////////
|
||||
|
||||
void SetEncoder(Algorithm* enc) { m_Encoder = enc; }
|
||||
Algorithm* GetEncoder() const { return m_Encoder; }
|
||||
|
||||
void SetDecoder(Algorithm* dec) { m_Decoder = dec; }
|
||||
Algorithm* GetDecoder() const { return m_Decoder; }
|
||||
|
||||
// Device preference ////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @brief Returns the preferred memory device for this algorithm.
|
||||
* CUDA-capable algorithms should override to return VRAM when their
|
||||
* data resides on the GPU.
|
||||
*/
|
||||
virtual MemoryDevice GetPreferredDevice() const { return m_PreferredDevice; }
|
||||
void SetPreferredDevice(MemoryDevice dev) { m_PreferredDevice = dev; }
|
||||
|
||||
/** @brief Returns true if this algorithm prefers GPU execution. */
|
||||
bool IsGPU() const { return GetPreferredDevice() == MemoryDevice::VRAM; }
|
||||
|
||||
// Signals //////////////////////////////////////////////////////////////////
|
||||
|
||||
signals:
|
||||
virtual void Started() { ULIB_SIGNAL_EMIT(Algorithm::Started); }
|
||||
virtual void Finished() { ULIB_SIGNAL_EMIT(Algorithm::Finished); }
|
||||
|
||||
protected:
|
||||
Algorithm* m_Encoder;
|
||||
Algorithm* m_Decoder;
|
||||
MemoryDevice m_PreferredDevice;
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// ALGORITHM TASK ////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @brief AlgorithmTask manages the execution of an Algorithm within a
|
||||
* scheduled context. Uses uLib::Thread for execution and uLib::Mutex for
|
||||
* synchronization.
|
||||
*
|
||||
* Two execution modes:
|
||||
* - Cyclic: executes Process() periodically with configurable cycle time.
|
||||
* - Async: waits for Notify() or a connected signal before each execution.
|
||||
*
|
||||
* GPU synchronization is the algorithm's responsibility (see Algorithm::Process).
|
||||
*/
|
||||
template <typename T_enc, typename T_dec>
|
||||
class AlgorithmTask : public Thread {
|
||||
public:
|
||||
using AlgorithmType = Algorithm<T_enc, T_dec>;
|
||||
|
||||
enum Mode { Cyclic, Async };
|
||||
|
||||
AlgorithmTask()
|
||||
: Thread()
|
||||
, m_Algorithm(nullptr)
|
||||
, m_Mode(Cyclic)
|
||||
, m_CycleTime_ms(1000)
|
||||
, m_StopRequested(false)
|
||||
, m_Triggered(false)
|
||||
{}
|
||||
|
||||
virtual ~AlgorithmTask() { Stop(); }
|
||||
|
||||
virtual const char* GetClassName() const override { return "AlgorithmTask"; }
|
||||
|
||||
// Configuration ////////////////////////////////////////////////////////////
|
||||
|
||||
void SetAlgorithm(AlgorithmType* alg) { m_Algorithm = alg; }
|
||||
AlgorithmType* GetAlgorithm() const { return m_Algorithm; }
|
||||
|
||||
void SetMode(Mode mode) { m_Mode = mode; }
|
||||
Mode GetMode() const { return m_Mode; }
|
||||
|
||||
void SetCycleTime(int milliseconds) { m_CycleTime_ms = milliseconds; }
|
||||
int GetCycleTime() const { return m_CycleTime_ms; }
|
||||
|
||||
// Lifecycle ////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @brief Start the task execution in a separate thread (via Thread::Start).
|
||||
* In Cyclic mode, the algorithm is executed periodically.
|
||||
* In Async mode, call Notify() or connect a signal to trigger execution.
|
||||
*/
|
||||
void Run(const T_enc& input) {
|
||||
if (IsRunning()) return;
|
||||
m_StopRequested.store(false);
|
||||
m_Triggered.store(false);
|
||||
m_Input = input;
|
||||
Start();
|
||||
}
|
||||
|
||||
/** @brief Stop the task execution and join the thread. */
|
||||
void Stop() {
|
||||
m_StopRequested.store(true);
|
||||
ULIB_MUTEX_LOCK(m_WaitMutex, -1) {
|
||||
m_Condition.notify_all();
|
||||
}
|
||||
if (IsJoinable()) Join();
|
||||
}
|
||||
|
||||
// Async triggering /////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @brief Notify the task to execute one iteration (Async mode).
|
||||
* Can be called from a signal-slot connection or externally.
|
||||
*/
|
||||
void Notify() {
|
||||
m_Triggered.store(true);
|
||||
ULIB_MUTEX_LOCK(m_WaitMutex, -1) {
|
||||
m_Condition.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Connect an Object signal to trigger async execution.
|
||||
* Usage: task.ConnectTrigger(sender, &SenderClass::SomeSignal);
|
||||
*/
|
||||
template <typename Func1>
|
||||
Connection ConnectTrigger(typename FunctionPointer<Func1>::Object* sender, Func1 sigf) {
|
||||
return Object::connect(sender, sigf, [this]() { Notify(); });
|
||||
}
|
||||
|
||||
// Signals //////////////////////////////////////////////////////////////////
|
||||
|
||||
signals:
|
||||
virtual void Stopped() { ULIB_SIGNAL_EMIT(AlgorithmTask::Stopped); }
|
||||
|
||||
protected:
|
||||
/** @brief Thread entry point — dispatches to cyclic or async loop. */
|
||||
void Run() override {
|
||||
if (m_Mode == Cyclic)
|
||||
RunCyclic();
|
||||
else
|
||||
RunAsync();
|
||||
Stopped();
|
||||
}
|
||||
|
||||
private:
|
||||
void RunCyclic() {
|
||||
while (!m_StopRequested.load()) {
|
||||
if (m_Algorithm) m_Algorithm->Process(m_Input);
|
||||
std::unique_lock<std::timed_mutex> lock(m_WaitMutex.GetNative());
|
||||
m_Condition.wait_for(lock,
|
||||
std::chrono::milliseconds(m_CycleTime_ms),
|
||||
[this]() { return m_StopRequested.load(); });
|
||||
}
|
||||
}
|
||||
|
||||
void RunAsync() {
|
||||
while (!m_StopRequested.load()) {
|
||||
std::unique_lock<std::timed_mutex> lock(m_WaitMutex.GetNative());
|
||||
m_Condition.wait(lock, [this]() {
|
||||
return m_StopRequested.load() || m_Triggered.load();
|
||||
});
|
||||
if (m_StopRequested.load()) break;
|
||||
m_Triggered.store(false);
|
||||
if (m_Algorithm) m_Algorithm->Process(m_Input);
|
||||
}
|
||||
}
|
||||
|
||||
AlgorithmType* m_Algorithm;
|
||||
Mode m_Mode;
|
||||
int m_CycleTime_ms;
|
||||
T_enc m_Input;
|
||||
|
||||
std::atomic<bool> m_StopRequested;
|
||||
std::atomic<bool> m_Triggered;
|
||||
Mutex m_WaitMutex;
|
||||
std::condition_variable_any m_Condition;
|
||||
};
|
||||
|
||||
} // namespace uLib
|
||||
|
||||
#endif // U_CORE_ALGORITHM_H
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
set(HEADERS
|
||||
Archives.h
|
||||
Array.h
|
||||
set(HEADERS
|
||||
Algorithm.h
|
||||
Archives.h
|
||||
Array.h
|
||||
Collection.h
|
||||
DataAllocator.h
|
||||
Debug.h
|
||||
|
||||
@@ -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 || p->GetQualifiedName() == name) return p;
|
||||
}
|
||||
for (auto* p : d->m_DynamicProperties) {
|
||||
if (p->GetName() == name || p->GetQualifiedName() == 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.
|
||||
|
||||
|
||||
@@ -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 //
|
||||
|
||||
@@ -20,13 +20,13 @@ public:
|
||||
* @brief Adds an object to the context.
|
||||
* @param obj Pointer to the object to add.
|
||||
*/
|
||||
void AddObject(Object* obj);
|
||||
virtual void AddObject(Object* obj);
|
||||
|
||||
/**
|
||||
* @brief Removes an object from the context.
|
||||
* @param obj Pointer to the object to remove.
|
||||
*/
|
||||
void RemoveObject(Object* obj);
|
||||
virtual void RemoveObject(Object* obj);
|
||||
|
||||
/**
|
||||
* @brief Clears all objects from the context.
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
#define U_CORE_PROPERTY_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <typeinfo>
|
||||
#include <typeindex> // Added
|
||||
#include <boost/serialization/nvp.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <vector>
|
||||
#include <boost/type_traits/is_class.hpp>
|
||||
#include <boost/mpl/bool.hpp>
|
||||
#include <boost/serialization/serialization.hpp>
|
||||
#include "Core/Archives.h"
|
||||
#include "Core/Signal.h"
|
||||
#include "Core/Object.h"
|
||||
@@ -23,6 +28,18 @@ 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;
|
||||
}
|
||||
virtual const std::string& GetGroup() const = 0;
|
||||
virtual void SetGroup(const std::string& group) = 0;
|
||||
std::string GetQualifiedName() const {
|
||||
if (GetGroup().empty()) return GetName();
|
||||
return GetGroup() + "." + GetName();
|
||||
}
|
||||
|
||||
// Signal support
|
||||
signals:
|
||||
@@ -45,16 +62,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 = "", const std::string& group = "")
|
||||
: m_owner(owner), m_name(name), m_units(units), m_group(group), 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 = "", const std::string& group = "")
|
||||
: m_owner(owner), m_name(name), m_units(units), m_group(group), m_value(new T(defaultValue)), m_own(true) {
|
||||
if (m_owner) {
|
||||
m_owner->RegisterProperty(this);
|
||||
}
|
||||
@@ -68,6 +85,10 @@ 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; }
|
||||
virtual const std::string& GetGroup() const override { return m_group; }
|
||||
virtual void SetGroup(const std::string& group) override { m_group = group; }
|
||||
|
||||
|
||||
std::string GetValueAsString() const override {
|
||||
@@ -118,6 +139,8 @@ public:
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
std::string m_units;
|
||||
std::string m_group;
|
||||
T* m_value;
|
||||
bool m_own;
|
||||
Object* m_owner;
|
||||
@@ -135,6 +158,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 = "", const std::string& group = "")
|
||||
: Property<int>(owner, name, valuePtr, units, group), 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)
|
||||
@@ -184,25 +223,53 @@ public:
|
||||
boost::archive::detail::common_oarchive<property_register_archive>(boost::archive::no_header),
|
||||
m_Object(obj) {}
|
||||
|
||||
std::string GetCurrentGroup() const {
|
||||
std::string group;
|
||||
for (const auto& g : m_GroupStack) {
|
||||
if (!group.empty()) group += ".";
|
||||
group += g;
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
// Core logic: encounter HRP -> Create Dynamic Property
|
||||
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() : "", GetCurrentGroup());
|
||||
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() : "", GetCurrentGroup());
|
||||
m_Object->RegisterDynamicProperty(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle standard NVPs by recursing (important for base classes)
|
||||
template<class T>
|
||||
void save_override(const boost::serialization::nvp<T> &t) {
|
||||
boost::archive::detail::common_oarchive<property_register_archive>::save_override(t.const_value());
|
||||
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();
|
||||
}
|
||||
|
||||
// Ignore everything else
|
||||
template<class T> void save_override(const T &t) {}
|
||||
// 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::false_) {}
|
||||
|
||||
// Required attribute overrides for common_oarchive
|
||||
void save_override(const boost::archive::object_id_type & t) {}
|
||||
@@ -213,6 +280,9 @@ public:
|
||||
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:
|
||||
std::vector<std::string> m_GroupStack;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
206
src/Core/testing/AlgorithmTest.cpp
Normal file
206
src/Core/testing/AlgorithmTest.cpp
Normal file
@@ -0,0 +1,206 @@
|
||||
#include "Core/Algorithm.h"
|
||||
#include <iostream>
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
|
||||
using namespace uLib;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Test algorithms
|
||||
|
||||
class DoubleAlgorithm : public Algorithm<int, int> {
|
||||
public:
|
||||
const char* GetClassName() const override { return "DoubleAlgorithm"; }
|
||||
int Process(const int& input) override {
|
||||
m_CallCount++;
|
||||
return input * 2;
|
||||
}
|
||||
std::atomic<int> m_CallCount{0};
|
||||
};
|
||||
|
||||
class StringifyAlgorithm : public Algorithm<int, std::string> {
|
||||
public:
|
||||
const char* GetClassName() const override { return "StringifyAlgorithm"; }
|
||||
std::string Process(const int& input) override {
|
||||
return std::to_string(input);
|
||||
}
|
||||
};
|
||||
|
||||
// Signal source to test ConnectTrigger
|
||||
class TriggerSource : public Object {
|
||||
public:
|
||||
const char* GetClassName() const override { return "TriggerSource"; }
|
||||
signals:
|
||||
virtual void DataReady() { ULIB_SIGNAL_EMIT(TriggerSource::DataReady); }
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Tests
|
||||
|
||||
void TestBasicProcess() {
|
||||
std::cout << "Testing basic Algorithm::Process..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
assert(alg.Process(5) == 10);
|
||||
assert(alg.Process(-3) == -6);
|
||||
assert(alg.Process(0) == 0);
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestOperatorCall() {
|
||||
std::cout << "Testing Algorithm::operator()..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
assert(alg(7) == 14);
|
||||
assert(alg(0) == 0);
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestEncoderDecoderChain() {
|
||||
std::cout << "Testing encoder/decoder chain pointers..." << std::endl;
|
||||
DoubleAlgorithm a, b;
|
||||
a.SetDecoder(&b);
|
||||
b.SetEncoder(&a);
|
||||
assert(a.GetDecoder() == &b);
|
||||
assert(b.GetEncoder() == &a);
|
||||
assert(a.GetEncoder() == nullptr);
|
||||
assert(b.GetDecoder() == nullptr);
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestAlgorithmSignals() {
|
||||
std::cout << "Testing Algorithm signals..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
bool started = false;
|
||||
bool finished = false;
|
||||
Object::connect(&alg, &DoubleAlgorithm::Started, [&]() { started = true; });
|
||||
Object::connect(&alg, &DoubleAlgorithm::Finished, [&]() { finished = true; });
|
||||
alg.Started();
|
||||
alg.Finished();
|
||||
assert(started);
|
||||
assert(finished);
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestCyclicTask() {
|
||||
std::cout << "Testing AlgorithmTask cyclic mode (Thread-based)..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
AlgorithmTask<int, int> task;
|
||||
task.SetAlgorithm(&alg);
|
||||
task.SetMode(AlgorithmTask<int, int>::Cyclic);
|
||||
task.SetCycleTime(50);
|
||||
|
||||
assert(!task.IsRunning());
|
||||
task.Run(5);
|
||||
|
||||
// Let it run for ~200ms -> expect ~4 cycles
|
||||
Thread::Sleep(220);
|
||||
task.Stop();
|
||||
|
||||
assert(!task.IsRunning());
|
||||
int count = alg.m_CallCount.load();
|
||||
std::cout << " Cyclic iterations: " << count << std::endl;
|
||||
assert(count >= 3 && count <= 6);
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestAsyncTask() {
|
||||
std::cout << "Testing AlgorithmTask async mode (Mutex + condition_variable)..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
AlgorithmTask<int, int> task;
|
||||
task.SetAlgorithm(&alg);
|
||||
task.SetMode(AlgorithmTask<int, int>::Async);
|
||||
|
||||
task.Run(42);
|
||||
Thread::Sleep(50); // let the thread start and wait
|
||||
|
||||
// Trigger 3 notifications
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
task.Notify();
|
||||
Thread::Sleep(30);
|
||||
}
|
||||
|
||||
task.Stop();
|
||||
int count = alg.m_CallCount.load();
|
||||
std::cout << " Async invocations: " << count << std::endl;
|
||||
assert(count == 3);
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestConnectTrigger() {
|
||||
std::cout << "Testing AlgorithmTask::ConnectTrigger (signal-slot async)..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
AlgorithmTask<int, int> task;
|
||||
task.SetAlgorithm(&alg);
|
||||
task.SetMode(AlgorithmTask<int, int>::Async);
|
||||
|
||||
TriggerSource source;
|
||||
task.ConnectTrigger(&source, &TriggerSource::DataReady);
|
||||
|
||||
task.Run(10);
|
||||
Thread::Sleep(50);
|
||||
|
||||
// Emit signal 3 times
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
source.DataReady();
|
||||
Thread::Sleep(30);
|
||||
}
|
||||
|
||||
task.Stop();
|
||||
int count = alg.m_CallCount.load();
|
||||
std::cout << " Signal-triggered invocations: " << count << std::endl;
|
||||
assert(count == 3);
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestTaskStoppedSignal() {
|
||||
std::cout << "Testing AlgorithmTask Stopped signal..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
AlgorithmTask<int, int> task;
|
||||
task.SetAlgorithm(&alg);
|
||||
task.SetMode(AlgorithmTask<int, int>::Cyclic);
|
||||
task.SetCycleTime(20);
|
||||
|
||||
std::atomic<bool> stopped{false};
|
||||
Object::connect(&task, &AlgorithmTask<int, int>::Stopped,
|
||||
[&]() { stopped.store(true); });
|
||||
|
||||
task.Run(1);
|
||||
Thread::Sleep(50);
|
||||
task.Stop();
|
||||
Thread::Sleep(50);
|
||||
|
||||
assert(stopped.load());
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestClassName() {
|
||||
std::cout << "Testing GetClassName..." << std::endl;
|
||||
DoubleAlgorithm alg;
|
||||
AlgorithmTask<int, int> task;
|
||||
assert(std::string(alg.GetClassName()) == "DoubleAlgorithm");
|
||||
assert(std::string(task.GetClassName()) == "AlgorithmTask");
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
void TestDifferentTypes() {
|
||||
std::cout << "Testing Algorithm with different enc/dec types..." << std::endl;
|
||||
StringifyAlgorithm alg;
|
||||
assert(alg.Process(42) == "42");
|
||||
assert(alg.Process(-1) == "-1");
|
||||
assert(alg(100) == "100");
|
||||
std::cout << " Passed." << std::endl;
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestBasicProcess();
|
||||
TestOperatorCall();
|
||||
TestEncoderDecoderChain();
|
||||
TestAlgorithmSignals();
|
||||
TestDifferentTypes();
|
||||
TestCyclicTask();
|
||||
TestAsyncTask();
|
||||
TestConnectTrigger();
|
||||
TestTaskStoppedSignal();
|
||||
TestClassName();
|
||||
std::cout << "All Algorithm tests passed!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
@@ -23,11 +23,13 @@ set( TESTS
|
||||
VectorMetaAllocatorTest
|
||||
PropertyTypesTest
|
||||
HRPTest
|
||||
PropertyGroupingTest
|
||||
MutexTest
|
||||
ThreadsTest
|
||||
OpenMPTest
|
||||
TeamTest
|
||||
AffinityTest
|
||||
AlgorithmTest
|
||||
)
|
||||
|
||||
set(LIBRARIES
|
||||
|
||||
78
src/Core/testing/PropertyGroupingTest.cpp
Normal file
78
src/Core/testing/PropertyGroupingTest.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cassert>
|
||||
#include "Core/Object.h"
|
||||
#include "Core/Property.h"
|
||||
|
||||
using namespace uLib;
|
||||
|
||||
struct Nested {
|
||||
float x = 1.0f;
|
||||
float y = 2.0f;
|
||||
|
||||
ULIB_SERIALIZE_ACCESS
|
||||
template<class Archive>
|
||||
void serialize(Archive & ar, const unsigned int version) {
|
||||
ar & HRP(x);
|
||||
ar & HRP(y);
|
||||
}
|
||||
};
|
||||
|
||||
class GroupObject : public Object {
|
||||
uLibTypeMacro(GroupObject, Object)
|
||||
public:
|
||||
Nested position;
|
||||
Nested orientation;
|
||||
float weight = 50.0f;
|
||||
|
||||
ULIB_SERIALIZE_ACCESS
|
||||
template<class Archive>
|
||||
void serialize(Archive & ar, const unsigned int version) {
|
||||
ar & boost::serialization::make_nvp("Position", position);
|
||||
ar & boost::serialization::make_nvp("Orientation", orientation);
|
||||
ar & HRP(weight);
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
std::cout << "Testing Property Grouping..." << std::endl;
|
||||
|
||||
GroupObject obj;
|
||||
ULIB_ACTIVATE_PROPERTIES(obj);
|
||||
|
||||
auto props = obj.GetProperties();
|
||||
std::cout << "Registered " << props.size() << " properties." << std::endl;
|
||||
|
||||
for (auto* p : props) {
|
||||
std::cout << "Prop: " << p->GetName()
|
||||
<< " Group: " << p->GetGroup()
|
||||
<< " Qualified: " << p->GetQualifiedName() << std::endl;
|
||||
}
|
||||
|
||||
// Check if nested properties are registered
|
||||
PropertyBase* p1 = obj.GetProperty("Position.x");
|
||||
PropertyBase* p2 = obj.GetProperty("Position.y");
|
||||
PropertyBase* p3 = obj.GetProperty("Orientation.x");
|
||||
PropertyBase* p4 = obj.GetProperty("Orientation.y");
|
||||
PropertyBase* p5 = obj.GetProperty("weight");
|
||||
|
||||
assert(p1 != nullptr && "Position.x not found");
|
||||
assert(p2 != nullptr && "Position.y not found");
|
||||
assert(p3 != nullptr && "Orientation.x not found");
|
||||
assert(p4 != nullptr && "Orientation.y not found");
|
||||
assert(p5 != nullptr && "weight not found");
|
||||
|
||||
assert(p1->GetGroup() == "Position");
|
||||
assert(p2->GetGroup() == "Position");
|
||||
assert(p3->GetGroup() == "Orientation");
|
||||
assert(p4->GetGroup() == "Orientation");
|
||||
assert(p5->GetGroup() == "");
|
||||
|
||||
assert(p1->GetQualifiedName() == "Position.x");
|
||||
assert(p5->GetQualifiedName() == "weight");
|
||||
|
||||
std::cout << "Property Grouping Tests PASSED!" << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -151,6 +151,7 @@ TessellatedSolid::TessellatedSolid(const char *name)
|
||||
}
|
||||
|
||||
void TessellatedSolid::SetMesh(TriangleMesh &mesh) {
|
||||
this->m_Mesh = mesh;
|
||||
G4TessellatedSolid *ts = this->m_Solid;
|
||||
for (int i = 0; i < mesh.Triangles().size(); ++i) {
|
||||
const Vector3i &trg = mesh.Triangles().at(i);
|
||||
@@ -165,6 +166,9 @@ void TessellatedSolid::SetMesh(TriangleMesh &mesh) {
|
||||
}
|
||||
}
|
||||
|
||||
void TessellatedSolid::Update() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -93,11 +93,14 @@ public:
|
||||
void SetMesh(TriangleMesh &mesh);
|
||||
uLibGetMacro(Solid, G4TessellatedSolid *)
|
||||
virtual G4VSolid* GetG4Solid() const override { return (G4VSolid*)m_Solid; }
|
||||
|
||||
const TriangleMesh& GetMesh() const { return m_Mesh; }
|
||||
|
||||
public slots:
|
||||
void Update();
|
||||
|
||||
private :
|
||||
TriangleMesh m_Mesh;
|
||||
G4TessellatedSolid *m_Solid;
|
||||
};
|
||||
|
||||
|
||||
146
src/Math/Assembly.cpp
Normal file
146
src/Math/Assembly.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#include "Math/Assembly.h"
|
||||
#include "Math/ContainerBox.h"
|
||||
#include "Math/Cylinder.h"
|
||||
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace uLib {
|
||||
|
||||
Assembly::Assembly()
|
||||
: ObjectsContext(),
|
||||
AffineTransform(),
|
||||
m_BBoxMin(Vector3f::Zero()),
|
||||
m_BBoxMax(Vector3f::Zero()),
|
||||
m_ShowBoundingBox(false),
|
||||
m_GroupSelection(true) {}
|
||||
|
||||
Assembly::Assembly(const Assembly ©)
|
||||
: ObjectsContext(copy),
|
||||
AffineTransform(copy),
|
||||
m_BBoxMin(copy.m_BBoxMin),
|
||||
m_BBoxMax(copy.m_BBoxMax),
|
||||
m_ShowBoundingBox(copy.m_ShowBoundingBox),
|
||||
m_GroupSelection(copy.m_GroupSelection) {}
|
||||
|
||||
Assembly::~Assembly() {}
|
||||
|
||||
void Assembly::AddObject(Object *obj) {
|
||||
if (auto *at = dynamic_cast<AffineTransform *>(obj)) {
|
||||
at->SetParent(this);
|
||||
}
|
||||
ObjectsContext::AddObject(obj);
|
||||
}
|
||||
|
||||
void Assembly::RemoveObject(Object *obj) {
|
||||
if (auto *at = dynamic_cast<AffineTransform *>(obj)) {
|
||||
if (at->GetParent() == this)
|
||||
at->SetParent(nullptr);
|
||||
}
|
||||
ObjectsContext::RemoveObject(obj);
|
||||
}
|
||||
|
||||
void Assembly::ComputeBoundingBox() {
|
||||
const auto &objects = this->GetObjects();
|
||||
if (objects.empty()) {
|
||||
m_BBoxMin = Vector3f::Zero();
|
||||
m_BBoxMax = Vector3f::Zero();
|
||||
return;
|
||||
}
|
||||
|
||||
float inf = std::numeric_limits<float>::max();
|
||||
m_BBoxMin = Vector3f(inf, inf, inf);
|
||||
m_BBoxMax = Vector3f(-inf, -inf, -inf);
|
||||
|
||||
Matrix4f invAsm = this->GetWorldMatrix().inverse();
|
||||
|
||||
for (Object *obj : objects) {
|
||||
if (auto *box = dynamic_cast<ContainerBox *>(obj)) {
|
||||
// ContainerBox: wm is matrix from unit cube [0,1] to assembly base
|
||||
Matrix4f m = invAsm * box->GetWorldMatrix();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
float x = (i & 1) ? 1.0f : 0.0f;
|
||||
float y = (i & 2) ? 1.0f : 0.0f;
|
||||
float z = (i & 4) ? 1.0f : 0.0f;
|
||||
Vector4f corner = m * Vector4f(x, y, z, 1.0f);
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
m_BBoxMin(a) = std::min(m_BBoxMin(a), corner(a));
|
||||
m_BBoxMax(a) = std::max(m_BBoxMax(a), corner(a));
|
||||
}
|
||||
}
|
||||
} else if (auto *cyl = dynamic_cast<Cylinder *>(obj)) {
|
||||
// Cylinder: centered [-1, 1] radial, [-0.5, 0.5] height
|
||||
Matrix4f m = invAsm * cyl->GetWorldMatrix();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
float x = (i & 1) ? 1.0f : -1.0f;
|
||||
float y = (i & 2) ? 0.5f : -0.5f;
|
||||
float z = (i & 4) ? 1.0f : -1.0f;
|
||||
Vector4f corner = m * Vector4f(x, y, z, 1.0f);
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
m_BBoxMin(a) = std::min(m_BBoxMin(a), corner(a));
|
||||
m_BBoxMax(a) = std::max(m_BBoxMax(a), corner(a));
|
||||
}
|
||||
}
|
||||
} else if (auto *subAsm = dynamic_cast<Assembly *>(obj)) {
|
||||
// Recursive AABB for nested assemblies
|
||||
subAsm->ComputeBoundingBox();
|
||||
Vector3f subMin, subMax;
|
||||
subAsm->GetBoundingBox(subMin, subMax);
|
||||
Matrix4f m = invAsm * subAsm->GetWorldMatrix();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
float x = (i & 1) ? subMax(0) : subMin(0);
|
||||
float y = (i & 2) ? subMax(1) : subMin(1);
|
||||
float z = (i & 4) ? subMax(2) : subMin(2);
|
||||
Vector4f corner = m * Vector4f(x, y, z, 1.0f);
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
m_BBoxMin(a) = std::min(m_BBoxMin(a), corner(a));
|
||||
m_BBoxMax(a) = std::max(m_BBoxMax(a), corner(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Assembly::GetBoundingBox(Vector3f &bbMin, Vector3f &bbMax) const {
|
||||
bbMin = m_BBoxMin;
|
||||
bbMax = m_BBoxMax;
|
||||
}
|
||||
|
||||
ContainerBox Assembly::GetBoundingBoxAsContainer() const {
|
||||
ContainerBox bb;
|
||||
Vector3f size = m_BBoxMax - m_BBoxMin;
|
||||
bb.SetSize(size);
|
||||
bb.SetPosition(m_BBoxMin);
|
||||
return bb;
|
||||
}
|
||||
|
||||
void Assembly::SetShowBoundingBox(bool show) {
|
||||
m_ShowBoundingBox = show;
|
||||
this->Updated();
|
||||
}
|
||||
|
||||
bool Assembly::GetShowBoundingBox() const {
|
||||
return m_ShowBoundingBox;
|
||||
}
|
||||
|
||||
void Assembly::SetGroupSelection(bool group) {
|
||||
m_GroupSelection = group;
|
||||
}
|
||||
|
||||
bool Assembly::GetGroupSelection() const {
|
||||
return m_GroupSelection;
|
||||
}
|
||||
|
||||
} // namespace uLib
|
||||
109
src/Math/Assembly.h
Normal file
109
src/Math/Assembly.h
Normal file
@@ -0,0 +1,109 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
------------------------------------------------------------------
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3.0 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library.
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#ifndef U_ASSEMBLY_H
|
||||
#define U_ASSEMBLY_H
|
||||
|
||||
#include "Core/ObjectsContext.h"
|
||||
#include "Math/ContainerBox.h"
|
||||
#include "Math/Transform.h"
|
||||
|
||||
namespace uLib {
|
||||
|
||||
/**
|
||||
* @brief Assembly groups geometric objects (ContainerBox, Cylinder, etc.)
|
||||
* under a common transformation.
|
||||
*
|
||||
* Assembly derives from ObjectsContext so objects can be added/removed
|
||||
* dynamically. It also inherits AffineTransform to provide a group-level
|
||||
* transformation that is applied on top of each child's own transform.
|
||||
*
|
||||
* A bounding box is automatically computed from all contained objects and
|
||||
* can be queried or shown/hidden through the VTK puppet.
|
||||
*/
|
||||
class Assembly : public ObjectsContext, public AffineTransform {
|
||||
public:
|
||||
virtual const char *GetClassName() const override { return "Assembly"; }
|
||||
|
||||
Assembly();
|
||||
Assembly(const Assembly ©);
|
||||
virtual ~Assembly();
|
||||
|
||||
virtual void AddObject(Object* obj) override;
|
||||
virtual void RemoveObject(Object* obj) override;
|
||||
|
||||
/**
|
||||
* @brief Recomputes the axis-aligned bounding box enclosing all children.
|
||||
* Stores the result internally.
|
||||
*/
|
||||
void ComputeBoundingBox();
|
||||
|
||||
/**
|
||||
* @brief Returns the bounding box as min/max corners (in assembly-local
|
||||
* coordinates).
|
||||
*/
|
||||
void GetBoundingBox(Vector3f &bbMin, Vector3f &bbMax) const;
|
||||
|
||||
/**
|
||||
* @brief Returns the bounding box as a ContainerBox (positioned
|
||||
* at bbMin, sized bbMax-bbMin, parented to this transform).
|
||||
*/
|
||||
ContainerBox GetBoundingBoxAsContainer() const;
|
||||
|
||||
/**
|
||||
* @brief Controls whether the bounding box wireframe should be shown
|
||||
* in the viewer (used by the VTK puppet).
|
||||
*/
|
||||
void SetShowBoundingBox(bool show);
|
||||
bool GetShowBoundingBox() const;
|
||||
|
||||
/**
|
||||
* @brief Controls selection behavior.
|
||||
* If true (default), clicking any child within the assembly will select
|
||||
* the assembly itself. If false, individual children can be picked.
|
||||
*/
|
||||
void SetGroupSelection(bool group);
|
||||
bool GetGroupSelection() const;
|
||||
|
||||
signals:
|
||||
virtual void Updated() override {
|
||||
if (m_InUpdated) return; // break signal recursion
|
||||
m_InUpdated = true;
|
||||
this->ComputeBoundingBox();
|
||||
ULIB_SIGNAL_EMIT(Assembly::Updated);
|
||||
m_InUpdated = false;
|
||||
}
|
||||
|
||||
private:
|
||||
Vector3f m_BBoxMin;
|
||||
Vector3f m_BBoxMax;
|
||||
bool m_ShowBoundingBox;
|
||||
bool m_GroupSelection;
|
||||
bool m_InUpdated = false;
|
||||
};
|
||||
|
||||
} // namespace uLib
|
||||
|
||||
#endif // U_ASSEMBLY_H
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
set(HEADERS ContainerBox.h
|
||||
Cylinder.h
|
||||
Assembly.h
|
||||
Dense.h
|
||||
Geometry.h
|
||||
Transform.h
|
||||
@@ -33,6 +34,7 @@ set(SOURCES VoxRaytracer.cpp
|
||||
VoxImage.cpp
|
||||
TriangleMesh.cpp
|
||||
QuadMesh.cpp
|
||||
Assembly.cpp
|
||||
Dense.cpp
|
||||
Structured2DGrid.cpp
|
||||
Structured4DGrid.cpp
|
||||
|
||||
@@ -52,7 +52,9 @@ public:
|
||||
Vector3f GetPoint(const Id_t id) const;
|
||||
|
||||
inline std::vector<Vector3f> & Points() { return this->m_Points; }
|
||||
inline const std::vector<Vector3f> & Points() const { return this->m_Points; }
|
||||
inline std::vector<Vector4i> & Quads() { return this->m_Quads; }
|
||||
inline const std::vector<Vector4i> & Quads() const { return this->m_Quads; }
|
||||
|
||||
const Vector4i & GetQuad(const Id_t id) const { return m_Quads.at(id); }
|
||||
Vector3f GetNormal(const Id_t id) const;
|
||||
|
||||
@@ -69,6 +69,8 @@ public:
|
||||
m_Parent(NULL)
|
||||
{}
|
||||
|
||||
virtual ~AffineTransform() {}
|
||||
|
||||
AffineTransform(AffineTransform *parent) :
|
||||
m_T(Matrix4f::Identity()),
|
||||
m_Parent(parent)
|
||||
|
||||
@@ -55,7 +55,9 @@ public:
|
||||
Vector3f GetPoint(const Id_t id) const;
|
||||
|
||||
inline std::vector<Vector3f> & Points() { return this->m_Points; }
|
||||
inline const std::vector<Vector3f> & Points() const { return this->m_Points; }
|
||||
inline std::vector<Vector3i> & Triangles() { return this->m_Triangles; }
|
||||
inline const std::vector<Vector3i> & Triangles() const { return this->m_Triangles; }
|
||||
|
||||
const Vector3i & GetTriangle(const Id_t id) const { return m_Triangles.at(id); }
|
||||
Vector3f GetNormal(const Id_t id) const;
|
||||
|
||||
@@ -27,12 +27,16 @@
|
||||
#define VOXIMAGEFILTER_H
|
||||
|
||||
#include "Core/StaticInterface.h"
|
||||
#include "Core/Algorithm.h"
|
||||
#include "Math/Dense.h"
|
||||
|
||||
#include "Math/VoxImage.h"
|
||||
|
||||
namespace uLib {
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Kernel shape interface (static check for operator()(float) and operator()(Vector3f))
|
||||
|
||||
namespace Interface {
|
||||
struct VoxImageFilterShape {
|
||||
template <class Self> void check_structural() {
|
||||
@@ -42,63 +46,95 @@ struct VoxImageFilterShape {
|
||||
};
|
||||
} // namespace Interface
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Forward declaration
|
||||
|
||||
template <typename VoxelT> class Kernel;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Abstract interface (type-erased, used by python bindings)
|
||||
|
||||
namespace Abstract {
|
||||
class VoxImageFilter {
|
||||
public:
|
||||
virtual void Run() = 0;
|
||||
|
||||
virtual void SetImage(Abstract::VoxImage *image) = 0;
|
||||
|
||||
protected:
|
||||
virtual ~VoxImageFilter() {}
|
||||
};
|
||||
} // namespace Abstract
|
||||
|
||||
template <typename VoxelT, typename AlgorithmT>
|
||||
class VoxImageFilter : public Abstract::VoxImageFilter, public Object {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// VoxImageFilter — kernel-based voxel filter using CRTP + Algorithm
|
||||
//
|
||||
// Template parameters:
|
||||
// VoxelT — voxel data type (must satisfy Interface::Voxel)
|
||||
// CrtpImplT — concrete filter subclass (CRTP), must provide:
|
||||
// float Evaluate(const VoxImage<VoxelT>& buffer, int index)
|
||||
//
|
||||
// Inherits Algorithm<VoxImage<VoxelT>*, VoxImage<VoxelT>*> so that filters
|
||||
// can be used with AlgorithmTask for scheduled/async execution, and chained
|
||||
// via encoder/decoder.
|
||||
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
class VoxImageFilter : public Abstract::VoxImageFilter,
|
||||
public Algorithm<VoxImage<VoxelT>*, VoxImage<VoxelT>*> {
|
||||
public:
|
||||
|
||||
virtual const char * GetClassName() const { return "VoxImageFilter"; }
|
||||
virtual const char* GetClassName() const { return "VoxImageFilter"; }
|
||||
|
||||
VoxImageFilter(const Vector3i &size);
|
||||
|
||||
// Algorithm interface ////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @brief Process implements Algorithm::Process.
|
||||
* Applies the filter in-place on the input image and returns it.
|
||||
*/
|
||||
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override;
|
||||
|
||||
/**
|
||||
* @brief Run implements Abstract::VoxImageFilter::Run.
|
||||
* Calls Process on the current image.
|
||||
*/
|
||||
void Run();
|
||||
|
||||
// Device awareness ///////////////////////////////////////////////////////////
|
||||
|
||||
/** @brief Returns VRAM if image or kernel data is on GPU, RAM otherwise. */
|
||||
MemoryDevice GetPreferredDevice() const override {
|
||||
if (m_Image && m_Image->Data().GetDevice() == MemoryDevice::VRAM)
|
||||
return MemoryDevice::VRAM;
|
||||
if (m_KernelData.ConstData().GetDevice() == MemoryDevice::VRAM)
|
||||
return MemoryDevice::VRAM;
|
||||
return MemoryDevice::RAM;
|
||||
}
|
||||
|
||||
// Kernel setup ///////////////////////////////////////////////////////////////
|
||||
|
||||
void SetKernelNumericXZY(const std::vector<float> &numeric);
|
||||
|
||||
void SetKernelSpherical(float (*shape)(float));
|
||||
|
||||
template <class ShapeT> void SetKernelSpherical(ShapeT shape);
|
||||
|
||||
void SetKernelWeightFunction(float (*shape)(const Vector3f &));
|
||||
|
||||
template <class ShapeT> void SetKernelWeightFunction(ShapeT shape);
|
||||
|
||||
inline const Kernel<VoxelT> &GetKernelData() const {
|
||||
return this->m_KernelData;
|
||||
}
|
||||
inline Kernel<VoxelT> &GetKernelData() { return this->m_KernelData; }
|
||||
// Accessors //////////////////////////////////////////////////////////////////
|
||||
|
||||
inline VoxImage<VoxelT> *GetImage() const { return this->m_Image; }
|
||||
const Kernel<VoxelT> &GetKernelData() const { return m_KernelData; }
|
||||
Kernel<VoxelT> &GetKernelData() { return m_KernelData; }
|
||||
|
||||
VoxImage<VoxelT> *GetImage() const { return m_Image; }
|
||||
void SetImage(Abstract::VoxImage *image);
|
||||
|
||||
protected:
|
||||
float Convolve(const VoxImage<VoxelT> &buffer, int index); // remove //
|
||||
|
||||
void SetKernelOffset();
|
||||
|
||||
float Distance2(const Vector3i &v);
|
||||
|
||||
// protected members for algorithm access //
|
||||
Kernel<VoxelT> m_KernelData;
|
||||
VoxImage<VoxelT> *m_Image;
|
||||
|
||||
private:
|
||||
AlgorithmT *t_Algoritm;
|
||||
CrtpImplT *m_CrtpImpl;
|
||||
};
|
||||
|
||||
} // namespace uLib
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
|
||||
namespace uLib {
|
||||
|
||||
// KERNEL //////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// KERNEL ////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
template <typename T> class Kernel : public StructuredData {
|
||||
typedef StructuredData BaseClass;
|
||||
@@ -41,13 +43,12 @@ template <typename T> class Kernel : public StructuredData {
|
||||
public:
|
||||
Kernel(const Vector3i &size);
|
||||
|
||||
inline T &operator[](const Vector3i &id) { return m_Data[Map(id)]; }
|
||||
inline T &operator[](const int &id) { return m_Data[id]; }
|
||||
inline int GetCenterData() const;
|
||||
T &operator[](const Vector3i &id) { return m_Data[Map(id)]; }
|
||||
T &operator[](const int &id) { return m_Data[id]; }
|
||||
int GetCenterData() const;
|
||||
|
||||
inline DataAllocator<T> &Data() { return this->m_Data; }
|
||||
|
||||
inline const DataAllocator<T> &ConstData() const { return this->m_Data; }
|
||||
DataAllocator<T> &Data() { return m_Data; }
|
||||
const DataAllocator<T> &ConstData() const { return m_Data; }
|
||||
|
||||
void PrintSelf(std::ostream &o) const;
|
||||
|
||||
@@ -60,12 +61,14 @@ Kernel<T>::Kernel(const Vector3i &size) : BaseClass(size), m_Data(size.prod()) {
|
||||
Interface::IsA<T, Interface::Voxel>();
|
||||
}
|
||||
|
||||
template <typename T> inline int Kernel<T>::GetCenterData() const {
|
||||
template <typename T>
|
||||
int Kernel<T>::GetCenterData() const {
|
||||
static int center = Map(this->GetDims() / 2);
|
||||
return center;
|
||||
}
|
||||
|
||||
template <typename T> void Kernel<T>::PrintSelf(std::ostream &o) const {
|
||||
template <typename T>
|
||||
void Kernel<T>::PrintSelf(std::ostream &o) const {
|
||||
o << " Filter Kernel Dump [XZ_Y]: \n";
|
||||
Vector3i index;
|
||||
o << "\n Value: \n\n"
|
||||
@@ -96,26 +99,42 @@ template <typename T> void Kernel<T>::PrintSelf(std::ostream &o) const {
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// VOXIMAGEFILTER IMPLEMENTATION /////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#define _TPL_ template <typename VoxelT, typename AlgorithmT>
|
||||
#define _TPLT_ VoxelT, AlgorithmT
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
VoxImageFilter<VoxelT, CrtpImplT>::VoxImageFilter(const Vector3i &size)
|
||||
: m_KernelData(size)
|
||||
, m_Image(nullptr)
|
||||
, m_CrtpImpl(static_cast<CrtpImplT *>(this))
|
||||
{}
|
||||
|
||||
_TPL_
|
||||
VoxImageFilter<_TPLT_>::VoxImageFilter(const Vector3i &size)
|
||||
: m_KernelData(size), t_Algoritm(static_cast<AlgorithmT *>(this)) {}
|
||||
|
||||
_TPL_
|
||||
void VoxImageFilter<_TPLT_>::Run() {
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
VoxImage<VoxelT>* VoxImageFilter<VoxelT, CrtpImplT>::Process(
|
||||
VoxImage<VoxelT>* const& image) {
|
||||
if (m_Image != image) SetImage(image);
|
||||
VoxImage<VoxelT> buffer = *m_Image;
|
||||
#pragma omp parallel for
|
||||
for (int i = 0; i < m_Image->Data().size(); ++i)
|
||||
m_Image->operator[](i).Value = this->t_Algoritm->Evaluate(buffer, i);
|
||||
m_Image->operator[](i).Value = m_CrtpImpl->Evaluate(buffer, i);
|
||||
#pragma omp barrier
|
||||
return m_Image;
|
||||
}
|
||||
|
||||
_TPL_
|
||||
void VoxImageFilter<_TPLT_>::SetKernelOffset() {
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::Run() {
|
||||
Process(m_Image);
|
||||
}
|
||||
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::SetImage(Abstract::VoxImage *image) {
|
||||
m_Image = reinterpret_cast<VoxImage<VoxelT> *>(image);
|
||||
SetKernelOffset();
|
||||
}
|
||||
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::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,10 +146,10 @@ void VoxImageFilter<_TPLT_>::SetKernelOffset() {
|
||||
}
|
||||
}
|
||||
|
||||
_TPL_
|
||||
float VoxImageFilter<_TPLT_>::Distance2(const Vector3i &v) {
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
float VoxImageFilter<VoxelT, CrtpImplT>::Distance2(const Vector3i &v) {
|
||||
Vector3i tmp = v;
|
||||
const Vector3i &dim = this->m_KernelData.GetDims();
|
||||
const Vector3i &dim = m_KernelData.GetDims();
|
||||
Vector3i center = dim / 2;
|
||||
tmp = tmp - center;
|
||||
center = center.cwiseProduct(center);
|
||||
@@ -140,12 +159,9 @@ 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 CrtpImplT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelNumericXZY(
|
||||
const std::vector<float> &numeric) {
|
||||
// set data order //
|
||||
StructuredData::Order order = m_KernelData.GetDataOrder();
|
||||
// m_KernelData.SetDataOrder(StructuredData::XZY);
|
||||
Vector3i id;
|
||||
int index = 0;
|
||||
for (int y = 0; y < m_KernelData.GetDims()(1); ++y) {
|
||||
@@ -156,38 +172,39 @@ void VoxImageFilter<_TPLT_>::SetKernelNumericXZY(
|
||||
}
|
||||
}
|
||||
}
|
||||
// m_KernelData.SetDataOrder(order);
|
||||
}
|
||||
|
||||
_TPL_
|
||||
void VoxImageFilter<_TPLT_>::SetKernelSpherical(float (*shape)(float)) {
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::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) {
|
||||
for (int x = 0; x < m_KernelData.GetDims()(0); ++x) {
|
||||
id << x, y, z;
|
||||
m_KernelData[id].Value = shape(this->Distance2(id));
|
||||
m_KernelData[id].Value = shape(Distance2(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_TPL_ template <class ShapeT>
|
||||
void VoxImageFilter<_TPLT_>::SetKernelSpherical(ShapeT shape) {
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
template <class ShapeT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelSpherical(ShapeT shape) {
|
||||
Interface::IsA<ShapeT, Interface::VoxImageFilterShape>();
|
||||
Vector3i id;
|
||||
for (int y = 0; y < m_KernelData.GetDims()(1); ++y) {
|
||||
for (int z = 0; z < m_KernelData.GetDims()(2); ++z) {
|
||||
for (int x = 0; x < m_KernelData.GetDims()(0); ++x) {
|
||||
id << x, y, z;
|
||||
m_KernelData[id].Value = shape(this->Distance2(id));
|
||||
m_KernelData[id].Value = shape(Distance2(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_TPL_
|
||||
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelWeightFunction(
|
||||
float (*shape)(const Vector3f &)) {
|
||||
const Vector3i &dim = m_KernelData.GetDims();
|
||||
Vector3i id;
|
||||
@@ -195,20 +212,19 @@ void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(
|
||||
for (int y = 0; y < dim(1); ++y) {
|
||||
for (int z = 0; z < dim(2); ++z) {
|
||||
for (int x = 0; x < dim(0); ++x) {
|
||||
// get voxels centroid coords from kernel center //
|
||||
id << x, y, z;
|
||||
pt << id(0) - dim(0) / 2 + 0.5 * !(dim(0) % 2),
|
||||
id(1) - dim(1) / 2 + 0.5 * !(dim(1) % 2),
|
||||
id(2) - dim(2) / 2 + 0.5 * !(dim(2) % 2);
|
||||
// compute function using given shape //
|
||||
m_KernelData[id].Value = shape(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_TPL_ template <class ShapeT>
|
||||
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(ShapeT shape) {
|
||||
template <typename VoxelT, typename CrtpImplT>
|
||||
template <class ShapeT>
|
||||
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelWeightFunction(ShapeT shape) {
|
||||
Interface::IsA<ShapeT, Interface::VoxImageFilterShape>();
|
||||
const Vector3i &dim = m_KernelData.GetDims();
|
||||
Vector3i id;
|
||||
@@ -216,45 +232,16 @@ void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(ShapeT shape) {
|
||||
for (int y = 0; y < dim(1); ++y) {
|
||||
for (int z = 0; z < dim(2); ++z) {
|
||||
for (int x = 0; x < dim(0); ++x) {
|
||||
// get voxels centroid coords from kernel center //
|
||||
id << x, y, z;
|
||||
pt << id(0) - dim(0) / 2 + 0.5 * !(dim(0) % 2),
|
||||
id(1) - dim(1) / 2 + 0.5 * !(dim(1) % 2),
|
||||
id(2) - dim(2) / 2 + 0.5 * !(dim(2) % 2);
|
||||
// compute function using given shape //
|
||||
m_KernelData[id].Value = shape(pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_TPL_
|
||||
void VoxImageFilter<_TPLT_>::SetImage(Abstract::VoxImage *image) {
|
||||
this->m_Image = reinterpret_cast<VoxImage<VoxelT> *>(image);
|
||||
this->SetKernelOffset();
|
||||
}
|
||||
|
||||
_TPL_
|
||||
float VoxImageFilter<_TPLT_>::Convolve(const VoxImage<VoxelT> &buffer,
|
||||
int index) {
|
||||
const DataAllocator<VoxelT> &vbuf = buffer.ConstData();
|
||||
const DataAllocator<VoxelT> &vker = m_KernelData.ConstData();
|
||||
int vox_size = vbuf.size();
|
||||
int ker_size = vker.size();
|
||||
int pos;
|
||||
float conv = 0, ksum = 0;
|
||||
for (int ik = 0; ik < ker_size; ++ik) {
|
||||
pos = index + vker[ik].Count - vker[m_KernelData.GetCenterData()].Count;
|
||||
pos = (pos + vox_size) % vox_size;
|
||||
conv += vbuf[pos].Value * vker[ik].Value;
|
||||
ksum += vker[ik].Value;
|
||||
}
|
||||
return conv / ksum;
|
||||
}
|
||||
|
||||
#undef _TPLT_
|
||||
#undef _TPL_
|
||||
|
||||
} // namespace uLib
|
||||
|
||||
#endif // VOXIMAGEFILTER_HPP
|
||||
|
||||
@@ -109,7 +109,8 @@ public:
|
||||
}
|
||||
|
||||
#if defined(USE_CUDA) && defined(__CUDACC__)
|
||||
void Run() {
|
||||
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
|
||||
if (this->m_Image != image) this->SetImage(image);
|
||||
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
|
||||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
|
||||
|
||||
@@ -136,8 +137,9 @@ public:
|
||||
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count,
|
||||
mAtrim, mBtrim);
|
||||
cudaDeviceSynchronize();
|
||||
return this->m_Image;
|
||||
} else {
|
||||
BaseClass::Run();
|
||||
return BaseClass::Process(image);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -207,7 +209,8 @@ public:
|
||||
}
|
||||
|
||||
#if defined(USE_CUDA) && defined(__CUDACC__)
|
||||
void Run() {
|
||||
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
|
||||
if (this->m_Image != image) this->SetImage(image);
|
||||
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
|
||||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
|
||||
|
||||
@@ -234,8 +237,9 @@ public:
|
||||
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count,
|
||||
mAtrim, mBtrim);
|
||||
cudaDeviceSynchronize();
|
||||
return this->m_Image;
|
||||
} else {
|
||||
BaseClass::Run();
|
||||
return BaseClass::Process(image);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
#include "VoxImageFilter.h"
|
||||
#include <Math/Dense.h>
|
||||
|
||||
#define likely(expr) __builtin_expect(!!(expr), 1)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///// VOXIMAGE FILTER CUSTOM /////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -50,7 +48,7 @@ public:
|
||||
: BaseClass(size), m_CustomEvaluate(NULL) {}
|
||||
|
||||
float Evaluate(const VoxImage<VoxelT> &buffer, int index) {
|
||||
if (likely(m_CustomEvaluate)) {
|
||||
if (m_CustomEvaluate) {
|
||||
const DataAllocator<VoxelT> &vbuf = buffer.ConstData();
|
||||
const DataAllocator<VoxelT> &vker = this->m_KernelData.ConstData();
|
||||
int vox_size = vbuf.size();
|
||||
|
||||
@@ -67,7 +67,8 @@ public:
|
||||
VoxFilterAlgorithmLinear(const Vector3i &size) : BaseClass(size) {}
|
||||
|
||||
#if defined(USE_CUDA) && defined(__CUDACC__)
|
||||
void Run() {
|
||||
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
|
||||
if (this->m_Image != image) this->SetImage(image);
|
||||
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
|
||||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
|
||||
|
||||
@@ -92,8 +93,9 @@ public:
|
||||
LinearFilterKernel<<<blocksPerGrid, threadsPerBlock>>>(
|
||||
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count);
|
||||
cudaDeviceSynchronize();
|
||||
return this->m_Image;
|
||||
} else {
|
||||
BaseClass::Run();
|
||||
return BaseClass::Process(image);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
|
||||
|
||||
#ifndef VOXIMAGEFILTERTHRESHOLD_HPP
|
||||
#define VOXIMAGEFILTERTHRESHOLD_HPP
|
||||
|
||||
@@ -39,40 +37,24 @@
|
||||
namespace uLib {
|
||||
|
||||
template <typename VoxelT>
|
||||
class VoxFilterAlgorithmThreshold :
|
||||
public VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT> > {
|
||||
class VoxFilterAlgorithmThreshold
|
||||
: public VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT>> {
|
||||
|
||||
typedef VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT> > BaseClass;
|
||||
// ULIB_OBJECT_PARAMETERS(BaseClass) {
|
||||
// float threshold;
|
||||
// };
|
||||
typedef VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT>> BaseClass;
|
||||
|
||||
float m_threshold;
|
||||
float m_threshold;
|
||||
|
||||
public:
|
||||
VoxFilterAlgorithmThreshold(const Vector3i &size) : BaseClass(size)
|
||||
{
|
||||
// init_parameters();
|
||||
m_threshold = 0;
|
||||
}
|
||||
VoxFilterAlgorithmThreshold(const Vector3i &size)
|
||||
: BaseClass(size), m_threshold(0) {}
|
||||
|
||||
inline void SetThreshold(float th) { m_threshold = th; }
|
||||
|
||||
float Evaluate(const VoxImage<VoxelT> &buffer, int index)
|
||||
{
|
||||
return static_cast<float>(buffer.ConstData().at(index).Value >=
|
||||
// parameters().threshold);
|
||||
m_threshold );
|
||||
}
|
||||
void SetThreshold(float th) { m_threshold = th; }
|
||||
|
||||
float Evaluate(const VoxImage<VoxelT> &buffer, int index) {
|
||||
return static_cast<float>(buffer.ConstData().at(index).Value >= m_threshold);
|
||||
}
|
||||
};
|
||||
|
||||
//template <typename VoxelT>
|
||||
//inline void VoxFilterAlgorithmThreshold<VoxelT>::init_parameters()
|
||||
//{
|
||||
// parameters().threshold = 0;
|
||||
//}
|
||||
|
||||
}
|
||||
} // namespace uLib
|
||||
|
||||
#endif // VOXIMAGEFILTERTHRESHOLD_HPP
|
||||
|
||||
408
src/Math/testing/AlgorithmCudaChainTest.cpp
Normal file
408
src/Math/testing/AlgorithmCudaChainTest.cpp
Normal file
@@ -0,0 +1,408 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
------------------------------------------------------------------
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3.0 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library.
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#include "testing-prototype.h"
|
||||
|
||||
#include "Core/Algorithm.h"
|
||||
#include "Math/VoxImage.h"
|
||||
#include "Math/VoxImageFilter.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
using namespace uLib;
|
||||
|
||||
struct TestVoxel {
|
||||
Scalarf Value;
|
||||
unsigned int Count;
|
||||
};
|
||||
|
||||
int main() {
|
||||
BEGIN_TESTING(AlgorithmCudaChain);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 1: Single filter — GetPreferredDevice reflects data location
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 1: GetPreferredDevice reflects data location ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
|
||||
image[Vector3i(5, 5, 5)].Value = 1;
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights(27, 1.0f);
|
||||
filter.SetImage(&image);
|
||||
filter.SetKernelNumericXZY(weights);
|
||||
|
||||
// Before VRAM move: should prefer RAM
|
||||
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
|
||||
TEST1(!filter.IsGPU());
|
||||
std::cout << " RAM mode: PreferredDevice=RAM, IsGPU=false OK\n";
|
||||
|
||||
// Move image data to VRAM
|
||||
image.Data().MoveToVRAM();
|
||||
|
||||
// After VRAM move: should prefer VRAM
|
||||
TEST1(filter.GetPreferredDevice() == MemoryDevice::VRAM);
|
||||
TEST1(filter.IsGPU());
|
||||
std::cout << " VRAM mode: PreferredDevice=VRAM, IsGPU=true OK\n";
|
||||
|
||||
// Move back to RAM
|
||||
image.Data().MoveToRAM();
|
||||
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
|
||||
std::cout << " Back to RAM: PreferredDevice=RAM OK\n";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 2: Kernel data on VRAM also triggers GPU preference
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 2: Kernel on VRAM triggers GPU preference ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights(27, 1.0f);
|
||||
filter.SetImage(&image);
|
||||
filter.SetKernelNumericXZY(weights);
|
||||
|
||||
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
|
||||
|
||||
// Only kernel on VRAM
|
||||
filter.GetKernelData().Data().MoveToVRAM();
|
||||
TEST1(filter.GetPreferredDevice() == MemoryDevice::VRAM);
|
||||
std::cout << " Kernel on VRAM: PreferredDevice=VRAM OK\n";
|
||||
|
||||
filter.GetKernelData().Data().MoveToRAM();
|
||||
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 3: Algorithm interface — Process through base pointer
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 3: Process through Algorithm base pointer ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
|
||||
image[Vector3i(5, 5, 5)].Value = 10;
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights(27, 1.0f);
|
||||
filter.SetImage(&image);
|
||||
filter.SetKernelNumericXZY(weights);
|
||||
|
||||
// Use through Algorithm base class pointer
|
||||
Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>* alg = &filter;
|
||||
|
||||
VoxImage<TestVoxel>* result = alg->Process(&image);
|
||||
TEST1(result == &image);
|
||||
std::cout << " Process through base pointer returned correct image OK\n";
|
||||
|
||||
// Verify filter actually ran (center voxel should be averaged)
|
||||
// With uniform 3x3x3 kernel and single non-zero voxel at center,
|
||||
// the center value should be 10/27 ≈ 0.37
|
||||
TEST1(image[Vector3i(5, 5, 5)].Value < 10.0f);
|
||||
std::cout << " Filter modified voxel values OK\n";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 4: Encoder/decoder chain — two filters linked
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 4: Encoder/decoder chain ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
|
||||
image[Vector3i(5, 5, 5)].Value = 100;
|
||||
|
||||
// First filter: linear smoothing
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filter1(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights1(27, 1.0f);
|
||||
filter1.SetImage(&image);
|
||||
filter1.SetKernelNumericXZY(weights1);
|
||||
|
||||
// Second filter: threshold
|
||||
VoxFilterAlgorithmThreshold<TestVoxel> filter2(Vector3i(1, 1, 1));
|
||||
filter2.SetThreshold(0.5f);
|
||||
filter2.SetImage(&image);
|
||||
// 1x1x1 kernel with value 1
|
||||
std::vector<float> weights2(1, 1.0f);
|
||||
filter2.SetKernelNumericXZY(weights2);
|
||||
|
||||
// Chain: filter1 → filter2
|
||||
filter1.SetDecoder(&filter2);
|
||||
filter2.SetEncoder(&filter1);
|
||||
|
||||
TEST1(filter1.GetDecoder() == &filter2);
|
||||
TEST1(filter2.GetEncoder() == &filter1);
|
||||
std::cout << " Chain linked: filter1 -> filter2 OK\n";
|
||||
|
||||
// Execute chain manually (encoder first, then decoder)
|
||||
filter1.Process(&image);
|
||||
float smoothed_center = image[Vector3i(5, 5, 5)].Value;
|
||||
std::cout << " After linear: center = " << smoothed_center << "\n";
|
||||
|
||||
filter2.Process(&image);
|
||||
float thresholded_center = image[Vector3i(5, 5, 5)].Value;
|
||||
std::cout << " After threshold: center = " << thresholded_center << "\n";
|
||||
|
||||
// After threshold, values should be 0 or 1
|
||||
TEST1(thresholded_center == 0.0f || thresholded_center == 1.0f);
|
||||
std::cout << " Chain execution produced valid results OK\n";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 5: CUDA chain — VRAM data through chained filters
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 5: VRAM data through chained filters ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
|
||||
image[Vector3i(5, 5, 5)].Value = 50;
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filter1(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights1(27, 1.0f);
|
||||
filter1.SetImage(&image);
|
||||
filter1.SetKernelNumericXZY(weights1);
|
||||
|
||||
VoxFilterAlgorithmAbtrim<TestVoxel> filter2(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights2(27, 1.0f);
|
||||
filter2.SetImage(&image);
|
||||
filter2.SetKernelNumericXZY(weights2);
|
||||
filter2.SetABTrim(1, 1);
|
||||
|
||||
// Chain
|
||||
filter1.SetDecoder(&filter2);
|
||||
filter2.SetEncoder(&filter1);
|
||||
|
||||
// Move data to VRAM
|
||||
image.Data().MoveToVRAM();
|
||||
filter1.GetKernelData().Data().MoveToVRAM();
|
||||
filter2.GetKernelData().Data().MoveToVRAM();
|
||||
|
||||
// Both filters should report VRAM preference
|
||||
TEST1(filter1.GetPreferredDevice() == MemoryDevice::VRAM);
|
||||
TEST1(filter2.GetPreferredDevice() == MemoryDevice::VRAM);
|
||||
TEST1(filter1.IsGPU());
|
||||
TEST1(filter2.IsGPU());
|
||||
std::cout << " Both filters detect VRAM preference OK\n";
|
||||
|
||||
// Verify the chain's device consistency
|
||||
auto* encoder = filter2.GetEncoder();
|
||||
TEST1(encoder != nullptr);
|
||||
TEST1(encoder->IsGPU());
|
||||
std::cout << " Encoder in chain also reports GPU OK\n";
|
||||
|
||||
#ifdef USE_CUDA
|
||||
// With CUDA: filters execute on GPU via Process()
|
||||
image.Data().MoveToRAM(); // reset for clean test
|
||||
image[Vector3i(5, 5, 5)].Value = 50;
|
||||
image.Data().MoveToVRAM();
|
||||
|
||||
filter1.Process(&image);
|
||||
TEST1(image.Data().GetDevice() == MemoryDevice::VRAM);
|
||||
std::cout << " CUDA: data stays in VRAM after filter1 OK\n";
|
||||
|
||||
filter2.Process(&image);
|
||||
TEST1(image.Data().GetDevice() == MemoryDevice::VRAM);
|
||||
std::cout << " CUDA: data stays in VRAM after filter2 OK\n";
|
||||
#else
|
||||
// Without CUDA: verify Process still works via CPU fallback
|
||||
image.Data().MoveToRAM();
|
||||
image[Vector3i(5, 5, 5)].Value = 50;
|
||||
|
||||
filter1.GetKernelData().Data().MoveToRAM();
|
||||
filter2.GetKernelData().Data().MoveToRAM();
|
||||
|
||||
filter1.Process(&image);
|
||||
filter2.Process(&image);
|
||||
std::cout << " No CUDA: CPU fallback executed correctly OK\n";
|
||||
#endif
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 6: AlgorithmTask with VRAM-aware filter
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 6: AlgorithmTask with VRAM-aware filter ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
|
||||
image[Vector3i(4, 4, 4)].Value = 20;
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights(27, 1.0f);
|
||||
filter.SetImage(&image);
|
||||
filter.SetKernelNumericXZY(weights);
|
||||
|
||||
// Set up task
|
||||
AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*> task;
|
||||
task.SetAlgorithm(&filter);
|
||||
task.SetMode(AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>::Cyclic);
|
||||
task.SetCycleTime(50);
|
||||
|
||||
// Run task for a few cycles
|
||||
task.Run(&image);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
task.Stop();
|
||||
|
||||
// After cyclic execution, the filter should have smoothed values
|
||||
TEST1(image[Vector3i(4, 4, 4)].Value < 20.0f);
|
||||
std::cout << " Task cyclic execution modified image OK\n";
|
||||
std::cout << " Center value after smoothing: "
|
||||
<< image[Vector3i(4, 4, 4)].Value << "\n";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 7: AlgorithmTask async with chained filters
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 7: AlgorithmTask async with filter ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
|
||||
image[Vector3i(4, 4, 4)].Value = 30;
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
|
||||
std::vector<float> weights(27, 1.0f);
|
||||
filter.SetImage(&image);
|
||||
filter.SetKernelNumericXZY(weights);
|
||||
|
||||
AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*> task;
|
||||
task.SetAlgorithm(&filter);
|
||||
task.SetMode(AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>::Async);
|
||||
|
||||
float before = image[Vector3i(4, 4, 4)].Value;
|
||||
|
||||
task.Run(&image);
|
||||
|
||||
// Trigger one execution
|
||||
task.Notify();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
task.Stop();
|
||||
|
||||
float after = image[Vector3i(4, 4, 4)].Value;
|
||||
TEST1(after < before);
|
||||
std::cout << " Async trigger: value " << before << " -> " << after << " OK\n";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 8: Device preference propagation in chain
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 8: Device preference propagation check ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
|
||||
image[Vector3i(4, 4, 4)].Value = 10;
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filterA(Vector3i(3, 3, 3));
|
||||
VoxFilterAlgorithmAbtrim<TestVoxel> filterB(Vector3i(3, 3, 3));
|
||||
VoxFilterAlgorithmThreshold<TestVoxel> filterC(Vector3i(1, 1, 1));
|
||||
|
||||
std::vector<float> w27(27, 1.0f);
|
||||
std::vector<float> w1(1, 1.0f);
|
||||
|
||||
filterA.SetImage(&image);
|
||||
filterA.SetKernelNumericXZY(w27);
|
||||
filterB.SetImage(&image);
|
||||
filterB.SetKernelNumericXZY(w27);
|
||||
filterB.SetABTrim(1, 1);
|
||||
filterC.SetImage(&image);
|
||||
filterC.SetKernelNumericXZY(w1);
|
||||
filterC.SetThreshold(0.1f);
|
||||
|
||||
// Chain: A → B → C
|
||||
filterA.SetDecoder(&filterB);
|
||||
filterB.SetEncoder(&filterA);
|
||||
filterB.SetDecoder(&filterC);
|
||||
filterC.SetEncoder(&filterB);
|
||||
|
||||
// All on RAM
|
||||
TEST1(!filterA.IsGPU());
|
||||
TEST1(!filterB.IsGPU());
|
||||
TEST1(!filterC.IsGPU());
|
||||
std::cout << " All filters on RAM OK\n";
|
||||
|
||||
// Move image to VRAM — filters A and B should detect it
|
||||
image.Data().MoveToVRAM();
|
||||
TEST1(filterA.IsGPU());
|
||||
TEST1(filterB.IsGPU());
|
||||
// filterC with 1x1x1 kernel doesn't have CUDA override, but still detects VRAM
|
||||
TEST1(filterC.IsGPU());
|
||||
std::cout << " Image on VRAM: all filters report GPU OK\n";
|
||||
|
||||
// Can walk the chain and check device consistency
|
||||
auto* step = static_cast<Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>*>(&filterA);
|
||||
bool all_gpu = true;
|
||||
while (step) {
|
||||
if (!step->IsGPU()) all_gpu = false;
|
||||
step = static_cast<Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>*>(step->GetDecoder());
|
||||
}
|
||||
TEST1(all_gpu);
|
||||
std::cout << " Chain walk: all steps report GPU OK\n";
|
||||
|
||||
image.Data().MoveToRAM();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// TEST 9: Process through chain with Algorithm interface
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
std::cout << "\n--- Test 9: Sequential chain processing via Algorithm interface ---\n";
|
||||
|
||||
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
|
||||
// Set a pattern: single bright voxel
|
||||
image[Vector3i(5, 5, 5)].Value = 100;
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filterA(Vector3i(3, 3, 3));
|
||||
std::vector<float> w(27, 1.0f);
|
||||
filterA.SetImage(&image);
|
||||
filterA.SetKernelNumericXZY(w);
|
||||
|
||||
VoxFilterAlgorithmLinear<TestVoxel> filterB(Vector3i(3, 3, 3));
|
||||
filterB.SetImage(&image);
|
||||
filterB.SetKernelNumericXZY(w);
|
||||
|
||||
// Chain
|
||||
filterA.SetDecoder(&filterB);
|
||||
filterB.SetEncoder(&filterA);
|
||||
|
||||
// Process chain through base pointer
|
||||
using AlgType = Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>;
|
||||
AlgType* chain = &filterA;
|
||||
|
||||
// Walk and process
|
||||
AlgType* current = chain;
|
||||
while (current) {
|
||||
current->Process(&image);
|
||||
current = static_cast<AlgType*>(current->GetDecoder());
|
||||
}
|
||||
|
||||
// After two rounds of smoothing, the peak should be smaller than original
|
||||
float final_val = image[Vector3i(5, 5, 5)].Value;
|
||||
TEST1(final_val < 100.0f);
|
||||
std::cout << " Two-stage smoothing: peak = " << final_val << " OK\n";
|
||||
}
|
||||
|
||||
END_TESTING;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ set(TESTS
|
||||
QuadMeshTest
|
||||
BitCodeTest
|
||||
UnitsTest
|
||||
AlgorithmCudaChainTest
|
||||
)
|
||||
|
||||
set(LIBRARIES
|
||||
@@ -28,6 +29,6 @@ set(LIBRARIES
|
||||
uLib_add_tests(Math)
|
||||
|
||||
if(USE_CUDA)
|
||||
set_source_files_properties(VoxImageTest.cpp VoxImageCopyTest.cpp VoxImageFilterTest.cpp VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp PROPERTIES LANGUAGE CUDA)
|
||||
set_source_files_properties(VoxImageTest.cpp VoxImageCopyTest.cpp VoxImageFilterTest.cpp VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp AlgorithmCudaChainTest.cpp PROPERTIES LANGUAGE CUDA)
|
||||
set_source_files_properties(VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp PROPERTIES CXX_STANDARD 17 CUDA_STANDARD 17)
|
||||
endif()
|
||||
|
||||
@@ -432,9 +432,9 @@ void init_math(py::module_ &m) {
|
||||
.def("AddPoint", &TriangleMesh::AddPoint)
|
||||
.def("AddTriangle",
|
||||
py::overload_cast<const Vector3i &>(&TriangleMesh::AddTriangle))
|
||||
.def("Points", &TriangleMesh::Points,
|
||||
.def("Points", py::overload_cast<>(&TriangleMesh::Points),
|
||||
py::return_value_policy::reference_internal)
|
||||
.def("Triangles", &TriangleMesh::Triangles,
|
||||
.def("Triangles", py::overload_cast<>(&TriangleMesh::Triangles),
|
||||
py::return_value_policy::reference_internal)
|
||||
.def("GetTriangle", &TriangleMesh::GetTriangle)
|
||||
.def("GetNormal", &TriangleMesh::GetNormal);
|
||||
@@ -444,9 +444,9 @@ void init_math(py::module_ &m) {
|
||||
.def("AddPoint", &QuadMesh::AddPoint)
|
||||
.def("AddQuad",
|
||||
py::overload_cast<const Vector4i &>(&QuadMesh::AddQuad))
|
||||
.def("Points", &QuadMesh::Points,
|
||||
.def("Points", py::overload_cast<>(&QuadMesh::Points),
|
||||
py::return_value_policy::reference_internal)
|
||||
.def("Quads", &QuadMesh::Quads,
|
||||
.def("Quads", py::overload_cast<>(&QuadMesh::Quads),
|
||||
py::return_value_policy::reference_internal)
|
||||
.def("GetQuad", &QuadMesh::GetQuad)
|
||||
.def("GetNormal", &QuadMesh::GetNormal);
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
set(HEP_GEANT_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkGeantEvent.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkGeantSolid.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkBoxSolid.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkTessellatedSolid.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkGeantScene.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkEmitterPrimary.cpp
|
||||
PARENT_SCOPE)
|
||||
@@ -13,6 +15,8 @@ set(HEP_GEANT_SOURCES
|
||||
set(HEP_GEANT_HEADERS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkGeantEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkGeantSolid.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkBoxSolid.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkTessellatedSolid.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkGeantScene.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkEmitterPrimary.h
|
||||
PARENT_SCOPE)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
set(TESTS
|
||||
vtkGeantEventTest
|
||||
vtkGeantSceneTest
|
||||
vtkSolidsTest
|
||||
vtkEmitterPrimaryTest
|
||||
vtkSkyPlaneEmitterPrimaryTest
|
||||
vtkCylinderEmitterPrimaryTest
|
||||
|
||||
84
src/Vtk/HEP/Geant/testing/vtkSolidsTest.cpp
Normal file
84
src/Vtk/HEP/Geant/testing/vtkSolidsTest.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#include "Vtk/HEP/Geant/vtkBoxSolid.h"
|
||||
#include "Vtk/HEP/Geant/vtkTessellatedSolid.h"
|
||||
#include "Vtk/uLibVtkViewer.h"
|
||||
#include "HEP/Geant/Solid.h"
|
||||
#include "Math/ContainerBox.h"
|
||||
#include "Math/TriangleMesh.h"
|
||||
#include "Math/Units.h"
|
||||
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkActor.h>
|
||||
|
||||
using namespace uLib;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
bool interactive = (argc > 1 && std::string(argv[1]) == "-i");
|
||||
|
||||
// 1. Create a BoxSolid
|
||||
ContainerBox box;
|
||||
box.Scale(Vector3f(1_m, 2_m, 3_m));
|
||||
Geant::BoxSolid gBox("MyBox", &box);
|
||||
gBox.Update();
|
||||
|
||||
Vtk::vtkBoxSolid vtkBox(&gBox);
|
||||
|
||||
// 2. Create a TessellatedSolid
|
||||
Geant::TessellatedSolid gTess("MyTess");
|
||||
TriangleMesh mesh;
|
||||
// Create a simple pyramid
|
||||
mesh.Points().push_back(Vector3f(0, 0, 1_m)); // apex
|
||||
mesh.Points().push_back(Vector3f(-1_m, -1_m, 0));
|
||||
mesh.Points().push_back(Vector3f( 1_m, -1_m, 0));
|
||||
mesh.Points().push_back(Vector3f( 1_m, 1_m, 0));
|
||||
mesh.Points().push_back(Vector3f(-1_m, 1_m, 0));
|
||||
|
||||
mesh.Triangles().push_back(Vector3i(1, 2, 0));
|
||||
mesh.Triangles().push_back(Vector3i(2, 3, 0));
|
||||
mesh.Triangles().push_back(Vector3i(3, 4, 0));
|
||||
mesh.Triangles().push_back(Vector3i(4, 1, 0));
|
||||
mesh.Triangles().push_back(Vector3i(1, 3, 2)); // base
|
||||
mesh.Triangles().push_back(Vector3i(1, 4, 3)); // base
|
||||
|
||||
gTess.SetMesh(mesh);
|
||||
gTess.Update();
|
||||
|
||||
Vtk::vtkTessellatedSolid vtkTess(&gTess);
|
||||
|
||||
// 3. Visualization setup
|
||||
Vtk::Viewer viewer;
|
||||
vtkBox.AddToViewer(viewer);
|
||||
vtkTess.AddToViewer(viewer);
|
||||
|
||||
// Color them differently
|
||||
vtkActor::SafeDownCast(vtkBox.GetProp())->GetProperty()->SetColor(0.8, 0.2, 0.2); // Redish box
|
||||
vtkActor::SafeDownCast(vtkTess.GetProp())->GetProperty()->SetColor(0.2, 0.8, 0.2); // Greenish tess
|
||||
|
||||
// Position tessellated solid away from box
|
||||
Matrix4f trans = Matrix4f::Identity();
|
||||
trans.block<3,1>(0,3) = Vector3f(5_m, 0, 0);
|
||||
gTess.SetTransform(trans);
|
||||
vtkTess.Update();
|
||||
|
||||
std::cout << "..:: Testing vtkSolidsTest ::.." << std::endl;
|
||||
std::cout << "Box and Tessellated solids initialized." << std::endl;
|
||||
|
||||
if (interactive) {
|
||||
viewer.ZoomAuto();
|
||||
viewer.Start();
|
||||
} else {
|
||||
std::cout << "Non-interactive test passed." << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
57
src/Vtk/HEP/Geant/vtkBoxSolid.cpp
Normal file
57
src/Vtk/HEP/Geant/vtkBoxSolid.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#include "vtkBoxSolid.h"
|
||||
#include <vtkCubeSource.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkActor.h>
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
|
||||
vtkBoxSolid::vtkBoxSolid(Geant::BoxSolid *content)
|
||||
: vtkGeantSolid(content), m_BoxContent(content) {
|
||||
// Re-run Update for box-specific pipe
|
||||
this->Update();
|
||||
}
|
||||
|
||||
vtkBoxSolid::~vtkBoxSolid() {}
|
||||
|
||||
void vtkBoxSolid::Update() {
|
||||
this->UpdateGeometry();
|
||||
this->UpdateTransform();
|
||||
}
|
||||
|
||||
void vtkBoxSolid::UpdateGeometry() {
|
||||
if (!m_BoxContent || !m_BoxContent->GetObject()) {
|
||||
// Fallback to base tessellation if no model object
|
||||
vtkGeantSolid::UpdateGeometry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the underlying ContainerBox for precise geometry
|
||||
Vector3f size = m_BoxContent->GetObject()->GetSize();
|
||||
|
||||
vtkNew<vtkCubeSource> cube;
|
||||
cube->SetXLength(size(0));
|
||||
cube->SetYLength(size(1));
|
||||
cube->SetZLength(size(2));
|
||||
cube->Update();
|
||||
|
||||
vtkPolyData *poly = GetPolyData();
|
||||
if (poly) {
|
||||
poly->ShallowCopy(cube->GetOutput());
|
||||
poly->Modified();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Vtk
|
||||
} // namespace uLib
|
||||
52
src/Vtk/HEP/Geant/vtkBoxSolid.h
Normal file
52
src/Vtk/HEP/Geant/vtkBoxSolid.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
------------------------------------------------------------------
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3.0 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library.
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#ifndef U_VTKBOXSOLID_H
|
||||
#define U_VTKBOXSOLID_H
|
||||
|
||||
#include "vtkGeantSolid.h"
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
|
||||
/**
|
||||
* @brief VTK Puppet for visualizing a Geant::BoxSolid.
|
||||
*/
|
||||
class vtkBoxSolid : public vtkGeantSolid {
|
||||
public:
|
||||
vtkBoxSolid(Geant::BoxSolid *content);
|
||||
virtual ~vtkBoxSolid();
|
||||
|
||||
virtual void Update() override;
|
||||
virtual void UpdateGeometry() override;
|
||||
|
||||
protected:
|
||||
Geant::BoxSolid *m_BoxContent;
|
||||
};
|
||||
|
||||
} // namespace Vtk
|
||||
} // namespace uLib
|
||||
|
||||
#endif // U_VTKBOXSOLID_H
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
#include "vtkGeantScene.h"
|
||||
#include "vtkGeantSolid.h"
|
||||
#include "vtkBoxSolid.h"
|
||||
#include "vtkTessellatedSolid.h"
|
||||
#include "Vtk/vtkViewport.h"
|
||||
|
||||
namespace uLib {
|
||||
@@ -54,8 +56,19 @@ vtkGeantScene::vtkGeantScene(Geant::Scene *scene)
|
||||
|
||||
// Only create a puppet if the solid has a valid G4VSolid
|
||||
if (solid->GetG4Solid()) {
|
||||
vtkGeantSolid *vtkSolid = new vtkGeantSolid(solid);
|
||||
m_SolidPuppets.push_back(vtkSolid);
|
||||
vtkGeantSolid *vtkSolid = nullptr;
|
||||
if (auto *box = dynamic_cast<Geant::BoxSolid *>(solid)) {
|
||||
vtkSolid = new vtkBoxSolid(box);
|
||||
} else if (auto *tess = dynamic_cast<Geant::TessellatedSolid *>(solid)) {
|
||||
vtkSolid = new vtkTessellatedSolid(tess);
|
||||
} else {
|
||||
vtkSolid = new vtkGeantSolid(solid);
|
||||
vtkSolid->Update();
|
||||
}
|
||||
|
||||
if (vtkSolid) {
|
||||
m_SolidPuppets.push_back(vtkSolid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ namespace Vtk {
|
||||
vtkGeantSolid::vtkGeantSolid(Content *content)
|
||||
: m_SolidActor(vtkActor::New()), m_Content(content) {
|
||||
this->InstallPipe();
|
||||
this->Update();
|
||||
}
|
||||
|
||||
vtkGeantSolid::~vtkGeantSolid() {
|
||||
@@ -59,6 +58,11 @@ vtkPolyData *vtkGeantSolid::GetPolyData() const {
|
||||
}
|
||||
|
||||
void vtkGeantSolid::Update() {
|
||||
this->UpdateGeometry();
|
||||
this->UpdateTransform();
|
||||
}
|
||||
|
||||
void vtkGeantSolid::UpdateGeometry() {
|
||||
if (!m_Content)
|
||||
return;
|
||||
|
||||
@@ -103,6 +107,11 @@ void vtkGeantSolid::Update() {
|
||||
polyData->SetPolys(polys);
|
||||
polyData->Modified();
|
||||
}
|
||||
}
|
||||
|
||||
void vtkGeantSolid::UpdateTransform() {
|
||||
if (!m_Content || !m_SolidActor)
|
||||
return;
|
||||
|
||||
// Apply the Geant4 transform (position/rotation) if placed
|
||||
if (m_Content->GetPhysical()) {
|
||||
|
||||
@@ -51,7 +51,10 @@ public:
|
||||
|
||||
virtual class vtkPolyData *GetPolyData() const override;
|
||||
|
||||
virtual void Update();
|
||||
virtual void Update() override;
|
||||
|
||||
virtual void UpdateGeometry();
|
||||
virtual void UpdateTransform();
|
||||
|
||||
protected:
|
||||
virtual void InstallPipe();
|
||||
|
||||
62
src/Vtk/HEP/Geant/vtkTessellatedSolid.cpp
Normal file
62
src/Vtk/HEP/Geant/vtkTessellatedSolid.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#include "vtkTessellatedSolid.h"
|
||||
|
||||
#include <vtkPoints.h>
|
||||
#include <vtkCellArray.h>
|
||||
#include <vtkPolyData.h>
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
|
||||
vtkTessellatedSolid::vtkTessellatedSolid(Geant::TessellatedSolid *content)
|
||||
: vtkGeantSolid(content), m_TessContent(content) {
|
||||
this->Update();
|
||||
}
|
||||
|
||||
vtkTessellatedSolid::~vtkTessellatedSolid() {}
|
||||
|
||||
void vtkTessellatedSolid::Update() {
|
||||
this->UpdateGeometry();
|
||||
this->UpdateTransform();
|
||||
}
|
||||
|
||||
void vtkTessellatedSolid::UpdateGeometry() {
|
||||
if (!m_TessContent || m_TessContent->GetMesh().Points().empty()) {
|
||||
// Fallback to base tessellation if no model mesh
|
||||
vtkGeantSolid::UpdateGeometry();
|
||||
return;
|
||||
}
|
||||
|
||||
const TriangleMesh &mesh = m_TessContent->GetMesh();
|
||||
|
||||
vtkNew<vtkPoints> points;
|
||||
for (const auto& pt : mesh.Points()) {
|
||||
points->InsertNextPoint(pt(0), pt(1), pt(2));
|
||||
}
|
||||
|
||||
vtkNew<vtkCellArray> polys;
|
||||
for (const auto& trg : mesh.Triangles()) {
|
||||
vtkIdType ids[3] = { (vtkIdType)trg(0), (vtkIdType)trg(1), (vtkIdType)trg(2) };
|
||||
polys->InsertNextCell(3, ids);
|
||||
}
|
||||
|
||||
vtkPolyData *poly = GetPolyData();
|
||||
if (poly) {
|
||||
poly->SetPoints(points);
|
||||
poly->SetPolys(polys);
|
||||
poly->Modified();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Vtk
|
||||
} // namespace uLib
|
||||
38
src/Vtk/HEP/Geant/vtkTessellatedSolid.h
Normal file
38
src/Vtk/HEP/Geant/vtkTessellatedSolid.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#ifndef U_VTKTESSELLATEDSOLID_H
|
||||
#define U_VTKTESSELLATEDSOLID_H
|
||||
|
||||
#include "vtkGeantSolid.h"
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
|
||||
/**
|
||||
* @brief VTK Puppet for visualizing a Geant::TessellatedSolid.
|
||||
*/
|
||||
class vtkTessellatedSolid : public vtkGeantSolid {
|
||||
public:
|
||||
vtkTessellatedSolid(Geant::TessellatedSolid *content);
|
||||
virtual ~vtkTessellatedSolid();
|
||||
|
||||
virtual void Update() override;
|
||||
virtual void UpdateGeometry() override;
|
||||
|
||||
protected:
|
||||
Geant::TessellatedSolid *m_TessContent;
|
||||
};
|
||||
|
||||
} // namespace Vtk
|
||||
} // namespace uLib
|
||||
|
||||
#endif // U_VTKTESSELLATEDSOLID_H
|
||||
@@ -10,6 +10,7 @@ set(MATH_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkVoxImage.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkContainerBox.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkCylinder.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkAssembly.cpp
|
||||
PARENT_SCOPE)
|
||||
|
||||
set(MATH_HEADERS
|
||||
@@ -20,6 +21,7 @@ set(MATH_HEADERS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkVoxImage.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkContainerBox.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkCylinder.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/vtkAssembly.h
|
||||
PARENT_SCOPE)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
|
||||
@@ -7,6 +7,7 @@ set(TESTS
|
||||
vtkVoxImageInteractiveTest
|
||||
vtkContainerBoxTest
|
||||
vtkContainerBoxTest2
|
||||
vtkAssemblyTest
|
||||
)
|
||||
|
||||
set(LIBRARIES
|
||||
|
||||
@@ -1,109 +1,104 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
// All rights reserved
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#include "Vtk/uLibVtkViewer.h"
|
||||
#include "Vtk/Math/vtkAssembly.h"
|
||||
#include "Vtk/Math/vtkCylinder.h"
|
||||
#include "Vtk/Math/vtkContainerBox.h"
|
||||
#include "Math/Assembly.h"
|
||||
#include "Math/Cylinder.h"
|
||||
#include "Math/ContainerBox.h"
|
||||
#include "Math/Cylinder.h"
|
||||
#include "Vtk/Math/vtkAssembly.h"
|
||||
#include "Vtk/vtkObjectsContext.h"
|
||||
#include "Vtk/uLibVtkViewer.h"
|
||||
#include "Math/Units.h"
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkPropCollection.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace uLib;
|
||||
|
||||
/**
|
||||
* @brief This test verifies that uLib::Vtk::Assembly correctly visualizes a collection
|
||||
* of objects and that transformations applied to the assembly are propagated to its children.
|
||||
* It also checks that the assembly appears as a bounding box of its contents.
|
||||
*/
|
||||
int main() {
|
||||
std::cout << "Starting vtkAssemblyTest..." << std::endl;
|
||||
int main(int argc, char **argv) {
|
||||
bool interactive = (argc > 1 && std::string(argv[1]) == "-i");
|
||||
|
||||
// 1. Setup Core Geometry
|
||||
std::cout << " - Creating core Assembly..." << std::endl;
|
||||
uLib::Assembly* core_assembly = new uLib::Assembly();
|
||||
core_assembly->SetInstanceName("MainAssembly");
|
||||
// ---- 1. Build model objects ----
|
||||
ContainerBox box1;
|
||||
box1.Scale(Vector3f(1_m, 2_m, 0.5_m));
|
||||
box1.SetPosition(Vector3f(0, 0, 0));
|
||||
|
||||
// Add a box
|
||||
std::cout << " - Adding ChildBox (Red)..." << std::endl;
|
||||
ContainerBox* box = new ContainerBox();
|
||||
box->SetInstanceName("ChildBox");
|
||||
box->Translate(Vector3f(1.0, 0, 0));
|
||||
core_assembly->AddObject(box);
|
||||
ContainerBox box2;
|
||||
box2.Scale(Vector3f(0.5_m, 0.5_m, 3_m));
|
||||
box2.SetPosition(Vector3f(2_m, 0, 0));
|
||||
|
||||
// Add a cylinder
|
||||
// std::cout << " - Adding ChildCylinder (Blue)..." << std::endl;
|
||||
// Cylinder* cyl = new Cylinder(0.5, 2.0);
|
||||
// cyl->SetInstanceName("ChildCylinder");
|
||||
// cyl->Translate(Vector3f(-2.0, 0, 0));
|
||||
// core_assembly->AddObject(cyl);
|
||||
Cylinder cyl(0.3_m, 1.5_m, 1);
|
||||
cyl.SetPosition(Vector3f(0, 3_m, 0));
|
||||
|
||||
std::cout << " - Adding another box (Green)..." << std::endl;
|
||||
ContainerBox* box2 = new ContainerBox();
|
||||
box2->Scale(Vector3f(1.0, 1.0, 2.0));
|
||||
box2->SetInstanceName("ChildBox2");
|
||||
box2->Translate(Vector3f(0, 0, 1.0));
|
||||
core_assembly->AddObject(box2);
|
||||
// ---- 2. Create an Assembly and add objects ----
|
||||
Assembly assembly;
|
||||
assembly.AddObject(&box1);
|
||||
assembly.AddObject(&box2);
|
||||
assembly.AddObject(&cyl);
|
||||
assembly.SetShowBoundingBox(true);
|
||||
|
||||
// 2. Setup VTK Representation
|
||||
std::cout << " - Creating VTK Assembly representation..." << std::endl;
|
||||
Vtk::Assembly vtk_assembly(core_assembly);
|
||||
|
||||
// Find and colorized the children puppets
|
||||
Vtk::Puppet* p_box = vtk_assembly.GetPuppet(box);
|
||||
if (p_box) {
|
||||
p_box->SetColor(1.0, 0.0, 0.0); // Red
|
||||
} else {
|
||||
std::cerr << "Warning: Could not find puppet for box!" << std::endl;
|
||||
}
|
||||
// ---- 3. Apply a group transform ----
|
||||
assembly.SetPosition(Vector3f(1_m, 1_m, 0));
|
||||
|
||||
// Vtk::Puppet* p_cyl = vtk_assembly.GetPuppet(cyl);
|
||||
// if (p_cyl) {
|
||||
// p_cyl->SetColor(0.0, 0.0, 1.0); // Blue
|
||||
// } else {
|
||||
// std::cerr << "Warning: Could not find puppet for cylinder!" << std::endl;
|
||||
// }
|
||||
|
||||
Vtk::Puppet* p_box2 = vtk_assembly.GetPuppet(box2);
|
||||
if (p_box2) {
|
||||
p_box2->SetColor(0.0, 1.0, 0.0); // Green
|
||||
} else {
|
||||
std::cerr << "Warning: Could not find puppet for box2!" << std::endl;
|
||||
}
|
||||
|
||||
// 3. Test Transformation Propagation
|
||||
std::cout << " - Rotating Assembly 45 degrees around Z..." << std::endl;
|
||||
core_assembly->Rotate(45.0_deg, Vector3f::UnitZ());
|
||||
|
||||
std::cout << " - Translating Assembly up (+Y)..." << std::endl;
|
||||
core_assembly->Translate(Vector3f(0, 2.0, 0));
|
||||
|
||||
// Notify all puppets of the change
|
||||
core_assembly->Updated();
|
||||
|
||||
// 4. Run Visualization
|
||||
std::cout << "Starting viewer (close to exit)..." << std::endl;
|
||||
std::cout << "Expected: A white bounding box containing a red box and a blue cylinder, "
|
||||
<< "all rotated and translated as a group." << std::endl;
|
||||
// ---- 5. Visualize (create puppets to set properties) ----
|
||||
Vtk::Assembly vtkAsm(&assembly);
|
||||
|
||||
Vtk::Viewer viewer;
|
||||
viewer.AddPuppet(*p_box);
|
||||
viewer.AddPuppet(*p_box2);
|
||||
viewer.AddPuppet(vtk_assembly);
|
||||
viewer.Start();
|
||||
vtkAsm.AddToViewer(viewer); // This triggers puppet creation via ConnectRenderer which eventually calls Puppet::GetProp
|
||||
|
||||
// Explicitly update to ensure puppets exist and are added to assemblies
|
||||
vtkAsm.Update();
|
||||
|
||||
// Clean up
|
||||
delete core_assembly;
|
||||
delete box;
|
||||
delete box2;
|
||||
// delete cyl;
|
||||
// Use the child context to find child puppets and set colors
|
||||
if (auto* childCtx = vtkAsm.GetChildrenContext()) {
|
||||
auto setProps = [](Vtk::Puppet* p, float r, float g, float b) {
|
||||
if (!p) return;
|
||||
vtkPropCollection* props = p->GetProps();
|
||||
props->InitTraversal();
|
||||
for (int i=0; i < props->GetNumberOfItems(); ++i) {
|
||||
if (auto* actor = vtkActor::SafeDownCast(props->GetNextProp())) {
|
||||
actor->GetProperty()->SetColor(r, g, b);
|
||||
actor->GetProperty()->SetRepresentationToSurface();
|
||||
actor->GetProperty()->SetOpacity(0.5);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setProps(childCtx->GetPuppet(&box1), 1.0, 0.0, 0.0); // Red
|
||||
setProps(childCtx->GetPuppet(&box2), 0.0, 1.0, 0.0); // Green
|
||||
setProps(childCtx->GetPuppet(&cyl), 0.0, 0.0, 1.0); // Blue
|
||||
}
|
||||
|
||||
std::cout << "Puppets in viewport: " << viewer.getPuppets().size() << " (Expected 4: 1 assembly + 3 children)" << std::endl;
|
||||
|
||||
// ---- 4. Query the bounding box for terminal output ----
|
||||
Vector3f bbMin, bbMax;
|
||||
assembly.GetBoundingBox(bbMin, bbMax);
|
||||
std::cout << "Assembly bounding box:" << std::endl;
|
||||
std::cout << " min = " << bbMin.transpose() << std::endl;
|
||||
std::cout << " max = " << bbMax.transpose() << std::endl;
|
||||
|
||||
std::cout << "==================================================\n";
|
||||
std::cout << " vtkAssemblyTest\n";
|
||||
std::cout << " 2 boxes + 1 cylinder grouped in an assembly\n";
|
||||
std::cout << "==================================================" << std::endl;
|
||||
|
||||
if (interactive) {
|
||||
viewer.ZoomAuto();
|
||||
viewer.Start();
|
||||
} else {
|
||||
std::cout << "Non-interactive test passed." << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
210
src/Vtk/Math/vtkAssembly.cpp
Normal file
210
src/Vtk/Math/vtkAssembly.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#include "vtkAssembly.h" // uLib::Vtk::Assembly
|
||||
#include "Vtk/vtkObjectsContext.h"
|
||||
#include "Math/vtkDense.h"
|
||||
|
||||
#include <vtkAssembly.h> // VTK library ::vtkAssembly
|
||||
#include <vtkActor.h>
|
||||
#include <vtkCubeSource.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkMatrix4x4.h>
|
||||
#include <vtkPropCollection.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkProp3D.h>
|
||||
#include <vtkPoints.h>
|
||||
#include <vtkCellArray.h>
|
||||
#include <vtkPolyData.h>
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
Assembly::Assembly(uLib::Assembly *content)
|
||||
: m_Content(content),
|
||||
m_ChildContext(nullptr),
|
||||
m_BBoxActor(nullptr),
|
||||
m_VtkAsm(nullptr),
|
||||
m_InUpdate(false),
|
||||
m_BlockUpdate(false) {
|
||||
this->InstallPipe();
|
||||
if (m_Content) {
|
||||
Object::connect(m_Content, &uLib::Assembly::Updated,
|
||||
this, &Assembly::contentUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
Assembly::~Assembly() {
|
||||
delete m_ChildContext;
|
||||
if (m_BBoxActor) m_BBoxActor->Delete();
|
||||
if (m_VtkAsm) m_VtkAsm->Delete();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
void Assembly::InstallPipe() {
|
||||
// 1. Create the VTK library assembly that groups everything
|
||||
m_VtkAsm = ::vtkAssembly::New();
|
||||
this->SetProp(m_VtkAsm);
|
||||
|
||||
// 2. Create the bounding-box wireframe actor
|
||||
vtkNew<vtkCubeSource> cube;
|
||||
cube->SetBounds(0, 1, 0, 1, 0, 1);
|
||||
cube->Update();
|
||||
|
||||
vtkNew<vtkPolyDataMapper> mapper;
|
||||
mapper->SetInputConnection(cube->GetOutputPort());
|
||||
|
||||
m_BBoxActor = vtkActor::New();
|
||||
m_BBoxActor->SetMapper(mapper);
|
||||
m_BBoxActor->GetProperty()->SetRepresentationToWireframe();
|
||||
m_BBoxActor->GetProperty()->SetColor(1.0, 0.85, 0.0); // gold wireframe
|
||||
m_BBoxActor->GetProperty()->SetLineWidth(1.5);
|
||||
m_BBoxActor->GetProperty()->SetOpacity(0.6);
|
||||
m_BBoxActor->SetVisibility(m_Content ? m_Content->GetShowBoundingBox() : false);
|
||||
|
||||
m_VtkAsm->AddPart(m_BBoxActor);
|
||||
|
||||
// 3. Build a child-objects context (auto-creates puppets for each child)
|
||||
if (m_Content) {
|
||||
m_ChildContext = new vtkObjectsContext(m_Content);
|
||||
// The vtkObjectsContext's own prop is already a ::vtkAssembly;
|
||||
// nest it inside ours so everything moves together.
|
||||
if (auto *childProp = vtkProp3D::SafeDownCast(m_ChildContext->GetProp()))
|
||||
m_VtkAsm->AddPart(childProp);
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
m_BlockUpdate = true;
|
||||
Puppet::Update();
|
||||
m_InUpdate = false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
void Assembly::Update() {
|
||||
if (m_InUpdate) return;
|
||||
if (!m_Content || !m_VtkAsm) return;
|
||||
|
||||
if (m_BlockUpdate) {
|
||||
m_BlockUpdate = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_InUpdate = true;
|
||||
|
||||
// Pull VTK transform back into the uLib model
|
||||
vtkMatrix4x4* vmat = m_VtkAsm->GetUserMatrix();
|
||||
if (vmat) {
|
||||
Matrix4f transform = VtkToMatrix4f(vmat);
|
||||
m_Content->SetMatrix(transform);
|
||||
}
|
||||
|
||||
this->UpdateBoundingBox();
|
||||
if (m_ChildContext)
|
||||
m_ChildContext->Update();
|
||||
|
||||
m_Content->Updated(); // Notify change in model
|
||||
|
||||
m_InUpdate = false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
void Assembly::UpdateTransform() {
|
||||
if (!m_Content || !m_VtkAsm) return;
|
||||
|
||||
Matrix4f mat = m_Content->GetMatrix();
|
||||
vtkNew<vtkMatrix4x4> vmat;
|
||||
Matrix4fToVtk(mat, vmat);
|
||||
m_VtkAsm->SetUserMatrix(vmat);
|
||||
m_VtkAsm->Modified();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
void Assembly::UpdateBoundingBox() {
|
||||
if (!m_Content || !m_BBoxActor) return;
|
||||
|
||||
m_BBoxActor->SetVisibility(m_Content->GetShowBoundingBox());
|
||||
|
||||
Vector3f bbMin, bbMax;
|
||||
m_Content->GetBoundingBox(bbMin, bbMax);
|
||||
|
||||
// Avoid degenerate boxes
|
||||
Vector3f size = bbMax - bbMin;
|
||||
if (size.norm() < 1e-6f) return;
|
||||
|
||||
// Rebuild the corner segments to match the AABB
|
||||
vtkNew<vtkPoints> points;
|
||||
vtkNew<vtkCellArray> lines;
|
||||
|
||||
float bounds[2][3] = {
|
||||
{bbMin(0), bbMin(1), bbMin(2)},
|
||||
{bbMax(0), bbMax(1), bbMax(2)}
|
||||
};
|
||||
|
||||
// Corner segment length: 10% of dimension
|
||||
float len[3] = {
|
||||
(bbMax(0) - bbMin(0)) * 0.1f,
|
||||
(bbMax(1) - bbMin(1)) * 0.1f,
|
||||
(bbMax(2) - bbMin(2)) * 0.1f
|
||||
};
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
float p[3];
|
||||
p[0] = bounds[(i & 1) ? 1 : 0][0];
|
||||
p[1] = bounds[(i & 2) ? 1 : 0][1];
|
||||
p[2] = bounds[(i & 4) ? 1 : 0][2];
|
||||
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
float p2[3] = {p[0], p[1], p[2]};
|
||||
float delta = (i & (1 << axis)) ? -len[axis] : len[axis];
|
||||
p2[axis] += delta;
|
||||
|
||||
vtkIdType id1 = points->InsertNextPoint(p);
|
||||
vtkIdType id2 = points->InsertNextPoint(p2);
|
||||
lines->InsertNextCell(2);
|
||||
lines->InsertCellPoint(id1);
|
||||
lines->InsertCellPoint(id2);
|
||||
}
|
||||
}
|
||||
|
||||
vtkNew<vtkPolyData> poly;
|
||||
poly->SetPoints(points);
|
||||
poly->SetLines(lines);
|
||||
|
||||
vtkNew<vtkPolyDataMapper> mapper;
|
||||
mapper->SetInputData(poly);
|
||||
|
||||
m_BBoxActor->SetMapper(mapper);
|
||||
m_BBoxActor->Modified();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
vtkObjectsContext *Assembly::GetChildrenContext() const {
|
||||
return m_ChildContext;
|
||||
}
|
||||
|
||||
} // namespace Vtk
|
||||
} // namespace uLib
|
||||
71
src/Vtk/Math/vtkAssembly.h
Normal file
71
src/Vtk/Math/vtkAssembly.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/*//////////////////////////////////////////////////////////////////////////////
|
||||
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
|
||||
All rights reserved
|
||||
|
||||
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
#ifndef U_VTK_ASSEMBLY_H
|
||||
#define U_VTK_ASSEMBLY_H
|
||||
|
||||
#include "Math/Assembly.h"
|
||||
#include "uLibVtkInterface.h"
|
||||
|
||||
class vtkActor;
|
||||
class vtkAssembly; // VTK library forward declaration (must be before namespace)
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
|
||||
class vtkObjectsContext; // forward
|
||||
|
||||
/**
|
||||
* @brief VTK Puppet for visualizing uLib::Assembly.
|
||||
*
|
||||
* Manages a VTK assembly (vtkAssembly from the VTK library) that groups
|
||||
* all child puppets and applies the Assembly's AffineTransform. It also
|
||||
* renders an optional bounding box wireframe computed from the Assembly's AABB.
|
||||
*
|
||||
* @note This class is uLib::Vtk::Assembly. It internally uses
|
||||
* the VTK library class vtkAssembly for grouping, but the two
|
||||
* are distinct.
|
||||
*/
|
||||
class Assembly : public Puppet {
|
||||
public:
|
||||
virtual const char *GetClassName() const override { return "Vtk.Assembly"; }
|
||||
|
||||
Assembly(uLib::Assembly *content);
|
||||
virtual ~Assembly();
|
||||
|
||||
/** @brief Updates the VTK representation from the model (model→VTK). */
|
||||
virtual void Update() override;
|
||||
|
||||
virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; }
|
||||
|
||||
/** @brief Called when the model signals an update (model→VTK push). */
|
||||
void contentUpdate();
|
||||
|
||||
/** @brief Returns the puppet managing child objects. */
|
||||
vtkObjectsContext *GetChildrenContext() const;
|
||||
|
||||
private:
|
||||
void UpdateTransform();
|
||||
void UpdateBoundingBox();
|
||||
void InstallPipe();
|
||||
|
||||
uLib::Assembly *m_Content;
|
||||
vtkObjectsContext *m_ChildContext;
|
||||
vtkActor *m_BBoxActor;
|
||||
::vtkAssembly *m_VtkAsm; // VTK library assembly — NOT this class
|
||||
bool m_InUpdate; // re-entrancy guard
|
||||
bool m_BlockUpdate; // block feedback from contentUpdate→Update
|
||||
};
|
||||
|
||||
} // namespace Vtk
|
||||
} // namespace uLib
|
||||
|
||||
#endif // U_VTK_ASSEMBLY_H
|
||||
@@ -51,6 +51,8 @@ public:
|
||||
|
||||
virtual void Update();
|
||||
|
||||
virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; }
|
||||
|
||||
protected:
|
||||
virtual void InstallPipe();
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
#include "Vtk/Math/vtkCylinder.h"
|
||||
#include <vtkActor.h>
|
||||
#include <vtkAssembly.h>
|
||||
#include <vtkCylinderSource.h>
|
||||
#include <vtkMatrix4x4.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
@@ -37,13 +38,14 @@ namespace uLib {
|
||||
namespace Vtk {
|
||||
|
||||
vtkCylinder::vtkCylinder(vtkCylinder::Content *content)
|
||||
: m_Actor(vtkActor::New()), m_Content(content) {
|
||||
: m_Content(content), m_Actor(nullptr), m_VtkAsm(nullptr) {
|
||||
this->InstallPipe();
|
||||
Object::connect(m_Content, &Content::Updated, this, &vtkCylinder::contentUpdate);
|
||||
}
|
||||
|
||||
vtkCylinder::~vtkCylinder() {
|
||||
m_Actor->Delete();
|
||||
if (m_Actor) m_Actor->Delete();
|
||||
if (m_VtkAsm) m_VtkAsm->Delete();
|
||||
}
|
||||
|
||||
void vtkCylinder::contentUpdate() {
|
||||
@@ -53,28 +55,31 @@ void vtkCylinder::contentUpdate() {
|
||||
vtkProp3D* root = vtkProp3D::SafeDownCast(this->GetProp());
|
||||
if (!root) return;
|
||||
|
||||
// 1. Placement (Position/Rotation/Model-level Scale) goes to the root prop
|
||||
vtkMatrix4x4* vmat = root->GetUserMatrix();
|
||||
if (!vmat) {
|
||||
vtkNew<vtkMatrix4x4> mat;
|
||||
root->SetUserMatrix(mat);
|
||||
vmat = mat;
|
||||
}
|
||||
|
||||
// Multiply the placement matrix by the volume scaling (Radius, Radius, Height)
|
||||
Matrix4f transform = m_Content->GetMatrix() * m_Content->GetLocalMatrix();
|
||||
Matrix4f transform = m_Content->GetMatrix();
|
||||
Matrix4fToVtk(transform, vmat);
|
||||
|
||||
// Update internal alignment based on active axis
|
||||
// 2. Shape-local properties (Radius, Height, Axis alignment) go to the internal actor
|
||||
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
|
||||
|
||||
// We keep it centered as per latest user preference in Step 677
|
||||
// alignment->Translate(0, 0, 0); // Implicit
|
||||
}
|
||||
|
||||
root->Modified();
|
||||
@@ -90,16 +95,9 @@ void vtkCylinder::Update() {
|
||||
vtkMatrix4x4* vmat = root->GetUserMatrix();
|
||||
if (!vmat) return;
|
||||
|
||||
Matrix4f fullTransform = VtkToMatrix4f(vmat);
|
||||
Matrix4f placementScale = m_Content->GetLocalMatrix().inverse();
|
||||
Matrix4f transform = fullTransform * placementScale;
|
||||
|
||||
if (m_Content->GetParent()) {
|
||||
Matrix4f localT = m_Content->GetParent()->GetWorldMatrix().inverse() * transform;
|
||||
m_Content->SetMatrix(localT);
|
||||
} else {
|
||||
m_Content->SetMatrix(transform);
|
||||
}
|
||||
// Pull the placement matrix directly from VTK
|
||||
Matrix4f transform = VtkToMatrix4f(vmat);
|
||||
m_Content->SetMatrix(transform);
|
||||
|
||||
m_Content->Updated();
|
||||
}
|
||||
@@ -108,36 +106,27 @@ void vtkCylinder::InstallPipe() {
|
||||
if (!m_Content)
|
||||
return;
|
||||
|
||||
m_VtkAsm = ::vtkAssembly::New();
|
||||
this->SetProp(m_VtkAsm);
|
||||
|
||||
vtkNew<vtkCylinderSource> cylinder;
|
||||
cylinder->SetRadius(1.0);
|
||||
cylinder->SetHeight(1.0);
|
||||
cylinder->SetResolution(32);
|
||||
|
||||
m_Actor = vtkActor::New();
|
||||
vtkNew<vtkTransform> alignment;
|
||||
alignment->Identity();
|
||||
alignment->Translate(0, 0, -0.5);
|
||||
|
||||
// Default to Y alignment (Identity) as per latest request
|
||||
int axis = m_Content->GetAxis();
|
||||
if (axis == 0) alignment->RotateZ(-90);
|
||||
else if (axis == 2) alignment->RotateX(90);
|
||||
m_Actor->SetUserTransform(alignment);
|
||||
|
||||
vtkNew<vtkPolyDataMapper> mapper;
|
||||
mapper->SetInputConnection(cylinder->GetOutputPort());
|
||||
|
||||
m_Actor->SetMapper(mapper);
|
||||
m_Actor->SetUserTransform(alignment);
|
||||
m_Actor->GetProperty()->SetRepresentationToWireframe();
|
||||
m_Actor->GetProperty()->SetAmbient(0.6);
|
||||
|
||||
this->SetProp(m_Actor);
|
||||
|
||||
vtkProp3D* root = vtkProp3D::SafeDownCast(this->GetProp());
|
||||
if (root) {
|
||||
vtkNew<vtkMatrix4x4> vmat;
|
||||
Matrix4fToVtk(m_Content->GetMatrix() * m_Content->GetLocalMatrix(), vmat);
|
||||
root->SetUserMatrix(vmat);
|
||||
}
|
||||
m_VtkAsm->AddPart(m_Actor);
|
||||
|
||||
this->contentUpdate();
|
||||
}
|
||||
|
||||
} // namespace Vtk
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "Math/Cylinder.h"
|
||||
#include "Vtk/uLibVtkInterface.h"
|
||||
#include <vtkActor.h>
|
||||
class vtkAssembly;
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
@@ -53,11 +54,14 @@ public:
|
||||
/** Synchronizes the uLib model matrix with the VTK actor (e.g., after UI manipulation) */
|
||||
virtual void Update();
|
||||
|
||||
virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_Content; }
|
||||
|
||||
protected:
|
||||
/** Sets up the VTK visualization pipeline */
|
||||
virtual void InstallPipe();
|
||||
|
||||
vtkActor *m_Actor;
|
||||
::vtkAssembly *m_VtkAsm;
|
||||
Content *m_Content;
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,11 +39,11 @@
|
||||
|
||||
#include <string>
|
||||
#include <vtkVersion.h>
|
||||
#include <vtkProp.h>
|
||||
#include <vtkActor.h>
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
#include "vtkViewport.h"
|
||||
#include "uLibVtkInterface.h"
|
||||
#include <vtkActor.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkPropCollection.h>
|
||||
#include <vtkProp3DCollection.h>
|
||||
#include <vtkRendererCollection.h>
|
||||
@@ -57,9 +57,11 @@
|
||||
#include <vtkPolyData.h>
|
||||
#include <vtkFeatureEdges.h>
|
||||
#include <vtkTransform.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
|
||||
#include "uLibVtkInterface.h"
|
||||
#include "vtkHandlerWidget.h"
|
||||
#include "Math/Dense.h"
|
||||
#include "Core/Property.h"
|
||||
|
||||
|
||||
@@ -87,12 +89,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 = Vector3d::Zero();
|
||||
m_Orientation = Vector3d::Zero();
|
||||
m_Scale = Vector3d::Ones();
|
||||
}
|
||||
|
||||
~PuppetData() {
|
||||
@@ -110,25 +117,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;
|
||||
Vector3d m_Position;
|
||||
Vector3d m_Orientation;
|
||||
Vector3d m_Scale;
|
||||
|
||||
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.data());
|
||||
// p3d->SetOrientation(m_Orientation.data());
|
||||
// p3d->SetScale(m_Scale.data());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,9 +219,6 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool m_Selectable;
|
||||
bool m_Selected;
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
@@ -226,6 +254,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,14 +321,14 @@ void Puppet::DisconnectRenderer(vtkRenderer *renderer)
|
||||
}
|
||||
}
|
||||
|
||||
void Puppet::ConnectViewer(Viewer *viewer)
|
||||
void Puppet::AddToViewer(Viewport &viewer)
|
||||
{
|
||||
// TODO
|
||||
viewer.AddPuppet(*this);
|
||||
}
|
||||
|
||||
void Puppet::DisonnectViewer(Viewer *viewer)
|
||||
void Puppet::RemoveFromViewer(Viewport &viewer)
|
||||
{
|
||||
// TODO
|
||||
viewer.RemovePuppet(*this);
|
||||
}
|
||||
|
||||
vtkRendererCollection *Puppet::GetRenderers() const
|
||||
@@ -370,13 +413,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 +428,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 +494,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.data());
|
||||
p3d->SetOrientation(pd->m_Orientation.data());
|
||||
p3d->SetScale(pd->m_Scale.data());
|
||||
}
|
||||
}
|
||||
|
||||
vtkProp3DCollection *props = pd->m_Assembly->GetParts();
|
||||
props->InitTraversal();
|
||||
for (int i = 0; i < props->GetNumberOfItems(); ++i) {
|
||||
@@ -473,18 +526,79 @@ 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)
|
||||
{
|
||||
}
|
||||
|
||||
struct TransformProxy {
|
||||
PuppetData* pd;
|
||||
template<class Archive>
|
||||
void serialize(Archive & ar, const unsigned int version) {
|
||||
ar & boost::serialization::make_hrp("Position", pd->m_Position, "mm");
|
||||
ar & boost::serialization::make_hrp("Orientation", pd->m_Orientation, "deg");
|
||||
ar & boost::serialization::make_hrp("Scale", pd->m_Scale, "");
|
||||
}
|
||||
};
|
||||
|
||||
struct AppearanceProxy {
|
||||
PuppetData* pd;
|
||||
template<class Archive>
|
||||
void serialize(Archive & ar, const unsigned int version) {
|
||||
ar & boost::serialization::make_hrp("ColorR", pd->m_Color[0]);
|
||||
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_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);
|
||||
}
|
||||
};
|
||||
|
||||
void Puppet::serialize_display(Archive::display_properties_archive & ar, const unsigned int version) {
|
||||
ar & boost::serialization::make_hrp("ColorR", pd->m_Color[0]);
|
||||
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);
|
||||
AppearanceProxy appearance{pd};
|
||||
ar & boost::serialization::make_nvp("Appearance", appearance);
|
||||
|
||||
TransformProxy transform{pd};
|
||||
ar & boost::serialization::make_nvp("Transform", transform);
|
||||
}
|
||||
|
||||
void Puppet::serialize(Archive::xml_oarchive & ar, const unsigned int v) { }
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
#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"
|
||||
@@ -61,6 +64,8 @@ public:
|
||||
|
||||
virtual vtkPropCollection *GetProps();
|
||||
|
||||
virtual uLib::Object *GetContent() const { return nullptr; }
|
||||
|
||||
void ConnectRenderer(vtkRenderer *renderer);
|
||||
|
||||
void DisconnectRenderer(vtkRenderer *renderer);
|
||||
@@ -80,8 +85,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);
|
||||
|
||||
@@ -107,6 +113,9 @@ public:
|
||||
|
||||
virtual void ConnectInteractor(class vtkRenderWindowInteractor *interactor);
|
||||
|
||||
void AddToViewer(class Viewport &viewer);
|
||||
void RemoveFromViewer(class Viewport &viewer);
|
||||
|
||||
protected:
|
||||
void SetProp(vtkProp *prop);
|
||||
|
||||
@@ -126,6 +135,17 @@ private:
|
||||
} // namespace Vtk
|
||||
} // namespace uLib
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// DISPLAY PROPERTIES SERIALIZE
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
namespace uLib {
|
||||
@@ -140,20 +160,53 @@ public:
|
||||
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;
|
||||
}
|
||||
|
||||
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() : "", 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) {
|
||||
boost::archive::detail::common_oarchive<display_properties_archive>::save_override(t.const_value());
|
||||
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 T &t) {}
|
||||
// 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::false_) {}
|
||||
|
||||
void save_override(const boost::archive::object_id_type & t) {}
|
||||
void save_override(const boost::archive::object_reference_type & t) {}
|
||||
@@ -166,6 +219,7 @@ public:
|
||||
|
||||
private:
|
||||
Vtk::Puppet* m_Puppet;
|
||||
std::vector<std::string> m_GroupStack;
|
||||
};
|
||||
|
||||
} // namespace Archive
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "vtkObjectsContext.h"
|
||||
#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>
|
||||
@@ -42,12 +44,8 @@ void vtkObjectsContext::Synchronize() {
|
||||
it->second->DisconnectRenderer(nullptr); // If we have a ref to a renderer we should disconnect but Puppet doesn't store it easily
|
||||
// Actually Puppet::DisconnectRenderer(vtkRenderer*) needs the renderer.
|
||||
// For now we just remove from assembly
|
||||
vtkPropCollection* props = it->second->GetProps();
|
||||
props->InitTraversal();
|
||||
while(vtkProp* prop = props->GetNextProp()) {
|
||||
if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop))
|
||||
m_Assembly->RemovePart(p3d);
|
||||
}
|
||||
if (auto* p3d = vtkProp3D::SafeDownCast(it->second->GetProp()))
|
||||
m_Assembly->RemovePart(p3d);
|
||||
this->PuppetRemoved(it->second);
|
||||
delete it->second;
|
||||
it = m_Puppets.erase(it);
|
||||
@@ -62,12 +60,8 @@ void vtkObjectsContext::Synchronize() {
|
||||
Puppet* puppet = this->CreatePuppet(obj);
|
||||
if (puppet) {
|
||||
m_Puppets[obj] = puppet;
|
||||
vtkPropCollection* props = puppet->GetProps();
|
||||
props->InitTraversal();
|
||||
while(vtkProp* prop = props->GetNextProp()) {
|
||||
if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop))
|
||||
m_Assembly->AddPart(p3d);
|
||||
}
|
||||
if (auto* p3d = vtkProp3D::SafeDownCast(puppet->GetProp()))
|
||||
m_Assembly->AddPart(p3d);
|
||||
this->PuppetAdded(puppet);
|
||||
}
|
||||
}
|
||||
@@ -80,12 +74,8 @@ void vtkObjectsContext::OnObjectAdded(uLib::Object* obj) {
|
||||
Puppet* puppet = this->CreatePuppet(obj);
|
||||
if (puppet) {
|
||||
m_Puppets[obj] = puppet;
|
||||
vtkPropCollection* props = puppet->GetProps();
|
||||
props->InitTraversal();
|
||||
while(vtkProp* prop = props->GetNextProp()) {
|
||||
if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop))
|
||||
m_Assembly->AddPart(p3d);
|
||||
}
|
||||
if (auto* p3d = vtkProp3D::SafeDownCast(puppet->GetProp()))
|
||||
m_Assembly->AddPart(p3d);
|
||||
this->PuppetAdded(puppet);
|
||||
}
|
||||
}
|
||||
@@ -97,12 +87,8 @@ void vtkObjectsContext::OnObjectRemoved(uLib::Object* obj) {
|
||||
if (it != m_Puppets.end()) {
|
||||
// For now we just remove from assembly.
|
||||
// Puppet::DisconnectRenderer(vtkRenderer*) needs the renderer, but we don't have it here easily.
|
||||
vtkPropCollection* props = it->second->GetProps();
|
||||
props->InitTraversal();
|
||||
while(vtkProp* prop = props->GetNextProp()) {
|
||||
if (vtkProp3D* p3d = vtkProp3D::SafeDownCast(prop))
|
||||
m_Assembly->RemovePart(p3d);
|
||||
}
|
||||
if (auto* p3d = vtkProp3D::SafeDownCast(it->second->GetProp()))
|
||||
m_Assembly->RemovePart(p3d);
|
||||
this->PuppetRemoved(it->second);
|
||||
delete it->second;
|
||||
m_Puppets.erase(it);
|
||||
@@ -131,6 +117,10 @@ 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));
|
||||
}
|
||||
|
||||
// Fallback if we don't know the exact class but it might be a context itself
|
||||
|
||||
@@ -25,6 +25,8 @@ public:
|
||||
/** @brief Returns the puppet associated with a specific core object. */
|
||||
Puppet* GetPuppet(uLib::Object* obj);
|
||||
|
||||
const std::map<uLib::Object*, Puppet*>& GetPuppets() const { return m_Puppets; }
|
||||
|
||||
/** @brief Updates all managed puppets. */
|
||||
virtual void Update() override;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <vtkProp3DCollection.h>
|
||||
#include <vtkCamera.h>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <vtkInteractorStyleTrackballCamera.h>
|
||||
#include <vtkObjectFactory.h>
|
||||
#include <vtkAxesActor.h>
|
||||
@@ -29,6 +30,12 @@
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include "vtkHandlerWidget.h"
|
||||
#include "vtkObjectsContext.h"
|
||||
#include "Math/Assembly.h"
|
||||
#include "Math/ContainerBox.h"
|
||||
#include "Math/Cylinder.h"
|
||||
#include "Math/Transform.h"
|
||||
#include "Vtk/Math/vtkAssembly.h"
|
||||
|
||||
namespace uLib {
|
||||
namespace Vtk {
|
||||
@@ -201,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();
|
||||
}
|
||||
}
|
||||
@@ -220,32 +228,67 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
|
||||
self->pv->m_Picker->Pick(pos[0], pos[1], 0, self->pv->m_Renderer);
|
||||
vtkProp* picked = self->pv->m_Picker->GetViewProp();
|
||||
|
||||
// 1. Recursive helper to check if a container prop contains a target prop
|
||||
std::function<bool(vtkProp*, vtkProp*)> containsProp;
|
||||
containsProp = [&containsProp](vtkProp* container, vtkProp* target) -> bool {
|
||||
if (container == target) return true;
|
||||
vtkPropCollection* parts = nullptr;
|
||||
if (auto* pa = vtkPropAssembly::SafeDownCast(container))
|
||||
parts = pa->GetParts();
|
||||
else if (auto* aa = vtkAssembly::SafeDownCast(container))
|
||||
parts = aa->GetParts();
|
||||
if (parts) {
|
||||
parts->InitTraversal();
|
||||
for (int i = 0; i < parts->GetNumberOfItems(); ++i) {
|
||||
if (containsProp(parts->GetNextProp(), target))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Puppet* target = nullptr;
|
||||
if (picked) {
|
||||
// 2. Find the leaf puppet: the one that contains 'picked' and is not a parent of another that also contains it.
|
||||
// Actually, we can just find all matches and pick the one with most 'nested' prop?
|
||||
// A simpler way: we know 'picked' is the LEAF prop from VTK.
|
||||
// Find a puppet that contains it.
|
||||
Puppet* leafPuppet = nullptr;
|
||||
for (auto* p : self->m_Puppets) {
|
||||
if (p->GetProp() == picked) {
|
||||
target = p;
|
||||
break;
|
||||
if (containsProp(p->GetProp(), picked)) {
|
||||
// If we already have a candidate, check if this one is smaller (nested)
|
||||
if (!leafPuppet || containsProp(leafPuppet->GetProp(), p->GetProp())) {
|
||||
leafPuppet = p;
|
||||
}
|
||||
}
|
||||
auto* propAssembly = vtkPropAssembly::SafeDownCast(p->GetProp());
|
||||
auto* actorAssembly = vtkAssembly::SafeDownCast(p->GetProp());
|
||||
vtkPropCollection* parts = nullptr;
|
||||
if (propAssembly) parts = propAssembly->GetParts();
|
||||
else if (actorAssembly) parts = actorAssembly->GetParts();
|
||||
}
|
||||
|
||||
if (parts) {
|
||||
bool found = false;
|
||||
parts->InitTraversal();
|
||||
for (int i=0; i<parts->GetNumberOfItems(); ++i) {
|
||||
if (parts->GetNextProp() == picked) {
|
||||
found = true;
|
||||
break;
|
||||
if (leafPuppet) {
|
||||
target = leafPuppet;
|
||||
|
||||
// 3. Model-driven hierarchy climb:
|
||||
// If the leaf puppet has a uLib object, climb its parents.
|
||||
// If any parent is an Assembly with GroupSelection=true, select the assembly puppet instead.
|
||||
uLib::Object* currentObj = leafPuppet->GetContent();
|
||||
|
||||
while (currentObj) {
|
||||
// Object doesn't have parent, but AffineTransform does
|
||||
uLib::Object* parentObj = nullptr;
|
||||
if (auto* at = dynamic_cast<uLib::AffineTransform*>(currentObj)) {
|
||||
parentObj = dynamic_cast<uLib::Object*>(at->GetParent());
|
||||
}
|
||||
|
||||
if (auto* parentAsm = dynamic_cast<::uLib::Assembly*>(parentObj)) {
|
||||
if (parentAsm->GetGroupSelection()) {
|
||||
// Find the puppet for this parent assembly
|
||||
auto it = self->m_ObjectToPuppet.find(parentAsm);
|
||||
if (it != self->m_ObjectToPuppet.end()) {
|
||||
target = it->second;
|
||||
// Keep climbing to find even larger groups
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
target = p;
|
||||
break;
|
||||
}
|
||||
currentObj = parentObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,20 +430,74 @@ void Viewport::ZoomSelected()
|
||||
|
||||
void Viewport::AddPuppet(Puppet& prop)
|
||||
{
|
||||
m_Puppets.push_back(&prop);
|
||||
prop.ConnectRenderer(pv->m_Renderer);
|
||||
this->RegisterPuppet(&prop, false);
|
||||
Render();
|
||||
}
|
||||
|
||||
void Viewport::RemovePuppet(Puppet& prop)
|
||||
{
|
||||
if (prop.IsSelected()) SelectPuppet(nullptr);
|
||||
auto it = std::find(m_Puppets.begin(), m_Puppets.end(), &prop);
|
||||
if (it != m_Puppets.end()) m_Puppets.erase(it);
|
||||
prop.DisconnectRenderer(pv->m_Renderer);
|
||||
this->UnregisterPuppet(&prop);
|
||||
Render();
|
||||
}
|
||||
|
||||
void Viewport::RegisterPuppet(Puppet* p, bool isPart) {
|
||||
if (!p) return;
|
||||
if (std::find(m_Puppets.begin(), m_Puppets.end(), p) != m_Puppets.end()) return;
|
||||
|
||||
m_Puppets.push_back(p);
|
||||
p->ConnectRenderer(pv->m_Renderer);
|
||||
|
||||
// If it's a part of an assembly, we don't want to draw it twice.
|
||||
// Assembly itself already draws its parts.
|
||||
// But we need ConnectRenderer above to allow highliting and property updates.
|
||||
if (isPart) {
|
||||
pv->m_Renderer->RemoveViewProp(p->GetProp());
|
||||
}
|
||||
|
||||
// Get the object and register in map
|
||||
uLib::Object* obj = p->GetContent();
|
||||
|
||||
// If it's an assembly, we need to observe its children
|
||||
if (auto* as = dynamic_cast<::uLib::Vtk::Assembly*>(p)) {
|
||||
this->ObserveContext(as->GetChildrenContext());
|
||||
}
|
||||
|
||||
if (obj) m_ObjectToPuppet[obj] = p;
|
||||
}
|
||||
|
||||
void Viewport::UnregisterPuppet(Puppet* p) {
|
||||
if (!p) return;
|
||||
if (p->IsSelected()) SelectPuppet(nullptr);
|
||||
|
||||
auto it = std::find(m_Puppets.begin(), m_Puppets.end(), p);
|
||||
if (it != m_Puppets.end()) m_Puppets.erase(it);
|
||||
|
||||
// Remove from map
|
||||
for (auto mapIt = m_ObjectToPuppet.begin(); mapIt != m_ObjectToPuppet.end(); ) {
|
||||
if (mapIt->second == p) mapIt = m_ObjectToPuppet.erase(mapIt);
|
||||
else ++mapIt;
|
||||
}
|
||||
|
||||
p->DisconnectRenderer(pv->m_Renderer);
|
||||
}
|
||||
|
||||
void Viewport::ObserveContext(vtkObjectsContext* ctx) {
|
||||
if (!ctx) return;
|
||||
|
||||
// Process existing puppets
|
||||
for (auto const& [obj, puppet] : ctx->GetPuppets()) {
|
||||
this->RegisterPuppet(puppet, true);
|
||||
}
|
||||
|
||||
// Listen for future puppets
|
||||
uLib::Object::connect(ctx, &vtkObjectsContext::PuppetAdded, [this](Puppet* p){
|
||||
this->RegisterPuppet(p, true);
|
||||
});
|
||||
uLib::Object::connect(ctx, &vtkObjectsContext::PuppetRemoved, [this](Puppet* p){
|
||||
this->UnregisterPuppet(p);
|
||||
});
|
||||
}
|
||||
|
||||
void Viewport::SelectPuppet(Puppet* prop)
|
||||
{
|
||||
for (auto* p : m_Puppets) {
|
||||
@@ -422,6 +519,7 @@ void Viewport::SelectPuppet(Puppet* prop)
|
||||
}
|
||||
|
||||
Render();
|
||||
OnSelectionChanged(prop);
|
||||
}
|
||||
|
||||
void Viewport::SetGridVisible(bool visible)
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
#include "uLibVtkInterface.h"
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
namespace uLib { class Object; }
|
||||
|
||||
// VTK classes are in the global namespace
|
||||
class vtkRenderer;
|
||||
@@ -59,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
|
||||
@@ -73,10 +77,16 @@ protected:
|
||||
void SetupPipeline(vtkRenderWindowInteractor* iren);
|
||||
|
||||
void UpdateGrid();
|
||||
|
||||
// Internal puppet registration
|
||||
void RegisterPuppet(Puppet* p, bool isPart = false);
|
||||
void UnregisterPuppet(Puppet* p);
|
||||
void ObserveContext(class vtkObjectsContext* ctx);
|
||||
|
||||
struct ViewportData *pv;
|
||||
Axis m_GridAxis;
|
||||
std::vector<Puppet*> m_Puppets;
|
||||
std::map<uLib::Object*, Puppet*> m_ObjectToPuppet;
|
||||
};
|
||||
|
||||
} // namespace Vtk
|
||||
|
||||
Reference in New Issue
Block a user