9 Commits

Author SHA1 Message Date
AndreaRigoni
e09a614fa5 refactor: add override specifier to type_name method in Core/Types.h 2026-04-03 10:14:20 +00:00
AndreaRigoni
7f6323403d merge andrea-geo: geometry/material/property system features
Merges all work from andrea-geo branch:
- Geant material management classes
- Serialization enhancements (read-only, NVP/HRP macros)
- Transform/assembly system improvements
- VTK puppet/viewport updates (orthographic toggle, voxel rendering)
- Property grouping, dynamic properties, NotifyPropertiesUpdated
- Object type identification via uLibTypeMacro
- New tests (PropertyGrouping, ReadOnly, vtkQViewport, PuppetParenting)
- Various gcompose UI fixes

Conflict resolved in CMakePresets.json: kept both 'fast' (clang/lld)
and 'mutom' (stub) presets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 08:47:32 +00:00
AndreaRigoni
a53b3051de fix EXPAT::EXPAT-NOTFOUND when building with Geant4 on conda
Geant4's G4EXPATShim creates EXPAT::EXPAT (uppercase) with
IMPORTED_LOCATION set to ${EXPAT_LIBRARY}, which is empty when EXPAT
is found via conda's config-mode package (expat::expat, lowercase).

After find_package(Geant4), patch EXPAT::EXPAT with the real library
path taken from expat::expat IMPORTED_LOCATION_NOCONFIG, falling back
to find_library if needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 08:41:00 +00:00
AndreaRigoni
c53570192f switch to Ninja+ccache, add clang/lld fast build profile
- CMakePresets.json: add 'fast' preset (clang+lld+ccache)
- .gitignore: generalize build/ to build*/, add CMakeUserPresets.json
- CMakeUserPresets.json: untrack (conan-generated, now gitignored)
- src/Core/Archives.h: remove redundant 'using basic_xml_iarchive::load_override'
  in xml_iarchive; caused ambiguous overload with clang (diamond inheritance)
- src/Core/Object.cpp: remove invalid explicit instantiations of non-template
  virtual Object::serialize (GCC extension, clang rejects)
- README.md, CLAUDE.md: document GCC and LLVM/clang build workflows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 08:24:50 +00:00
AndreaRigoni
6396bdfebf feat: add projection toggle button to switch between perspective and orthographic views 2026-04-02 14:42:38 +00:00
AndreaRigoni
96ab3b0930 fix: restore ULIB_ACTIVATE_DISPLAY_PROPERTIES to vtkVoxImage constructor 2026-04-02 14:32:39 +00:00
AndreaRigoni
5c04d00d4c refactor: remove redundant UpdateGrid call from QViewport::Render and add Claude configuration settings 2026-04-02 14:30:31 +00:00
AndreaRigoni
72e69cfca5 test: add unit test for vtkQViewport and register in CMakeLists.txt 2026-04-02 14:27:49 +00:00
AndreaRigoni
59a9e829fc refactor: enhance vtkVoxImage volume rendering with dynamic shader range scaling, improved transfer function management, and synchronized VTK property updates. 2026-04-02 14:08:32 +00:00
23 changed files with 453 additions and 110 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,7 @@
CMakeFiles/
build/
build*/
.cache/
CMakeUserPresets.json
build_warnings*.log
final_build.log
cmake_configure.log
@@ -16,3 +17,4 @@ src/Python/uLib/.nfs*
test_props.xml
test_props2.xml
test_boost.cpp
.claude/settings.json

View File

