// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2023 Werner Mayer * * * * This file is part of FreeCAD. * * * * FreeCAD 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 2.1 of the * * License, or (at your option) any later version. * * * * FreeCAD 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 FreeCAD. If not, see * * . * * * **************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "TaskImage.h" #include "ui_TaskImage.h" using namespace Gui; /* TRANSLATOR Gui::TaskImage */ TaskImage::TaskImage(Image::ImagePlane* obj, QWidget* parent) : QWidget(parent) , ui(new Ui_TaskImage) , feature(obj) , aspectRatio(1.0) { ui->setupUi(this); ui->groupBoxCalibration->hide(); initialiseTransparency(); // NOLINTNEXTLINE aspectRatio = obj->XSize.getValue() / obj->YSize.getValue(); connectSignals(); } TaskImage::~TaskImage() { if (!feature.expired() && scale) { if (scale->isActive()) { scale->deactivate(); } scale->deleteLater(); } } void TaskImage::connectSignals() { // clang-format off connect(ui->Reverse_checkBox, &QCheckBox::clicked, this, &TaskImage::onPreview); connect(ui->XY_radioButton, &QRadioButton::clicked, this, &TaskImage::onPreview); connect(ui->XZ_radioButton, &QRadioButton::clicked, this, &TaskImage::onPreview); connect(ui->YZ_radioButton, &QRadioButton::clicked, this, &TaskImage::onPreview); connect(ui->spinBoxZ, qOverload(&QuantitySpinBox::valueChanged), this, &TaskImage::onPreview); connect(ui->spinBoxX, qOverload(&QuantitySpinBox::valueChanged), this, &TaskImage::onPreview); connect(ui->spinBoxY, qOverload(&QuantitySpinBox::valueChanged), this, &TaskImage::onPreview); connect(ui->spinBoxRotation, qOverload(&QuantitySpinBox::valueChanged), this, &TaskImage::onPreview); connect(ui->spinBoxTransparency, qOverload(&QSpinBox::valueChanged), this, &TaskImage::changeTransparency); connect(ui->sliderTransparency, qOverload(&QSlider::valueChanged), this, &TaskImage::changeTransparency); connect(ui->spinBoxWidth, &QuantitySpinBox::editingFinished, this, &TaskImage::changeWidth); connect(ui->spinBoxHeight, &QuantitySpinBox::editingFinished, this, &TaskImage::changeHeight); connect(ui->pushButtonScale, &QPushButton::clicked, this, &TaskImage::onInteractiveScale); connect(ui->pushButtonApply, &QPushButton::clicked, this, &TaskImage::acceptScale); connect(ui->pushButtonCancel, &QPushButton::clicked, this, &TaskImage::rejectScale); // clang-format on } void TaskImage::initialiseTransparency() { // NOLINTBEGIN auto vp = Application::Instance->getViewProvider(feature.get()); App::Property* prop = vp->getPropertyByName("Transparency"); if (prop && prop->isDerivedFrom()) { auto Transparency = static_cast(prop); ui->spinBoxTransparency->setValue(Transparency->getValue()); ui->sliderTransparency->setValue(Transparency->getValue()); } // NOLINTEND } void TaskImage::changeTransparency(int val) { if (feature.expired()) { return; } auto vp = Application::Instance->getViewProvider(feature.get()); App::Property* prop = vp->getPropertyByName("Transparency"); if (auto Transparency = dynamic_cast(prop)) { Transparency->setValue(val); QSignalBlocker block(ui->spinBoxTransparency); QSignalBlocker blocks(ui->sliderTransparency); ui->spinBoxTransparency->setValue(val); ui->sliderTransparency->setValue(val); } } void TaskImage::changeWidth() { if (!feature.expired()) { double val = ui->spinBoxWidth->value().getValue(); feature->XSize.setValue(val); if (ui->checkBoxRatio->isChecked()) { feature->YSize.setValue(val / aspectRatio); QSignalBlocker block(ui->spinBoxHeight); ui->spinBoxHeight->setValue(val / aspectRatio); } } } void TaskImage::changeHeight() { if (!feature.expired()) { double val = ui->spinBoxHeight->value().getValue(); feature->YSize.setValue(val); if (ui->checkBoxRatio->isChecked()) { feature->XSize.setValue(val * aspectRatio); QSignalBlocker block(ui->spinBoxWidth); ui->spinBoxWidth->setValue(val * aspectRatio); } } } View3DInventorViewer* TaskImage::getViewer() const { if (!feature.expired()) { auto vp = Application::Instance->getViewProvider(feature.get()); auto doc = static_cast(vp)->getDocument(); // NOLINT auto view = qobject_cast(doc->getViewOfViewProvider(vp)); if (view) { return view->getViewer(); } } return nullptr; } void TaskImage::scaleImage(double factor) { if (!feature.expired()) { feature->XSize.setValue(feature->XSize.getValue() * factor); feature->YSize.setValue(feature->YSize.getValue() * factor); QSignalBlocker blockW(ui->spinBoxWidth); ui->spinBoxWidth->setValue(feature->XSize.getValue()); QSignalBlocker blockH(ui->spinBoxHeight); ui->spinBoxHeight->setValue(feature->YSize.getValue()); } } void TaskImage::startScale() { if (scale) { scale->activate(); ui->pushButtonScale->hide(); ui->groupBoxCalibration->show(); ui->pushButtonApply->setEnabled(false); } } void TaskImage::acceptScale() { if (scale) { scaleImage(scale->getScaleFactor()); rejectScale(); } } void TaskImage::enableApplyBtn() { ui->pushButtonApply->setEnabled(true); } void TaskImage::rejectScale() { if (scale) { scale->deactivate(); ui->pushButtonScale->show(); ui->groupBoxCalibration->hide(); } } void TaskImage::onInteractiveScale() { if (!feature.expired() && !scale) { View3DInventorViewer* viewer = getViewer(); if (viewer) { auto vp = Application::Instance->getViewProvider(feature.get()); scale = new InteractiveScale(viewer, vp, feature->globalPlacement()); connect(scale, &InteractiveScale::scaleRequired, this, &TaskImage::acceptScale); connect(scale, &InteractiveScale::scaleCanceled, this, &TaskImage::rejectScale); connect(scale, &InteractiveScale::enableApplyBtn, this, &TaskImage::enableApplyBtn); } } startScale(); } void TaskImage::open() { if (!feature.expired()) { App::Document* doc = feature->getDocument(); doc->openTransaction(QT_TRANSLATE_NOOP("Command", "Edit image")); restore(feature->Placement.getValue()); } } void TaskImage::accept() { if (!feature.expired()) { App::Document* doc = feature->getDocument(); doc->commitTransaction(); doc->recompute(); } } void TaskImage::reject() { if (!feature.expired()) { App::Document* doc = feature->getDocument(); doc->abortTransaction(); feature->purgeTouched(); } } void TaskImage::onPreview() { updateIcon(); updatePlacement(); } // NOLINTNEXTLINE void TaskImage::restoreAngles(const Base::Rotation& rot) { Base::Vector3d vec(0, 0, 1); rot.multVec(vec, vec); double yaw {}; double pitch {}; double roll {}; rot.getYawPitchRoll(yaw, pitch, roll); bool reverse = false; const double tol = 1.0e-5; const double angle1 = 90.0; const double angle2 = 180.0; auto isTopOrBottom = [=](bool& reverse) { if (std::fabs(vec.z - 1.0) < tol) { return true; } if (std::fabs(vec.z + 1.0) < tol) { reverse = true; return true; } return false; }; auto isFrontOrRear = [&](bool& reverse) { if (std::fabs(vec.y + 1.0) < tol) { if (std::fabs(yaw - angle2) < tol) { pitch = -angle2 - pitch; } return true; } if (std::fabs(vec.y - 1.0) < tol) { if (std::fabs(yaw) < tol) { pitch = -angle2 - pitch; } reverse = true; return true; } return false; }; auto isRightOrLeft = [&](bool& reverse) { if (std::fabs(vec.x - 1.0) < tol) { if (std::fabs(yaw + angle1) < tol) { pitch = -angle2 - pitch; } return true; } if (std::fabs(vec.x + 1.0) < tol) { if (std::fabs(yaw - angle1) < tol) { pitch = -angle2 - pitch; } reverse = true; return true; } return false; }; if (isTopOrBottom(reverse)) { int inv = reverse ? -1 : 1; ui->XY_radioButton->setChecked(true); ui->spinBoxRotation->setValue(yaw * inv); } else if (isFrontOrRear(reverse)) { ui->XZ_radioButton->setChecked(true); ui->spinBoxRotation->setValue(-pitch); } else if (isRightOrLeft(reverse)) { ui->YZ_radioButton->setChecked(true); ui->spinBoxRotation->setValue(-pitch); } ui->Reverse_checkBox->setChecked(reverse); } void TaskImage::restore(const Base::Placement& plm) { if (feature.expired()) { return; } QSignalBlocker blockW(ui->spinBoxWidth); QSignalBlocker blockH(ui->spinBoxHeight); ui->spinBoxWidth->setValue(feature->XSize.getValue()); ui->spinBoxHeight->setValue(feature->YSize.getValue()); Base::Rotation rot = plm.getRotation(); // NOLINT Base::Vector3d pos = plm.getPosition(); restoreAngles(rot); Base::Vector3d R0(0, 0, 0); Base::Vector3d RX(1, 0, 0); Base::Vector3d RY(0, 1, 0); RX = rot.multVec(RX); RY = rot.multVec(RY); pos.TransformToCoordinateSystem(R0, RX, RY); ui->spinBoxX->setValue(pos.x); ui->spinBoxY->setValue(pos.y); ui->spinBoxZ->setValue(pos.z); onPreview(); } void TaskImage::updatePlacement() { double angle = ui->spinBoxRotation->value().getValue(); bool reverse = ui->Reverse_checkBox->isChecked(); // NOLINTBEGIN Base::Placement Pos; Base::Rotation rot; double dir = reverse ? 180. : 0.; int inv = reverse ? -1 : 1; if (ui->XY_radioButton->isChecked()) { rot.setYawPitchRoll(inv * angle, 0., dir); } else if (ui->XZ_radioButton->isChecked()) { rot.setYawPitchRoll(dir, -angle, 90.); } else if (ui->YZ_radioButton->isChecked()) { rot.setYawPitchRoll(90. - dir, -angle, 90.); } else if (!feature.expired()) { Base::Placement plm = feature->Placement.getValue(); rot = plm.getRotation(); } // NOLINTEND Base::Vector3d offset = Base::Vector3d( ui->spinBoxX->value().getValue(), ui->spinBoxY->value().getValue(), ui->spinBoxZ->value().getValue() ); offset = rot.multVec(offset); Pos = Base::Placement(offset, rot); if (!feature.expired()) { feature->Placement.setValue(Pos); if (scale) { scale->setPlacement(feature->globalPlacement()); } } } void TaskImage::updateIcon() { std::string icon; bool reverse = ui->Reverse_checkBox->isChecked(); if (ui->XY_radioButton->isChecked()) { icon = reverse ? "view-bottom" : "view-top"; } else if (ui->XZ_radioButton->isChecked()) { icon = reverse ? "view-rear" : "view-front"; } else if (ui->YZ_radioButton->isChecked()) { icon = reverse ? "view-left" : "view-right"; } ui->previewLabel->setPixmap( Gui::BitmapFactory().pixmapFromSvg(icon.c_str(), ui->previewLabel->size()) ); } // ---------------------------------------------------------------------------- InteractiveScale::InteractiveScale( View3DInventorViewer* view, ViewProvider* vp, const Base::Placement& plc ) // NOLINT : active(false) , placement(plc) , viewer(view) , viewProv(vp) , midPoint(SbVec3f(0, 0, 0)) { measureLabel = new EditableDatumLabel(viewer, placement, SbColor(1.0F, 0.149F, 0.0F)); // NOLINT } InteractiveScale::~InteractiveScale() { delete measureLabel; } void InteractiveScale::activate() { if (viewer) { viewer->setEditing(true); viewer->addEventCallback( SoLocation2Event::getClassTypeId(), InteractiveScale::getMousePosition, this ); viewer->addEventCallback(SoButtonEvent::getClassTypeId(), InteractiveScale::soEventFilter, this); viewer->setSelectionEnabled(false); viewer->getWidget()->setCursor(QCursor(Qt::CrossCursor)); active = true; } } void InteractiveScale::deactivate() { if (viewer) { points.clear(); measureLabel->deactivate(); viewer->setEditing(false); viewer->removeEventCallback( SoLocation2Event::getClassTypeId(), InteractiveScale::getMousePosition, this ); viewer->removeEventCallback( SoButtonEvent::getClassTypeId(), InteractiveScale::soEventFilter, this ); viewer->setSelectionEnabled(true); viewer->getWidget()->setCursor(QCursor(Qt::ArrowCursor)); active = false; } } double InteractiveScale::getScaleFactor() const { if ((points[0] - points[1]).length() == 0.) { return 1.0; } return measureLabel->getValue() / (points[0] - points[1]).length(); } double InteractiveScale::getDistance(const SbVec3f& pt) const { if (points.empty()) { return 0.0; } return (points[0] - pt).length(); } void InteractiveScale::setDistance(const SbVec3f& pos3d) { Base::Quantity quantity; quantity.setValue(getDistance(pos3d)); quantity.setUnit(Base::Unit::Length); // Update the displayed distance double factor {}; std::string unitStr; std::string valueStr; valueStr = quantity.getUserString(factor, unitStr); measureLabel->label->string = SbString(valueStr.c_str()); measureLabel->label->setPoints(getCoordsOnImagePlane(points[0]), getCoordsOnImagePlane(pos3d)); } void InteractiveScale::findPointOnImagePlane(SoEventCallback* ecb) { const SoEvent* mbe = ecb->getEvent(); auto view = static_cast(ecb->getUserData()); std::unique_ptr pp(view->getPointOnRay(mbe->getPosition(), viewProv)); if (pp) { auto pos3d = pp->getPoint(); collectPoint(pos3d); } } void InteractiveScale::collectPoint(const SbVec3f& pos3d) { if (points.empty()) { points.push_back(pos3d); measureLabel->label->setPoints(getCoordsOnImagePlane(pos3d), getCoordsOnImagePlane(pos3d)); measureLabel->activate(); } else if (points.size() == 1) { double distance = getDistance(pos3d); if (distance > Base::Precision::Confusion()) { points.push_back(pos3d); midPoint = (points[0] + points[1]) / 2; measureLabel->startEdit(getDistance(points[1]), this, true); Q_EMIT enableApplyBtn(); } else { Base::Console().warning( std::string("Image scale"), "The second point is too close. Retry!\n" ); } } } void InteractiveScale::getMousePosition(void* ud, SoEventCallback* ecb) { auto scale = static_cast(ud); const SoEvent* l2e = ecb->getEvent(); auto view = static_cast(ecb->getUserData()); if (scale->points.size() == 1) { ecb->setHandled(); std::unique_ptr pp(view->getPointOnRay(l2e->getPosition(), scale->viewProv)); if (pp) { SbVec3f pos3d = pp->getPoint(); scale->setDistance(pos3d); } } } void InteractiveScale::soEventFilter(void* ud, SoEventCallback* ecb) { auto scale = static_cast(ud); const SoEvent* soEvent = ecb->getEvent(); if (soEvent->isOfType(SoKeyboardEvent::getClassTypeId())) { /* If user presses escape, then we cancel the tool.*/ const auto kbe = static_cast(soEvent); // NOLINT if (kbe->getKey() == SoKeyboardEvent::ESCAPE && kbe->getState() == SoButtonEvent::UP) { ecb->setHandled(); Q_EMIT scale->scaleCanceled(); } } else if (soEvent->isOfType(SoMouseButtonEvent::getClassTypeId())) { const auto mbe = static_cast(soEvent); // NOLINT if (mbe->getButton() == SoMouseButtonEvent::BUTTON1 && mbe->getState() == SoButtonEvent::DOWN) { ecb->setHandled(); scale->findPointOnImagePlane(ecb); } if (mbe->getButton() == SoMouseButtonEvent::BUTTON2 && mbe->getState() == SoButtonEvent::DOWN) { ecb->setHandled(); Q_EMIT scale->scaleCanceled(); } } } bool InteractiveScale::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::KeyRelease) { auto keyEvent = static_cast(event); // NOLINT /* If user press enter in the spinbox, then we validate the tool.*/ if ((keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) && qobject_cast(object)) { Q_EMIT scaleRequired(); } /* If user press escape, then we cancel the tool. Required here as well for when checkbox * has focus.*/ if (keyEvent->key() == Qt::Key_Escape) { Q_EMIT scaleCanceled(); } } return false; } void InteractiveScale::setPlacement(const Base::Placement& plc) { placement = plc; measureLabel->setPlacement(plc); } SbVec3f InteractiveScale::getCoordsOnImagePlane(const SbVec3f& point) { // Plane form Base::Vector3d RX(1, 0, 0); Base::Vector3d RY(0, 1, 0); // move to position of Sketch Base::Rotation tmp(placement.getRotation()); RX = tmp.multVec(RX); RY = tmp.multVec(RY); Base::Vector3d pos = placement.getPosition(); // we use pos as the Base because in setPlacement we set transform->translation using // placement.getPosition() to fix the Zoffset. But this applies the X & Y translation too. Base::Vector3d pnt(point[0], point[1], point[2]); pnt.TransformToCoordinateSystem(pos, RX, RY); return {float(pnt.x), float(pnt.y), 0.0F}; } // ---------------------------------------------------------------------------- TaskImageDialog::TaskImageDialog(Image::ImagePlane* obj) : widget {new TaskImage(obj)} { addTaskBox(Gui::BitmapFactory().pixmap("image-plane"), widget); associateToObject3dView(obj); } void TaskImageDialog::open() { widget->open(); } bool TaskImageDialog::accept() { widget->accept(); return true; } bool TaskImageDialog::reject() { widget->reject(); return true; } #include "moc_TaskImage.cpp"