5 Commits

37 changed files with 817 additions and 273 deletions

View File

@@ -17,6 +17,8 @@ add_executable(gcompose
src/PropertyWidgets.cpp src/PropertyWidgets.cpp
src/PropertiesPanel.h src/PropertiesPanel.h
src/PropertiesPanel.cpp src/PropertiesPanel.cpp
src/PreferencesDialog.h
src/PreferencesDialog.cpp
) )
set_target_properties(gcompose PROPERTIES set_target_properties(gcompose PROPERTIES

View File

@@ -148,6 +148,7 @@ QVariant ContextModel::data(const QModelIndex& index, int role) const {
if (!index.isValid()) return QVariant(); if (!index.isValid()) return QVariant();
uLib::Object* obj = static_cast<uLib::Object*>(index.internalPointer()); uLib::Object* obj = static_cast<uLib::Object*>(index.internalPointer());
if (!obj) return QVariant();
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
QString typeName = getDemangledName(typeid(*obj)); QString typeName = getDemangledName(typeid(*obj));

View File

@@ -13,11 +13,14 @@
#include <QPushButton> #include <QPushButton>
#include <QMenu> #include <QMenu>
#include <QAction> #include <QAction>
#include <QShortcut>
#include <QApplication> #include <QApplication>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
#include "StyleManager.h" #include "StyleManager.h"
#include "Math/VoxImage.h" #include "Math/VoxImage.h"
#include "PreferencesDialog.h"
#include "Settings.h"
MainPanel::MainPanel(QWidget* parent) : QWidget(parent), m_context(nullptr), m_mainVtkContext(nullptr) { MainPanel::MainPanel(QWidget* parent) : QWidget(parent), m_context(nullptr), m_mainVtkContext(nullptr) {
this->setObjectName("MainPanel"); this->setObjectName("MainPanel");
@@ -45,17 +48,12 @@ MainPanel::MainPanel(QWidget* parent) : QWidget(parent), m_context(nullptr), m_m
fileMenu->addAction("Open", this, &MainPanel::onOpen); fileMenu->addAction("Open", this, &MainPanel::onOpen);
fileMenu->addAction("Save", this, &MainPanel::onSave); fileMenu->addAction("Save", this, &MainPanel::onSave);
fileMenu->addAction("Save As", this, &MainPanel::onSaveAs); fileMenu->addAction("Save As", this, &MainPanel::onSaveAs);
fileMenu->addSeparator();
fileMenu->addAction("Preferences", this, &MainPanel::onPreferences);
fileMenu->addSeparator();
fileMenu->addAction("Exit", this, &MainPanel::onExit); fileMenu->addAction("Exit", this, &MainPanel::onExit);
btnFile->setMenu(fileMenu); btnFile->setMenu(fileMenu);
// Theme Menu Button
auto* btnTheme = new QPushButton("Theme", menuPanel);
btnTheme->setObjectName("MenuButton");
auto* themeMenu = new QMenu(btnTheme);
themeMenu->addAction("Dark", this, &MainPanel::onDarkTheme);
themeMenu->addAction("Bright", this, &MainPanel::onBrightTheme);
btnTheme->setMenu(themeMenu);
// New Menu Button // New Menu Button
auto* btnNew = new QPushButton("Add", menuPanel); auto* btnNew = new QPushButton("Add", menuPanel);
btnNew->setObjectName("MenuButton"); btnNew->setObjectName("MenuButton");
@@ -73,7 +71,6 @@ MainPanel::MainPanel(QWidget* parent) : QWidget(parent), m_context(nullptr), m_m
menuLayout->addWidget(logo); menuLayout->addWidget(logo);
menuLayout->addWidget(btnFile); menuLayout->addWidget(btnFile);
menuLayout->addWidget(btnNew); menuLayout->addWidget(btnNew);
menuLayout->addWidget(btnTheme);
menuLayout->addStretch(); menuLayout->addStretch();
mainLayout->addWidget(menuPanel); mainLayout->addWidget(menuPanel);
@@ -108,6 +105,14 @@ MainPanel::MainPanel(QWidget* parent) : QWidget(parent), m_context(nullptr), m_m
m_rootSplitter->setSizes(sizes); m_rootSplitter->setSizes(sizes);
mainLayout->addWidget(m_rootSplitter, 1); mainLayout->addWidget(m_rootSplitter, 1);
// Shortcuts
auto* groupShortcut = new QShortcut(QKeySequence("Ctrl+G"), this);
connect(groupShortcut, &QShortcut::activated, [this]() {
if (auto* viewport = qobject_cast<uLib::Vtk::QViewport*>(m_firstPane->currentViewport())) {
viewport->GroupSelection(m_context);
}
});
} }
void MainPanel::setContext(uLib::ObjectsContext* context) { void MainPanel::setContext(uLib::ObjectsContext* context) {
@@ -236,12 +241,21 @@ void MainPanel::onExit() {
qApp->quit(); qApp->quit();
} }
void MainPanel::onDarkTheme() { void MainPanel::onPreferences() {
StyleManager::applyStyle(qApp, "dark"); uLib::Qt::PreferencesDialog dlg(this);
} if (dlg.exec() == QDialog::Accepted) {
// Apply theme
auto theme = uLib::Qt::Settings::Instance().GetTheme();
StyleManager::applyStyle(qApp, theme == uLib::Qt::Settings::Dark ? "dark" : "bright");
void MainPanel::onBrightTheme() { // Apply rendering preference to all viewports
StyleManager::applyStyle(qApp, "bright"); bool throttled = uLib::Qt::Settings::Instance().GetThrottledRendering();
auto viewports = this->findChildren<uLib::Vtk::QViewport*>();
for (auto* vp : viewports) {
vp->SetThrottledRendering(throttled);
vp->Render();
}
}
} }
MainPanel::~MainPanel() {} MainPanel::~MainPanel() {}

View File

@@ -30,8 +30,7 @@ private slots:
void onSaveAs(); void onSaveAs();
void onExit(); void onExit();
void onDarkTheme(); void onPreferences();
void onBrightTheme();
void onCreateObject(const std::string& className); void onCreateObject(const std::string& className);

View File

@@ -0,0 +1,99 @@
#include "PreferencesDialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QPushButton>
#include <QLabel>
#include <QGroupBox>
namespace uLib {
namespace Qt {
PreferencesDialog::PreferencesDialog(QWidget* parent) : QDialog(parent) {
setWindowTitle("Preferences");
setMinimumWidth(400);
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(20);
mainLayout->setContentsMargins(20, 20, 20, 20);
// ── General / Rendering Settings ────────────────────────────────────────
auto* renderingGroup = new QGroupBox("Appearance & Performance", this);
auto* renderingLayout = new QVBoxLayout(renderingGroup);
auto* themeLayout = new QHBoxLayout();
themeLayout->addWidget(new QLabel("Color Theme:"));
m_themeCombo = new QComboBox(renderingGroup);
m_themeCombo->addItem("Dark");
m_themeCombo->addItem("Bright");
m_themeCombo->setCurrentIndex(Settings::Instance().GetTheme() == Settings::Dark ? 0 : 1);
themeLayout->addWidget(m_themeCombo);
themeLayout->addStretch();
renderingLayout->addLayout(themeLayout);
renderingLayout->addSpacing(10);
m_throttledRendering = new QCheckBox("Enable throttled rendering (recommended for performance)", renderingGroup);
m_throttledRendering->setChecked(Settings::Instance().GetThrottledRendering());
m_throttledRendering->setToolTip("Limits framerate to ~60fps to reduce CPU/GPU usage.");
renderingLayout->addWidget(m_throttledRendering);
mainLayout->addWidget(renderingGroup);
// ── Units Settings ──────────────────────────────────────────────────────
auto* unitsGroup = new QGroupBox("Preferred Units", this);
auto* unitsLayout = new QFormLayout(unitsGroup);
unitsLayout->setLabelAlignment(::Qt::AlignRight);
unitsLayout->setSpacing(10);
auto addUnitRow = [&](const QString& label, Settings::Dimension dim, const QStringList& units) {
auto* combo = new QComboBox(unitsGroup);
combo->addItems(units);
std::string current = Settings::Instance().GetPreferredUnit(dim);
int idx = combo->findText(QString::fromStdString(current));
if (idx >= 0) combo->setCurrentIndex(idx);
unitsLayout->addRow(label + ":", combo);
m_unitCombos[dim] = combo;
};
addUnitRow("Length", Settings::Length, {"m", "cm", "mm", "um", "nm"});
addUnitRow("Angle", Settings::Angle, {"deg", "rad"});
addUnitRow("Energy", Settings::Energy, {"MeV", "GeV", "eV", "keV", "TeV"});
addUnitRow("Time", Settings::Time, {"ns", "s", "ms", "us"});
mainLayout->addWidget(unitsGroup);
mainLayout->addStretch();
// ── Buttons ─────────────────────────────────────────────────────────────
auto* buttonLayout = new QHBoxLayout();
buttonLayout->addStretch();
auto* btnCancel = new QPushButton("Cancel", this);
connect(btnCancel, &QPushButton::clicked, this, &QDialog::reject);
auto* btnOk = new QPushButton("Apply", this);
btnOk->setDefault(true);
btnOk->setObjectName("DisplayToggleBtn"); // Reusing high-contrast style
btnOk->setMinimumWidth(100);
connect(btnOk, &QPushButton::clicked, this, &PreferencesDialog::onAccept);
buttonLayout->addWidget(btnCancel);
buttonLayout->addWidget(btnOk);
mainLayout->addLayout(buttonLayout);
}
void PreferencesDialog::onAccept() {
Settings::Instance().SetThrottledRendering(m_throttledRendering->isChecked());
Settings::Instance().SetTheme(m_themeCombo->currentIndex() == 0 ? Settings::Dark : Settings::Bright);
for (auto const& pair : m_unitCombos) {
Settings::Instance().SetPreferredUnit(pair.first, pair.second->currentText().toStdString());
}
accept();
}
} // namespace Qt
} // namespace uLib

View File

@@ -0,0 +1,31 @@
#ifndef GCOMPOSE_PREFERENCESDIALOG_H
#define GCOMPOSE_PREFERENCESDIALOG_H
#include <QDialog>
#include <QCheckBox>
#include <QComboBox>
#include <map>
#include <string>
#include "Settings.h"
namespace uLib {
namespace Qt {
class PreferencesDialog : public QDialog {
Q_OBJECT
public:
explicit PreferencesDialog(QWidget* parent = nullptr);
private slots:
void onAccept();
private:
QCheckBox* m_throttledRendering;
QComboBox* m_themeCombo;
std::map<Settings::Dimension, QComboBox*> m_unitCombos;
};
} // namespace Qt
} // namespace uLib
#endif

View File

@@ -94,8 +94,8 @@ void QViewportPane::setObject(uLib::Object* obj) {
void QViewportPane::setViewport(QWidget* viewport, const QString& title) { void QViewportPane::setViewport(QWidget* viewport, const QString& title) {
if (m_viewport) { if (m_viewport) {
m_viewport->parentWidget()->layout()->removeWidget(m_viewport); // Use deleteLater() instead of delete to avoid crashes during repaint cycles
delete m_viewport; m_viewport->deleteLater();
} }
m_viewport = viewport; m_viewport = viewport;
m_titleLabel->setText(title); m_titleLabel->setText(title);
@@ -105,21 +105,8 @@ void QViewportPane::setViewport(QWidget* viewport, const QString& title) {
mainAreaLayout->insertWidget(0, m_viewport); mainAreaLayout->insertWidget(0, m_viewport);
} }
QViewportPane::~QViewportPane() {} QViewportPane::~QViewportPane() {}
void QViewportPane::setViewport(QWidget* viewport, const QString& title) {
if (m_viewport) {
m_layout->removeWidget(m_viewport);
delete m_viewport;
}
m_viewport = viewport;
m_titleLabel->setText(title);
m_viewport->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_layout->addWidget(m_viewport);
}
void QViewportPane::addVtkViewport() { void QViewportPane::addVtkViewport() {
auto* viewport = new uLib::Vtk::QViewport(this); auto* viewport = new uLib::Vtk::QViewport(this);
setViewport(viewport, "VTK Viewport"); setViewport(viewport, "VTK Viewport");

View File

@@ -23,6 +23,11 @@ public:
Dimensionless Dimensionless
}; };
enum Theme {
Dark,
Bright
};
void SetPreferredUnit(Dimension dim, const std::string& unit) { void SetPreferredUnit(Dimension dim, const std::string& unit) {
m_PreferredUnits[dim] = unit; m_PreferredUnits[dim] = unit;
} }
@@ -64,9 +69,17 @@ public:
return Dimensionless; return Dimensionless;
} }
bool GetThrottledRendering() const { return m_ThrottledRendering; }
void SetThrottledRendering(bool enabled) { m_ThrottledRendering = enabled; }
Theme GetTheme() const { return m_Theme; }
void SetTheme(Theme theme) { m_Theme = theme; }
private: private:
Settings() {} Settings() : m_ThrottledRendering(true), m_Theme(Dark) {}
std::map<Dimension, std::string> m_PreferredUnits; std::map<Dimension, std::string> m_PreferredUnits;
bool m_ThrottledRendering;
Theme m_Theme;
}; };
} // namespace Qt } // namespace Qt

View File

@@ -115,7 +115,7 @@ void ViewportPane::setObject(uLib::Object* obj) {
void ViewportPane::setViewport(QWidget* viewport, const QString& title) { void ViewportPane::setViewport(QWidget* viewport, const QString& title) {
if (m_viewport) { if (m_viewport) {
delete m_viewport; m_viewport->deleteLater();
} }
m_viewport = viewport; m_viewport = viewport;
m_titleLabel->setText(title); m_titleLabel->setText(title);

View File

@@ -1,36 +1 @@
make: Entering directory '/home/rigoni/devel/cmt/ulib/build' ninja: error: loading 'build.ninja': No such file or directory
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/VoxImage.cpp.o
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/TriangleMesh.cpp.o
[ 30%] Building CXX object src/Core/CMakeFiles/mutomCore.dir/Options.cpp.o
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/Dense.cpp.o
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/StructuredGrid.cpp.o
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/VoxRaytracer.cpp.o
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/StructuredData.cpp.o
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/Structured2DGrid.cpp.o
[ 30%] Building CXX object src/Math/CMakeFiles/mutomMath.dir/Structured4DGrid.cpp.o
[ 33%] Linking CXX shared library libmutomCore.so
[ 33%] Built target mutomCore
[ 36%] Linking CXX shared library libmutomMath.so
[ 36%] Built target mutomMath
[ 63%] Building CXX object src/Vtk/CMakeFiles/mutomVtk.dir/vtkContainerBox.cpp.o
[ 63%] Building CXX object src/Vtk/CMakeFiles/mutomVtk.dir/uLibVtkInterface.cxx.o
[ 63%] Building CXX object src/Vtk/CMakeFiles/mutomVtk.dir/vtkStructuredGrid.cpp.o
[ 63%] Building CXX object src/Vtk/CMakeFiles/mutomVtk.dir/vtkMuonScatter.cxx.o
[ 63%] Building CXX object src/Vtk/CMakeFiles/mutomVtk.dir/uLibVtkViewer.cpp.o
[ 63%] Generating mutomRootDict.cxx, libmutomRootDict_rdict.pcm, libmutomRootDict.rootmap
[ 63%] Building CXX object src/Vtk/CMakeFiles/mutomVtk.dir/vtkVoxImage.cpp.o
[ 63%] Building CXX object src/Vtk/CMakeFiles/mutomVtk.dir/vtkVoxRaytracerRepresentation.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/muCastorSkinHit.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/muCastorHit.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/muCastorMCTrack.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/muCastorInfo.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/RootMuonScatter.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/muCastorPrimaryVertex.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/muCastorMuDetDIGI.cpp.o
[ 90%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/SkinDetectorWriter.cpp.o
[ 93%] Building CXX object src/Root/CMakeFiles/mutomRoot.dir/mutomRootDict.cxx.o
[ 96%] Linking CXX shared library libmutomVtk.so
[ 96%] Built target mutomVtk
[100%] Linking CXX shared library libmutomRoot.so
[100%] Built target mutomRoot
make: Leaving directory '/home/rigoni/devel/cmt/ulib/build'

View File

@@ -9,7 +9,7 @@ dependencies:
- root - root
- vtk=9.4 # VTK 9.4 - vtk=9.4 # VTK 9.4
- pybind11 - pybind11
# - boost=1.86.0 # requested by VTK 9.4 #- boost=1.86.0 # requested by VTK 9.4
- ninja - ninja
- clang - clang
- clangxx - clangxx

View File

@@ -0,0 +1,48 @@
# Prop3D
`uLib::Vtk::Prop3D` is a bridge class that wraps VTK 3D representations (`vtkProp`, `vtkProp3D`) and integrates them into the `uLib` object model. It allows the framework to manage visual objects, synchronize them with underlying data models, and expose display-specific properties to the GUI.
## Inheritance
`uLib::Vtk::Prop3D` : `uLib::Object`
## Key Functionalities
### VTK Integration
The class provides access to the underlying VTK objects:
- `GetProp()`: Returns the `vtkProp`.
- `GetProxyProp()`: Returns the `vtkProp3D`.
- `GetParts()` / `GetProps()`: Returns `vtkPropCollection` for composite objects.
### Model-View Synchronization
`Prop3D` ensures that the visual representation stays in sync with the domain model:
- `Update()`: Synchronizes the VTK representation based on current internal state and properties. Should be called when model data changes.
- `SyncFromVtk()`: Updates internal state using data from the VTK representation (e.g., after user interaction via gizmos in the 3D view).
- `GetContent()`: Returns the `uLib::Object` that this `Prop3D` represents visually.
### Visual Appearance
- **Color & Opacity**: `SetColor(r, g, b)` and `SetOpacity(alpha)`.
- **Selection**: `SetSelectable(bool)` and `SetSelected(bool)` to manage interactivity and highlighting.
- **BBox/Scale**: `ShowBoundingBox(bool)` and `ShowScaleMeasures(bool)`.
### Rendering Modes
The rendering style can be controlled via the `Representation` enum:
- `Points`
- `Wireframe`
- `Surface`
- `SurfaceWithEdges`
- `Volume`
- `Outline`
- `Slice`
### Display Properties System
`Prop3D` implements a system to expose specific properties (often marked as `hrp` - human readable properties) to a property editor in the GUI.
- `GetDisplayProperties()`: Returns the list of properties registered for display.
- `RegisterDisplayProperty(uLib::PropertyBase*)`: Adds a property to the display list.
- `serialize_display(...)`: A virtual method that subclasses implement to define which properties should be exposed.
#### Activating Display Properties
To automatically populate the display properties list, the `ULIB_ACTIVATE_DISPLAY_PROPERTIES` macro should be called in the constructor. This triggers `serialize_display` using a `display_properties_archive`.
## Implementation Details
`Prop3D` uses the Pimpl idiom (via `Prop3DData *pd`) to hide VTK-specific implementation details and reduce header dependencies.

View File

@@ -46,6 +46,8 @@ public:
DataAllocator(size_t size = 0, bool owns_objects = true) DataAllocator(size_t size = 0, bool owns_objects = true)
: m_Size(size), m_RamData(nullptr), m_VramData(nullptr), : m_Size(size), m_RamData(nullptr), m_VramData(nullptr),
m_Device(MemoryDevice::RAM), m_OwnsObjects(owns_objects) { m_Device(MemoryDevice::RAM), m_OwnsObjects(owns_objects) {
if (m_Size >= (static_cast<size_t>(1) << 60))
throw std::invalid_argument("DataAllocator: absurdly large size requested");
if (m_Size > 0) { if (m_Size > 0) {
if (m_OwnsObjects) if (m_OwnsObjects)
m_RamData = new T[m_Size](); m_RamData = new T[m_Size]();
@@ -139,7 +141,7 @@ public:
void MoveToRAM() { void MoveToRAM() {
if (m_Device == MemoryDevice::RAM) if (m_Device == MemoryDevice::RAM)
return; return;
if (!m_RamData && m_Size > 0) { if (!m_RamData && m_Size > 0 && m_Size < (static_cast<size_t>(1) << 60)) {
if (m_OwnsObjects) if (m_OwnsObjects)
m_RamData = new T[m_Size](); m_RamData = new T[m_Size]();
else else
@@ -178,7 +180,7 @@ public:
T *newRam = nullptr; T *newRam = nullptr;
T *newVram = nullptr; T *newVram = nullptr;
if (size > 0) { if (size > 0 && size < (static_cast<size_t>(1) << 60)) {
if (m_OwnsObjects) if (m_OwnsObjects)
newRam = new T[size](); newRam = new T[size]();
else else

View File

@@ -208,7 +208,8 @@ void Object::PrintSelf(std::ostream &o) const {
bool Object::addSignalImpl(SignalBase *sig, GenericMFPtr fptr, bool Object::addSignalImpl(SignalBase *sig, GenericMFPtr fptr,
const char *name) { const char *name) {
ObjectPrivate::Signal s = {fptr, std::string(name), sig}; if (!d) return false;
ObjectPrivate::Signal s = {fptr, std::string(name ? name : "unnamed"), sig};
d->sigv.push_back(s); d->sigv.push_back(s);
return true; return true;
} }

View File

@@ -8,7 +8,9 @@ ObjectsContext::ObjectsContext() : Object() {}
ObjectsContext::~ObjectsContext() {} ObjectsContext::~ObjectsContext() {}
void ObjectsContext::AddObject(Object* obj) { void ObjectsContext::AddObject(Object* obj) {
if (obj && std::find(m_objects.begin(), m_objects.end(), obj) == m_objects.end()) { if (!obj || obj == this) return;
if (std::find(m_objects.begin(), m_objects.end(), obj) == m_objects.end()) {
m_objects.push_back(obj); m_objects.push_back(obj);
// Connect child's update to context's update to trigger re-renders // Connect child's update to context's update to trigger re-renders
Object::connect(obj, &Object::Updated, this, &Object::Updated); Object::connect(obj, &Object::Updated, this, &Object::Updated);

View File

@@ -37,27 +37,18 @@ Assembly::Assembly(const Assembly &copy)
m_GroupSelection(copy.m_GroupSelection) {} m_GroupSelection(copy.m_GroupSelection) {}
Assembly::~Assembly() { Assembly::~Assembly() {
for (auto const& [obj, conn] : m_ChildConnections) {
conn.disconnect();
}
m_ChildConnections.clear();
} }
void Assembly::AddObject(Object *obj) { void Assembly::AddObject(Object *obj) {
if (!obj || obj == this) return;
if (auto *at = dynamic_cast<AffineTransform *>(obj)) { if (auto *at = dynamic_cast<AffineTransform *>(obj)) {
at->SetParent(this); at->SetParent(this);
} }
// Base class already handles the list and child->parent update connection
ObjectsContext::AddObject(obj); ObjectsContext::AddObject(obj);
// Connect to child updates to recompute AABB
m_ChildConnections[obj] = Object::connect(obj, &Object::Updated, [this](){
this->ComputeBoundingBox();
this->Updated(); // Signal that assembly itself changed (AABB-wise)
});
// Parent -> Child propagation for world matrix updates
Object::connect(this, &Object::Updated, obj, &Object::Updated);
this->ComputeBoundingBox(); this->ComputeBoundingBox();
} }
@@ -67,12 +58,6 @@ void Assembly::RemoveObject(Object *obj) {
at->SetParent(nullptr); at->SetParent(nullptr);
} }
auto itConn = m_ChildConnections.find(obj);
if (itConn != m_ChildConnections.end()) {
itConn->second.disconnect();
m_ChildConnections.erase(itConn);
}
ObjectsContext::RemoveObject(obj); ObjectsContext::RemoveObject(obj);
this->ComputeBoundingBox(); this->ComputeBoundingBox();
} }

View File

@@ -31,6 +31,7 @@
#include "Math/Transform.h" #include "Math/Transform.h"
namespace uLib { namespace uLib {
namespace Vtk { class Assembly; }
/** /**
* @brief Assembly groups geometric objects (ContainerBox, Cylinder, etc.) * @brief Assembly groups geometric objects (ContainerBox, Cylinder, etc.)
@@ -46,6 +47,7 @@ namespace uLib {
class Assembly : public ObjectsContext, public TRS { class Assembly : public ObjectsContext, public TRS {
public: public:
uLibTypeMacro(Assembly, ObjectsContext, TRS) uLibTypeMacro(Assembly, ObjectsContext, TRS)
friend class Vtk::Assembly;
Assembly(); Assembly();
@@ -112,7 +114,6 @@ private:
bool m_ShowBoundingBox; bool m_ShowBoundingBox;
bool m_GroupSelection; bool m_GroupSelection;
bool m_InUpdated = false; bool m_InUpdated = false;
std::map<Object*, Connection> m_ChildConnections;
ULIB_DECLARE_PROPERTIES(Assembly) ULIB_DECLARE_PROPERTIES(Assembly)
}; };

View File

@@ -4,6 +4,7 @@ set(HEADERS uLibVtkInterface.h
vtkQViewport.h vtkQViewport.h
vtkViewport.h vtkViewport.h
vtkObjectsContext.h vtkObjectsContext.h
vtkMultiSelectionProp.h
) )
set(SOURCES uLibVtkInterface.cxx set(SOURCES uLibVtkInterface.cxx
@@ -12,6 +13,7 @@ set(SOURCES uLibVtkInterface.cxx
vtkQViewport.cpp vtkQViewport.cpp
vtkViewport.cpp vtkViewport.cpp
vtkObjectsContext.cpp vtkObjectsContext.cpp
vtkMultiSelectionProp.cpp
) )
## Pull in Math VTK wrappers (sets MATH_SOURCES / MATH_HEADERS) ## Pull in Math VTK wrappers (sets MATH_SOURCES / MATH_HEADERS)

View File

@@ -28,33 +28,33 @@ using namespace uLib;
int main(int argc, char **argv) { int main(int argc, char **argv) {
bool interactive = (argc > 1 && std::string(argv[1]) == "-i"); bool interactive = (argc > 1 && std::string(argv[1]) == "-i");
// ---- 1. Build model objects ---- // ---- 1. Build model objects on the heap (expected by uLib SmartPointer) ----
ContainerBox box1; auto* box1 = new ContainerBox();
box1.Scale(Vector3f(1_m, 2_m, 0.5_m)); box1->Scale(Vector3f(1, 2, 0.5));
box1.SetPosition(Vector3f(0, 0, 0)); // box1->SetPosition(Vector3f(0, 0, 0));
ContainerBox box2; auto* box2 = new ContainerBox();
box2.Scale(Vector3f(0.5_m, 0.5_m, 3_m)); box2->Scale(Vector3f(0.5, 0.5, 3));
box2.SetPosition(Vector3f(2_m, 0, 0)); box2->SetPosition(Vector3f(2, 0, 0));
Cylinder cyl(0.3_m, 1.5_m, 1); auto* cyl = new Cylinder(1, 1.5, 1);
cyl.SetPosition(Vector3f(0, 3_m, 0)); cyl->SetPosition(Vector3f(0, 3, 0));
// ---- 2. Create an Assembly and add objects ---- // ---- 2. Create an Assembly and add objects ----
Assembly assembly; auto* assembly = new Assembly();
assembly.AddObject(&box1); assembly->AddObject(box1);
assembly.AddObject(&box2); assembly->AddObject(box2);
assembly.AddObject(&cyl); assembly->AddObject(cyl);
assembly.SetShowBoundingBox(true); assembly->SetShowBoundingBox(true);
// ---- 3. Apply a group transform ---- // ---- 3. Apply a group transform ----
assembly.SetPosition(Vector3f(1_m, 1_m, 0)); // assembly->SetPosition(Vector3f(1_m, 1_m, 0));
// ---- 5. Visualize (create prop3ds to set properties) ---- // ---- 5. Visualize (create prop3ds to set properties) ----
Vtk::Assembly vtkAsm(&assembly); Vtk::Assembly vtkAsm(assembly); // Vtk::Assembly takes ownership of the model wrapper
Vtk::Viewer viewer; Vtk::Viewer viewer;
vtkAsm.AddToViewer(viewer); // This triggers prop3d creation via ConnectRenderer which eventually calls Prop3D::GetProp vtkAsm.AddToViewer(viewer);
// Explicitly update to ensure prop3ds exist and are added to assemblies // Explicitly update to ensure prop3ds exist and are added to assemblies
vtkAsm.Update(); vtkAsm.Update();
@@ -74,16 +74,16 @@ int main(int argc, char **argv) {
} }
}; };
setProps(childCtx->GetProp3D(&box1), 1.0, 0.0, 0.0); // Red setProps(childCtx->GetProp3D(box1), 1.0, 0.0, 0.0); // Red
setProps(childCtx->GetProp3D(&box2), 0.0, 1.0, 0.0); // Green setProps(childCtx->GetProp3D(box2), 0.0, 1.0, 0.0); // Green
setProps(childCtx->GetProp3D(&cyl), 0.0, 0.0, 1.0); // Blue setProps(childCtx->GetProp3D(cyl), 0.0, 0.0, 1.0); // Blue
} }
std::cout << "Prop3Ds in viewport: " << viewer.getProp3Ds().size() << " (Expected 4: 1 assembly + 3 children)" << std::endl; std::cout << "Prop3Ds in viewport: " << viewer.getProp3Ds().size() << " (Expected 4: 1 assembly + 3 children)" << std::endl;
// ---- 4. Query the bounding box for terminal output ---- // ---- 4. Query the bounding box for terminal output ----
Vector3f bbMin, bbMax; Vector3f bbMin, bbMax;
assembly.GetBoundingBox(bbMin, bbMax); assembly->GetBoundingBox(bbMin, bbMax);
std::cout << "Assembly bounding box:" << std::endl; std::cout << "Assembly bounding box:" << std::endl;
std::cout << " min = " << bbMin.transpose() << std::endl; std::cout << " min = " << bbMin.transpose() << std::endl;
std::cout << " max = " << bbMax.transpose() << std::endl; std::cout << " max = " << bbMax.transpose() << std::endl;

View File

@@ -45,15 +45,13 @@ Assembly::Assembly(uLib::Assembly *content)
Assembly::~Assembly() { Assembly::~Assembly() {
delete m_ChildContext; delete m_ChildContext;
if (m_BBoxActor) m_BBoxActor->Delete();
if (m_VtkAsm) m_VtkAsm->Delete();
} }
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
void Assembly::InstallPipe() { void Assembly::InstallPipe() {
// 1. Create the VTK library assembly that groups everything // 1. Setup the internal VTK assembly
m_VtkAsm = ::vtkAssembly::New(); m_VtkAsm = vtkSmartPointer<::vtkAssembly>::New();
m_VtkAsm->PickableOff(); m_BBoxActor = vtkSmartPointer<::vtkActor>::New();
this->SetProp(m_VtkAsm); this->SetProp(m_VtkAsm);
// 2. Create the bounding-box wireframe actor // 2. Create the bounding-box wireframe actor
@@ -64,7 +62,6 @@ void Assembly::InstallPipe() {
vtkNew<vtkPolyDataMapper> mapper; vtkNew<vtkPolyDataMapper> mapper;
mapper->SetInputConnection(cube->GetOutputPort()); mapper->SetInputConnection(cube->GetOutputPort());
m_BBoxActor = vtkActor::New();
m_BBoxActor->SetMapper(mapper); m_BBoxActor->SetMapper(mapper);
m_BBoxActor->GetProperty()->SetRepresentationToWireframe(); m_BBoxActor->GetProperty()->SetRepresentationToWireframe();
m_BBoxActor->GetProperty()->SetColor(1.0, 0.85, 0.0); // gold wireframe m_BBoxActor->GetProperty()->SetColor(1.0, 0.85, 0.0); // gold wireframe
@@ -93,14 +90,7 @@ void Assembly::Update() {
if (m_InUpdate) return; if (m_InUpdate) return;
m_InUpdate = true; m_InUpdate = true;
if (this->m_model && m_VtkAsm) { // Delegate to Prop3D to handle standard transformation application (uses GetContent())
// Apply world matrix from the assembly content
vtkNew<vtkMatrix4x4> m;
Matrix4fToVtk(this->m_model->GetMatrix(), m);
m_VtkAsm->SetUserMatrix(m);
m_VtkAsm->Modified();
}
this->Prop3D::Update(); this->Prop3D::Update();
this->UpdateBoundingBox(); this->UpdateBoundingBox();
if (m_ChildContext) if (m_ChildContext)
@@ -110,25 +100,29 @@ void Assembly::Update() {
void Assembly::SyncFromVtk() { void Assembly::SyncFromVtk() {
if (m_InUpdate) return; if (m_InUpdate) return;
if (!this->m_model || !m_VtkAsm) return;
m_InUpdate = true; m_InUpdate = true;
// VTK -> Model: Update world matrix (accounting for model parents) // Sync the group-level transformation from VTK to the domain model
if (vtkProp3D* proxy = this->GetProxyProp()) { this->Prop3D::SyncFromVtk();
this->m_model->SetWorldMatrix(VtkToMatrix4f(proxy->GetUserMatrix()));
this->m_model->FromMatrix(this->m_model->GetMatrix());
}
this->UpdateBoundingBox(); // Propagate sync to children
if (m_ChildContext) if (m_ChildContext)
m_ChildContext->SyncFromVtk(); m_ChildContext->SyncFromVtk();
this->m_model->Updated(); // Notify change in model
m_InUpdate = false; m_InUpdate = false;
} }
void Assembly::serialize_display(uLib::Archive::display_properties_archive &ar, const unsigned int version) {
// 1. Register base class appearance/transform
this->Prop3D::serialize_display(ar, version);
// 2. Map domain model properties (Bounding Box visibility, etc.)
if (this->m_model) {
ar & HRP("ShowBoundingBox", m_model->m_ShowBoundingBox);
}
}
// ------------------------------------------------------------------ // // ------------------------------------------------------------------ //
void Assembly::UpdateBoundingBox() { void Assembly::UpdateBoundingBox() {
if (!this->m_model || !m_BBoxActor) return; if (!this->m_model || !m_BBoxActor) return;
@@ -188,10 +182,6 @@ void Assembly::UpdateBoundingBox() {
m_BBoxActor->Modified(); m_BBoxActor->Modified();
} }
// ------------------------------------------------------------------ //
ObjectsContext *Assembly::GetChildrenContext() const {
return m_ChildContext;
}
} // namespace Vtk } // namespace Vtk
} // namespace uLib } // namespace uLib

View File

@@ -16,8 +16,9 @@
#include "Math/Assembly.h" #include "Math/Assembly.h"
#include "Vtk/uLibVtkInterface.h" #include "Vtk/uLibVtkInterface.h"
class vtkActor; #include <vtkSmartPointer.h>
class vtkAssembly; // VTK library forward declaration (must be before namespace) #include <vtkActor.h>
#include <vtkAssembly.h>
namespace uLib { namespace uLib {
namespace Vtk { namespace Vtk {
@@ -39,33 +40,36 @@ class Assembly : public Prop3D, public uLib::ObjectWrapper<uLib::Assembly> {
public: public:
uLibTypeMacro(Assembly, Prop3D) uLibTypeMacro(Assembly, Prop3D)
Assembly(uLib::Assembly *content); /**
* @brief Constructor.
* @param content Pointer to the domain assembly model.
*/
explicit Assembly(uLib::Assembly *content);
virtual ~Assembly(); virtual ~Assembly();
/** @brief Updates the VTK representation from the model (model→VTK). */ /** @brief Updates the VTK representation from the model (model→VTK). */
virtual void Update() override; virtual void Update() override;
/** @brief Synchronizes the model from the VTK representation (VTK→model). */
virtual void SyncFromVtk() override; virtual void SyncFromVtk() override;
virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_model.get(); } virtual uLib::Object* GetContent() const override { return (uLib::Object*)m_model.get(); }
virtual uLib::ObjectsContext* GetChildren() override { return (uLib::ObjectsContext*)m_model.get(); } virtual uLib::ObjectsContext* GetChildren() override { return (uLib::ObjectsContext*)m_model.get(); }
/** /** @brief Returns the visualization context for children. */
* @brief Returns the prop3d managing child objects. uLib::Vtk::ObjectsContext* GetChildrenContext() const { return m_ChildContext; }
*/
/** @brief Returns the prop3d managing child objects. */ /** @brief Property serialization for Display Properties Panel. */
ObjectsContext *GetChildrenContext() const; void serialize_display(Archive::display_properties_archive &ar, const unsigned int version = 0) override;
private: private:
void UpdateBoundingBox(); void UpdateBoundingBox();
void InstallPipe(); void InstallPipe();
ObjectsContext *m_ChildContext; vtkSmartPointer<::vtkAssembly> m_VtkAsm;
vtkActor *m_BBoxActor; vtkSmartPointer<::vtkActor> m_BBoxActor;
::vtkAssembly *m_VtkAsm; // VTK library assembly — NOT this class
bool m_InUpdate; // re-entrancy guard uLib::Vtk::ObjectsContext *m_ChildContext;
bool m_InUpdate;
}; };
} // namespace Vtk } // namespace Vtk

View File

@@ -56,7 +56,9 @@ struct ContainerBoxData {
: m_Cube(vtkSmartPointer<vtkActor>::New()), : m_Cube(vtkSmartPointer<vtkActor>::New()),
m_Axes(vtkSmartPointer<vtkActor>::New()), m_Axes(vtkSmartPointer<vtkActor>::New()),
m_VtkAsm(vtkSmartPointer<vtkAssembly>::New()) {} m_VtkAsm(vtkSmartPointer<vtkAssembly>::New()) {}
~ContainerBoxData() {} ~ContainerBoxData() {
m_UpdateSignal.disconnect();
}
}; };
ContainerBox::ContainerBox(ContainerBox::Content *content) ContainerBox::ContainerBox(ContainerBox::Content *content)
@@ -93,27 +95,7 @@ void ContainerBox::Update() {
this->Prop3D::Update(); this->Prop3D::Update();
} }
void ContainerBox::SyncFromVtk() {
RecursiveMutex::ScopedLock lock(this->m_UpdateMutex);
if (!this->m_model)
return;
vtkProp3D *root = this->GetProxyProp();
if (!root)
return;
// VTK -> Model: Extract new world TRS from proxy, which matches the model's
// TRS center
vtkMatrix4x4 *rootMat = root->GetUserMatrix();
Matrix4f vtkWorld = VtkToMatrix4f(rootMat);
// Synchronize TRS property members from the updated local matrix
this->m_model->FromMatrix(vtkWorld);
// Since we modified the model, notify observers, but block the loop back to
// VTK ConnectionBlock blocker(d->m_UpdateSignal);
this->m_model->Updated();
}
void ContainerBox::InstallPipe() { void ContainerBox::InstallPipe() {
if (!this->m_model) if (!this->m_model)

View File

@@ -59,7 +59,7 @@ public:
/** /**
* @brief Synchronizes the model from the VTK representation (VTK→model). * @brief Synchronizes the model from the VTK representation (VTK→model).
*/ */
virtual void SyncFromVtk() override;
virtual uLib::Object *GetContent() const override { virtual uLib::Object *GetContent() const override {
return (uLib::Object *)m_model.get(); return (uLib::Object *)m_model.get();

View File

@@ -44,8 +44,7 @@ Cylinder::Cylinder(Cylinder::Content *content)
} }
Cylinder::~Cylinder() { Cylinder::~Cylinder() {
if (m_Actor) m_Actor->Delete(); m_UpdateSignal.disconnect();
if (m_VtkAsm) m_VtkAsm->Delete();
} }
void Cylinder::Update() { void Cylinder::Update() {
@@ -85,26 +84,13 @@ void Cylinder::Update() {
this->Prop3D::Update(); this->Prop3D::Update();
} }
void Cylinder::SyncFromVtk() {
if (!this->m_model) return;
vtkProp3D* root = this->GetProxyProp();
if (!root) return;
// VTK -> Model: Extract new world TRS from proxy
vtkMatrix4x4* rootMat = root->GetUserMatrix();
Matrix4f vtkWorld = VtkToMatrix4f(rootMat);
// Directly sync model from the world matrix
this->m_model->FromMatrix(vtkWorld);
this->m_model->Updated();
}
void Cylinder::InstallPipe() { void Cylinder::InstallPipe() {
if (!this->m_model) if (!this->m_model)
return; return;
m_VtkAsm = ::vtkAssembly::New(); m_VtkAsm = vtkSmartPointer<::vtkAssembly>::New();
this->SetProp(m_VtkAsm); this->SetProp(m_VtkAsm);
vtkNew<vtkCylinderSource> cylinder; vtkNew<vtkCylinderSource> cylinder;
@@ -112,7 +98,7 @@ void Cylinder::InstallPipe() {
cylinder->SetHeight(1.0); cylinder->SetHeight(1.0);
cylinder->SetResolution(32); cylinder->SetResolution(32);
m_Actor = vtkActor::New(); m_Actor = vtkSmartPointer<vtkActor>::New();
vtkNew<vtkTransform> alignment; vtkNew<vtkTransform> alignment;
m_Actor->SetUserTransform(alignment); m_Actor->SetUserTransform(alignment);

View File

@@ -30,6 +30,7 @@
#include "Math/Cylinder.h" #include "Math/Cylinder.h"
#include "Vtk/uLibVtkInterface.h" #include "Vtk/uLibVtkInterface.h"
#include <vtkActor.h> #include <vtkActor.h>
#include <vtkSmartPointer.h>
class vtkAssembly; class vtkAssembly;
namespace uLib { namespace uLib {
@@ -53,7 +54,7 @@ public:
virtual void Update() override; virtual void Update() override;
/** Synchronizes the uLib model matrix with the VTK actor specifically for gizmo interactions */ /** Synchronizes the uLib model matrix with the VTK actor specifically for gizmo interactions */
virtual void SyncFromVtk() override;
virtual uLib::Object *GetContent() const override { virtual uLib::Object *GetContent() const override {
return (uLib::Object *)m_model.get(); return (uLib::Object *)m_model.get();
@@ -63,8 +64,8 @@ protected:
/** Sets up the VTK visualization pipeline */ /** Sets up the VTK visualization pipeline */
virtual void InstallPipe(); virtual void InstallPipe();
vtkActor *m_Actor; vtkSmartPointer<vtkActor> m_Actor;
::vtkAssembly *m_VtkAsm; vtkSmartPointer<::vtkAssembly> m_VtkAsm;
uLib::Connection m_UpdateSignal; uLib::Connection m_UpdateSignal;
}; };

View File

@@ -308,17 +308,7 @@ void VoxImage::serialize_display(uLib::Archive::display_properties_archive & ar,
{"MIP", "Composite", "Composite Shaded", "MIP Bone", "MIP Hot", "Additive"}); {"MIP", "Composite", "Composite Shaded", "MIP Bone", "MIP Hot", "Additive"});
} }
void VoxImage::SyncFromVtk() {
if (auto *root = this->GetProxyProp()) {
vtkMatrix4x4 *rootMat = root->GetUserMatrix();
if (rootMat) {
Matrix4f vtkLocal = VtkToMatrix4f(rootMat);
// Synchronize TRS from VTK, compensating for local volume offset
this->m_model->FromMatrix(vtkLocal); // * this->m_model->GetLocalMatrix().inverse());
this->m_model->Updated();
}
}
}
void VoxImage::Update() { void VoxImage::Update() {
if (auto *root = vtkProp3D::SafeDownCast(this->GetProp())) { if (auto *root = vtkProp3D::SafeDownCast(this->GetProp())) {

View File

@@ -77,7 +77,7 @@ public:
void RescaleShaderRange(); void RescaleShaderRange();
void Update() override; void Update() override;
void SyncFromVtk() override;
void serialize_display(uLib::Archive::display_properties_archive &ar, void serialize_display(uLib::Archive::display_properties_archive &ar,
const unsigned int version = 0) override; const unsigned int version = 0) override;

View File

@@ -93,14 +93,28 @@ public:
m_Selectable(true), m_Selectable(true),
m_Selected(false), m_Selected(false),
m_Visibility(true), m_Visibility(true),
m_Dragable(true) m_Dragable(true),
m_HighlightMode(Prop3D::HighlightPlain)
{ {
m_Color = Vector3d(-1, -1, -1); m_Color = Vector3d(-1, -1, -1);
m_PrevMatrix = vtkSmartPointer<vtkMatrix4x4>::New();
m_PrevMatrix->Identity();
} }
~Prop3DData() { ~Prop3DData() {
// No manual Delete needed for smart pointers if (m_Renderers) {
m_Renderers->InitTraversal();
for (int i = 0; i < m_Renderers->GetNumberOfItems(); ++i) {
vtkRenderer* ren = m_Renderers->GetNextItem();
if (ren) {
if (m_Prop) ren->RemoveViewProp(m_Prop);
if (m_OutlineActor) ren->RemoveActor(m_OutlineActor);
if (m_CubeAxesActor) ren->RemoveActor(m_CubeAxesActor);
if (m_HighlightActor) ren->RemoveActor(m_HighlightActor);
}
}
m_Renderers->RemoveAllItems();
}
} }
Prop3D *m_Prop3D; Prop3D *m_Prop3D;
@@ -125,6 +139,10 @@ public:
bool m_Visibility; bool m_Visibility;
bool m_Dragable; bool m_Dragable;
int m_HighlightMode; // 0: Plain, 1: Corners
vtkSmartPointer<vtkMatrix4x4> m_PrevMatrix;
// //
TRS m_Transform; TRS m_Transform;
@@ -212,39 +230,71 @@ public:
} }
if (!m_HighlightActor) { if (!m_HighlightActor) {
m_HighlightActor = vtkSmartPointer<vtkActor>::New();
vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
m_HighlightActor->SetMapper(mapper);
m_HighlightActor->GetProperty()->SetRepresentationToWireframe();
m_HighlightActor->GetProperty()->SetColor(1.0, 0.0, 0.0); // Red
m_HighlightActor->GetProperty()->SetLineWidth(2.0);
m_HighlightActor->GetProperty()->SetLighting(0);
}
if (m_HighlightMode == Prop3D::HighlightPlain) {
vtkSmartPointer<vtkCubeSource> cube = vtkSmartPointer<vtkCubeSource>::New(); vtkSmartPointer<vtkCubeSource> cube = vtkSmartPointer<vtkCubeSource>::New();
double bounds[6]; double bounds[6];
polydata->GetBounds(bounds); polydata->GetBounds(bounds);
// Add a small padding to prevent z-fighting
double maxDim = std::max({bounds[1]-bounds[0], bounds[3]-bounds[2], bounds[5]-bounds[4]}); double maxDim = std::max({bounds[1]-bounds[0], bounds[3]-bounds[2], bounds[5]-bounds[4]});
double pad = maxDim * 0.02; double pad = maxDim * 0.02;
if(pad < 1e-4) pad = 0.05; if(pad < 1e-4) pad = 0.05;
cube->SetBounds(bounds[0]-pad, bounds[1]+pad, cube->SetBounds(bounds[0]-pad, bounds[1]+pad,
bounds[2]-pad, bounds[3]+pad, bounds[2]-pad, bounds[3]+pad,
bounds[4]-pad, bounds[5]+pad); bounds[4]-pad, bounds[5]+pad);
cube->Update();
m_HighlightActor = vtkSmartPointer<vtkActor>::New(); m_HighlightActor->GetMapper()->SetInputConnection(cube->GetOutputPort());
vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(cube->GetOutputPort());
m_HighlightActor->SetMapper(mapper);
m_HighlightActor->GetProperty()->SetRepresentationToWireframe();
m_HighlightActor->GetProperty()->SetColor(1.0, 0.0, 0.0); // Red
m_HighlightActor->GetProperty()->SetLineWidth(2.0);
m_HighlightActor->GetProperty()->SetLighting(0);
} else { } else {
if (auto* mapper = vtkPolyDataMapper::SafeDownCast(m_HighlightActor->GetMapper())) { // Corners mode logic
if (auto* cube = vtkCubeSource::SafeDownCast(mapper->GetInputAlgorithm())) { double bounds[6];
double bounds[6]; polydata->GetBounds(bounds);
polydata->GetBounds(bounds); double maxDim = std::max({bounds[1]-bounds[0], bounds[3]-bounds[2], bounds[5]-bounds[4]});
double maxDim = std::max({bounds[1]-bounds[0], bounds[3]-bounds[2], bounds[5]-bounds[4]}); double pad = maxDim * 0.02;
double pad = maxDim * 0.02; if(pad < 1e-4) pad = 0.05;
if(pad < 1e-4) pad = 0.05;
cube->SetBounds(bounds[0]-pad, bounds[1]+pad, double b[6] = {bounds[0]-pad, bounds[1]+pad, bounds[2]-pad, bounds[3]+pad, bounds[4]-pad, bounds[5]+pad};
bounds[2]-pad, bounds[3]+pad,
bounds[4]-pad, bounds[5]+pad); vtkNew<vtkPoints> points;
cube->Modified(); vtkNew<vtkCellArray> lines;
float len[3] = {
(float)(b[1] - b[0]) * 0.15f,
(float)(b[3] - b[2]) * 0.15f,
(float)(b[5] - b[4]) * 0.15f
};
for (int i = 0; i < 8; ++i) {
double p[3];
p[0] = b[(i & 1) ? 1 : 0];
p[1] = b[(i & 2) ? 1 : 0];
p[2] = b[(i & 4) ? 1 : 0];
for (int axis = 0; axis < 3; ++axis) {
double p2[3] = {p[0], p[1], p[2]};
double 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> cornerPoly;
cornerPoly->SetPoints(points);
cornerPoly->SetLines(lines);
if (auto* mapper = vtkPolyDataMapper::SafeDownCast(m_HighlightActor->GetMapper())) {
mapper->SetInputData(cornerPoly);
}
} }
// Update highlight matrix from the model world matrix // Update highlight matrix from the model world matrix
@@ -254,6 +304,7 @@ public:
vtkNew<vtkMatrix4x4> vwm; vtkNew<vtkMatrix4x4> vwm;
Matrix4fToVtk(tr->GetWorldMatrix(), vwm); Matrix4fToVtk(tr->GetWorldMatrix(), vwm);
m_HighlightActor->SetUserMatrix(vwm); m_HighlightActor->SetUserMatrix(vwm);
m_PrevMatrix->DeepCopy(vwm);
} }
} }
} }
@@ -514,6 +565,12 @@ void Prop3D::SetRepresentation(const char *mode)
else if (s == "slice") SetRepresentation(Slice); else if (s == "slice") SetRepresentation(Slice);
} }
void Prop3D::SetHighlightMode(HighlightMode mode)
{
pd->m_HighlightMode = static_cast<int>(mode);
pd->UpdateHighlight();
}
void Prop3D::SetColor(double r, double g, double b) void Prop3D::SetColor(double r, double g, double b)
{ {
pd->m_Color[0] = r; pd->m_Color[0] = r;
@@ -528,6 +585,18 @@ void Prop3D::SetOpacity(double alpha)
pd->ApplyAppearance(pd->m_Prop); pd->ApplyAppearance(pd->m_Prop);
} }
void Prop3D::GetColor(double &r, double &g, double &b) const
{
r = pd->m_Color[0];
g = pd->m_Color[1];
b = pd->m_Color[2];
}
double Prop3D::GetOpacity() const
{
return pd->m_Opacity;
}
@@ -567,6 +636,7 @@ void Prop3D::ApplyProp3DTransform(vtkProp3D* prop)
Matrix4fToVtk(tr->GetMatrix(), m); Matrix4fToVtk(tr->GetMatrix(), m);
prop->SetUserMatrix(m); prop->SetUserMatrix(m);
prop->Modified(); prop->Modified();
pd->m_PrevMatrix->DeepCopy(m);
} }
} }
} }
@@ -577,7 +647,18 @@ void Prop3D::SyncFromVtk()
if (auto* tr = dynamic_cast<uLib::TRS*>(content)) { if (auto* tr = dynamic_cast<uLib::TRS*>(content)) {
if (auto* proxy = this->GetProxyProp()) { if (auto* proxy = this->GetProxyProp()) {
if (vtkMatrix4x4* mat = proxy->GetUserMatrix()) { if (vtkMatrix4x4* mat = proxy->GetUserMatrix()) {
tr->FromMatrix(VtkToMatrix4f(mat)); // Calculate Delta: currentMatrix * Inv(m_PrevMatrix)
vtkNew<vtkMatrix4x4> invPrev;
vtkMatrix4x4::Invert(pd->m_PrevMatrix, invPrev);
vtkNew<vtkMatrix4x4> delta;
vtkMatrix4x4::Multiply4x4(mat, invPrev, delta);
// Apply delta to world matrix
Matrix4f nextWorldMatrix = VtkToMatrix4f(delta) * tr->GetWorldMatrix();
tr->SetWorldMatrix(nextWorldMatrix);
pd->m_PrevMatrix->DeepCopy(mat);
content->Updated(); content->Updated();
} }
} }
@@ -655,6 +736,8 @@ struct AppearanceProxy {
ar & boost::serialization::make_hrp("Dragable", pd->m_Dragable); ar & boost::serialization::make_hrp("Dragable", pd->m_Dragable);
ar & boost::serialization::make_hrp("ShowBoundingBox", pd->m_ShowBoundingBox); ar & boost::serialization::make_hrp("ShowBoundingBox", pd->m_ShowBoundingBox);
ar & boost::serialization::make_hrp("ShowScaleMeasures", pd->m_ShowScaleMeasures); ar & boost::serialization::make_hrp("ShowScaleMeasures", pd->m_ShowScaleMeasures);
ar & boost::serialization::make_hrp_enum("HighlightMode",
pd->m_HighlightMode, {"Plain", "Corners"});
} }
}; };

View File

@@ -86,8 +86,10 @@ public:
void DisonnectViewer(Viewer *viewer); void DisonnectViewer(Viewer *viewer);
void SetColor(double r, double g, double b); void SetColor(double r, double g, double b);
void GetColor(double &r, double &g, double &b) const;
void SetOpacity(double alpha); void SetOpacity(double alpha);
double GetOpacity() const;
void SetSelectable(bool selectable = true); void SetSelectable(bool selectable = true);
bool IsSelectable() const; bool IsSelectable() const;
@@ -123,6 +125,12 @@ public:
void SetRepresentation(Representation mode); void SetRepresentation(Representation mode);
void SetRepresentation(const char *mode); void SetRepresentation(const char *mode);
enum HighlightMode {
HighlightPlain = 0,
HighlightCorners = 1
};
void SetHighlightMode(HighlightMode mode);
virtual void PrintSelf(std::ostream &o) const; virtual void PrintSelf(std::ostream &o) const;
void ShowBoundingBox(bool show); void ShowBoundingBox(bool show);

View File

@@ -0,0 +1,175 @@
#include "uLibVtkInterface.h"
#include "vtkMultiSelectionProp.h"
#include <vtkActor.h>
#include <vtkPolyDataMapper.h>
#include <vtkCubeSource.h>
#include <vtkProperty.h>
#include <vtkMatrix4x4.h>
#include <vtkRenderer.h>
#include <vtkRendererCollection.h>
#include "Math/Transform.h"
#include "Vtk/Math/vtkDense.h"
namespace uLib {
namespace Vtk {
MultiSelectionProp::MultiSelectionProp() : Prop3D() {
((::uLib::Object*)this)->SetInstanceName("Selection Group");
m_PrevMatrix = vtkSmartPointer<vtkMatrix4x4>::New();
m_GroupHighlightActor = vtkSmartPointer<vtkActor>::New();
vtkNew<vtkPolyDataMapper> mapper;
m_GroupHighlightActor->SetMapper(mapper);
m_GroupHighlightActor->GetProperty()->SetRepresentationToWireframe();
m_GroupHighlightActor->GetProperty()->SetLineWidth(2.0);
m_GroupHighlightActor->GetProperty()->SetLighting(0);
m_GroupHighlightActor->PickableOff();
// Set default display color in Prop3D state
((Prop3D*)this)->SetColor(0.0, 1.0, 0.0);
}
MultiSelectionProp::~MultiSelectionProp() {
}
MultiSelectionProp* MultiSelectionProp::Clone() const {
auto* copy = new MultiSelectionProp();
copy->SetMembers(this->m_Members);
((::uLib::Object*)copy)->SetInstanceName(((::uLib::Object*)this)->GetInstanceName());
return copy;
}
void MultiSelectionProp::SetMembers(const std::vector<Prop3D*>& members) {
m_Members = members;
Update();
// Reset prev matrix to current highlight position
if (m_GroupHighlightActor->GetUserMatrix()) {
m_PrevMatrix->DeepCopy(m_GroupHighlightActor->GetUserMatrix());
} else {
m_PrevMatrix->Identity();
}
}
void MultiSelectionProp::Update() {
if (m_Members.empty()) {
m_GroupHighlightActor->VisibilityOff();
return;
}
m_GroupHighlightActor->VisibilityOn();
double combinedBounds[6] = {VTK_DOUBLE_MAX, VTK_DOUBLE_MIN,
VTK_DOUBLE_MAX, VTK_DOUBLE_MIN,
VTK_DOUBLE_MAX, VTK_DOUBLE_MIN};
for (auto* member : m_Members) {
if (vtkProp* prop = member->GetProp()) {
double* b = prop->GetBounds();
if (b) {
for (int i = 0; i < 3; ++i) {
if (b[2*i] < combinedBounds[2*i]) combinedBounds[2*i] = b[2*i];
if (b[2*i+1] > combinedBounds[2*i+1]) combinedBounds[2*i+1] = b[2*i+1];
}
}
}
}
if (combinedBounds[0] > combinedBounds[1]) return;
vtkNew<vtkCubeSource> cube;
double maxDim = std::max({combinedBounds[1]-combinedBounds[0],
combinedBounds[3]-combinedBounds[2],
combinedBounds[5]-combinedBounds[4]});
double pad = maxDim * 0.02;
if (pad < 1e-4) pad = 0.05;
cube->SetBounds(combinedBounds[0]-pad, combinedBounds[1]+pad,
combinedBounds[2]-pad, combinedBounds[3]+pad,
combinedBounds[4]-pad, combinedBounds[5]+pad);
cube->Update();
if (auto* mapper = vtkPolyDataMapper::SafeDownCast(m_GroupHighlightActor->GetMapper())) {
mapper->SetInputConnection(cube->GetOutputPort());
}
// Apply TRS from m_SelectionTransform
vtkNew<vtkMatrix4x4> trsMatrix;
Matrix4fToVtk(m_SelectionTransform.GetWorldMatrix(), trsMatrix);
m_GroupHighlightActor->SetUserMatrix(trsMatrix);
// Apply Display Properties (Color, Opacity)
double r, g, b;
((Prop3D*)this)->GetColor(r, g, b);
m_GroupHighlightActor->GetProperty()->SetColor(r, g, b);
m_GroupHighlightActor->GetProperty()->SetOpacity(((Prop3D*)this)->GetOpacity());
// Update Prev Matrix for delta calculations
m_PrevMatrix->DeepCopy(trsMatrix);
// Ensure it's in the renderers
vtkRendererCollection* rens = ((Prop3D*)this)->GetRenderers();
rens->InitTraversal();
for (int i = 0; i < rens->GetNumberOfItems(); ++i) {
vtkRenderer* ren = rens->GetNextItem();
ren->AddActor(m_GroupHighlightActor);
}
}
void MultiSelectionProp::SyncFromVtk() {
if (m_Members.empty()) return;
vtkMatrix4x4* currentMatrix = m_GroupHighlightActor->GetUserMatrix();
if (!currentMatrix) return;
// Calculate Delta: currentMatrix * Inv(m_PrevMatrix)
vtkNew<vtkMatrix4x4> invPrev;
vtkMatrix4x4::Invert(m_PrevMatrix, invPrev);
vtkNew<vtkMatrix4x4> delta;
vtkMatrix4x4::Multiply4x4(currentMatrix, invPrev, delta);
// Apply delta to all members
for (auto* member : m_Members) {
if (auto* content = member->GetContent()) {
if (auto* tr = dynamic_cast<uLib::TRS*>(content)) {
vtkNew<vtkMatrix4x4> memberWorldMatrix;
Matrix4fToVtk(tr->GetWorldMatrix(), memberWorldMatrix);
vtkNew<vtkMatrix4x4> nextWorldMatrix;
vtkMatrix4x4::Multiply4x4(delta, memberWorldMatrix, nextWorldMatrix);
// Set the new world matrix.
if (tr->GetParent()) {
Matrix4f invParentWorld = tr->GetParent()->GetWorldMatrix().inverse();
Matrix4f nextLocalMatrix = invParentWorld * VtkToMatrix4f(nextWorldMatrix);
tr->FromMatrix(nextLocalMatrix);
} else {
tr->FromMatrix(VtkToMatrix4f(nextWorldMatrix));
}
member->Update();
}
}
}
m_PrevMatrix->DeepCopy(currentMatrix);
m_SelectionTransform.FromMatrix(VtkToMatrix4f(currentMatrix));
}
vtkProp* MultiSelectionProp::GetProp() {
return m_GroupHighlightActor;
}
vtkProp3D* MultiSelectionProp::GetProxyProp() {
return m_GroupHighlightActor;
}
void MultiSelectionProp::serialize(Archive::property_register_archive &ar, const unsigned int version) {
ar & boost::serialization::make_nvp("Transform", m_SelectionTransform);
}
void MultiSelectionProp::serialize_display(uLib::Archive::display_properties_archive &ar, const unsigned int version) {
// Call base class to register standard display properties (Color, Opacity)
((Prop3D*)this)->serialize_display(ar, version);
}
} // namespace Vtk
} // namespace uLib

View File

@@ -0,0 +1,61 @@
#ifndef ULIB_VTK_MULTISELECTIONPROP_H
#define ULIB_VTK_MULTISELECTIONPROP_H
#include "uLibVtkInterface.h"
#include "Math/Transform.h"
#include <vector>
#include <vtkSmartPointer.h>
#include <vtkMatrix4x4.h>
class vtkActor;
class vtkProp;
class vtkProp3D;
namespace uLib {
namespace Vtk {
/**
* @class MultiSelectionProp
* @brief A proxy Prop3D that represents a group of selected Prop3Ds.
* It manages a combined highlight and propagates transformations to its members.
*/
class MultiSelectionProp : public Prop3D {
public:
uLibTypeMacro(MultiSelectionProp, Prop3D)
MultiSelectionProp();
virtual ~MultiSelectionProp();
/** @brief Creates a new instance that is a copy of this one's selection state. */
MultiSelectionProp* Clone() const;
void SetMembers(const std::vector<Prop3D*>& members);
const std::vector<Prop3D*>& GetMembers() const { return m_Members; }
virtual void Update() override;
virtual void SyncFromVtk() override;
virtual vtkProp* GetProp() override;
virtual vtkProp3D* GetProxyProp() override;
// Serialization for Properties Panel (TRS)
void serialize(Archive::property_register_archive &ar, const unsigned int version);
// Serialization for Display Properties Panel (Color, Opacity)
void serialize_display(uLib::Archive::display_properties_archive &ar, const unsigned int version = 0) override;
virtual uLib::Object* GetContent() const override { return const_cast<MultiSelectionProp*>(this); }
private:
std::vector<Prop3D*> m_Members;
uLib::TRS m_SelectionTransform;
vtkSmartPointer<vtkMatrix4x4> m_PrevMatrix;
vtkSmartPointer<vtkActor> m_GroupHighlightActor;
};
} // namespace Vtk
} // namespace uLib
#endif // ULIB_VTK_MULTISELECTIONPROP_H

View File

@@ -26,15 +26,17 @@ ObjectsContext::ObjectsContext(uLib::ObjectsContext *context)
: m_Context(context), m_Assembly(::vtkAssembly::New()) { : m_Context(context), m_Assembly(::vtkAssembly::New()) {
this->SetProp(m_Assembly); this->SetProp(m_Assembly);
if (m_Context) { if (m_Context) {
Object::connect(m_Context, &uLib::ObjectsContext::ObjectAdded, this, m_AddedConnection = Object::connect(m_Context, &uLib::ObjectsContext::ObjectAdded, this,
&ObjectsContext::OnObjectAdded); &ObjectsContext::OnObjectAdded);
Object::connect(m_Context, &uLib::ObjectsContext::ObjectRemoved, this, m_RemovedConnection = Object::connect(m_Context, &uLib::ObjectsContext::ObjectRemoved, this,
&ObjectsContext::OnObjectRemoved); &ObjectsContext::OnObjectRemoved);
this->Synchronize(); this->Synchronize();
} }
} }
ObjectsContext::~ObjectsContext() { ObjectsContext::~ObjectsContext() {
m_AddedConnection.disconnect();
m_RemovedConnection.disconnect();
for (auto const &[obj, prop3d] : m_Prop3Ds) { for (auto const &[obj, prop3d] : m_Prop3Ds) {
delete prop3d; delete prop3d;
} }
@@ -136,6 +138,10 @@ Prop3D *ObjectsContext::CreateProp3D(uLib::Object *obj) {
if (!obj) if (!obj)
return nullptr; return nullptr;
if (auto* p3d = dynamic_cast<Prop3D*>(obj)) {
return p3d;
}
if (auto *vox = dynamic_cast<uLib::Abstract::VoxImage *>(obj)) { if (auto *vox = dynamic_cast<uLib::Abstract::VoxImage *>(obj)) {
return new VoxImage(vox); return new VoxImage(vox);
} else if (auto *box = dynamic_cast<uLib::ContainerBox *>(obj)) { } else if (auto *box = dynamic_cast<uLib::ContainerBox *>(obj)) {

View File

@@ -51,6 +51,8 @@ private:
uLib::ObjectsContext *m_Context; uLib::ObjectsContext *m_Context;
std::map<uLib::Object*, Prop3D*> m_Prop3Ds; std::map<uLib::Object*, Prop3D*> m_Prop3Ds;
vtkAssembly *m_Assembly; vtkAssembly *m_Assembly;
uLib::Connection m_AddedConnection;
uLib::Connection m_RemovedConnection;
}; };
} // namespace Vtk } // namespace Vtk

View File

@@ -20,6 +20,8 @@ QViewport::QViewport(QWidget* parent)
, m_VtkWidget(nullptr) , m_VtkWidget(nullptr)
, m_GridButton(nullptr) , m_GridButton(nullptr)
, m_ProjButton(nullptr) , m_ProjButton(nullptr)
, m_renderTimer(nullptr)
, m_renderPending(false)
{ {
// Build the layout zero margins so VTK fills the entire widget // Build the layout zero margins so VTK fills the entire widget
auto* layout = new QVBoxLayout(this); auto* layout = new QVBoxLayout(this);
@@ -29,8 +31,14 @@ QViewport::QViewport(QWidget* parent)
m_VtkWidget = new QVTKOpenGLNativeWidget(this); m_VtkWidget = new QVTKOpenGLNativeWidget(this);
layout->addWidget(m_VtkWidget); layout->addWidget(m_VtkWidget);
// Initialize render timer
m_renderTimer = new QTimer(this);
m_renderTimer->setSingleShot(true);
connect(m_renderTimer, &QTimer::timeout, this, &QViewport::doRender);
// Grid Toggle Button // Grid Toggle Button
m_GridButton = new QPushButton(m_VtkWidget); m_GridButton = new QPushButton(m_VtkWidget);
m_GridButton->setText("#"); m_GridButton->setText("#");
m_GridButton->setFixedSize(40, 40); m_GridButton->setFixedSize(40, 40);
m_GridButton->setToolTip("Toggle Grid"); m_GridButton->setToolTip("Toggle Grid");
@@ -96,6 +104,9 @@ QViewport::QViewport(QWidget* parent)
QViewport::~QViewport() QViewport::~QViewport()
{ {
if (m_VtkWidget && m_VtkWidget->renderWindow()) {
m_VtkWidget->renderWindow()->RemoveRenderer(this->GetRenderer());
}
} }
void QViewport::SetupPipeline() void QViewport::SetupPipeline()
@@ -112,10 +123,22 @@ void QViewport::SetupPipeline()
void QViewport::Render() void QViewport::Render()
{ {
if (m_VtkWidget && m_VtkWidget->renderWindow()) if (!m_throttledRendering) {
m_VtkWidget->renderWindow()->Render(); doRender();
return;
}
if (m_renderPending) return;
m_renderPending = true;
m_renderTimer->start(16);
} }
void QViewport::doRender()
{
m_renderPending = false;
if (m_VtkWidget && m_VtkWidget->renderWindow())
m_VtkWidget->renderWindow()->Render();
}
vtkRenderWindow* QViewport::GetRenderWindow() vtkRenderWindow* QViewport::GetRenderWindow()
{ {
return m_VtkWidget->renderWindow(); return m_VtkWidget->renderWindow();

View File

@@ -4,6 +4,7 @@
#include <QWidget> #include <QWidget>
#include <QVTKOpenGLNativeWidget.h> #include <QVTKOpenGLNativeWidget.h>
#include <QPushButton> #include <QPushButton>
#include <QTimer>
#include <vtkCornerAnnotation.h> #include <vtkCornerAnnotation.h>
#include <vtkOrientationMarkerWidget.h> #include <vtkOrientationMarkerWidget.h>
@@ -39,6 +40,9 @@ public:
// Render scene // Render scene
virtual void Render() override; virtual void Render() override;
void SetThrottledRendering(bool enabled) { m_throttledRendering = enabled; }
bool GetThrottledRendering() const { return m_throttledRendering; }
// Direct access to VTK internals // Direct access to VTK internals
virtual vtkRenderWindow* GetRenderWindow() override; virtual vtkRenderWindow* GetRenderWindow() override;
virtual vtkRenderWindowInteractor* GetInteractor() override; virtual vtkRenderWindowInteractor* GetInteractor() override;
@@ -46,20 +50,25 @@ public:
virtual void OnSelectionChanged(Prop3D* p) override; virtual void OnSelectionChanged(Prop3D* p) override;
protected: protected:
virtual void resizeEvent(QResizeEvent* event) override; virtual void resizeEvent(QResizeEvent* event) override;
private slots: private slots:
void onGridButtonClicked(); void onGridButtonClicked();
void onProjButtonClicked(); void onProjButtonClicked();
void doRender();
private: private:
void SetupPipeline(); void SetupPipeline();
QVTKOpenGLNativeWidget* m_VtkWidget;
QPushButton* m_GridButton;
QPushButton* m_ProjButton;
QTimer* m_renderTimer;
bool m_renderPending = false;
bool m_throttledRendering = true;
};
QVTKOpenGLNativeWidget* m_VtkWidget;
QPushButton* m_GridButton;
QPushButton* m_ProjButton;
};
} // namespace Vtk } // namespace Vtk
} // namespace uLib } // namespace uLib

View File

@@ -36,6 +36,8 @@
#include "Vtk/Math/vtkCylinder.h" #include "Vtk/Math/vtkCylinder.h"
#include "Math/Transform.h" #include "Math/Transform.h"
#include "Vtk/Math/vtkAssembly.h" #include "Vtk/Math/vtkAssembly.h"
#include "vtkMultiSelectionProp.h"
#include <vtkRendererCollection.h>
namespace uLib { namespace uLib {
namespace Vtk { namespace Vtk {
@@ -69,6 +71,7 @@ struct ViewportData {
Viewport::Viewport() Viewport::Viewport()
: pv(new ViewportData()) : pv(new ViewportData())
, m_GridAxis(Y) , m_GridAxis(Y)
, m_MultiSelectionProp(new MultiSelectionProp())
{ {
} }
@@ -100,6 +103,10 @@ Viewport::~Viewport()
pv->m_CameraWidget->Off(); pv->m_CameraWidget->Off();
pv->m_CameraWidget->SetInteractor(nullptr); pv->m_CameraWidget->SetInteractor(nullptr);
} }
if (m_MultiSelectionProp) {
delete m_MultiSelectionProp;
m_MultiSelectionProp = nullptr;
}
delete pv; delete pv;
} }
@@ -192,6 +199,11 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
pv->m_Renderer->SetLayer(0); pv->m_Renderer->SetLayer(0);
} }
// Connect MultiSelectionProp
if (m_MultiSelectionProp) {
m_MultiSelectionProp->ConnectRenderer(pv->m_Renderer);
}
// Setup Handler Widget // Setup Handler Widget
if (!std::getenv("CTEST_PROJECT_NAME")) { if (!std::getenv("CTEST_PROJECT_NAME")) {
pv->m_HandlerWidget = vtkSmartPointer<HandlerWidget>::New(); pv->m_HandlerWidget = vtkSmartPointer<HandlerWidget>::New();
@@ -206,8 +218,10 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
widgetInteractionCallback->SetClientData(this); widgetInteractionCallback->SetClientData(this);
widgetInteractionCallback->SetCallback([](vtkObject*, unsigned long, void* clientdata, void*){ widgetInteractionCallback->SetCallback([](vtkObject*, unsigned long, void* clientdata, void*){
auto* self = static_cast<Viewport*>(clientdata); auto* self = static_cast<Viewport*>(clientdata);
for (auto* p : self->m_Prop3Ds) { if (self->m_SelectedProps.size() > 1 && self->m_MultiSelectionProp) {
if (p->IsSelected()) { self->m_MultiSelectionProp->SyncFromVtk();
} else {
for (auto* p : self->m_SelectedProps) {
p->SyncFromVtk(); p->SyncFromVtk();
} }
} }
@@ -222,6 +236,7 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
clickCallback->SetCallback([](vtkObject* caller, unsigned long, void* clientdata, void*){ clickCallback->SetCallback([](vtkObject* caller, unsigned long, void* clientdata, void*){
auto* iren = static_cast<vtkRenderWindowInteractor*>(caller); auto* iren = static_cast<vtkRenderWindowInteractor*>(caller);
auto* self = static_cast<Viewport*>(clientdata); auto* self = static_cast<Viewport*>(clientdata);
bool multiSelect = iren->GetShiftKey() != 0;
int* pos = iren->GetEventPosition(); int* pos = iren->GetEventPosition();
self->pv->m_Picker->Pick(pos[0], pos[1], 0, self->pv->m_Renderer); self->pv->m_Picker->Pick(pos[0], pos[1], 0, self->pv->m_Renderer);
@@ -291,7 +306,7 @@ void Viewport::SetupPipeline(vtkRenderWindowInteractor* iren)
} }
} }
} }
self->SelectProp3D(target); self->SelectProp3D(target, multiSelect);
}); });
iren->AddObserver(vtkCommand::LeftButtonPressEvent, clickCallback); iren->AddObserver(vtkCommand::LeftButtonPressEvent, clickCallback);
@@ -446,6 +461,11 @@ void Viewport::RegisterProp3D(Prop3D* p, bool isPart) {
m_Prop3Ds.push_back(p); m_Prop3Ds.push_back(p);
p->ConnectRenderer(pv->m_Renderer); p->ConnectRenderer(pv->m_Renderer);
// Ensure m_MultiSelectionProp also has the same renderers
if (m_MultiSelectionProp) {
m_MultiSelectionProp->GetRenderers()->AddItem(pv->m_Renderer);
}
// If it's a part of an assembly, we don't want to draw it twice. // If it's a part of an assembly, we don't want to draw it twice.
// Assembly itself already draws its parts. // Assembly itself already draws its parts.
// But we need ConnectRenderer above to allow highliting and property updates. // But we need ConnectRenderer above to allow highliting and property updates.
@@ -497,28 +517,59 @@ void Viewport::ObserveContext(ObjectsContext* ctx) {
}); });
} }
void Viewport::SelectProp3D(Prop3D* prop) void Viewport::SelectProp3D(Prop3D* prop, bool multi)
{ {
for (auto* p : m_Prop3Ds) { if (multi) {
p->SetSelected(p == prop); if (prop) {
auto it = std::find(m_SelectedProps.begin(), m_SelectedProps.end(), prop);
if (it != m_SelectedProps.end()) {
prop->SetSelected(false);
m_SelectedProps.erase(it);
} else {
prop->SetSelected(true);
m_SelectedProps.push_back(prop);
}
}
} else {
for (auto* p : m_SelectedProps) {
p->SetSelected(false);
}
m_SelectedProps.clear();
if (prop) {
prop->SetSelected(true);
m_SelectedProps.push_back(prop);
}
} }
// Update HandlerWidget
if (pv->m_HandlerWidget) { if (pv->m_HandlerWidget) {
if (prop) { if (m_SelectedProps.empty()) {
vtkProp3D* prop3d = prop->GetProxyProp(); pv->m_HandlerWidget->SetEnabled(0);
pv->m_HandlerWidget->SetProp3D(nullptr);
if (m_MultiSelectionProp) m_MultiSelectionProp->SetMembers({});
} else if (m_SelectedProps.size() == 1) {
Prop3D* selected = m_SelectedProps[0];
vtkProp3D* prop3d = selected->GetProxyProp();
if (prop3d) { if (prop3d) {
pv->m_HandlerWidget->SetProp3D(prop3d); pv->m_HandlerWidget->SetProp3D(prop3d);
pv->m_HandlerWidget->SetEnabled(1); pv->m_HandlerWidget->SetEnabled(1);
pv->m_HandlerWidget->PlaceWidget(prop3d->GetBounds()); //TODO: FIX ! pv->m_HandlerWidget->PlaceWidget(prop3d->GetBounds());
} }
if (m_MultiSelectionProp) m_MultiSelectionProp->SetMembers({});
} else { } else {
pv->m_HandlerWidget->SetEnabled(0); // Multi-selection
pv->m_HandlerWidget->SetProp3D(nullptr); if (m_MultiSelectionProp) {
m_MultiSelectionProp->SetMembers(m_SelectedProps);
vtkProp3D* proxy = m_MultiSelectionProp->GetProxyProp();
pv->m_HandlerWidget->SetProp3D(proxy);
pv->m_HandlerWidget->SetEnabled(1);
pv->m_HandlerWidget->PlaceWidget(proxy->GetBounds());
}
} }
} }
Render(); Render();
OnSelectionChanged(prop); OnSelectionChanged(m_SelectedProps.empty() ? nullptr : m_SelectedProps.back());
} }
void Viewport::SetGridVisible(bool visible) void Viewport::SetGridVisible(bool visible)
@@ -657,5 +708,19 @@ void Viewport::UpdateGrid()
pv->m_Annotation->SetText(1, gridLabel); pv->m_Annotation->SetText(1, gridLabel);
} }
void Viewport::GroupSelection(uLib::ObjectsContext* targetCtx) {
if (!targetCtx || m_SelectedProps.size() <= 1 || !m_MultiSelectionProp) return;
// Clone the current multi-selection proxy
MultiSelectionProp* group = m_MultiSelectionProp->Clone();
// Add it to the context
targetCtx->AddObject(group);
// Select the new group and clear multi-selection
m_SelectedProps.clear();
SelectProp3D(group);
}
} // namespace Vtk } // namespace Vtk
} // namespace uLib } // namespace uLib

View File

@@ -29,6 +29,7 @@ namespace Vtk {
struct ViewportData; struct ViewportData;
class HandlerWidget; class HandlerWidget;
class MultiSelectionProp;
class ObjectsContext; class ObjectsContext;
/** /**
@@ -49,7 +50,11 @@ public:
// Prop3D / prop management // Prop3D / prop management
void AddProp3D(Prop3D &prop); void AddProp3D(Prop3D &prop);
void RemoveProp3D(Prop3D &prop); void RemoveProp3D(Prop3D &prop);
void SelectProp3D(Prop3D *prop); /** @brief Selects a specific Prop3D. If multi is true, it toggles selection in a group. */
void SelectProp3D(Prop3D* target, bool multi = false);
/** @brief Creates a persistent Selection Group from the current multi-selection. */
void GroupSelection(uLib::ObjectsContext* targetCtx);
void addProp(vtkProp *prop); void addProp(vtkProp *prop);
void RemoveProp(vtkProp *prop); void RemoveProp(vtkProp *prop);
@@ -91,6 +96,8 @@ protected:
struct ViewportData *pv; struct ViewportData *pv;
Axis m_GridAxis; Axis m_GridAxis;
std::vector<Prop3D*> m_Prop3Ds; std::vector<Prop3D*> m_Prop3Ds;
std::vector<Prop3D*> m_SelectedProps;
MultiSelectionProp* m_MultiSelectionProp;
std::map<uLib::Object*, Prop3D*> m_ObjectToProp3D; std::map<uLib::Object*, Prop3D*> m_ObjectToProp3D;
}; };