Files
create/src/Gui/TaskTransform.cpp
2025-04-28 23:30:14 +02:00

764 lines
25 KiB
C++

/***************************************************************************
* Copyright (c) 2015 Thomas Anderson <blobfish[at]gmx.com> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 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 Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include "PreCompiled.h"
#ifndef _PreComp_
#include <cassert>
#include <limits>
#include <QApplication>
#endif
#include <View3DInventorViewer.h>
#include <Utilities.h>
#include <App/Document.h>
#include <App/GeoFeature.h>
#include <App/Services.h>
#include <Base/Precision.h>
#include <Base/ServiceProvider.h>
#include <Base/Tools.h>
#include "Document.h" // must be before TaskTransform.h
#include "Application.h"
#include "BitmapFactory.h"
#include "Command.h"
#include "Inventor/Draggers/SoTransformDragger.h"
#include "QuantitySpinBox.h"
#include "ViewProviderDragger.h"
#include "TaskView/TaskView.h"
#include "TaskTransform.h"
#include "ui_TaskTransform.h"
#include <Inventor/nodes/SoPickStyle.h>
using namespace Gui;
namespace
{
void alignGridLayoutColumns(const std::list<QGridLayout*>& layouts, unsigned column = 0)
{
std::vector<int> widths;
auto getActualWidth = [&](const QGridLayout* layout) -> int {
if (auto const item = layout->itemAtPosition(0, column)) {
return item->geometry().width();
}
return 0;
};
for (const auto layout : layouts) {
widths.push_back(getActualWidth(layout));
}
const auto maxWidth = *std::max_element(widths.begin(), widths.end());
for (const auto layout : layouts) {
layout->setColumnMinimumWidth(column, maxWidth);
}
}
} // namespace
TaskTransform::TaskTransform(Gui::ViewProviderDragger* vp,
Gui::SoTransformDragger* dragger,
QWidget* parent,
App::SubObjectPlacementProvider* subObjectPlacemenProvider,
App::CenterOfMassProvider* centerOfMassProvider)
: TaskBox(Gui::BitmapFactory().pixmap("Std_TransformManip.svg"), tr("Transform"), false, parent)
, vp(vp)
, subObjectPlacementProvider(subObjectPlacemenProvider)
, centerOfMassProvider(centerOfMassProvider)
, dragger(dragger)
, ui(new Ui_TaskTransformDialog)
{
blockSelection(true);
dragger->addStartCallback(dragStartCallback, this);
dragger->addMotionCallback(dragMotionCallback, this);
vp->resetTransformOrigin();
referencePlacement = vp->getObjectPlacement();
referenceRotation = referencePlacement.getRotation();
globalOrigin = vp->getObjectPlacement() * App::GeoFeature::getGlobalPlacement(vp->getObject()).inverse();
setupGui();
}
TaskTransform::~TaskTransform()
{
Gui::Application::Instance->commandManager()
.getCommandByName("Std_OrthographicCamera")
->setEnabled(true);
Gui::Application::Instance->commandManager()
.getCommandByName("Std_PerspectiveCamera")
->setEnabled(true);
savePreferences();
}
void TaskTransform::dragStartCallback([[maybe_unused]] void* data,
[[maybe_unused]] SoDragger* dragger)
{
// This is called when a manipulator is about to manipulating
if (firstDrag) {
Gui::Application::Instance->activeDocument()->openCommand(
QT_TRANSLATE_NOOP("Command", "Transform"));
firstDrag = false;
}
}
void TaskTransform::dragMotionCallback(void* data, [[maybe_unused]] SoDragger* dragger)
{
auto task = static_cast<TaskTransform*>(data);
const auto currentRotation = task->vp->getOriginalDraggerPlacement().getRotation();
const auto updatedRotation = task->vp->getDraggerPlacement().getRotation();
const auto rotationAxisHasChanged = [task](auto first, auto second) {
double alpha, beta, gamma;
(first.inverse() * second).getEulerAngles(task->eulerSequence(), alpha, beta, gamma);
auto angles = {alpha, beta, gamma};
const int changed = std::count_if(angles.begin(), angles.end(), [](double angle) {
return std::fabs(angle) > tolerance;
});
// if representation of both differs by more than one axis the axis of rotation must be
// different
return changed > 1;
};
if (!updatedRotation.isSame(currentRotation, tolerance)) {
task->resetReferencePlacement();
if (rotationAxisHasChanged(task->referenceRotation, updatedRotation)) {
task->referenceRotation = currentRotation;
}
}
task->updatePositionAndRotationUi();
}
void TaskTransform::loadPlacementModeItems() const
{
ui->placementComboBox->clear();
ui->placementComboBox->addItem(tr("Object origin"),
QVariant::fromValue(PlacementMode::ObjectOrigin));
if (centerOfMassProvider->ofDocumentObject(vp->getObject()).has_value()) {
ui->placementComboBox->addItem(tr("Center of mass / Centroid"),
QVariant::fromValue(PlacementMode::Centroid));
}
if (subObjectPlacementProvider) {
ui->placementComboBox->addItem(tr("Custom"), QVariant::fromValue(PlacementMode::Custom));
}
}
void TaskTransform::loadPositionModeItems() const
{
ui->positionModeComboBox->clear();
ui->positionModeComboBox->addItem(tr("Local"), QVariant::fromValue(PositionMode::Local));
ui->positionModeComboBox->addItem(tr("Global"), QVariant::fromValue(PositionMode::Global));
}
void TaskTransform::setupGui()
{
auto proxy = new QWidget(this);
ui->setupUi(proxy);
this->groupLayout()->addWidget(proxy);
loadPlacementModeItems();
loadPositionModeItems();
ui->referencePickerWidget->hide();
ui->alignRotationCheckBox->hide();
for (auto positionSpinBox : {ui->translationIncrementSpinBox,
ui->xPositionSpinBox,
ui->yPositionSpinBox,
ui->zPositionSpinBox}) {
positionSpinBox->setUnit(Base::Unit::Length);
}
for (auto rotationSpinBox : {ui->rotationIncrementSpinBox,
ui->xRotationSpinBox,
ui->yRotationSpinBox,
ui->zRotationSpinBox}) {
rotationSpinBox->setUnit(Base::Unit::Angle);
}
connect(ui->translationIncrementSpinBox,
qOverload<double>(&QuantitySpinBox::valueChanged),
this,
[this](double) {
updateIncrements();
});
connect(ui->rotationIncrementSpinBox,
qOverload<double>(&QuantitySpinBox::valueChanged),
this,
[this](double) {
updateIncrements();
});
connect(ui->positionModeComboBox,
qOverload<int>(&QComboBox::currentIndexChanged),
this,
&TaskTransform::onCoordinateSystemChange);
connect(ui->placementComboBox,
qOverload<int>(&QComboBox::currentIndexChanged),
this,
&TaskTransform::onPlacementModeChange);
connect(ui->pickTransformOriginButton,
&QPushButton::clicked,
this,
&TaskTransform::onPickTransformOrigin);
connect(ui->alignToOtherObjectButton,
&QPushButton::clicked,
this,
&TaskTransform::onAlignToOtherObject);
connect(ui->flipPartButton, &QPushButton::clicked, this, &TaskTransform::onFlip);
connect(ui->alignRotationCheckBox,
&QCheckBox::clicked,
this,
&TaskTransform::onAlignRotationChanged);
for (auto positionSpinBox :
{ui->xPositionSpinBox, ui->yPositionSpinBox, ui->zPositionSpinBox}) {
connect(positionSpinBox,
qOverload<double>(&QuantitySpinBox::valueChanged),
this,
[this](double) {
onPositionChange();
});
}
for (auto rotationSpinBox :
{ui->xRotationSpinBox, ui->yRotationSpinBox, ui->zRotationSpinBox}) {
connect(rotationSpinBox,
qOverload<double>(&QuantitySpinBox::valueChanged),
this,
[this,rotationSpinBox](double) {
onRotationChange(rotationSpinBox);
});
}
alignGridLayoutColumns({ui->absolutePositionLayout,
ui->absoluteRotationLayout,
ui->transformOriginLayout,
ui->referencePickerLayout});
loadPreferences();
updateInputLabels();
updateDraggerLabels();
updateIncrements();
updatePositionAndRotationUi();
}
void TaskTransform::loadPreferences()
{
double lastTranslationIncrement = hGrp->GetFloat("LastTranslationIncrement", 1.0);
double lastRotationIncrement = hGrp->GetFloat("LastRotationIncrement", 5.0);
ui->translationIncrementSpinBox->setValue(lastTranslationIncrement);
ui->rotationIncrementSpinBox->setValue(lastRotationIncrement);
}
void TaskTransform::savePreferences()
{
hGrp->SetFloat("LastTranslationIncrement", ui->translationIncrementSpinBox->rawValue());
hGrp->SetFloat("LastRotationIncrement", ui->rotationIncrementSpinBox->rawValue());
}
void TaskTransform::updatePositionAndRotationUi() const
{
const auto referencePlacement = currentCoordinateSystem().origin;
const auto xyzPlacement = vp->getDraggerPlacement();
const auto uvwPlacement = referencePlacement.inverse() * xyzPlacement;
auto fixNegativeZero = [](const double value) {
return std::fabs(value) < Base::Precision::Confusion() ? 0.0 : value;
};
auto setPositionValues = [&](const Base::Vector3d& vec, auto* x, auto* y, auto* z) {
[[maybe_unused]]
auto blockers = {QSignalBlocker(x), QSignalBlocker(y), QSignalBlocker(z)};
x->setValue(fixNegativeZero(vec.x));
y->setValue(fixNegativeZero(vec.y));
z->setValue(fixNegativeZero(vec.z));
};
auto setRotationValues = [&](const Base::Rotation& rot, auto* x, auto* y, auto* z) {
[[maybe_unused]]
auto blockers = {QSignalBlocker(x), QSignalBlocker(y), QSignalBlocker(z)};
double alpha, beta, gamma;
rot.getEulerAngles(eulerSequence(), alpha, beta, gamma);
x->setValue(fixNegativeZero(alpha));
y->setValue(fixNegativeZero(beta));
z->setValue(fixNegativeZero(gamma));
};
setPositionValues(uvwPlacement.getPosition(),
ui->xPositionSpinBox,
ui->yPositionSpinBox,
ui->zPositionSpinBox);
setRotationValues(positionMode == PositionMode::Local
? referenceRotation.inverse() * xyzPlacement.getRotation()
: uvwPlacement.getRotation(),
ui->xRotationSpinBox,
ui->yRotationSpinBox,
ui->zRotationSpinBox);
}
void TaskTransform::updateInputLabels() const
{
auto [xLabel, yLabel, zLabel] = currentCoordinateSystem().labels;
ui->xPositionLabel->setText(QString::fromStdString(xLabel));
ui->yPositionLabel->setText(QString::fromStdString(yLabel));
ui->zPositionLabel->setText(QString::fromStdString(zLabel));
ui->xRotationLabel->setText(QString::fromStdString(xLabel));
ui->yRotationLabel->setText(QString::fromStdString(yLabel));
ui->zRotationLabel->setText(QString::fromStdString(zLabel));
}
void TaskTransform::updateDraggerLabels() const
{
auto coordinateSystem =
isDraggerAlignedToCoordinateSystem() ? globalCoordinateSystem() : localCoordinateSystem();
auto [xLabel, yLabel, zLabel] = coordinateSystem.labels;
dragger->xAxisLabel.setValue(xLabel.c_str());
dragger->yAxisLabel.setValue(yLabel.c_str());
dragger->zAxisLabel.setValue(zLabel.c_str());
}
void TaskTransform::updateIncrements() const
{
dragger->translationIncrement.setValue(
std::max(ui->translationIncrementSpinBox->rawValue(), 0.001));
dragger->rotationIncrement.setValue(
Base::toRadians(std::max(ui->rotationIncrementSpinBox->rawValue(), 0.01)));
}
void TaskTransform::setSelectionMode(SelectionMode mode)
{
Gui::Selection().clearSelection();
SoPickStyle* draggerPickStyle = SO_GET_PART(dragger, "pickStyle", SoPickStyle);
ui->pickTransformOriginButton->setText(tr("Pick reference"));
ui->alignToOtherObjectButton->setText(tr("Move to other object"));
switch (mode) {
case SelectionMode::SelectTransformOrigin:
draggerPickStyle->style = SoPickStyle::UNPICKABLE;
draggerPickStyle->setOverride(true);
blockSelection(false);
ui->referenceLineEdit->setText(tr("Select face, edge or vertex..."));
ui->pickTransformOriginButton->setText(tr("Cancel"));
break;
case SelectionMode::SelectAlignTarget:
draggerPickStyle->style = SoPickStyle::UNPICKABLE;
draggerPickStyle->setOverride(true);
ui->alignToOtherObjectButton->setText(tr("Cancel"));
blockSelection(false);
break;
case SelectionMode::None:
draggerPickStyle->style = SoPickStyle::SHAPE_ON_TOP;
draggerPickStyle->setOverride(false);
blockSelection(true);
vp->setTransformOrigin(vp->getTransformOrigin());
break;
}
selectionMode = mode;
updateSpinBoxesReadOnlyStatus();
}
TaskTransform::SelectionMode TaskTransform::getSelectionMode() const
{
return selectionMode;
}
TaskTransform::CoordinateSystem TaskTransform::localCoordinateSystem() const
{
auto origin = referencePlacement;
origin.setRotation(vp->getDraggerPlacement().getRotation());
return {{"U", "V", "W"}, origin};
}
TaskTransform::CoordinateSystem TaskTransform::globalCoordinateSystem() const
{
return {{"X", "Y", "Z"}, globalOrigin};
}
TaskTransform::CoordinateSystem TaskTransform::currentCoordinateSystem() const
{
return ui->positionModeComboBox->currentIndex() == 0 ? localCoordinateSystem()
: globalCoordinateSystem();
}
Base::Rotation::EulerSequence TaskTransform::eulerSequence() const
{
return positionMode == PositionMode::Local ? Base::Rotation::Intrinsic_XYZ
: Base::Rotation::Extrinsic_XYZ;
}
void TaskTransform::onSelectionChanged(const SelectionChanges& msg)
{
const auto isSupportedMessage =
msg.Type == SelectionChanges::AddSelection || msg.Type == SelectionChanges::SetPreselect;
if (!isSupportedMessage) {
return;
}
if (!subObjectPlacementProvider) {
return;
}
if (!msg.pOriginalMsg) {
// this should not happen! Original should contain unresolved message.
return;
}
auto doc = Application::Instance->getDocument(msg.pDocName);
auto obj = doc->getDocument()->getObject(msg.pObjectName);
auto orgDoc = Application::Instance->getDocument(msg.pOriginalMsg->pDocName);
auto orgObj = orgDoc->getDocument()->getObject(msg.pOriginalMsg->pObjectName);
auto globalPlacement = App::GeoFeature::getGlobalPlacement(obj, orgObj, msg.pOriginalMsg->pSubName);
auto localPlacement = App::GeoFeature::getPlacementFromProp(obj, "Placement");
auto rootPlacement = App::GeoFeature::getGlobalPlacement(vp->getObject());
auto attachedPlacement = subObjectPlacementProvider->calculate(msg.Object, localPlacement);
auto selectedObjectPlacement = rootPlacement.inverse() * globalPlacement * attachedPlacement;
auto label = QStringLiteral("%1#%2.%3")
.arg(QLatin1String(msg.pOriginalMsg->pObjectName),
QLatin1String(msg.pObjectName),
QLatin1String(msg.pSubName));
switch (selectionMode) {
case SelectionMode::SelectTransformOrigin: {
if (msg.Type == SelectionChanges::AddSelection) {
ui->referenceLineEdit->setText(label);
customTransformOrigin = selectedObjectPlacement;
updateTransformOrigin();
setSelectionMode(SelectionMode::None);
} else {
vp->setTransformOrigin(selectedObjectPlacement);
}
break;
}
case SelectionMode::SelectAlignTarget: {
vp->setDraggerPlacement(vp->getObjectPlacement() * selectedObjectPlacement);
if (msg.Type == SelectionChanges::AddSelection) {
moveObjectToDragger();
setSelectionMode(SelectionMode::None);
}
break;
}
default:
// no-op
break;
}
}
void TaskTransform::onAlignRotationChanged()
{
updateDraggerLabels();
updateTransformOrigin();
}
void TaskTransform::onAlignToOtherObject()
{
if (selectionMode == SelectionMode::SelectAlignTarget) {
setSelectionMode(SelectionMode::None);
return;
}
setSelectionMode(SelectionMode::SelectAlignTarget);
}
void TaskTransform::moveObjectToDragger()
{
vp->updateTransformFromDragger();
vp->updatePlacementFromDragger();
resetReferenceRotation();
resetReferencePlacement();
updatePositionAndRotationUi();
}
void TaskTransform::onFlip()
{
auto placement = vp->getDraggerPlacement();
placement.setRotation(placement.getRotation()
* Base::Rotation::fromNormalVector(Base::Vector3d(0, 0, -1)));
vp->setDraggerPlacement(placement);
moveObjectToDragger();
}
void TaskTransform::onPickTransformOrigin()
{
setSelectionMode(selectionMode == SelectionMode::None ? SelectionMode::SelectTransformOrigin
: SelectionMode::None);
}
void TaskTransform::onPlacementModeChange([[maybe_unused]] int index)
{
placementMode = ui->placementComboBox->currentData().value<PlacementMode>();
updateTransformOrigin();
}
void TaskTransform::updateTransformOrigin()
{
auto getTransformOrigin = [this](const PlacementMode& mode) -> Base::Placement {
switch (mode) {
case PlacementMode::ObjectOrigin:
return {};
case PlacementMode::Centroid:
if (const auto com = centerOfMassProvider->ofDocumentObject(vp->getObject())) {
return {*com, {}};
}
return {};
case PlacementMode::Custom:
return customTransformOrigin.value_or(Base::Placement {});
default:
return {};
}
};
ui->referencePickerWidget->setVisible(placementMode == PlacementMode::Custom);
if (placementMode == PlacementMode::Custom && !customTransformOrigin.has_value()) {
setSelectionMode(SelectionMode::SelectTransformOrigin);
return;
}
auto transformOrigin = getTransformOrigin(placementMode);
if (isDraggerAlignedToCoordinateSystem()) {
transformOrigin.setRotation(
(vp->getObjectPlacement().inverse() * globalCoordinateSystem().origin).getRotation());
}
vp->setTransformOrigin(transformOrigin);
resetReferencePlacement();
resetReferenceRotation();
updatePositionAndRotationUi();
updateDraggerLabels();
}
void TaskTransform::updateSpinBoxesReadOnlyStatus() const
{
const bool isReadOnly = selectionMode != SelectionMode::None;
const auto controls = {
ui->xPositionSpinBox,
ui->yPositionSpinBox,
ui->zPositionSpinBox,
ui->xRotationSpinBox,
ui->yRotationSpinBox,
ui->zRotationSpinBox,
};
for (const auto& control : controls) {
control->setReadOnly(isReadOnly);
}
}
void TaskTransform::resetReferencePlacement()
{
referencePlacement = vp->getDraggerPlacement();
}
void TaskTransform::resetReferenceRotation()
{
referenceRotation = vp->getDraggerPlacement().getRotation();
}
bool TaskTransform::isDraggerAlignedToCoordinateSystem() const
{
return positionMode == PositionMode::Global && ui->alignRotationCheckBox->isChecked();
}
void TaskTransform::onTransformOriginReset()
{
vp->resetTransformOrigin();
}
void TaskTransform::onCoordinateSystemChange([[maybe_unused]] int mode)
{
positionMode = ui->positionModeComboBox->currentData().value<PositionMode>();
ui->alignRotationCheckBox->setVisible(positionMode != PositionMode::Local);
updateInputLabels();
updatePositionAndRotationUi();
updateTransformOrigin();
}
void TaskTransform::onPositionChange()
{
const auto uvwPosition = Base::Vector3d(ui->xPositionSpinBox->rawValue(),
ui->yPositionSpinBox->rawValue(),
ui->zPositionSpinBox->rawValue());
const auto xyzPosition = currentCoordinateSystem().origin.getPosition()
+ currentCoordinateSystem().origin.getRotation().multVec(uvwPosition);
const auto placement = vp->getDraggerPlacement();
vp->setDraggerPlacement({xyzPosition, placement.getRotation()});
vp->updateTransformFromDragger();
vp->updatePlacementFromDragger();
}
void TaskTransform::onRotationChange(QuantitySpinBox* changed)
{
if (positionMode == PositionMode::Local) {
for (auto rotationSpinBox : {ui->xRotationSpinBox,
ui->yRotationSpinBox,
ui->zRotationSpinBox}) {
QSignalBlocker blocker(rotationSpinBox);
// if any other spinbox contains non-zero value we need to reset rotation reference first
if (std::fabs(rotationSpinBox->rawValue()) > tolerance && rotationSpinBox != changed) {
resetReferenceRotation();
rotationSpinBox->setValue(0.0);
}
}
}
const auto uvwRotation = Base::Rotation::fromEulerAngles(eulerSequence(),
ui->xRotationSpinBox->rawValue(),
ui->yRotationSpinBox->rawValue(),
ui->zRotationSpinBox->rawValue());
auto referenceRotation = positionMode == PositionMode::Local
? this->referenceRotation
: currentCoordinateSystem().origin.getRotation();
const auto xyzRotation = referenceRotation * uvwRotation;
const auto placement = vp->getDraggerPlacement();
vp->setDraggerPlacement({placement.getPosition(), xyzRotation});
vp->updateTransformFromDragger();
vp->updatePlacementFromDragger();
resetReferencePlacement();
}
TaskTransformDialog::TaskTransformDialog(ViewProviderDragger* vp, SoTransformDragger* dragger)
: vp(vp)
{
transform = new TaskTransform(vp, dragger);
Content.push_back(transform);
}
void TaskTransformDialog::open()
{
// we can't have user switching camera types while dragger is shown.
Gui::Application::Instance->commandManager()
.getCommandByName("Std_OrthographicCamera")
->setEnabled(false);
Gui::Application::Instance->commandManager()
.getCommandByName("Std_PerspectiveCamera")
->setEnabled(false);
Gui::TaskView::TaskDialog::open();
Gui::Application::Instance->activeDocument()->openCommand(
QT_TRANSLATE_NOOP("Command", "Transform"));
}
bool TaskTransformDialog::accept()
{
if (auto documentObject = vp->getObject()) {
Gui::Document* document =
Gui::Application::Instance->getDocument(documentObject->getDocument());
assert(document);
document->commitCommand();
document->resetEdit();
document->getDocument()->recompute();
}
return Gui::TaskView::TaskDialog::accept();
}
bool TaskTransformDialog::reject()
{
if (auto documentObject = vp->getObject()) {
Gui::Document* document =
Gui::Application::Instance->getDocument(documentObject->getDocument());
assert(document);
document->abortCommand();
document->resetEdit();
document->getDocument()->recompute();
}
return Gui::TaskView::TaskDialog::reject();
}
#include "moc_TaskTransform.cpp"