/*************************************************************************** * Copyright (c) 2021 Werner Mayer * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include "ui_TaskPadPocketParameters.h" #include "TaskExtrudeParameters.h" #include "TaskTransformedParameters.h" #include "ReferenceSelection.h" using namespace PartDesignGui; using namespace Gui; /* TRANSLATOR PartDesignGui::TaskExtrudeParameters */ TaskExtrudeParameters::TaskExtrudeParameters( ViewProviderExtrude* SketchBasedView, QWidget* parent, const std::string& pixmapname, const QString& parname ) : TaskSketchBasedParameters(SketchBasedView, parent, pixmapname, parname) , propReferenceAxis(nullptr) , ui(new Ui_TaskPadPocketParameters) { // we need a separate container widget to add all controls to proxy = new QWidget(this); ui->setupUi(proxy); handleLineFaceNameNo(ui->lineFaceName); handleLineFaceNameNo(ui->lineFaceName2); Gui::ButtonGroup* group = new Gui::ButtonGroup(this); group->addButton(ui->checkBoxReversed); group->setExclusive(true); this->groupLayout()->addWidget(proxy); } void TaskExtrudeParameters::setupDialog() { auto createRemoveAction = [this]() -> QAction* { auto action = new QAction(tr("Remove"), this); action->setShortcut(Gui::QtTools::deleteKeySequence()); action->setShortcutVisibleInContextMenu(true); return action; }; unselectShapeFaceAction = createRemoveAction(); unselectShapeFaceAction2 = createRemoveAction(); createSideControllers(); // --- Global, Non-Side-Specific Setup --- auto extrude = getObject(); int UserDecimals = Base::UnitsApi::getDecimals(); ui->XDirectionEdit->setDecimals(UserDecimals); ui->YDirectionEdit->setDecimals(UserDecimals); ui->ZDirectionEdit->setDecimals(UserDecimals); ui->checkBoxAlongDirection->setChecked(extrude->AlongSketchNormal.getValue()); ui->XDirectionEdit->setValue(extrude->Direction.getValue().x); ui->YDirectionEdit->setValue(extrude->Direction.getValue().y); ui->ZDirectionEdit->setValue(extrude->Direction.getValue().z); ui->XDirectionEdit->bind(App::ObjectIdentifier::parse(extrude, "Direction.x")); ui->YDirectionEdit->bind(App::ObjectIdentifier::parse(extrude, "Direction.y")); ui->ZDirectionEdit->bind(App::ObjectIdentifier::parse(extrude, "Direction.z")); ui->checkBoxReversed->setChecked(extrude->Reversed.getValue()); // --- Per-Side Setup using the Helper --- setupSideDialog(m_side1); setupSideDialog(m_side2); // --- Final Global UI State Setup --- translateSidesList(extrude->SideType.getValue()); connectSlots(); this->propReferenceAxis = &(extrude->ReferenceAxis); updateUI(Side::First); setupGizmos(); // trigger recompute to ensure external geometry references update correctly. // see freecad issue #25794 tryRecomputeFeature(); } void TaskExtrudeParameters::setupSideDialog(SideController& side) { // --- Get initial values from the correct side's properties --- Base::Quantity length = side.Length->getQuantityValue(); Base::Quantity offset = side.Offset->getQuantityValue(); Base::Quantity taper = side.TaperAngle->getQuantityValue(); int typeIndex = side.Type->getValue(); // The Type/Type2 properties are stuck with the deprecated 'TwoLength' mode. // Because enums do not store text just an index. So this create // a inconsistence between the UI and the property. if (typeIndex > static_cast(Mode::ToShape)) { typeIndex--; } // --- Set up UI widgets with initial values --- side.lengthEdit->setValue(length); side.offsetEdit->setValue(offset); side.taperEdit->setMinimum(side.TaperAngle->getMinimum()); side.taperEdit->setMaximum(side.TaperAngle->getMaximum()); side.taperEdit->setSingleStep(side.TaperAngle->getStepSize()); side.taperEdit->setValue(taper); // --- Bind UI widgets to the correct properties --- side.lengthEdit->bind(*side.Length); side.offsetEdit->bind(*side.Offset); side.taperEdit->bind(*side.TaperAngle); // --- Handle "Up to face" label logic --- App::DocumentObject* faceObj = side.UpToFace->getValue(); std::vector subStrings = side.UpToFace->getSubValues(); std::string upToFaceName; int faceId = -1; if (faceObj && !subStrings.empty()) { upToFaceName = subStrings.front(); if (upToFaceName.rfind("Face", 0) == 0) { // starts_with faceId = std::atoi(&upToFaceName[4]); } } if (faceObj && PartDesign::Feature::isDatum(faceObj)) { side.lineFaceName->setText(QString::fromUtf8(faceObj->Label.getValue())); side.lineFaceName->setProperty("FeatureName", QByteArray(faceObj->getNameInDocument())); } else if (faceObj && faceId >= 0) { side.lineFaceName->setText(QStringLiteral("%1:%2%3").arg( QString::fromUtf8(faceObj->Label.getValue()), tr("Face"), QString::number(faceId) )); side.lineFaceName->setProperty("FeatureName", QByteArray(faceObj->getNameInDocument())); } else { side.lineFaceName->clear(); side.lineFaceName->setProperty("FeatureName", QVariant()); } side.lineFaceName->setProperty("FaceName", QByteArray(upToFaceName.c_str())); // --- Update shape-related UI --- updateShapeName(side.lineShapeName, *side.UpToShape); updateShapeFaces(side.listWidgetReferences, *side.UpToShape); // --- Set up the mode combobox and list widget context menu --- translateModeList(side.changeMode, typeIndex); side.listWidgetReferences->addAction(side.unselectShapeFaceAction); side.listWidgetReferences->setContextMenuPolicy(Qt::ActionsContextMenu); side.checkBoxAllFaces->setChecked(side.listWidgetReferences->count() == 0); } void TaskExtrudeParameters::createSideControllers() { auto extrude = getObject(); // --- Initialize Side 1 Controller --- m_side1.changeMode = ui->changeMode; m_side1.labelLength = ui->labelLength; m_side1.labelOffset = ui->labelOffset; m_side1.labelTaperAngle = ui->labelTaperAngle; m_side1.lengthEdit = ui->lengthEdit; m_side1.offsetEdit = ui->offsetEdit; m_side1.taperEdit = ui->taperEdit; m_side1.lineFaceName = ui->lineFaceName; m_side1.buttonFace = ui->buttonFace; m_side1.lineShapeName = ui->lineShapeName; m_side1.buttonShape = ui->buttonShape; m_side1.listWidgetReferences = ui->listWidgetReferences; m_side1.buttonShapeFace = ui->buttonShapeFace; m_side1.checkBoxAllFaces = ui->checkBoxAllFaces; m_side1.upToShapeList = ui->upToShapeList; m_side1.upToShapeFaces = ui->upToShapeFaces; m_side1.unselectShapeFaceAction = unselectShapeFaceAction; m_side1.Type = &extrude->Type; m_side1.Length = &extrude->Length; m_side1.Offset = &extrude->Offset; m_side1.TaperAngle = &extrude->TaperAngle; m_side1.UpToFace = &extrude->UpToFace; m_side1.UpToShape = &extrude->UpToShape; // --- Initialize Side 2 Controller --- m_side2.changeMode = ui->changeMode2; m_side2.labelLength = ui->labelLength2; m_side2.labelOffset = ui->labelOffset2; m_side2.labelTaperAngle = ui->labelTaperAngle2; m_side2.lengthEdit = ui->lengthEdit2; m_side2.offsetEdit = ui->offsetEdit2; m_side2.taperEdit = ui->taperEdit2; m_side2.lineFaceName = ui->lineFaceName2; m_side2.buttonFace = ui->buttonFace2; m_side2.lineShapeName = ui->lineShapeName2; m_side2.buttonShape = ui->buttonShape2; m_side2.listWidgetReferences = ui->listWidgetReferences2; m_side2.buttonShapeFace = ui->buttonShapeFace2; m_side2.checkBoxAllFaces = ui->checkBoxAllFaces2; m_side2.upToShapeList = ui->upToShapeList2; m_side2.upToShapeFaces = ui->upToShapeFaces2; m_side2.unselectShapeFaceAction = unselectShapeFaceAction2; m_side2.Type = &extrude->Type2; m_side2.Length = &extrude->Length2; m_side2.Offset = &extrude->Offset2; m_side2.TaperAngle = &extrude->TaperAngle2; m_side2.UpToFace = &extrude->UpToFace2; m_side2.UpToShape = &extrude->UpToShape2; } void TaskExtrudeParameters::readValuesFromHistory() { ui->lengthEdit->setToLastUsedValue(); ui->lengthEdit->selectNumber(); ui->lengthEdit2->setToLastUsedValue(); ui->lengthEdit2->selectNumber(); ui->offsetEdit->setToLastUsedValue(); ui->offsetEdit->selectNumber(); ui->offsetEdit2->setToLastUsedValue(); ui->offsetEdit2->selectNumber(); ui->taperEdit->setToLastUsedValue(); ui->taperEdit->selectNumber(); ui->taperEdit2->setToLastUsedValue(); ui->taperEdit2->selectNumber(); } void TaskExtrudeParameters::connectSlots() { QMetaObject::connectSlotsByName(this); auto connectSideSlots = [this](auto& side, Side sideEnum, auto modeChangedSlot) { connect( side.lengthEdit, qOverload(&Gui::PrefQuantitySpinBox::valueChanged), this, [this, sideEnum](double val) { onLengthChanged(val, sideEnum); } ); connect( side.offsetEdit, qOverload(&Gui::PrefQuantitySpinBox::valueChanged), this, [this, sideEnum](double val) { onOffsetChanged(val, sideEnum); } ); connect( side.taperEdit, qOverload(&Gui::PrefQuantitySpinBox::valueChanged), this, [this, sideEnum](double val) { onTaperChanged(val, sideEnum); } ); connect(side.changeMode, qOverload(&QComboBox::currentIndexChanged), this, modeChangedSlot); connect(side.buttonFace, &QToolButton::toggled, this, [this, sideEnum](bool checked) { onSelectFaceToggle(checked, sideEnum); }); connect(side.lineFaceName, &QLineEdit::textEdited, this, [this, sideEnum](const QString& text) { onFaceName(text, sideEnum); }); connect(side.checkBoxAllFaces, &QCheckBox::toggled, this, [this, sideEnum](bool checked) { onAllFacesToggled(checked, sideEnum); }); connect(side.buttonShape, &QToolButton::toggled, this, [this, sideEnum](bool checked) { onSelectShapeToggle(checked, sideEnum); }); connect(side.buttonShapeFace, &QToolButton::toggled, this, [this, sideEnum](bool checked) { onSelectShapeFacesToggle(checked, sideEnum); }); connect(side.unselectShapeFaceAction, &QAction::triggered, this, [this, sideEnum]() { onUnselectShapeFacesTrigger(sideEnum); }); }; // Use the lambda to connect slots for both sides connectSideSlots(m_side1, Side::First, &TaskExtrudeParameters::onModeChanged_Side1); connectSideSlots(m_side2, Side::Second, &TaskExtrudeParameters::onModeChanged_Side2); // clang-format off connect(ui->directionCB, qOverload(&QComboBox::activated), this, &TaskExtrudeParameters::onDirectionCBChanged); connect(ui->checkBoxAlongDirection, &QCheckBox::toggled, this, &TaskExtrudeParameters::onAlongSketchNormalChanged); connect(ui->XDirectionEdit, qOverload(&QDoubleSpinBox::valueChanged), this, &TaskExtrudeParameters::onXDirectionEditChanged); connect(ui->YDirectionEdit, qOverload(&QDoubleSpinBox::valueChanged), this, &TaskExtrudeParameters::onYDirectionEditChanged); connect(ui->ZDirectionEdit, qOverload(&QDoubleSpinBox::valueChanged), this, &TaskExtrudeParameters::onZDirectionEditChanged); connect(ui->checkBoxReversed, &QCheckBox::toggled, this, &TaskExtrudeParameters::onReversedChanged); connect(ui->sidesMode, qOverload(&QComboBox::currentIndexChanged), this, &TaskExtrudeParameters::onSidesModeChanged); connect(ui->checkBoxUpdateView, &QCheckBox::toggled, this, &TaskExtrudeParameters::onUpdateView); // clang-format on } void TaskExtrudeParameters::onModeChanged_Side1(int index) { onModeChanged(index, Side::First); setGizmoPositions(); } void TaskExtrudeParameters::onModeChanged_Side2(int index) { onModeChanged(index, Side::Second); setGizmoPositions(); } void TaskExtrudeParameters::onSelectShapeFacesToggle(bool checked, Side side) { auto& sideCtrl = getSideController(side); if (checked) { setSelectionMode(SelectShapeFaces, side); sideCtrl.buttonShapeFace->setText(tr("Preview")); } else { setSelectionMode(None); sideCtrl.buttonShapeFace->setText(tr("Select Faces")); } } void PartDesignGui::TaskExtrudeParameters::onUnselectShapeFacesTrigger(Side side) { auto& sideCtrl = getSideController(side); auto selected = sideCtrl.listWidgetReferences->selectedItems(); auto faces = getShapeFaces(*sideCtrl.UpToShape); faces.erase(std::remove_if(faces.begin(), faces.end(), [selected](const std::string& face) { for (auto& item : selected) { if (item->text().toStdString() == face) { return true; } } return false; })); sideCtrl.UpToShape->setValue(sideCtrl.UpToShape->getValue(), faces); updateShapeFaces(sideCtrl.listWidgetReferences, *sideCtrl.UpToShape); } void TaskExtrudeParameters::setSelectionMode(SelectionMode mode, Side side) { if (selectionMode == mode && activeSelectionSide == side) { return; } const auto updateCheckedForSide = [mode, side]( Side relatedSide, QAbstractButton* buttonFace, QAbstractButton* buttonShape, QAbstractButton* buttonShapeFace ) { buttonFace->setChecked(mode == SelectFace && side == relatedSide); buttonShape->setChecked(mode == SelectShape && side == relatedSide); buttonShapeFace->setChecked(mode == SelectShapeFaces && side == relatedSide); }; updateCheckedForSide(Side::First, ui->buttonFace, ui->buttonShape, ui->buttonShapeFace); updateCheckedForSide(Side::Second, ui->buttonFace2, ui->buttonShape2, ui->buttonShapeFace2); selectionMode = mode; activeSelectionSide = side; switch (mode) { case SelectShape: onSelectReference(AllowSelection::WHOLE); Gui::Selection().addSelectionGate(new SelectionFilterGate("SELECT Part::Feature COUNT 1")); break; case SelectFace: onSelectReference(AllowSelection::FACE); break; case SelectShapeFaces: { onSelectReference(AllowSelection::FACE); auto& sideCtrl = getSideController(activeSelectionSide); getViewObject()->highlightShapeFaces( getShapeFaces(*sideCtrl.UpToShape) ); break; } case SelectReferenceAxis: onSelectReference(AllowSelection::EDGE | AllowSelection::PLANAR | AllowSelection::CIRCLE); break; default: getViewObject()->highlightShapeFaces({}); onSelectReference(AllowSelection::NONE); } } void TaskExtrudeParameters::tryRecomputeFeature() { try { // recompute and update the direction recomputeFeature(); } catch (const Base::Exception& e) { e.reportException(); } } void TaskExtrudeParameters::onSelectionChanged(const Gui::SelectionChanges& msg) { auto& sideCtrl = getSideController(activeSelectionSide); if (msg.Type == Gui::SelectionChanges::AddSelection) { switch (selectionMode) { case SelectShape: selectedShape(msg, sideCtrl); break; case SelectShapeFaces: selectedShapeFace(msg, sideCtrl); break; case SelectFace: selectedFace(msg, sideCtrl); break; case SelectReferenceAxis: selectedReferenceAxis(msg); break; default: // no-op break; } } else if (msg.Type == Gui::SelectionChanges::ClrSelection) { if (selectionMode == SelectFace) { clearFaceName(sideCtrl.lineFaceName); } } } void TaskExtrudeParameters::selectedReferenceAxis(const Gui::SelectionChanges& msg) { std::vector edge; App::DocumentObject* selObj; if (getReferencedSelection(getObject(), msg, selObj, edge) && selObj) { setSelectionMode(None); propReferenceAxis->setValue(selObj, edge); tryRecomputeFeature(); // update direction combobox fillDirectionCombo(); setGizmoPositions(); } } void TaskExtrudeParameters::selectedShapeFace(const Gui::SelectionChanges& msg, SideController& side) { auto extrude = getObject(); auto document = extrude->getDocument(); if (strcmp(msg.pDocName, document->getName()) != 0) { return; } // Get the base shape from the correct side's property auto base = static_cast(side.UpToShape->getValue()); if (!base) { base = static_cast(extrude); } else if (strcmp(msg.pObjectName, base->getNameInDocument()) != 0) { return; } std::vector faces = getShapeFaces(*side.UpToShape); const std::string subName(msg.pSubName); if (subName.empty()) { return; } // Add or remove the face from the list if (const auto positionInList = std::ranges::find(faces, subName); positionInList != faces.end()) { faces.erase(positionInList); // Remove it if it exists } else { faces.push_back(subName); // Add it if it's new } side.UpToShape->setValue(base, faces); updateShapeFaces(side.listWidgetReferences, *side.UpToShape); tryRecomputeFeature(); } void PartDesignGui::TaskExtrudeParameters::selectedFace( const Gui::SelectionChanges& msg, SideController& side ) { QString refText = onAddSelection(msg, *side.UpToFace); if (refText.length() > 0) { QSignalBlocker block(side.lineFaceName); side.lineFaceName->setText(refText); side.lineFaceName->setProperty("FeatureName", QByteArray(msg.pObjectName)); side.lineFaceName->setProperty("FaceName", QByteArray(msg.pSubName)); // Turn off reference selection mode side.buttonFace->setChecked(false); } else { clearFaceName(side.lineFaceName); } setSelectionMode(None); } void PartDesignGui::TaskExtrudeParameters::selectedShape( const Gui::SelectionChanges& msg, SideController& side ) { auto document = getObject()->getDocument(); if (strcmp(msg.pDocName, document->getName()) != 0) { return; } Gui::Selection().clearSelection(); auto ref = document->getObject(msg.pObjectName); side.UpToShape->setValue(ref); side.checkBoxAllFaces->setChecked(true); setSelectionMode(None); updateShapeName(side.lineShapeName, *side.UpToShape); updateShapeFaces(side.listWidgetReferences, *side.UpToShape); tryRecomputeFeature(); } void TaskExtrudeParameters::clearFaceName(QLineEdit* lineEdit) { QSignalBlocker block(lineEdit); lineEdit->clear(); lineEdit->setProperty("FeatureName", QVariant()); lineEdit->setProperty("FaceName", QVariant()); } void TaskExtrudeParameters::updateShapeName(QLineEdit* lineEdit, App::PropertyLinkSubList& prop) { QSignalBlocker block(lineEdit); auto shape = prop.getValue(); if (shape) { lineEdit->setText(QString::fromStdString(shape->getFullName())); } else { lineEdit->setText({}); lineEdit->setPlaceholderText(tr("No shape selected")); } } void TaskExtrudeParameters::updateShapeFaces(QListWidget* list, App::PropertyLinkSubList& prop) { auto faces = getShapeFaces(prop); list->clear(); for (auto& ref : faces) { list->addItem(QString::fromStdString(ref)); } if (selectionMode == SelectShapeFaces) { getViewObject()->highlightShapeFaces(faces); } } std::vector PartDesignGui::TaskExtrudeParameters::getShapeFaces( App::PropertyLinkSubList& prop ) { std::vector faces; auto allRefs = prop.getSubValues(); std::copy_if(allRefs.begin(), allRefs.end(), std::back_inserter(faces), [](const std::string& ref) { return boost::starts_with(ref, "Face"); }); return faces; } void TaskExtrudeParameters::onLengthChanged(double len, Side side) { getSideController(side).Length->setValue(len); tryRecomputeFeature(); } void TaskExtrudeParameters::onOffsetChanged(double len, Side side) { getSideController(side).Offset->setValue(len); tryRecomputeFeature(); } void TaskExtrudeParameters::onTaperChanged(double angle, Side side) { getSideController(side).TaperAngle->setValue(angle); tryRecomputeFeature(); } bool TaskExtrudeParameters::hasProfileFace(PartDesign::ProfileBased* profile) const { try { Part::Feature* pcFeature = profile->getVerifiedObject(); Base::Vector3d SketchVector = profile->getProfileNormal(); Q_UNUSED(pcFeature) Q_UNUSED(SketchVector) return true; } catch (const Base::Exception&) { } return false; } void TaskExtrudeParameters::fillDirectionCombo() { Base::StateLocker lock(getUpdateBlockRef(), true); if (axesInList.empty()) { bool hasFace = false; ui->directionCB->clear(); // we can have sketches or faces // for sketches just get the sketch normal auto pcFeat = getObject(); Part::Part2DObject* pcSketch = dynamic_cast(pcFeat->Profile.getValue()); // for faces we test if it is verified and if we can get its normal if (!pcSketch) { hasFace = hasProfileFace(pcFeat); } if (pcSketch) { addAxisToCombo(pcSketch, "N_Axis", tr("Sketch normal")); } else if (hasFace) { addAxisToCombo(pcFeat->Profile.getValue(), std::string(), tr("Face normal"), false); } // add the other entries addAxisToCombo(nullptr, std::string(), tr("Select reference…")); // we start with the sketch normal as proposal for the custom direction if (pcSketch) { addAxisToCombo(pcSketch, "N_Axis", tr("Custom direction")); } else if (hasFace) { addAxisToCombo(pcFeat->Profile.getValue(), std::string(), tr("Custom direction"), false); } } // add current link, if not in list // first, figure out the item number for current axis int indexOfCurrent = -1; App::DocumentObject* ax = propReferenceAxis->getValue(); const std::vector& subList = propReferenceAxis->getSubValues(); for (size_t i = 0; i < axesInList.size(); i++) { if (ax == axesInList[i]->getValue() && subList == axesInList[i]->getSubValues()) { indexOfCurrent = i; break; } } // if the axis is not yet listed in the combobox if (indexOfCurrent == -1 && ax) { assert(subList.size() <= 1); std::string sub; if (!subList.empty()) { sub = subList[0]; } addAxisToCombo(ax, sub, getRefStr(ax, subList)); indexOfCurrent = axesInList.size() - 1; // the axis is not the normal, thus enable along direction ui->checkBoxAlongDirection->setEnabled(true); // we don't have custom direction thus disable its settings ui->XDirectionEdit->setEnabled(false); ui->YDirectionEdit->setEnabled(false); ui->ZDirectionEdit->setEnabled(false); } // highlight either current index or set custom direction auto extrude = getObject(); bool hasCustom = extrude->UseCustomVector.getValue(); if (indexOfCurrent != -1 && !hasCustom) { ui->directionCB->setCurrentIndex(indexOfCurrent); updateDirectionEdits(); setDirectionMode(indexOfCurrent); } if (hasCustom) { ui->directionCB->setCurrentIndex(DirectionModes::Custom); setDirectionMode(ui->directionCB->currentIndex()); } } void TaskExtrudeParameters::addAxisToCombo( App::DocumentObject* linkObj, std::string linkSubname, QString itemText, bool hasSketch ) { this->ui->directionCB->addItem(itemText); this->axesInList.emplace_back(new App::PropertyLinkSub); App::PropertyLinkSub& lnk = *(axesInList.back()); // if we have a face, we leave the link empty since we cannot // store the face normal as sublink if (hasSketch) { lnk.setValue(linkObj, std::vector(1, linkSubname)); } } void TaskExtrudeParameters::updateWholeUI(Type type, Side side) { SidesMode sidesMode = static_cast(ui->sidesMode->currentIndex()); Mode mode1 = static_cast(ui->changeMode->currentIndex()); Mode mode2 = static_cast(ui->changeMode2->currentIndex()); // --- Global UI visibility based on SidesMode --- const bool isSide2GroupVisible = (sidesMode == SidesMode::TwoSides); ui->side1Label->setVisible(isSide2GroupVisible); ui->side2Label->setVisible(isSide2GroupVisible); ui->line2->setVisible(isSide2GroupVisible); ui->typeLabel2->setVisible(isSide2GroupVisible); ui->changeMode2->setVisible(isSide2GroupVisible); // --- Configure each side using the helper method --- // Side 1 is always conceptually visible, and we pass whether it should receive focus. updateSideUI(m_side1, type, mode1, true, (side == Side::First)); // Side 2 is only visible if in TwoSides mode, and we pass whether it should receive focus. updateSideUI(m_side2, type, mode2, isSide2GroupVisible, (side == Side::Second)); ui->checkBoxReversed->setEnabled(sidesMode != SidesMode::Symmetric || mode1 != Mode::Dimension); } void TaskExtrudeParameters::updateSideUI( const SideController& s, Type featureType, Mode sideMode, bool isParentVisible, bool setFocus ) { // Default states for all controls for this side bool isLengthVisible = false; bool isOffsetVisible = false; bool isTaperVisible = false; bool isFaceVisible = false; bool isShapeVisible = false; // This logic block is a direct translation of the original 'if/else if' chain if (sideMode == Mode::Dimension) { isLengthVisible = true; isTaperVisible = true; if (setFocus) { s.lengthEdit->selectNumber(); QMetaObject::invokeMethod(s.lengthEdit, "setFocus", Qt::QueuedConnection); } } else if (sideMode == Mode::ThroughAll && featureType == Type::Pocket) { isTaperVisible = true; } else if (sideMode == Mode::ToLast && featureType == Type::Pad) { isOffsetVisible = true; } else if (sideMode == Mode::ToFirst) { isOffsetVisible = true; } else if (sideMode == Mode::ToFace) { isOffsetVisible = true; isFaceVisible = true; if (setFocus) { QMetaObject::invokeMethod(s.lineFaceName, "setFocus", Qt::QueuedConnection); // Go into reference selection mode if no face has been selected yet if (s.lineFaceName->property("FeatureName").isNull()) { s.buttonFace->setChecked(true); } } } else if (sideMode == Mode::ToShape) { isShapeVisible = true; if (setFocus) { if (!s.checkBoxAllFaces->isChecked()) { s.buttonShapeFace->setChecked(true); } } } // Apply visibility based on the logic above AND the parent visibility. // This single 'isParentVisible' check correctly hides all of Side 2's UI at once. const bool finalLengthVisible = isParentVisible && isLengthVisible; s.labelLength->setVisible(finalLengthVisible); s.lengthEdit->setVisible(finalLengthVisible); s.lengthEdit->setEnabled(finalLengthVisible); const bool finalOffsetVisible = isParentVisible && isOffsetVisible; s.labelOffset->setVisible(finalOffsetVisible); s.offsetEdit->setVisible(finalOffsetVisible); s.offsetEdit->setEnabled(finalOffsetVisible); const bool finalTaperVisible = isParentVisible && isTaperVisible; s.labelTaperAngle->setVisible(finalTaperVisible); s.taperEdit->setVisible(finalTaperVisible); s.taperEdit->setEnabled(finalTaperVisible); s.buttonFace->setVisible(isParentVisible && isFaceVisible); s.lineFaceName->setVisible(isParentVisible && isFaceVisible); if (!isFaceVisible) { s.buttonFace->setChecked(false); // Ensure button is unchecked when hidden } s.upToShapeList->setVisible(isParentVisible && isShapeVisible); } void TaskExtrudeParameters::onDirectionCBChanged(int num) { if (axesInList.empty()) { return; } // we use this scheme for 'num' // 0: normal to sketch or face // 1: selection mode // 2: custom // 3-x: edges selected in the 3D model // check the axis // when the link is empty we are either in selection mode // or we are normal to a face App::PropertyLinkSub& lnk = *(axesInList[num]); if (num == DirectionModes::Select) { // to distinguish that this is the direction selection setSelectionMode(SelectReferenceAxis); setDirectionMode(num); } else if (auto extrude = getObject()) { if (lnk.getValue()) { if (!extrude->getDocument()->isIn(lnk.getValue())) { Base::Console().error("Object was deleted\n"); return; } propReferenceAxis->Paste(lnk); } // in case the user is in selection mode, but changed his mind before selecting anything setSelectionMode(None); setDirectionMode(num); extrude->ReferenceAxis.setValue(lnk.getValue(), lnk.getSubValues()); tryRecomputeFeature(); updateDirectionEdits(); setGizmoPositions(); } } void TaskExtrudeParameters::onAlongSketchNormalChanged(bool on) { if (auto extrude = getObject()) { extrude->AlongSketchNormal.setValue(on); tryRecomputeFeature(); setGizmoPositions(); } } void TaskExtrudeParameters::onAllFacesToggled(bool on, Side side) { auto& sideCtrl = getSideController(side); sideCtrl.upToShapeFaces->setVisible(!on); sideCtrl.buttonShapeFace->setChecked(false); if (on) { if (auto extrude = getObject()) { extrude->UpToShape.setValue(extrude->UpToShape.getValue()); updateShapeFaces(sideCtrl.listWidgetReferences, *sideCtrl.UpToShape); tryRecomputeFeature(); } } } void TaskExtrudeParameters::onXDirectionEditChanged(double len) { if (auto extrude = getObject()) { extrude->Direction .setValue(len, extrude->Direction.getValue().y, extrude->Direction.getValue().z); tryRecomputeFeature(); // checking for case of a null vector is done in FeatureExtrude.cpp // if there was a null vector, the normal vector of the sketch is used. // therefore the vector component edits must be updated updateDirectionEdits(); setGizmoPositions(); } } void TaskExtrudeParameters::onYDirectionEditChanged(double len) { if (auto extrude = getObject()) { extrude->Direction .setValue(extrude->Direction.getValue().x, len, extrude->Direction.getValue().z); tryRecomputeFeature(); updateDirectionEdits(); setGizmoPositions(); } } void TaskExtrudeParameters::onZDirectionEditChanged(double len) { if (auto extrude = getObject()) { extrude->Direction .setValue(extrude->Direction.getValue().x, extrude->Direction.getValue().y, len); tryRecomputeFeature(); updateDirectionEdits(); setGizmoPositions(); } } void TaskExtrudeParameters::updateDirectionEdits() { auto extrude = getObject(); // we don't want to execute the onChanged edits, but just update their contents QSignalBlocker xdir(ui->XDirectionEdit); QSignalBlocker ydir(ui->YDirectionEdit); QSignalBlocker zdir(ui->ZDirectionEdit); ui->XDirectionEdit->setValue(extrude->Direction.getValue().x); ui->YDirectionEdit->setValue(extrude->Direction.getValue().y); ui->ZDirectionEdit->setValue(extrude->Direction.getValue().z); } void TaskExtrudeParameters::setDirectionMode(int index) { auto extrude = getObject(); if (!extrude) { return; } switch (index) { case DirectionModes::Normal: ui->groupBoxDirection->hide(); extrude->UseCustomVector.setValue(false); ui->XDirectionEdit->setEnabled(false); ui->YDirectionEdit->setEnabled(false); ui->ZDirectionEdit->setEnabled(false); ui->checkBoxAlongDirection->setEnabled(false); ui->checkBoxAlongDirection->hide(); break; // Covered by the default option // case DirectionModes::Select: case DirectionModes::Custom: ui->groupBoxDirection->show(); extrude->UseCustomVector.setValue(true); ui->XDirectionEdit->setEnabled(true); ui->YDirectionEdit->setEnabled(true); ui->ZDirectionEdit->setEnabled(true); ui->checkBoxAlongDirection->setEnabled(true); ui->checkBoxAlongDirection->show(); break; default: ui->groupBoxDirection->show(); updateDirectionEdits(); extrude->UseCustomVector.setValue(false); ui->XDirectionEdit->setEnabled(false); ui->YDirectionEdit->setEnabled(false); ui->ZDirectionEdit->setEnabled(false); ui->checkBoxAlongDirection->setEnabled(true); ui->checkBoxAlongDirection->show(); break; } } void TaskExtrudeParameters::onReversedChanged(bool on) { if (auto extrude = getObject()) { extrude->Reversed.setValue(on); // update the direction tryRecomputeFeature(); updateDirectionEdits(); setGizmoPositions(); } } void TaskExtrudeParameters::getReferenceAxis(App::DocumentObject*& obj, std::vector& sub) const { if (axesInList.empty()) { throw Base::RuntimeError("Not initialized!"); } int num = ui->directionCB->currentIndex(); const App::PropertyLinkSub& lnk = *(axesInList[num]); if (!lnk.getValue()) { // Note: It is possible that a face of an object is directly padded/pocketed without // defining a profile shape obj = nullptr; sub.clear(); } else { auto pcDirection = getObject(); if (!pcDirection->getDocument()->isIn(lnk.getValue())) { throw Base::RuntimeError("Object was deleted"); } obj = lnk.getValue(); sub = lnk.getSubValues(); } } void TaskExtrudeParameters::onSelectFaceToggle(const bool checked, Side side) { auto& sideCtrl = getSideController(side); if (checked) { handleLineFaceNameClick(sideCtrl.lineFaceName); setSelectionMode(SelectFace, side); } else { handleLineFaceNameNo(sideCtrl.lineFaceName); } } void TaskExtrudeParameters::onSelectShapeToggle(bool checked, Side side) { auto& sideCtrl = getSideController(side); if (checked) { setSelectionMode(SelectShape, side); sideCtrl.lineShapeName->setText({}); sideCtrl.lineShapeName->setPlaceholderText(tr("Click on a shape in the model")); } else { setSelectionMode(None); updateShapeName(sideCtrl.lineShapeName, *sideCtrl.UpToShape); } } void TaskExtrudeParameters::onFaceName(const QString& text, Side side) { auto& sideCtrl = getSideController(side); changeFaceName(sideCtrl.lineFaceName, text); } void TaskExtrudeParameters::changeFaceName(QLineEdit* lineEdit, const QString& text) { if (text.isEmpty()) { // if user cleared the text field then also clear the properties lineEdit->setProperty("FeatureName", QVariant()); lineEdit->setProperty("FaceName", QVariant()); } else { // expect that the label of an object is used QStringList parts = text.split(QChar::fromLatin1(':')); QString label = parts[0]; QVariant name = objectNameByLabel(label, lineEdit->property("FeatureName")); if (name.isValid()) { parts[0] = name.toString(); QString uptoface = parts.join(QStringLiteral(":")); lineEdit->setProperty("FeatureName", name); lineEdit->setProperty("FaceName", setUpToFace(uptoface)); } else { lineEdit->setProperty("FeatureName", QVariant()); lineEdit->setProperty("FaceName", QVariant()); } } } void TaskExtrudeParameters::translateFaceName(QLineEdit* lineEdit) { handleLineFaceNameNo(lineEdit); QVariant featureName = lineEdit->property("FeatureName"); if (featureName.isValid()) { QStringList parts = lineEdit->text().split(QChar::fromLatin1(':')); QByteArray upToFace = lineEdit->property("FaceName").toByteArray(); int faceId = -1; bool ok = false; if (upToFace.indexOf("Face") == 0) { faceId = upToFace.remove(0, 4).toInt(&ok); } if (ok) { lineEdit->setText(QStringLiteral("%1:%2%3").arg(parts[0], tr("Face")).arg(faceId)); } else { lineEdit->setText(parts[0]); } } } double TaskExtrudeParameters::getOffset() const { return ui->offsetEdit->value().getValue(); } double TaskExtrudeParameters::getOffset2() const { return ui->offsetEdit2->value().getValue(); } bool TaskExtrudeParameters::getAlongSketchNormal() const { return ui->checkBoxAlongDirection->isChecked(); } bool TaskExtrudeParameters::getCustom() const { return (ui->directionCB->currentIndex() == DirectionModes::Custom); } std::string TaskExtrudeParameters::getReferenceAxis() const { std::vector sub; App::DocumentObject* obj; getReferenceAxis(obj, sub); return buildLinkSingleSubPythonStr(obj, sub); } double TaskExtrudeParameters::getXDirection() const { return ui->XDirectionEdit->value(); } double TaskExtrudeParameters::getYDirection() const { return ui->YDirectionEdit->value(); } double TaskExtrudeParameters::getZDirection() const { return ui->ZDirectionEdit->value(); } bool TaskExtrudeParameters::getReversed() const { return ui->checkBoxReversed->isChecked(); } int TaskExtrudeParameters::getMode() const { return ui->changeMode->currentIndex(); } int TaskExtrudeParameters::getMode2() const { return ui->changeMode2->currentIndex(); } int TaskExtrudeParameters::getSidesMode() const { return ui->sidesMode->currentIndex(); } QString TaskExtrudeParameters::getFaceName(QLineEdit* lineEdit) const { QVariant featureName = lineEdit->property("FeatureName"); if (featureName.isValid()) { QString faceName = lineEdit->property("FaceName").toString(); return getFaceReference(featureName.toString(), faceName); } return QStringLiteral("None"); } void TaskExtrudeParameters::changeEvent(QEvent* e) { TaskBox::changeEvent(e); if (e->type() == QEvent::LanguageChange) { QSignalBlocker length(ui->lengthEdit); QSignalBlocker length2(ui->lengthEdit2); QSignalBlocker offset(ui->offsetEdit); QSignalBlocker offset2(ui->offsetEdit2); QSignalBlocker taper(ui->taperEdit); QSignalBlocker taper2(ui->taperEdit2); QSignalBlocker xdir(ui->XDirectionEdit); QSignalBlocker ydir(ui->YDirectionEdit); QSignalBlocker zdir(ui->ZDirectionEdit); QSignalBlocker dir(ui->directionCB); QSignalBlocker face(ui->lineFaceName); QSignalBlocker face2(ui->lineFaceName2); QSignalBlocker mode(ui->changeMode); QSignalBlocker mode2(ui->changeMode2); QSignalBlocker sidesMode(ui->sidesMode); // Save all items QStringList items; for (int i = 0; i < ui->directionCB->count(); i++) { items << ui->directionCB->itemText(i); } // Translate direction items int index = ui->directionCB->currentIndex(); ui->retranslateUi(proxy); // Keep custom items for (int i = 0; i < ui->directionCB->count(); i++) { items.pop_front(); } ui->directionCB->addItems(items); ui->directionCB->setCurrentIndex(index); // Translate mode items translateModeList(ui->changeMode, ui->changeMode->currentIndex()); translateModeList(ui->changeMode2, ui->changeMode2->currentIndex()); translateSidesList(ui->sidesMode->currentIndex()); translateFaceName(ui->lineFaceName); translateFaceName(ui->lineFaceName2); } } void TaskExtrudeParameters::saveHistory() { // save the user values to history ui->lengthEdit->pushToHistory(); ui->lengthEdit2->pushToHistory(); ui->offsetEdit->pushToHistory(); ui->offsetEdit2->pushToHistory(); ui->taperEdit->pushToHistory(); ui->taperEdit2->pushToHistory(); } void TaskExtrudeParameters::applyParameters() { auto obj = getObject(); QString facename = QStringLiteral("None"); QString facename2 = QStringLiteral("None"); if (static_cast(getMode()) == Mode::ToFace) { facename = getFaceName(ui->lineFaceName); } if (static_cast(getMode2()) == Mode::ToFace) { facename2 = getFaceName(ui->lineFaceName2); } // Handle deprecated 'TwoLength' mode. int type1 = getMode(); if (static_cast(type1) == Mode::ToShape) { type1++; } int type2 = getMode2(); if (static_cast(type2) == Mode::ToShape) { type2++; } ui->lengthEdit->apply(); ui->lengthEdit2->apply(); ui->taperEdit->apply(); ui->taperEdit2->apply(); FCMD_OBJ_CMD(obj, "UseCustomVector = " << (getCustom() ? 1 : 0)); FCMD_OBJ_CMD( obj, "Direction = (" << getXDirection() << ", " << getYDirection() << ", " << getZDirection() << ")" ); FCMD_OBJ_CMD(obj, "ReferenceAxis = " << getReferenceAxis()); FCMD_OBJ_CMD(obj, "AlongSketchNormal = " << (getAlongSketchNormal() ? 1 : 0)); FCMD_OBJ_CMD(obj, "SideType = " << getSidesMode()); FCMD_OBJ_CMD(obj, "Type = " << type1); FCMD_OBJ_CMD(obj, "Type2 = " << type2); FCMD_OBJ_CMD(obj, "UpToFace = " << facename.toUtf8().data()); FCMD_OBJ_CMD(obj, "UpToFace2 = " << facename2.toUtf8().data()); FCMD_OBJ_CMD(obj, "Reversed = " << (getReversed() ? 1 : 0)); FCMD_OBJ_CMD(obj, "Offset = " << getOffset()); FCMD_OBJ_CMD(obj, "Offset2 = " << getOffset2()); } void TaskExtrudeParameters::onSidesModeChanged(int index) { auto extrude = getObject(); switch (static_cast(index)) { case SidesMode::OneSide: extrude->SideType.setValue("One side"); updateUI(Side::First); break; case SidesMode::TwoSides: extrude->SideType.setValue("Two sides"); updateUI(Side::Second); break; case SidesMode::Symmetric: extrude->SideType.setValue("Symmetric"); updateUI(Side::First); break; } recomputeFeature(); } void TaskExtrudeParameters::updateUI(Side) { // implement in sub-class } void TaskExtrudeParameters::translateModeList(QComboBox*, int) { // implement in sub-class } void TaskExtrudeParameters::translateSidesList(int index) { ui->sidesMode->clear(); ui->sidesMode->addItem(tr("One sided")); ui->sidesMode->addItem(tr("Two sided")); ui->sidesMode->addItem(tr("Symmetric")); ui->sidesMode->setCurrentIndex(index); } void TaskExtrudeParameters::handleLineFaceNameClick(QLineEdit* lineEdit) { lineEdit->setPlaceholderText(tr("Click on a face in the model")); } void TaskExtrudeParameters::handleLineFaceNameNo(QLineEdit* lineEdit) { lineEdit->setPlaceholderText(tr("No face selected")); } void TaskExtrudeParameters::setupGizmos() { if (GizmoContainer::isEnabled() == false) { return; } lengthGizmo1 = new Gui::LinearGizmo(ui->lengthEdit); lengthGizmo2 = new Gui::LinearGizmo(ui->lengthEdit2); taperAngleGizmo1 = new Gui::RotationGizmo(ui->taperEdit); taperAngleGizmo2 = new Gui::RotationGizmo(ui->taperEdit2); connect(ui->sidesMode, qOverload(&QComboBox::currentIndexChanged), [this](int) { setGizmoPositions(); }); gizmoContainer = GizmoContainer::create( {lengthGizmo1, lengthGizmo2, taperAngleGizmo1, taperAngleGizmo2}, vp ); setGizmoPositions(); } void TaskExtrudeParameters::setGizmoPositions() { if (!gizmoContainer) { return; } auto extrude = getObject(); if (!extrude || extrude->isError()) { gizmoContainer->visible = false; return; } gizmoContainer->visible = true; PartDesign::TopoShape shape = extrude->getProfileShape(); Base::Vector3d center = getMidPointFromProfile(shape); std::string sideType = std::string(extrude->SideType.getValueAsString()); std::string extrudeType = std::string(extrude->Type.getValueAsString()); std::string extrudeType2 = std::string(extrude->Type2.getValueAsString()); double dir = extrude->Reversed.getValue() ? -1 : 1; lengthGizmo1->Gizmo::setDraggerPlacement(center, extrude->Direction.getValue() * dir); lengthGizmo1->setVisibility(extrudeType == "Length"); taperAngleGizmo1->placeOverLinearGizmo(lengthGizmo1); taperAngleGizmo1->setVisibility(extrudeType == "Length"); lengthGizmo2->Gizmo::setDraggerPlacement(center, -extrude->Direction.getValue() * dir); lengthGizmo2->setVisibility(sideType == "Two sides" && extrudeType2 == "Length"); taperAngleGizmo2->placeOverLinearGizmo(lengthGizmo2); taperAngleGizmo2->setVisibility(sideType == "Two sides" && extrudeType2 == "Length"); Base::Vector3d padDir = extrude->Direction.getValue().Normalized(); Base::Vector3d sketchDir = extrude->getProfileNormal().Normalized(); double lengthFactor = padDir.Dot(sketchDir); double multFactor = (sideType == "Symmetric") ? 0.5 : 1.0; // Important note: This code assumes that nothing other than alongSketchNormal // and symmetric option influence the multFactor. If some custom gizmos changes // it then that also should be handled properly here if (extrude->AlongSketchNormal.getValue()) { lengthGizmo1->setMultFactor(multFactor / lengthFactor); lengthGizmo2->setMultFactor(multFactor / lengthFactor); } else { lengthGizmo1->setMultFactor(multFactor); lengthGizmo2->setMultFactor(multFactor); } gizmoContainer->calculateScaleAndOrientation(); } TaskDlgExtrudeParameters::TaskDlgExtrudeParameters(PartDesignGui::ViewProviderExtrude* vp) : TaskDlgSketchBasedParameters(vp) {} bool TaskDlgExtrudeParameters::accept() { getTaskParameters()->setSelectionMode(TaskExtrudeParameters::None); return TaskDlgSketchBasedParameters::accept(); } bool TaskDlgExtrudeParameters::reject() { getTaskParameters()->setSelectionMode(TaskExtrudeParameters::None); return TaskDlgSketchBasedParameters::reject(); } #include "moc_TaskExtrudeParameters.cpp"