From cec362d8e98586a00fccbefde7edbd4a55916604 Mon Sep 17 00:00:00 2001 From: Alan Morris Date: Fri, 3 Oct 2025 16:42:16 -0600 Subject: [PATCH] Add feature to export clipped meshes (constraints clipped) for #2436 --- Libs/Analyze/Shape.cpp | 10 +++++++ Libs/Analyze/Shape.h | 2 ++ Studio/Data/ExportUtils.cpp | 7 +++++ Studio/Data/ExportUtils.h | 3 ++ Studio/Interface/ShapeWorksStudioApp.cpp | 38 ++++++++++++++++-------- Studio/Interface/ShapeWorksStudioApp.h | 8 ++--- Studio/Interface/ShapeWorksStudioApp.ui | 30 +++++++++++-------- Studio/Visualization/Lightbox.h | 4 +-- Studio/Visualization/Visualizer.cpp | 15 +++++++--- Studio/Visualization/Visualizer.h | 4 +-- 10 files changed, 84 insertions(+), 37 deletions(-) diff --git a/Libs/Analyze/Shape.cpp b/Libs/Analyze/Shape.cpp index c029b419ce..379ebd9e43 100644 --- a/Libs/Analyze/Shape.cpp +++ b/Libs/Analyze/Shape.cpp @@ -915,6 +915,16 @@ Constraints& Shape::get_constraints(int domain_id) { return constraints_[domain_id]; } +//--------------------------------------------------------------------------- +bool Shape::has_constraints() { + for (int i = 0; i < constraints_.size(); i++) { + if (constraints_[i].hasConstraints()) { + return true; + } + } + return false; +} + //--------------------------------------------------------------------------- bool Shape::has_planes() { for (int i = 0; i < constraints_.size(); i++) { diff --git a/Libs/Analyze/Shape.h b/Libs/Analyze/Shape.h index 73fbbf4916..cf31afdb49 100644 --- a/Libs/Analyze/Shape.h +++ b/Libs/Analyze/Shape.h @@ -185,6 +185,8 @@ class Shape { Constraints& get_constraints(int domain_id); + bool has_constraints(); + bool has_planes(); std::vector> get_groomed_mesh_wrappers(); diff --git a/Studio/Data/ExportUtils.cpp b/Studio/Data/ExportUtils.cpp index 2182e19270..f080fa33b4 100644 --- a/Studio/Data/ExportUtils.cpp +++ b/Studio/Data/ExportUtils.cpp @@ -7,12 +7,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace shapeworks { @@ -225,4 +227,9 @@ bool ExportUtils::write_pca_scores(ShapeWorksStudioApp* app, ParticleShapeStatis return true; } +//---------------------------------------------------------------------------- +QString ExportUtils::get_mesh_file_filter() { + return QObject::tr("VTK files (*.vtk);;PLY files (*.ply);;VTP files (*.vtp);;OBJ files (*.obj);;STL files (*.stl)"); +} + } // namespace shapeworks diff --git a/Studio/Data/ExportUtils.h b/Studio/Data/ExportUtils.h index 98a28d70af..a213634dcf 100644 --- a/Studio/Data/ExportUtils.h +++ b/Studio/Data/ExportUtils.h @@ -23,6 +23,9 @@ class ExportUtils { static bool write_particle_scalars(ShapeWorksStudioApp* app, std::shared_ptr shape, QString filename); static bool write_pca_scores(ShapeWorksStudioApp* app, ParticleShapeStatistics* stats, QString filename); + + static QString get_mesh_file_filter(); + }; } // namespace shapeworks diff --git a/Studio/Interface/ShapeWorksStudioApp.cpp b/Studio/Interface/ShapeWorksStudioApp.cpp index 9c5d4426f6..bedfa60cc3 100644 --- a/Studio/Interface/ShapeWorksStudioApp.cpp +++ b/Studio/Interface/ShapeWorksStudioApp.cpp @@ -241,8 +241,17 @@ ShapeWorksStudioApp::ShapeWorksStudioApp() { connect(ui_->action_export_all_subjects_particle_scalars, &QAction::triggered, this, &ShapeWorksStudioApp::action_export_all_subjects_particle_scalars_triggered); - connect(ui_->action_export_current_mesh, &QAction::triggered, this, - &ShapeWorksStudioApp::action_export_current_mesh_triggered); + connect(ui_->action_export_current_mesh, &QAction::triggered, this, [&]() { + // get the first index of the visualizer (top left lightbox item) + int index = visualizer_->get_lightbox()->get_start_shape(); + ShapeWorksStudioApp::action_export_current_mesh_triggered(index); + }); + + connect(ui_->action_export_current_mesh_clipped, &QAction::triggered, this, [&]() { + // get the first index of the visualizer (top left lightbox item) + int index = visualizer_->get_lightbox()->get_start_shape(); + ShapeWorksStudioApp::action_export_current_mesh_triggered(index, true); + }); connect(ui_->save_as_swproj, &QAction::triggered, this, &ShapeWorksStudioApp::save_as_swproj_clicked); connect(ui_->save_as_xlsx, &QAction::triggered, this, &ShapeWorksStudioApp::save_as_xlsx_clicked); @@ -951,10 +960,15 @@ void ShapeWorksStudioApp::handle_lightbox_right_click(int index) { QMenu* menu = new QMenu(nullptr); menu->setAttribute(Qt::WA_DeleteOnClose); QAction* export_mesh_action = menu->addAction("Export Mesh"); + QAction* export_clipped_mesh_action = nullptr; QAction* mark_excluded_action = nullptr; QAction* unmark_excluded_action = nullptr; QAction* mark_fixed_action = nullptr; QAction* unmark_fixed_action = nullptr; + + if (shape->has_constraints()) { + export_clipped_mesh_action = menu->addAction("Export Clipped Mesh"); + } if (shape->is_subject() && !shape->is_excluded()) { mark_excluded_action = menu->addAction("Mark as excluded"); } @@ -971,7 +985,9 @@ void ShapeWorksStudioApp::handle_lightbox_right_click(int index) { menu->popup(QCursor::pos()); connect(menu, &QMenu::triggered, menu, [=](QAction* action) { if (action == export_mesh_action) { - action_export_current_mesh_triggered(index); + action_export_current_mesh_triggered(index, false); + } else if (action == export_clipped_mesh_action) { + action_export_current_mesh_triggered(index, true); } else if (action == mark_excluded_action) { shape->get_subject()->set_excluded(true); } else if (action == unmark_excluded_action) { @@ -1617,19 +1633,20 @@ void ShapeWorksStudioApp::on_action_preferences_triggered() { } //--------------------------------------------------------------------------- -void ShapeWorksStudioApp::action_export_current_mesh_triggered(int index) { +void ShapeWorksStudioApp::action_export_current_mesh_triggered(int index, bool clip_constraints) { bool single = StudioUtils::ask_multiple_domains_as_single(this, session_->get_project()); - QString filename = ExportUtils::get_save_filename(this, tr("Export Mesh"), get_mesh_file_filter(), ".vtk"); + QString filename = + ExportUtils::get_save_filename(this, tr("Export Mesh"), ExportUtils::get_mesh_file_filter(), ".vtk"); if (filename.isEmpty()) { return; } if (single) { - StudioUtils::write_mesh(visualizer_->get_current_mesh(index), filename); + StudioUtils::write_mesh(visualizer_->get_current_mesh(index, clip_constraints), filename); SW_MESSAGE("Wrote: " + filename.toStdString()); } else { - auto meshes = visualizer_->get_current_meshes_transformed(index); + auto meshes = visualizer_->get_current_meshes_transformed(index, clip_constraints); auto domain_names = session_->get_project()->get_domain_names(); if (meshes.empty()) { @@ -1756,7 +1773,7 @@ void ShapeWorksStudioApp::on_action_export_mesh_scalars_triggered() { } if (single) { - auto poly_data = visualizer_->get_current_mesh(0); + auto poly_data = visualizer_->get_current_mesh(0, false); write_scalars(poly_data, filename); } else { auto meshes = visualizer_->get_current_meshes_transformed(0); @@ -2092,11 +2109,6 @@ bool ShapeWorksStudioApp::write_particle_file(std::string filename, Eigen::Vecto return true; } -//--------------------------------------------------------------------------- -QString ShapeWorksStudioApp::get_mesh_file_filter() { - return tr("VTK files (*.vtk);;PLY files (*.ply);;VTP files (*.vtp);;OBJ files (*.obj);;STL files (*.stl)"); -} - //--------------------------------------------------------------------------- void ShapeWorksStudioApp::update_feature_map_selection(int index) { QString feature_map = ui_->features->itemText(index); diff --git a/Studio/Interface/ShapeWorksStudioApp.h b/Studio/Interface/ShapeWorksStudioApp.h index 339459158c..0b3005bff8 100644 --- a/Studio/Interface/ShapeWorksStudioApp.h +++ b/Studio/Interface/ShapeWorksStudioApp.h @@ -27,7 +27,7 @@ class Ui_ShapeWorksStudioApp; namespace monailabel { - class MonaiLabelTool; +class MonaiLabelTool; } namespace shapeworks { @@ -89,7 +89,7 @@ class ShapeWorksStudioApp : public QMainWindow { void on_actionExport_Eigenvectors_triggered(); void on_actionExport_PCA_Mode_Points_triggered(); void on_action_preferences_triggered(); - void action_export_current_mesh_triggered(int index = 0); + void action_export_current_mesh_triggered(int index = 0, bool clip_constraints = false); void on_action_export_current_particles_triggered(); void on_action_export_mesh_scalars_triggered(); void on_action_export_pca_scores_triggered(); @@ -162,6 +162,8 @@ class ShapeWorksStudioApp : public QMainWindow { Preferences& prefs() { return preferences_; } QSharedPointer session() { return session_; } + QSharedPointer get_visualizer() { return visualizer_; } + protected: void dragEnterEvent(QDragEnterEvent* event) override; void dragLeaveEvent(QDragLeaveEvent* event) override; @@ -180,8 +182,6 @@ class ShapeWorksStudioApp : public QMainWindow { static bool write_particle_file(std::string filename, Eigen::VectorXd particles); - static QString get_mesh_file_filter(); - static const std::string SETTING_ZOOM_C; void set_view_combo_item_enabled(int item, bool value); diff --git a/Studio/Interface/ShapeWorksStudioApp.ui b/Studio/Interface/ShapeWorksStudioApp.ui index c96b7b1519..ac46322f0f 100644 --- a/Studio/Interface/ShapeWorksStudioApp.ui +++ b/Studio/Interface/ShapeWorksStudioApp.ui @@ -6,7 +6,7 @@ 0 0 - 1243 + 1310 705 @@ -714,7 +714,7 @@ QToolBar QToolButton::checked { 0 0 - 1243 + 1310 33 @@ -727,6 +727,7 @@ QToolBar QToolButton::checked { Export... + @@ -1171,16 +1172,16 @@ QToolBar QToolButton::checked { - - true - - - - :/Studio/Images/MONAI-Label.png:/Studio/Images/MONAI-Label.png - - - MONAI - + + true + + + + :/Studio/Images/MONAI-Label.png:/Studio/Images/MONAI-Label.png + + + MONAI + @@ -1220,6 +1221,11 @@ QToolBar QToolButton::checked { QAction::ApplicationSpecificRole + + + Export Current Mesh Clipped... + + diff --git a/Studio/Visualization/Lightbox.h b/Studio/Visualization/Lightbox.h index c0f573f72e..5d86307ad7 100644 --- a/Studio/Visualization/Lightbox.h +++ b/Studio/Visualization/Lightbox.h @@ -94,6 +94,8 @@ class Lightbox : public QObject { vtkRenderWindow* get_render_window(); + int get_start_shape(); + public Q_SLOTS: void handle_timer_callback(); @@ -109,8 +111,6 @@ class Lightbox : public QObject { void insert_shape_into_viewer(std::shared_ptr shape, int position); - int get_start_shape(); - vtkSmartPointer renderer_; ShapeList shapes_; diff --git a/Studio/Visualization/Visualizer.cpp b/Studio/Visualization/Visualizer.cpp index a693a85274..619048f1ee 100644 --- a/Studio/Visualization/Visualizer.cpp +++ b/Studio/Visualization/Visualizer.cpp @@ -152,8 +152,8 @@ void Visualizer::handle_new_mesh() { } //----------------------------------------------------------------------------- -vtkSmartPointer Visualizer::get_current_mesh(int index) { - auto meshes = get_current_meshes_transformed(index); +vtkSmartPointer Visualizer::get_current_mesh(int index, bool clip_constraints) { + auto meshes = get_current_meshes_transformed(index, clip_constraints); if (meshes.empty()) { return nullptr; } @@ -167,7 +167,7 @@ vtkSmartPointer Visualizer::get_current_mesh(int index) { } //----------------------------------------------------------------------------- -std::vector> Visualizer::get_current_meshes_transformed(int index) { +std::vector> Visualizer::get_current_meshes_transformed(int index, bool clip_constraints) { std::vector> list; auto shapes = lightbox_->get_shapes(); if (shapes.size() > index) { @@ -178,10 +178,17 @@ std::vector> Visualizer::get_current_meshes_transfo if (!meshes[domain]->get_poly_data()) { return list; } + + Mesh mesh(meshes[domain]->get_poly_data()); + if (clip_constraints) { + auto constraint = shapes[index]->get_constraints(domain); + constraint.clipMesh(mesh); + } + // we have to transform each domain to its location in order to export an appended mesh auto filter = vtkSmartPointer::New(); filter->SetTransform(get_transform(shapes[index], get_alignment_domain(), domain)); - filter->SetInputData(meshes[domain]->get_poly_data()); + filter->SetInputData(mesh.getVTKMesh()); filter->Update(); list.push_back(filter->GetOutput()); } diff --git a/Studio/Visualization/Visualizer.h b/Studio/Visualization/Visualizer.h index 88ba1182e0..506ba048a2 100644 --- a/Studio/Visualization/Visualizer.h +++ b/Studio/Visualization/Visualizer.h @@ -80,8 +80,8 @@ class Visualizer : public QObject { vtkSmartPointer get_current_particle_poly_data(); void handle_new_mesh(); - vtkSmartPointer get_current_mesh(int index); - std::vector> get_current_meshes_transformed(int index); + vtkSmartPointer get_current_mesh(int index, bool clip_constraints); + std::vector> get_current_meshes_transformed(int index, bool clip_constraints = false); //! Get the currently selected feature map const std::string& get_feature_map() const;