@@ -12,7 +12,7 @@ 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)
# Configure (from repo root, using Conan preset — uses Ninja + ccache)
cmake --preset conan-release
# Build everything
@@ -40,6 +40,18 @@ conan install . --output-folder=build --build=missing
cmake --preset conan-release
```
### Build acceleration (already configured)
- **Ninja** generator — used automatically via the conan default profile (`~/.conan2/profiles/default`)
- **ccache** — enabled via `CMAKE_CXX_COMPILER_LAUNCHER=ccache`; cached rebuilds are nearly instant (~0.3s vs ~25s cold)
- **Clang 22 + lld** profile available (`~/.conan2/profiles/fast`) but blocked by template overload ambiguities in `src/Core/Archives.h` that need fixing for full compatibility
To reconfigure with the fast profile once Archives.h is fixed:
```bash
conan install . --output-folder=build --build=missing --profile=fast
cmake -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
```
## Architecture
**uLib** is a C++ framework for Cosmic Muon Tomography (CMT), structured as layered shared libraries:

View File

@@ -169,6 +169,26 @@ if(Geant4_FOUND)
add_compile_definitions(HAVE_GEANT4)
set(HAVE_GEANT4 1)
# Workaround: Geant4's G4EXPATShim creates EXPAT::EXPAT (uppercase) with
# IMPORTED_LOCATION "${EXPAT_LIBRARY}", but EXPAT_LIBRARY is empty when using
# conda's config-mode expat package (which installs as expat::expat lowercase).
# Resolve the actual library path from expat::expat or via find_library.
if(TARGET EXPAT::EXPAT)
get_target_property(_expat_loc EXPAT::EXPAT IMPORTED_LOCATION)
if(NOT _expat_loc OR _expat_loc MATCHES "NOTFOUND|^$")
if(TARGET expat::expat)
get_target_property(_expat_loc expat::expat IMPORTED_LOCATION_NOCONFIG)
endif()
if(NOT _expat_loc OR _expat_loc MATCHES "NOTFOUND|^$")
find_library(_expat_loc NAMES expat)
endif()
if(_expat_loc)
set_target_properties(EXPAT::EXPAT PROPERTIES IMPORTED_LOCATION "${_expat_loc}")
endif()
endif()
unset(_expat_loc)
endif()
# Sanitize Geant4 targets to remove Qt5 dependencies that conflict with VTK/Qt6
if(TARGET Geant4::G4interfaces)
set_target_properties(Geant4::G4interfaces PROPERTIES

View File

@@ -12,6 +12,22 @@
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}"
}
},
{
"name": "fast",
"displayName": "Fast build: Ninja + clang + ccache",
"description": "Uses Ninja generator, clang/lld compiler, and ccache",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_C_COMPILER": "clang",
"CMAKE_CXX_COMPILER": "clang++",
"CMAKE_EXE_LINKER_FLAGS": "-fuse-ld=lld",
"CMAKE_SHARED_LINKER_FLAGS": "-fuse-ld=lld",
"CMAKE_CXX_COMPILER_LAUNCHER": "ccache",
"CMAKE_C_COMPILER_LAUNCHER": "ccache"
}
},
{
"name": "mutom",
"description": "",
@@ -19,4 +35,4 @@
"inherits": []
}
]
}
}

View File

@@ -1,9 +0,0 @@
{
"version": 4,
"vendor": {
"conan": {}
},
"include": [
"build/CMakePresets.json"
]
}

View File

@@ -41,7 +41,11 @@ conda activate mutom
### Configure and Build
1. **Configure Conan profile (if you haven't yet on your machine):**
#### Standard build (GCC + Ninja + ccache)
The default conan profile uses **Ninja** as the generator and **ccache** for compiler caching, dramatically speeding up incremental rebuilds.
1. **Configure Conan profile (first time only):**
```bash
conan profile detect
```
@@ -51,17 +55,39 @@ conan profile detect
conan install . --output-folder=build --build=missing
```
3. **Configure the project with CMake:**
3. **Configure with CMake:**
```bash
cmake --preset conan-release
```
*(Alternatively: `cd build && cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release`)*
4. **Build the project:**
4. **Build:**
```bash
cmake --build build -j10
cmake --build build -j$(nproc)
```
#### LLVM/Clang build (clang + lld + ccache — fastest)
A `fast` conan profile is provided that uses **clang**, **lld** (LLVM linker), and **ccache**. Install them into your environment first:
```bash
micromamba install -n mutom -y clang clangxx lld -c conda-forge
```
Then build using the `fast` profile:
```bash
conan install . --output-folder=build --build=missing --profile=fast
cmake -B build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
```
The `fast` profile is defined at `~/.conan2/profiles/fast` and sets:
- `CMAKE_C_COMPILER=clang` / `CMAKE_CXX_COMPILER=clang++`
- `CMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld`
- `CMAKE_CXX_COMPILER_LAUNCHER=ccache`
### Make python package
```bash

View File

@@ -448,17 +448,17 @@ void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) {
}
} 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 {
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 = new PropertyWidgetBase(prop, m_Container);
widget->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")"));
}
}
}
if (widget) {

View File

@@ -338,8 +338,6 @@ public:
}
}
using basic_xml_iarchive::load_override;
// Anything not an attribute should be a name value pair as nvp or hrp
typedef boost::archive::detail::common_iarchive<Archive>
detail_common_iarchive;
@@ -357,6 +355,9 @@ public:
// class_name_type can't be handled here as it depends upon the
// char type used by the stream. So require the derived implementation.
// derived in this case is xml_iarchive_impl or base ..
// Note: using base::load_override covers all basic_xml_iarchive overloads
// transitively, so a separate 'using basic_xml_iarchive::load_override'
// is redundant and creates ambiguity with clang.
using base::load_override;
void load_override(const char *str) {

View File

@@ -121,14 +121,6 @@ void Object::PropertyUpdated() { ULIB_SIGNAL_EMIT(Object::PropertyUpdated); }
template <class ArchiveT>
void Object::save_override(ArchiveT &ar, const unsigned int version) {}
// Explicitly instantiate for all uLib archives
template void Object::serialize(Archive::xml_oarchive &, const unsigned int);
template void Object::serialize(Archive::xml_iarchive &, const unsigned int);
template void Object::serialize(Archive::text_oarchive &, const unsigned int);
template void Object::serialize(Archive::text_iarchive &, const unsigned int);
template void Object::serialize(Archive::hrt_oarchive &, const unsigned int);
template void Object::serialize(Archive::hrt_iarchive &, const unsigned int);
template void Object::serialize(Archive::log_archive &, const unsigned int);
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

View File

@@ -182,7 +182,7 @@ typedef bool Bool_t; // Boolean (0=false, 1=true) (bool)
\
public: \
typedef type_info::BaseClass BaseClass; \
virtual const char *type_name() const { return type_info::name; } \
virtual const char *type_name() const override { return type_info::name; } \
/**/
/**

View File

@@ -111,17 +111,17 @@ void vtkContainerBox::SyncFromVtk() {
// VTK -> Model: Extract new world TRS from proxy, which matches the model's TRS center
vtkMatrix4x4* rootMat = root->GetUserMatrix();
if (rootMat) {
std::cout << "[vtkContainerBox::SyncFromVtk] Read Proxy UserMatrix:" << std::endl;
rootMat->Print(std::cout);
}
// if (rootMat) {
// std::cout << "[vtkContainerBox::SyncFromVtk] Read Proxy UserMatrix:" << std::endl;
// rootMat->Print(std::cout);
// }
Matrix4f vtkWorld = VtkToMatrix4f(rootMat);
// Synchronize TRS property members from the updated local matrix
m_Content->FromMatrix(vtkWorld);
std::cout << "[vtkContainerBox::SyncFromVtk] New Model WorldMatrix:" << std::endl << m_Content->GetWorldMatrix() << std::endl;
// std::cout << "[vtkContainerBox::SyncFromVtk] New Model WorldMatrix:" << std::endl << m_Content->GetWorldMatrix() << std::endl;
// Since we modified the model, notify observers, but block the loop back to VTK
// ConnectionBlock blocker(d->m_UpdateSignal);

View File

@@ -41,6 +41,7 @@
#include <vtkPiecewiseFunction.h>
#include <vtkSmartVolumeMapper.h>
#include <vtkVolumeProperty.h>
#include <vtkMatrix4x4.h>
#include <vtkActor.h>
#include <vtkPolyDataMapper.h>
@@ -49,6 +50,7 @@
#include <Math/VoxImage.h>
#include "Vtk/Math/vtkVoxImage.h"
#include "Vtk/Math/vtkDense.h"
#include <vtkAutoInit.h>
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);
@@ -128,9 +130,15 @@ vtkVoxImage::vtkVoxImage(Content &content)
m_Image(vtkImageData::New()), m_Outline(vtkCubeSource::New()),
m_OutlineActor(vtkActor::New()),
m_Reader(NULL), m_Writer(NULL), writer_factor(1.E6),
m_Window(40/1.E6), m_Level(20/1.E6), m_ShadingPreset(0) {
m_Window(1.0), m_Level(0.5), m_ShadingPreset(0) {
// Transfer functions
m_ColorFun = vtkColorTransferFunction::New();
m_OpacityFun = vtkPiecewiseFunction::New();
m_UpdateConnection = Object::connect(&m_Content, &uLib::Object::Updated, this, &vtkVoxImage::Update);
GetContent();
InstallPipe();
RescaleShaderRange();
ULIB_ACTIVATE_DISPLAY_PROPERTIES;
}
@@ -140,6 +148,8 @@ vtkVoxImage::~vtkVoxImage() {
m_Asm->Delete();
m_Outline->Delete();
m_OutlineActor->Delete();
m_ColorFun->Delete();
m_OpacityFun->Delete();
}
vtkImageData *vtkVoxImage::GetImageData() {
@@ -181,6 +191,7 @@ void vtkVoxImage::ReadFromVKTFile(const char *fname) {
m_Image->DeepCopy(vtkscale->GetOutput());
SetContent();
RescaleShaderRange();
} else {
std::cerr << "Error: file does not contain structured points\n";
}
@@ -200,115 +211,134 @@ void vtkVoxImage::ReadFromXMLFile(const char *fname) {
m_Image->DeepCopy(vtkscale->GetOutput());
SetContent();
RescaleShaderRange();
}
void vtkVoxImage::setShadingPreset(int blendType) {
m_ShadingPreset = blendType;
vtkSmartVolumeMapper *mapper = (vtkSmartVolumeMapper *)m_Actor->GetMapper();
if (!mapper) return;
vtkVolumeProperty *property = m_Actor->GetProperty();
static vtkColorTransferFunction *colorFun = vtkColorTransferFunction::New();
static vtkPiecewiseFunction *opacityFun = vtkPiecewiseFunction::New();
float window = m_Window;
float level = m_Level;
property->SetColor(colorFun);
property->SetScalarOpacity(opacityFun);
property->SetColor(m_ColorFun);
property->SetScalarOpacity(m_OpacityFun);
property->SetInterpolationTypeToLinear();
if (blendType != 6) {
colorFun->RemoveAllPoints();
opacityFun->RemoveAllPoints();
}
m_ColorFun->RemoveAllPoints();
m_OpacityFun->RemoveAllPoints();
switch (blendType) {
case 0:
colorFun->AddRGBSegment(0.0, 1.0, 1.0, 1.0, 255.0, 1.0, 1.0, 1.0);
opacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window,
1.0);
case 0: // MIP
m_ColorFun->AddRGBPoint(level - 0.5 * window, 0, 0, 0);
m_ColorFun->AddRGBPoint(level + 0.5 * window, 1, 1, 1);
m_OpacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window, 1.0);
mapper->SetBlendModeToMaximumIntensity();
break;
case 1:
colorFun->AddRGBSegment(level - 0.5 * window, 0.0, 0.0, 0.0,
level + 0.5 * window, 1.0, 1.0, 1.0);
opacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window,
1.0);
case 1: // Composite
m_ColorFun->AddRGBPoint(level - 0.5 * window, 0, 0, 0);
m_ColorFun->AddRGBPoint(level + 0.5 * window, 1, 1, 1);
m_OpacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window, 1.0);
mapper->SetBlendModeToComposite();
property->ShadeOff();
break;
case 2:
colorFun->AddRGBSegment(0.0, 1.0, 1.0, 1.0, 255.0, 1.0, 1.0, 1.0);
opacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window,
1.0);
case 2: // Composite Shaded
m_ColorFun->AddRGBPoint(level - 0.5 * window, 0, 0, 0);
m_ColorFun->AddRGBPoint(level + 0.5 * window, 1, 1, 1);
m_OpacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window, 1.0);
mapper->SetBlendModeToComposite();
property->ShadeOn();
break;
case 3:
colorFun->AddRGBPoint(-3024, 0, 0, 0, 0.5, 0.0);
colorFun->AddRGBPoint(-1000, .62, .36, .18, 0.5, 0.0);
colorFun->AddRGBPoint(-500, .88, .60, .29, 0.33, 0.45);
colorFun->AddRGBPoint(3071, .83, .66, 1, 0.5, 0.0);
opacityFun->AddPoint(-3024, 0, 0.5, 0.0);
opacityFun->AddPoint(-1000, 0, 0.5, 0.0);
opacityFun->AddPoint(-500, 1.0, 0.33, 0.45);
opacityFun->AddPoint(3071, 1.0, 0.5, 0.0);
mapper->SetBlendModeToComposite();
property->ShadeOn();
property->SetAmbient(0.1);
property->SetDiffuse(0.9);
property->SetSpecular(0.2);
property->SetSpecularPower(10.0);
property->SetScalarOpacityUnitDistance(0.8919);
break;
case 4:
colorFun->AddRGBPoint(0.0, 0, 0, 1); // Blue
colorFun->AddRGBPoint(level, 0, 1, 0); // Green
colorFun->AddRGBPoint(level + 0.5*window, 1, 1, 0); // Yellow
colorFun->AddRGBPoint(level + window, 1, 0, 0); // Red
opacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window, 1.0);
case 3: // Rainbow MIP
m_ColorFun->AddRGBPoint(level - 0.5 * window, 0, 0, 1);
m_ColorFun->AddRGBPoint(level, 0, 1, 0);
m_ColorFun->AddRGBPoint(level + 0.5 * window, 1, 1, 0);
m_ColorFun->AddRGBPoint(level + window, 1, 0, 0);
m_OpacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window, 1.0);
mapper->SetBlendModeToMaximumIntensity();
break;
case 5:
colorFun->AddRGBSegment(0.0, 1.0, 1.0, 1.0, 255.0, 1.0, 1.0, 1.0);
opacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window, 1.0);
case 4: // Additive
m_ColorFun->AddRGBPoint(level - 0.5 * window, 0, 0, 0);
m_ColorFun->AddRGBPoint(level + 0.5 * window, 1, 1, 1);
m_OpacityFun->AddSegment(level - 0.5 * window, 0.0, level + 0.5 * window, 1.0);
mapper->SetBlendModeToAdditive();
break;
default:
vtkGenericWarningMacro("Unknown blend type.");
break;
}
}
void vtkVoxImage::RescaleShaderRange() {
double range[2];
m_Image->GetScalarRange(range);
m_Level = (range[0] + range[1]) / 2.0;
m_Window = range[1] - range[0];
if (m_Window <= 1e-9)
m_Window = 1.0;
setShadingPreset(m_ShadingPreset);
}
void vtkVoxImage::SetRepresentation(Representation mode) {
Puppet::SetRepresentation(mode); // Ensure base class data state is updated
if (mode == Wireframe) {
m_Actor->SetVisibility(0);
m_OutlineActor->SetVisibility(1);
} else if (mode == Surface) {
m_OutlineActor->GetProperty()->SetRepresentationToWireframe();
} else if (mode == Volume) {
m_Actor->SetVisibility(1);
m_OutlineActor->SetVisibility(1); // Keep outline visible as boundary
m_OutlineActor->SetVisibility(1);
m_OutlineActor->GetProperty()->SetRepresentationToWireframe();
} else {
Puppet::SetRepresentation(mode);
// Other representations (Points, Surface, etc) are handled by basic Puppet
// behavior which affects the m_Asm parts.
}
}
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);
// Call base class to show Transform and Appearance properties
// Puppet::serialize_display(ar, version);
// Use the member variables if they are available
// Use the member variables for volume rendering parameters
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"});
ar & boost::serialization::make_hrp_enum("Shading", m_ShadingPreset,
{"MIP", "Composite", "Composite Shaded", "MIP Bone", "MIP Hot", "Additive"});
}
void vtkVoxImage::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
m_Content.FromMatrix(vtkLocal); // * m_Content.GetLocalMatrix().inverse());
m_Content.Updated();
}
}
}
void vtkVoxImage::Update() {
if (auto *root = vtkProp3D::SafeDownCast(this->GetProp())) {
vtkNew<vtkMatrix4x4> m;
Matrix4fToVtk(m_Content.GetMatrix(), m); // * m_Content.GetLocalMatrix(), m);
root->SetUserMatrix(m);
root->Modified();
// std::cout << "[vtkVoxImage::Update] Set Proxy UserMatrix:" << std::endl;
// std::cout << m_Content.GetMatrix() << std::endl;
}
setShadingPreset(m_ShadingPreset);
m_Actor->Update();
m_Outline->SetBounds(m_Image->GetBounds());
m_Outline->Update();
ConnectionBlock blocker(m_UpdateConnection);
this->Puppet::Update();
}
void vtkVoxImage::InstallPipe() {
vtkSmartPointer<vtkSmartVolumeMapper> mapper =
vtkSmartPointer<vtkSmartVolumeMapper>::New();
@@ -320,7 +350,7 @@ void vtkVoxImage::InstallPipe() {
mapper->Update();
m_Actor->SetMapper(mapper);
this->setShadingPreset(0);
this->setShadingPreset(m_ShadingPreset);
mapper->Update();
m_Outline->SetBounds(m_Image->GetBounds());
@@ -331,13 +361,13 @@ void vtkVoxImage::InstallPipe() {
m_OutlineActor->SetMapper(mmapper);
m_OutlineActor->GetProperty()->SetRepresentationToWireframe();
m_OutlineActor->GetProperty()->SetAmbient(0.7);
m_Asm->AddPart(m_Actor);
m_Asm->AddPart(m_OutlineActor);
this->SetProp(m_Asm);
// Default look
this->SetRepresentation(Surface);
this->SetRepresentation(Volume);
}
} // namespace Vtk

View File

@@ -39,6 +39,8 @@
class vtkImageData;
class vtkActor;
class vtkColorTransferFunction;
class vtkPiecewiseFunction;
namespace uLib {
namespace Vtk {
@@ -55,6 +57,8 @@ public:
void SetContent();
vtkProp3D *GetProp() override { return m_Asm; }
vtkImageData *GetImageData();
void SaveToXMLFile(const char *fname);
@@ -65,8 +69,10 @@ public:
void setShadingPreset(int blendType = 2);
void SetRepresentation(Representation mode);
void RescaleShaderRange();
void Update() override;
void SyncFromVtk() override;
void serialize_display(uLib::Archive::display_properties_archive & ar, const unsigned int version = 0) override;
protected:
@@ -88,6 +94,11 @@ private:
float m_Window;
float m_Level;
int m_ShadingPreset;
Connection m_UpdateConnection;
class vtkColorTransferFunction *m_ColorFun;
class vtkPiecewiseFunction *m_OpacityFun;
};
} // namespace Vtk

View File

@@ -4,6 +4,7 @@ set(TESTS
vtkHandlerWidget
PuppetPropertyTest
PuppetParentingTest
vtkQViewportTest
# vtkVoxImageTest
# vtkTriangleMeshTest
)

View File

@@ -0,0 +1,87 @@
/*//////////////////////////////////////////////////////////////////////////////
// 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.
//////////////////////////////////////////////////////////////////////////////*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <QApplication>
#include <QtGlobal>
#include <Vtk/vtkQViewport.h>
#include <vtkSmartPointer.h>
#include <vtkCubeSource.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkProperty.h>
#include <vtkRenderer.h>
#include "testing-prototype.h"
using namespace uLib;
int main(int argc, char** argv)
{
// Force X11 on Linux to avoid Wayland connection issues in headless/X11 environments
#if defined(Q_OS_LINUX)
qputenv("QT_QPA_PLATFORM", "xcb");
#endif
BEGIN_TESTING(vtk QViewport Test);
QApplication app(argc, argv);
app.processEvents();
Vtk::QViewport viewport;
viewport.resize(800, 600);
viewport.show();
vtkSmartPointer<vtkCubeSource> cube = vtkSmartPointer<vtkCubeSource>::New();
cube->SetXLength(10);
cube->SetYLength(10);
cube->SetZLength(10);
cube->Update();
vtkSmartPointer<vtkPolyDataMapper> mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
mapper->SetInputConnection(cube->GetOutputPort());
vtkSmartPointer<vtkActor> actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
actor->GetProperty()->SetColor(1, 0, 0);
viewport.addProp(actor);
viewport.Render();
ASSERT_NOT_NULL(viewport.GetRenderWindow());
ASSERT_NOT_NULL(viewport.GetRenderer());
if (std::getenv("CTEST_PROJECT_NAME") == nullptr) {
// Run application for a while to see the result
return app.exec();
}
END_TESTING;
}

View File

@@ -134,11 +134,11 @@ public:
vtkActor *actor = vtkActor::SafeDownCast(p);
if (actor) {
if (m_Representation != -1) {
if (m_Representation != -1 && m_Representation != Puppet::Volume) {
if (m_Representation == Puppet::SurfaceWithEdges) {
actor->GetProperty()->SetRepresentation(VTK_SURFACE);
actor->GetProperty()->SetEdgeVisibility(1);
} else {
} else if (m_Representation != Puppet::Outline && m_Representation != Puppet::Slice) {
actor->GetProperty()->SetRepresentation(m_Representation);
actor->GetProperty()->SetEdgeVisibility(0);
}
@@ -628,6 +628,8 @@ struct AppearanceProxy {
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);
ar & boost::serialization::make_hrp("ShowBoundingBox", pd->m_ShowBoundingBox);
ar & boost::serialization::make_hrp("ShowScaleMeasures", pd->m_ShowScaleMeasures);
}
};

View File

@@ -72,6 +72,7 @@ struct ViewerData {
vtkRenderWindow *m_RenderWindow;
vtkSmartPointer<vtkRenderWindowInteractor> m_Interactor;
vtkSmartPointer<vtkButtonWidget> m_GridButton;
vtkSmartPointer<vtkButtonWidget> m_ProjButton;
ViewerData() : m_RenderWindow(vtkRenderWindow::New()) {}
~ViewerData() {
@@ -97,6 +98,11 @@ Viewer::~Viewer() {
dv->m_GridButton->SetInteractor(nullptr);
dv->m_GridButton = nullptr;
}
if (dv->m_ProjButton) {
dv->m_ProjButton->Off();
dv->m_ProjButton->SetInteractor(nullptr);
dv->m_ProjButton = nullptr;
}
if (this->GetRenderWindow()) {
this->GetRenderWindow()->RemoveAllObservers();
}
@@ -123,6 +129,7 @@ void Viewer::InstallPipe() {
// Setup native grid button
if (!std::getenv("CTEST_PROJECT_NAME")) {
SetupGridButton();
SetupProjButton();
}
// BUT we want to override the style with our custom NoSpin version
@@ -238,9 +245,91 @@ void Viewer::UpdateGridButtonPosition() {
rep->PlaceWidget(bds);
}
void Viewer::Start() {
void Viewer::SetupProjButton() {
if (!dv->m_RenderWindow || !dv->m_RenderWindow->GetInteractor()) return;
vtkNew<vtkImageCanvasSource2D> canvas;
canvas->SetScalarTypeToUnsignedChar();
canvas->SetNumberOfScalarComponents(4);
canvas->SetExtent(0, 63, 0, 63, 0, 0);
// State 0: Perspective (gray trapezoid-like lines)
canvas->SetDrawColor(0, 0, 0, 0);
canvas->FillBox(0, 63, 0, 63);
canvas->SetDrawColor(120, 120, 120, 255);
canvas->DrawSegment(16, 16, 48, 16);
canvas->DrawSegment(48, 16, 56, 48);
canvas->DrawSegment(56, 48, 8, 48);
canvas->DrawSegment(8, 48, 16, 16);
canvas->Update();
vtkNew<vtkImageData> imgPersp;
imgPersp->DeepCopy(canvas->GetOutput());
// State 1: Orthographic (white rectangle)
canvas->SetDrawColor(0, 0, 0, 0);
canvas->FillBox(0, 63, 0, 63);
canvas->SetDrawColor(255, 255, 255, 255);
canvas->DrawSegment(12, 16, 52, 16);
canvas->DrawSegment(52, 16, 52, 48);
canvas->DrawSegment(52, 48, 12, 48);
canvas->DrawSegment(12, 48, 12, 16);
canvas->Update();
vtkNew<vtkImageData> imgOrtho;
imgOrtho->DeepCopy(canvas->GetOutput());
vtkNew<vtkTexturedButtonRepresentation2D> rep;
rep->SetNumberOfStates(2);
rep->SetButtonTexture(0, imgPersp);
rep->SetButtonTexture(1, imgOrtho);
dv->m_ProjButton = vtkSmartPointer<vtkButtonWidget>::New();
dv->m_ProjButton->SetInteractor(dv->m_RenderWindow->GetInteractor());
dv->m_ProjButton->SetRepresentation(rep);
UpdateProjButtonPosition();
vtkNew<vtkCallbackCommand> resizeCallback;
resizeCallback->SetClientData(this);
resizeCallback->SetCallback([](vtkObject*, unsigned long, void* clientdata, void*){
static_cast<Viewer*>(clientdata)->UpdateProjButtonPosition();
});
dv->m_RenderWindow->AddObserver(vtkCommand::ModifiedEvent, resizeCallback);
vtkNew<vtkCallbackCommand> stateCallback;
stateCallback->SetClientData(this);
stateCallback->SetCallback([](vtkObject* caller, unsigned long, void* clientdata, void*){
auto* btn = vtkButtonWidget::SafeDownCast(caller);
auto* v = static_cast<Viewer*>(clientdata);
auto* r = vtkTexturedButtonRepresentation2D::SafeDownCast(btn->GetRepresentation());
v->SetParallelProjection(r->GetState() == 1);
});
dv->m_ProjButton->AddObserver(vtkCommand::StateChangedEvent, stateCallback);
dv->m_ProjButton->On();
rep->SetState(GetParallelProjection() ? 1 : 0);
}
void Viewer::UpdateProjButtonPosition() {
if (!dv->m_ProjButton || !dv->m_RenderWindow) return;
auto* rep = vtkTexturedButtonRepresentation2D::SafeDownCast(dv->m_ProjButton->GetRepresentation());
if (!rep) return;
int *sz = dv->m_RenderWindow->GetSize();
if (sz[0] == 0 || sz[1] == 0) return;
int margin_right = 23;
int margin_top = 220; // below the grid button (170 + 50)
int btnSz = 100;
double bds[6] = { (double)sz[0] - btnSz - margin_right, (double)sz[0] - margin_right,
(double)sz[1] - margin_top - btnSz/2.0, (double)sz[1] - margin_top + btnSz/2.0, 0, 0 };
rep->PlaceWidget(bds);
}
void Viewer::Start() {
if (std::getenv("CTEST_PROJECT_NAME")) return;
dv->m_RenderWindow->GetInteractor()->Start();
dv->m_RenderWindow->GetInteractor()->Start();
}
vtkRenderWindow *Viewer::GetRenderWindow() { return dv->m_RenderWindow; }

View File

@@ -38,6 +38,9 @@ private:
void SetupGridButton();
void UpdateGridButtonPosition();
void SetupProjButton();
void UpdateProjButtonPosition();
struct ViewerData *dv;
};

View File

@@ -118,18 +118,17 @@ void vtkObjectsContext::SyncFromVtk() {
Puppet* vtkObjectsContext::CreatePuppet(uLib::Object* obj) {
if (!obj) return nullptr;
if (auto* box = dynamic_cast<uLib::ContainerBox*>(obj)) {
if (auto* vox = dynamic_cast<uLib::Abstract::VoxImage*>(obj)) {
return new vtkVoxImage(*vox);
} else if (auto* box = dynamic_cast<uLib::ContainerBox*>(obj)) {
return new vtkContainerBox(box);
} else if (auto* chamber = dynamic_cast<uLib::DetectorChamber*>(obj)) {
return new vtkDetectorChamber(chamber);
} else if (auto* cylinder = dynamic_cast<uLib::Cylinder*>(obj)) {
return new vtkCylinder(cylinder);
} else if (auto* vox = dynamic_cast<uLib::Abstract::VoxImage*>(obj)) {
return new vtkVoxImage(*vox);
} else if (auto* assembly = dynamic_cast<uLib::Assembly*>(obj)) {
return new Assembly(assembly);
}
else if (auto* box = dynamic_cast<uLib::Geant::BoxSolid*>(obj)) {
} else if (auto* box = dynamic_cast<uLib::Geant::BoxSolid*>(obj)) {
return new vtkBoxSolid(box);
}

View File

@@ -19,6 +19,7 @@ QViewport::QViewport(QWidget* parent)
, Viewport()
, m_VtkWidget(nullptr)
, m_GridButton(nullptr)
, m_ProjButton(nullptr)
{
// Build the layout zero margins so VTK fills the entire widget
auto* layout = new QVBoxLayout(this);
@@ -58,6 +59,36 @@ QViewport::QViewport(QWidget* parent)
m_GridButton->setChecked(true); // Grid is on by default
connect(m_GridButton, &QPushButton::clicked, this, &QViewport::onGridButtonClicked);
// Projection Toggle Button (below grid button)
m_ProjButton = new QPushButton(m_VtkWidget);
m_ProjButton->setText("P");
m_ProjButton->setFixedSize(40, 40);
m_ProjButton->setToolTip("Toggle Perspective / Orthographic");
m_ProjButton->setStyleSheet(
"QPushButton {"
" border-radius: 20px;"
" background-color: rgba(40, 40, 40, 180);"
" color: white;"
" font-size: 22px;"
" border: 1.5px solid rgba(255, 255, 255, 60);"
"}"
"QPushButton:hover {"
" background-color: rgba(70, 70, 70, 200);"
" border: 1.5px solid rgba(255, 255, 255, 100);"
"}"
"QPushButton:checked {"
" background-color: rgba(0, 120, 215, 200);"
" color: white;"
" border: 1.5px solid rgba(255, 255, 255, 120);"
"}"
"QPushButton:pressed {"
" background-color: rgba(0, 90, 160, 220);"
"}"
);
m_ProjButton->setCheckable(true);
m_ProjButton->setChecked(false); // Perspective by default
connect(m_ProjButton, &QPushButton::clicked, this, &QViewport::onProjButtonClicked);
// After the Qt widget exists but before the first paint,
// attach the renderer and configure the pipeline.
SetupPipeline();
@@ -81,7 +112,6 @@ void QViewport::SetupPipeline()
void QViewport::Render()
{
UpdateGrid();
if (m_VtkWidget && m_VtkWidget->renderWindow())
m_VtkWidget->renderWindow()->Render();
}
@@ -101,6 +131,11 @@ void QViewport::onGridButtonClicked()
SetGridVisible(m_GridButton->isChecked());
}
void QViewport::onProjButtonClicked()
{
SetParallelProjection(m_ProjButton->isChecked());
}
void QViewport::OnSelectionChanged(Puppet* p)
{
emit puppetSelected(p);
@@ -113,9 +148,14 @@ void QViewport::resizeEvent(QResizeEvent* event)
// Position under the gizmo (top-right corner).
// Standard CameraOrientationWidget is usually 150-180px.
int x = width() - m_GridButton->width() - 10;
int y = 160;
int y = 160;
m_GridButton->move(x, y);
}
if (m_ProjButton) {
int x = width() - m_ProjButton->width() - 10;
int y = 210;
m_ProjButton->move(x, y);
}
}
} // namespace Vtk

View File

@@ -51,12 +51,14 @@ protected:
private slots:
void onGridButtonClicked();
void onProjButtonClicked();
private:
void SetupPipeline();
QVTKOpenGLNativeWidget* m_VtkWidget;
QPushButton* m_GridButton;
QPushButton* m_ProjButton;
};
} // namespace Vtk

View File

@@ -537,6 +537,21 @@ bool Viewport::GetGridVisible() const
return false;
}
void Viewport::SetParallelProjection(bool parallel)
{
if (pv->m_Renderer && pv->m_Renderer->GetActiveCamera()) {
pv->m_Renderer->GetActiveCamera()->SetParallelProjection(parallel ? 1 : 0);
Render();
}
}
bool Viewport::GetParallelProjection() const
{
if (pv->m_Renderer && pv->m_Renderer->GetActiveCamera())
return pv->m_Renderer->GetActiveCamera()->GetParallelProjection() != 0;
return false;
}
void Viewport::SetGridAxis(Axis axis)
{
m_GridAxis = axis;

View File

@@ -73,6 +73,10 @@ public:
void SetGridAxis(Axis axis);
Axis GetGridAxis() const { return m_GridAxis; }
// Projection control
void SetParallelProjection(bool parallel);
bool GetParallelProjection() const;
protected:
void SetupPipeline(vtkRenderWindowInteractor* iren);