ifcviewer-full: per-model ModelTransformation editor

ModelTransformationDialog edits one federation model at a time.  Top
combobox picks the model; below it the form covers the four pieces
of authoring intent:

  - AFrame radio: ModelLocal vs ModelGlobal
  - Point A: 3 doubles, label switches between "model project length
    unit" and "model map unit" with the radio
  - Point B: 3 doubles in federation units (label reflects current
    FederationConfig.unit_*)
  - Rotation: rx/ry/rz in degrees, intrinsic XYZ
  - Pivot: 3 doubles in federation units

Switching models discards unsaved form edits — Ok saves the
currently-visible model, Cancel discards.  On Ok calls
Federation::setModelTransformation, which fires
modelTransformationChanged → MainWindow recomposes that model in
the viewport.

Reachable from File > Model Transformations.

End-to-end is now editable: open a federation, edit federation unit /
false origin from one dialog, edit any model's transformation from
the other, watch the viewport recompose live.  Visual verification
on a real model still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-02 08:06:25 +10:00
parent 81b1efdfd6
commit 8f3eaa35d7
4 changed files with 351 additions and 0 deletions
+12
View File
@@ -21,6 +21,7 @@
#include "AppSettings.h"
#include "Federation.h"
#include "FederationSettingsDialog.h"
#include "ModelTransformationDialog.h"
#include "SettingsWindow.h"
#include "LodBuilder.h"
#include "SidecarCache.h"
@@ -192,6 +193,8 @@ void MainWindow::setupMenus() {
QKeySequence::SaveAs);
file_menu->addAction("Federation Se&ttings...",
this, &MainWindow::onFederationSettings);
file_menu->addAction("&Model Transformations...",
this, &MainWindow::onModelTransformations);
file_menu->addSeparator();
file_menu->addAction("&Settings...", this, &MainWindow::onFileSettings);
file_menu->addSeparator();
@@ -240,6 +243,15 @@ void MainWindow::onFederationSettings() {
federation_settings_->raise();
}
void MainWindow::onModelTransformations() {
if (model_transformations_ == nullptr) {
model_transformations_ = new ModelTransformationDialog(federation_, this);
}
model_transformations_->open();
model_transformations_->activateWindow();
model_transformations_->raise();
}
void MainWindow::addFiles(const QStringList& paths) {
QStringList accepted_paths;
QStringList accepted_fed_ids;
+3
View File
@@ -37,6 +37,7 @@
class Federation;
class SettingsWindow;
class FederationSettingsDialog;
class ModelTransformationDialog;
class MainWindow : public QMainWindow {
Q_OBJECT
@@ -63,6 +64,7 @@ private slots:
void onGoHomeView();
void onFileSettings();
void onFederationSettings();
void onModelTransformations();
void onObjectPicked(uint32_t object_id);
void onTreeSelectionChanged();
@@ -122,6 +124,7 @@ private:
Federation* federation_ = nullptr;
SettingsWindow* settings_ = nullptr;
FederationSettingsDialog* federation_settings_ = nullptr;
ModelTransformationDialog* model_transformations_ = nullptr;
QWidget* viewport_container_ = nullptr;
QTreeWidget* element_tree_ = nullptr;
QTableWidget* property_table_ = nullptr;
@@ -0,0 +1,248 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell 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 *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "ModelTransformationDialog.h"
#include "Federation.h"
#include <QButtonGroup>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QRadioButton>
#include <QShowEvent>
#include <QVBoxLayout>
namespace {
QString unitDisplay(const FederationConfig& cfg) {
QString s;
if (!cfg.unit_prefix.empty()) s += QString::fromStdString(cfg.unit_prefix) + " ";
s += QString::fromStdString(cfg.unit_name);
return s;
}
} // namespace
ModelTransformationDialog::ModelTransformationDialog(Federation* federation,
QWidget* parent)
: QDialog(parent), federation_(federation)
{
setWindowTitle("Model Transformation");
setupUi();
}
void ModelTransformationDialog::setupUi() {
auto* root = new QVBoxLayout(this);
// Model picker.
{
auto* row = new QHBoxLayout();
row->addWidget(new QLabel("Model:", this));
model_combo_ = new QComboBox(this);
row->addWidget(model_combo_, 1);
root->addLayout(row);
connect(model_combo_, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ModelTransformationDialog::onModelChanged);
}
auto build_xyz = [](QDoubleSpinBox*& sb) {
sb = new QDoubleSpinBox();
sb->setRange(-1e12, 1e12);
sb->setDecimals(6);
sb->setSingleStep(1.0);
};
// A-frame + A.
{
auto* group = new QGroupBox("Point A", this);
group->setToolTip(
"The model-side anchor point. ModelLocal expresses A in the "
"model's project length unit, before its CoordinateOperation. "
"ModelGlobal expresses A in the model's map unit, after its "
"CoordinateOperation.");
auto* form = new QFormLayout(group);
radio_local_ = new QRadioButton("ModelLocal", group);
radio_global_ = new QRadioButton("ModelGlobal", group);
radio_global_->setChecked(true);
auto* af_group = new QButtonGroup(this);
af_group->addButton(radio_local_);
af_group->addButton(radio_global_);
connect(radio_local_, &QRadioButton::toggled,
this, &ModelTransformationDialog::onAFrameToggled);
connect(radio_global_, &QRadioButton::toggled,
this, &ModelTransformationDialog::onAFrameToggled);
auto* af_row = new QHBoxLayout();
af_row->addWidget(radio_local_);
af_row->addWidget(radio_global_);
af_row->addStretch();
form->addRow("Frame", af_row);
build_xyz(a_x_); build_xyz(a_y_); build_xyz(a_z_);
form->addRow("X", a_x_);
form->addRow("Y", a_y_);
form->addRow("Z", a_z_);
a_unit_label_ = new QLabel("(model unit)", group);
form->addRow("Unit", a_unit_label_);
root->addWidget(group);
}
// B.
{
auto* group = new QGroupBox("Point B (federation target)", this);
group->setToolTip(
"Where in the federation point A should land. In federation units.");
auto* form = new QFormLayout(group);
build_xyz(b_x_); build_xyz(b_y_); build_xyz(b_z_);
form->addRow("X", b_x_);
form->addRow("Y", b_y_);
form->addRow("Z", b_z_);
b_unit_label_ = new QLabel("(federation unit)", group);
form->addRow("Unit", b_unit_label_);
root->addWidget(group);
}
// Rotation (intrinsic XYZ).
{
auto* group = new QGroupBox("Rotation (intrinsic XYZ, degrees)", this);
group->setToolTip(
"Composed as R = R_z(rz) · R_y(ry) · R_x(rx). Y-up models "
"rotated to Z-up: rx = 90, ry = 0, rz = 0.");
auto* form = new QFormLayout(group);
rx_ = new QDoubleSpinBox(); rx_->setRange(-360, 360); rx_->setDecimals(6);
ry_ = new QDoubleSpinBox(); ry_->setRange(-360, 360); ry_->setDecimals(6);
rz_ = new QDoubleSpinBox(); rz_->setRange(-360, 360); rz_->setDecimals(6);
form->addRow("rx (°)", rx_);
form->addRow("ry (°)", ry_);
form->addRow("rz (°)", rz_);
root->addWidget(group);
}
// Pivot.
{
auto* group = new QGroupBox("Rotation Pivot", this);
group->setToolTip(
"The point the rotation rotates around, in federation units. "
"Set pivot = B to keep A landing on B regardless of rotation.");
auto* form = new QFormLayout(group);
build_xyz(pivot_x_); build_xyz(pivot_y_); build_xyz(pivot_z_);
form->addRow("X", pivot_x_);
form->addRow("Y", pivot_y_);
form->addRow("Z", pivot_z_);
pivot_unit_label_ = new QLabel("(federation unit)", group);
form->addRow("Unit", pivot_unit_label_);
root->addWidget(group);
}
auto* button_box = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
root->addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, this,
&ModelTransformationDialog::onAccepted);
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
void ModelTransformationDialog::showEvent(QShowEvent* event) {
populateModelCombo();
refreshUnitLabels();
if (model_combo_->count() > 0) {
syncFromModel(model_combo_->currentData().toString());
}
QDialog::showEvent(event);
}
void ModelTransformationDialog::populateModelCombo() {
model_combo_->blockSignals(true);
QString prev_id = model_combo_->currentData().toString();
model_combo_->clear();
if (federation_) {
for (const auto& m : federation_->models()) {
QString label = m.display_name.isEmpty() ? m.id : m.display_name;
model_combo_->addItem(label, m.id);
}
}
int restore = -1;
for (int i = 0; i < model_combo_->count(); ++i) {
if (model_combo_->itemData(i).toString() == prev_id) { restore = i; break; }
}
if (restore >= 0) model_combo_->setCurrentIndex(restore);
model_combo_->blockSignals(false);
}
void ModelTransformationDialog::syncFromModel(const QString& fed_id) {
if (!federation_) return;
const Federation::Model* m = federation_->findById(fed_id);
if (!m) return;
const ModelTransformation& xf = m->model_transformation;
radio_local_->setChecked(xf.a_frame == AFrame::ModelLocal);
radio_global_->setChecked(xf.a_frame == AFrame::ModelGlobal);
a_x_->setValue(xf.a.x()); a_y_->setValue(xf.a.y()); a_z_->setValue(xf.a.z());
b_x_->setValue(xf.b.x()); b_y_->setValue(xf.b.y()); b_z_->setValue(xf.b.z());
rx_->setValue(xf.rxyz_deg.x());
ry_->setValue(xf.rxyz_deg.y());
rz_->setValue(xf.rxyz_deg.z());
pivot_x_->setValue(xf.pivot.x());
pivot_y_->setValue(xf.pivot.y());
pivot_z_->setValue(xf.pivot.z());
}
void ModelTransformationDialog::refreshUnitLabels() {
if (!federation_) return;
const QString fed_unit = unitDisplay(federation_->config());
b_unit_label_->setText("(federation: " + fed_unit + ")");
pivot_unit_label_->setText("(federation: " + fed_unit + ")");
onAFrameToggled();
}
void ModelTransformationDialog::onModelChanged(int /*idx*/) {
if (model_combo_->count() == 0) return;
syncFromModel(model_combo_->currentData().toString());
}
void ModelTransformationDialog::onAFrameToggled() {
if (!a_unit_label_) return;
if (radio_local_->isChecked()) {
a_unit_label_->setText("(model project length unit, e.g. millimetre)");
} else {
a_unit_label_->setText("(model map unit, e.g. metre)");
}
}
void ModelTransformationDialog::onAccepted() {
if (!federation_ || model_combo_->count() == 0) { accept(); return; }
const QString fed_id = model_combo_->currentData().toString();
ModelTransformation xf;
xf.a_frame = radio_local_->isChecked() ? AFrame::ModelLocal : AFrame::ModelGlobal;
xf.a = Eigen::Vector3d(a_x_->value(), a_y_->value(), a_z_->value());
xf.b = Eigen::Vector3d(b_x_->value(), b_y_->value(), b_z_->value());
xf.rxyz_deg = Eigen::Vector3d(rx_->value(), ry_->value(), rz_->value());
xf.pivot = Eigen::Vector3d(pivot_x_->value(), pivot_y_->value(), pivot_z_->value());
federation_->setModelTransformation(fed_id, xf);
accept();
}
@@ -0,0 +1,88 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell 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 *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#ifndef MODELTRANSFORMATIONDIALOG_H
#define MODELTRANSFORMATIONDIALOG_H
#include <QDialog>
#include <QString>
QT_BEGIN_NAMESPACE
class QComboBox;
class QDoubleSpinBox;
class QLabel;
class QRadioButton;
QT_END_NAMESPACE
class Federation;
// Edits one model's ModelTransformation (the per-model placement within
// the federation: a_frame, a, b, rxyz Euler degrees, pivot). A combobox
// at the top picks which model to edit. Switching models discards any
// unsaved form edits — Ok saves the currently-visible model. On Ok,
// calls Federation::setModelTransformation, which fires
// modelTransformationChanged → MainWindow recomposes the viewport for
// that model.
class ModelTransformationDialog : public QDialog {
Q_OBJECT
public:
explicit ModelTransformationDialog(Federation* federation, QWidget* parent = nullptr);
protected:
void showEvent(QShowEvent* event) override;
private slots:
void onModelChanged(int idx);
void onAFrameToggled();
void onAccepted();
private:
void setupUi();
void populateModelCombo();
void syncFromModel(const QString& fed_id);
void refreshUnitLabels();
Federation* federation_ = nullptr;
QComboBox* model_combo_ = nullptr;
QRadioButton* radio_local_ = nullptr;
QRadioButton* radio_global_ = nullptr;
QDoubleSpinBox* a_x_ = nullptr;
QDoubleSpinBox* a_y_ = nullptr;
QDoubleSpinBox* a_z_ = nullptr;
QLabel* a_unit_label_ = nullptr;
QDoubleSpinBox* b_x_ = nullptr;
QDoubleSpinBox* b_y_ = nullptr;
QDoubleSpinBox* b_z_ = nullptr;
QLabel* b_unit_label_ = nullptr;
QDoubleSpinBox* rx_ = nullptr;
QDoubleSpinBox* ry_ = nullptr;
QDoubleSpinBox* rz_ = nullptr;
QDoubleSpinBox* pivot_x_ = nullptr;
QDoubleSpinBox* pivot_y_ = nullptr;
QDoubleSpinBox* pivot_z_ = nullptr;
QLabel* pivot_unit_label_ = nullptr;
};
#endif