From 180c39709aea933ec510c79a71eb017201f9d6a3 Mon Sep 17 00:00:00 2001 From: Alfredo Monclus Date: Mon, 16 Mar 2026 17:52:36 -0300 Subject: [PATCH] PartDesign: thread texture (#22573) * PD: Hole Thread texture * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor: vp-texture remove logging * refactor: vp-hole cleanup unused declaration * refactor: vp-hole remove logging * refactor: vp-hole use convertTo * refactor: vp-hole use freecad_cast * refactor: make unwrapsurface private * fix: logic error with model-thread handling * fix: modelthreadchanged issues threaddepth doesnt depend on modeled or not so remove it * fix: model thread getting disabled when it shouldnt * revert: use base-0 poly triangulation previously had issues, but all is good now --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/Gui/ViewProviderTextureExtension.cpp | 8 +- src/Mod/PartDesign/App/FeatureHole.cpp | 15 +- src/Mod/PartDesign/App/FeatureHole.h | 3 +- .../PartDesign/Gui/Resources/PartDesign.qrc | 1 + .../Gui/Resources/images/ThreadOverlay.png | Bin 0 -> 730 bytes .../Gui/Resources/images/ThreadOverlay.svg | 293 ++++++++ src/Mod/PartDesign/Gui/TaskHoleParameters.cpp | 63 +- src/Mod/PartDesign/Gui/TaskHoleParameters.h | 4 +- src/Mod/PartDesign/Gui/TaskHoleParameters.ui | 89 ++- src/Mod/PartDesign/Gui/ViewProvider.h | 6 + src/Mod/PartDesign/Gui/ViewProviderBody.cpp | 72 +- src/Mod/PartDesign/Gui/ViewProviderBody.h | 8 +- src/Mod/PartDesign/Gui/ViewProviderHole.cpp | 660 +++++++++++++++++- src/Mod/PartDesign/Gui/ViewProviderHole.h | 78 ++- 14 files changed, 1226 insertions(+), 74 deletions(-) create mode 100644 src/Mod/PartDesign/Gui/Resources/images/ThreadOverlay.png create mode 100644 src/Mod/PartDesign/Gui/Resources/images/ThreadOverlay.svg diff --git a/src/Gui/ViewProviderTextureExtension.cpp b/src/Gui/ViewProviderTextureExtension.cpp index 42748636ff..4c3822b299 100644 --- a/src/Gui/ViewProviderTextureExtension.cpp +++ b/src/Gui/ViewProviderTextureExtension.cpp @@ -95,9 +95,7 @@ void ViewProviderTextureExtension::setCoinAppearance( const App::Material& source ) { -#if 0 if (!source.image.empty()) { - Base::Console().log("setCoinAppearance(Texture)\n"); activateTexture2D(); QByteArray by = QByteArray::fromBase64(QString::fromStdString(source.image).toUtf8()); @@ -106,12 +104,10 @@ void ViewProviderTextureExtension::setCoinAppearance( SoSFImage texture; Gui::BitmapFactory().convert(image, texture); pcShapeTexture2D->image = texture; - } else { - Base::Console().log("setCoinAppearance(Material)\n"); + } + else { activateMaterial(); } -#endif - activateMaterial(); // Always set the material for items such as lines that don't support textures pcShapeMaterial->ambientColor diff --git a/src/Mod/PartDesign/App/FeatureHole.cpp b/src/Mod/PartDesign/App/FeatureHole.cpp index 32dce84a7a..89e5289b28 100644 --- a/src/Mod/PartDesign/App/FeatureHole.cpp +++ b/src/Mod/PartDesign/App/FeatureHole.cpp @@ -558,6 +558,8 @@ Hole::Hole() ADD_PROPERTY_TYPE(ModelThread, (false), "Hole", App::Prop_None, "Model actual thread"); + ADD_PROPERTY_TYPE(CosmeticThread, (true), "Hole", App::Prop_None, "Texture the thread"); + ADD_PROPERTY_TYPE(ThreadType, (0L), "Hole", App::Prop_None, "Thread type"); ThreadType.setEnums(ThreadTypeEnums); @@ -1352,7 +1354,7 @@ void Hole::onChanged(const App::Property* prop) ThreadClass.setReadOnly(isNone || !isThreaded); ThreadDepthType.setReadOnly(isNone || !isThreaded); ThreadDepth.setReadOnly(isNone || !isThreaded); - ModelThread.setReadOnly(!isNone && isThreaded); + ModelThread.setReadOnly(isNone || !isThreaded); UseCustomThreadClearance.setReadOnly(isNone || !isThreaded || !ModelThread.getValue()); CustomThreadClearance.setReadOnly( !UseCustomThreadClearance.getValue() || UseCustomThreadClearance.isReadOnly() @@ -1413,6 +1415,7 @@ void Hole::onChanged(const App::Property* prop) // thread class and direction are only sensible if threaded // fit only sensible if not threaded if (Threaded.getValue()) { + CosmeticThread.setReadOnly(false); ThreadClass.setReadOnly(false); ThreadDirection.setReadOnly(false); ThreadFit.setReadOnly(true); @@ -1426,6 +1429,8 @@ void Hole::onChanged(const App::Property* prop) } } else { + CosmeticThread.setValue(false); + CosmeticThread.setReadOnly(true); ThreadClass.setReadOnly(true); ThreadDirection.setReadOnly(true); if (type == "None") { @@ -1448,6 +1453,14 @@ void Hole::onChanged(const App::Property* prop) // Diameter parameter depends on this updateDiameterParam(); UseCustomThreadClearance.setReadOnly(!ModelThread.getValue()); + if (CosmeticThread.getValue() && ModelThread.getValue()) { + CosmeticThread.setValue(false); + } + } + else if (prop == &CosmeticThread) { + if (CosmeticThread.getValue() && ModelThread.getValue()) { + ModelThread.setValue(false); + } } else if (prop == &DrillPoint) { if (DrillPoint.getValue() == 1) { diff --git a/src/Mod/PartDesign/App/FeatureHole.h b/src/Mod/PartDesign/App/FeatureHole.h index da8f7fe085..365edd7779 100644 --- a/src/Mod/PartDesign/App/FeatureHole.h +++ b/src/Mod/PartDesign/App/FeatureHole.h @@ -52,6 +52,7 @@ public: App::PropertyBool Threaded; App::PropertyBool ModelThread; + App::PropertyBool CosmeticThread; App::PropertyLength ThreadPitch; App::PropertyEnumeration ThreadType; App::PropertyEnumeration ThreadSize; @@ -129,6 +130,7 @@ public: virtual void updateProps(); bool isDynamicCounterbore(const std::string& thread, const std::string& holeCutType); bool isDynamicCountersink(const std::string& thread, const std::string& holeCutType); + double getThreadPitch() const; Base::Vector3d guessNormalDirection(const TopoShape& profileshape) const; TopoShape findHoles( @@ -288,7 +290,6 @@ private: double getCountersinkAngle() const; double getThreadClassClearance() const; double getThreadRunout(int mode = 1) const; - double getThreadPitch() const; double getThreadProfileAngle(); void findClosestDesignation(); void rotateToNormal(const gp_Dir& helixAxis, const gp_Dir& normalAxis, TopoDS_Shape& helixShape) const; diff --git a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc index 50e86ec8d7..6013918d61 100644 --- a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc +++ b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc @@ -68,5 +68,6 @@ images/hole_counterdrill_flat.svg images/hole_countersink_flat.svg images/hole_none_flat.svg + images/ThreadOverlay.png diff --git a/src/Mod/PartDesign/Gui/Resources/images/ThreadOverlay.png b/src/Mod/PartDesign/Gui/Resources/images/ThreadOverlay.png new file mode 100644 index 0000000000000000000000000000000000000000..802322eb2c634a09d30e7b3adf5080db8acee00f GIT binary patch literal 730 zcmeAS@N?(olHy`uVBq!ia0vp^4nVBR!3-pi_Q|sXsm}pEA+G=b{|7Qh6j)Fh2@03M zk|4ie21X`k7FITP4o)s^9$r3v0YPC=F$qa2X&G5Lc?BgEHFa%$V+$)A2N!ow-@wqQ zn7D+*q~w&;wDgRuoZP(pg2JNWlG3vBipr|$n%cVh2If;=L>U+uT|HeKLp+YJy&CC% zC_sSqfhI?Yh+~2Cods_$go(HoI>t@9OY&#x#;DjcyUIu!`G`D z6*fMW;#tAhW#RsD8bj8dT#blxySGfAFyq?e2^(BL$%%Yfsg!e0&fwzhWT!P&jfYmv zPCN4K{ac=y{+sS91?@OuDR=&5_!RdUAIfJ6XkTj68aR*AwJ$1UG^`E1Zi z-g7suPq6cuxQf_WUbmFkInAQ6E`IGZcHCMX|FCAlqI|}MPT5?r_Xtoo{*fC*dT5Sc5+`~-(qQ|6(2X%Yy7&E oIeYRVHRmq59lgowSr_*+I{htTJ+f%_a!~U0boFyt=akR{0L9+My8r+H literal 0 HcmV?d00001 diff --git a/src/Mod/PartDesign/Gui/Resources/images/ThreadOverlay.svg b/src/Mod/PartDesign/Gui/Resources/images/ThreadOverlay.svg new file mode 100644 index 0000000000..fc7cadabf2 --- /dev/null +++ b/src/Mod/PartDesign/Gui/Resources/images/ThreadOverlay.svg @@ -0,0 +1,293 @@ + + + +Alfredo Monclus diff --git a/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp b/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp index ac0058ace2..a6ec98ea8a 100644 --- a/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp +++ b/src/Mod/PartDesign/Gui/TaskHoleParameters.cpp @@ -86,7 +86,6 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole* HoleView, QWidget* pare bool isNone = std::string(pcHole->ThreadType.getValueAsString()) == "None"; bool isThreaded = pcHole->Threaded.getValue(); - ui->labelThreading->setHidden(isNone); ui->labelHoleType->setHidden(isNone); ui->HoleType->setHidden(isNone); ui->ThreadSize->setHidden(isNone); @@ -173,6 +172,7 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole* HoleView, QWidget* pare ui->Reversed->setChecked(pcHole->Reversed.getValue()); bool isModeled = pcHole->ModelThread.getValue(); + ui->ModelThread->setChecked(isModeled); ui->ThreadGroupBox->setVisible(isThreaded); ui->UseCustomThreadClearance->setChecked(pcHole->UseCustomThreadClearance.getValue()); ui->CustomThreadClearance->setValue(pcHole->CustomThreadClearance.getValue()); @@ -238,6 +238,8 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole* HoleView, QWidget* pare this, &TaskHoleParameters::taperedChanged); connect(ui->Reversed, &QCheckBox::clicked, this, &TaskHoleParameters::reversedChanged); + connect(ui->ModelThread, &QCheckBox::clicked, + this, &TaskHoleParameters::modelThreadChanged); connect(ui->TaperedAngle, qOverload(&Gui::QuantitySpinBox::valueChanged), this, &TaskHoleParameters::taperedAngleChanged); connect(ui->UpdateView, &QCheckBox::toggled, @@ -280,6 +282,30 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole* HoleView, QWidget* pare TaskHoleParameters::~TaskHoleParameters() = default; +void TaskHoleParameters::modelThreadChanged() +{ + // Model thread checkbox changed + auto pcHole = getObject(); + + bool isModeled = getModelThread(); + bool isCosmetic = getCosmeticThreaded(); + // When the combobox is "Threaded", represent the thread either as + // modeled geometry or as a cosmetic texture. + // Set both properties explicitly. + pcHole->ModelThread.setValue(isModeled); + pcHole->CosmeticThread.setValue(isCosmetic); + + // update view not active if modeling threads + // this will also ensure that the feature is recomputed. + ui->UpdateView->setVisible(isModeled); + setUpdateBlocked(isModeled && !(ui->UpdateView->isChecked())); + + // conditional enabling of thread modeling options + ui->CustomClearanceWidget->setVisible(isModeled); + + recomputeFeature(); +} + void TaskHoleParameters::holeTypeChanged(int index) { if (index < 0) { @@ -291,14 +317,18 @@ void TaskHoleParameters::holeTypeChanged(int index) } bool isThreaded = getThreaded(); bool isModeled = getModelThread(); + bool isCosmetic = getCosmeticThreaded(); + bool isNone = std::string(pcHole->ThreadType.getValueAsString()) == "None"; pcHole->Threaded.setValue(isThreaded); pcHole->ModelThread.setValue(isModeled); + pcHole->CosmeticThread.setValue(isCosmetic); - ui->ThreadFit->setHidden(isThreaded); - ui->labelThreadClearance->setHidden(isThreaded); + ui->ThreadFit->setHidden(isNone || isThreaded); + ui->labelThreadClearance->setHidden(isNone || isThreaded); + + ui->ThreadGroupBox->setVisible(isModeled || isCosmetic); - ui->ThreadGroupBox->setVisible(isThreaded); // update view not active if modeling threads // this will also ensure that the feature is recomputed. ui->UpdateView->setVisible(isModeled); @@ -308,7 +338,7 @@ void TaskHoleParameters::holeTypeChanged(int index) ui->CustomClearanceWidget->setVisible(isModeled); ui->CustomThreadClearance->setEnabled(pcHole->UseCustomThreadClearance.getValue()); - ui->ThreadDepthWidget->setVisible(isThreaded); + ui->ThreadDepthWidget->setVisible(isModeled || isCosmetic); ui->ThreadDepthDimensionWidget->setVisible( std::string(pcHole->ThreadDepthType.getValueAsString()) == "Dimension" ); @@ -665,7 +695,6 @@ void TaskHoleParameters::threadTypeChanged(int index) ui->ThreadGroupBox->setHidden(isNone || !isThreaded); ui->ThreadSize->setHidden(isNone); ui->labelSize->setHidden(isNone); - ui->labelThreading->setHidden(isNone); ui->labelHoleType->setHidden(isNone); ui->HoleType->setHidden(isNone); ui->ThreadFit->setHidden(isNone || isThreaded); @@ -831,9 +860,13 @@ void TaskHoleParameters::changedObject(const App::Document&, const App::Property widget->setDisabled(ro); }; - if (&Prop == &hole->Threaded || &Prop == &hole->ModelThread) { + if (&Prop == &hole->Threaded || &Prop == &hole->CosmeticThread) { updateHoleTypeCombo(); } + else if (&Prop == &hole->ModelThread) { + updateHoleTypeCombo(); + updateCheckable(ui->ModelThread, hole->ModelThread.getValue()); + } else if (&Prop == &hole->ThreadType) { ui->ThreadType->setEnabled(true); updateComboBox(ui->ThreadType, hole->ThreadType.getValue()); @@ -971,11 +1004,13 @@ void TaskHoleParameters::updateHoleTypeCombo() } [[maybe_unused]] QSignalBlocker blocker(ui->HoleType); if (hole->Threaded.getValue()) { - if (hole->ModelThread.getValue()) { - ui->HoleType->setCurrentIndex(ModeledThread); + if (hole->ModelThread.getValue() || hole->CosmeticThread.getValue()) { + ui->HoleType->setCurrentIndex(Threaded); + ui->ModelThread->setVisible(true); } else { ui->HoleType->setCurrentIndex(TapDrill); + ui->ModelThread->setVisible(false); } } else { @@ -995,7 +1030,12 @@ bool TaskHoleParameters::getThreaded() const bool TaskHoleParameters::getModelThread() const { - return ui->HoleType->currentIndex() == ModeledThread; + return ui->HoleType->currentIndex() == Threaded && ui->ModelThread->isChecked(); +} + +bool TaskHoleParameters::getCosmeticThreaded() const +{ + return ui->HoleType->currentIndex() == Threaded && !ui->ModelThread->isChecked(); } long TaskHoleParameters::getThreadType() const @@ -1151,6 +1191,9 @@ void TaskHoleParameters::apply() if (!hole->ModelThread.isReadOnly()) { FCMD_OBJ_CMD(hole, "ModelThread = " << (getModelThread() ? 1 : 0)); } + if (!hole->CosmeticThread.isReadOnly()) { + FCMD_OBJ_CMD(hole, "CosmeticThread = " << (getCosmeticThreaded() ? 1 : 0)); + } if (!hole->ThreadDepthType.isReadOnly()) { FCMD_OBJ_CMD(hole, "ThreadDepthType = " << getThreadDepthType()); } diff --git a/src/Mod/PartDesign/Gui/TaskHoleParameters.h b/src/Mod/PartDesign/Gui/TaskHoleParameters.h index 7b3117eb7b..e6e022300e 100644 --- a/src/Mod/PartDesign/Gui/TaskHoleParameters.h +++ b/src/Mod/PartDesign/Gui/TaskHoleParameters.h @@ -83,6 +83,7 @@ public: bool getUseCustomThreadClearance() const; double getCustomThreadClearance() const; bool getModelThread() const; + bool getCosmeticThreaded() const; long getThreadDepthType() const; double getThreadDepth() const; int getBaseProfileType() const; @@ -109,6 +110,7 @@ private Q_SLOTS: void taperedChanged(); void taperedAngleChanged(double value); void reversedChanged(); + void modelThreadChanged(); void useCustomThreadClearanceChanged(); void customThreadClearanceChanged(double value); void updateViewChanged(bool isChecked); @@ -132,7 +134,7 @@ private: { Clearance = 0, TapDrill = 1, - ModeledThread = 2 + Threaded = 2 }; protected: diff --git a/src/Mod/PartDesign/Gui/TaskHoleParameters.ui b/src/Mod/PartDesign/Gui/TaskHoleParameters.ui index 6ee004d7e4..f74ba73e2d 100644 --- a/src/Mod/PartDesign/Gui/TaskHoleParameters.ui +++ b/src/Mod/PartDesign/Gui/TaskHoleParameters.ui @@ -7,7 +7,7 @@ 0 0 414 - 945 + 903 @@ -263,10 +263,10 @@ mm - + 0.000000000000000 - + 0.100000000000000 @@ -312,10 +312,10 @@ the screw's top below the surface mm - + 0.000000000000000 - + 0.100000000000000 @@ -382,7 +382,7 @@ the screw's top below the surface deg - + 0.000000000000000 @@ -480,7 +480,7 @@ account for the depth of blind holes deg - + 0.000000000000000 @@ -579,7 +579,7 @@ account for the depth of blind holes mm - + 0.000000000000000 @@ -647,7 +647,7 @@ over 90: larger hole radius at the bottom deg - + 0.000000000000000 @@ -656,16 +656,6 @@ over 90: larger hole radius at the bottom - - - - <b>Threading</b> - - - Qt::TextFormat::RichText - - - @@ -680,12 +670,12 @@ over 90: larger hole radius at the bottom - Tap drill (to be threaded) + Tap drill - Modeled thread + Threaded @@ -799,24 +789,47 @@ Only available for holes without thread 6 - - - true + + + 0 - - - 0 - 0 - - - - Live update of changes to the thread + + + + + 0 + 0 + + + + Whether the hole gets a modelled thread + + + Model Thread + + + + + + + true + + + + 0 + 0 + + + + Live update of changes to the thread Note that the calculation can take some time - - - Update thread view - - + + + Update thread view + + + + @@ -1083,7 +1096,7 @@ Note that the calculation can take some time mm - + 0.100000000000000 diff --git a/src/Mod/PartDesign/Gui/ViewProvider.h b/src/Mod/PartDesign/Gui/ViewProvider.h index ad594f5d97..b4672b8729 100644 --- a/src/Mod/PartDesign/Gui/ViewProvider.h +++ b/src/Mod/PartDesign/Gui/ViewProvider.h @@ -91,6 +91,12 @@ public: QIcon mergeColorfulOverlayIcons(const QIcon& orig) const override; + /// Default implementation is a no-op; derived classes override if needed. + /// Called on recompute and when any feature in the Body changes visibility. + /// Serves as the entry point for ViewProviders that apply visual overlays + /// (e.g. textures, highlights, thread visualization). + virtual void updateOverlay() {}; + protected: void setupContextMenu(QMenu* menu, QObject* receiver, const char* member) override; bool setEdit(int ModNum) override; diff --git a/src/Mod/PartDesign/Gui/ViewProviderBody.cpp b/src/Mod/PartDesign/Gui/ViewProviderBody.cpp index e8e6e487a0..ff6281c79d 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderBody.cpp +++ b/src/Mod/PartDesign/Gui/ViewProviderBody.cpp @@ -24,11 +24,8 @@ #include -#include -#include #include - #include #include #include @@ -39,12 +36,8 @@ #include #include #include -#include -#include -#include #include #include -#include #include #include #include @@ -52,7 +45,6 @@ #include "ViewProviderBody.h" #include "Utils.h" #include "ViewProvider.h" -#include "ViewProviderDatum.h" using namespace PartDesignGui; @@ -72,8 +64,7 @@ ViewProviderBody::ViewProviderBody() Gui::ViewProviderOriginGroupExtension::initExtension(this); } -ViewProviderBody::~ViewProviderBody() -{} +ViewProviderBody::~ViewProviderBody() = default; void ViewProviderBody::attach(App::DocumentObject* pcFeat) { @@ -82,6 +73,66 @@ void ViewProviderBody::attach(App::DocumentObject* pcFeat) // set default display mode onChanged(&DisplayModeBody); + + if (App::Document* doc = pcFeat->getDocument()) { + m_RecomputedConn = doc->signalRecomputed.connect( + [this](const App::Document& doc, const std::vector& recomputedObjs) { + this->afterRecompute(doc, recomputedObjs); + } + ); + } + m_ChangedConn = Gui::Application::Instance->signalChangedObject.connect( + [this](const Gui::ViewProvider& vp, const App::Property& prop) { + this->onChangedObject(vp, prop); + } + ); +} + +void ViewProviderBody::onChangedObject(const Gui::ViewProvider& vp, const App::Property& prop) +{ + static const std::unordered_set watchedProps {"Visibility"}; + if (!watchedProps.contains(prop.getName())) { + return; + } + auto* vpd = dynamic_cast(&vp); + if (!vpd) { + return; + } + auto* changedObj = vpd->getObject(); + if (!changedObj) { + return; + } + + auto* body = this->getObject(); + if (!body) { + return; + } + const auto& features = body->Group.getValues(); + bool isRelevantChange = (changedObj == body) + || (std::ranges::find(features, changedObj) != features.end()); + + if (isRelevantChange) { + refreshOverlays(); + } +} + +void ViewProviderBody::afterRecompute(const App::Document& /* doc */, const std::vector& /* recomputedObjs */) +{ + refreshOverlays(); +} + +void ViewProviderBody::refreshOverlays() +{ + auto* body = getObject(); + if (!body) { + return; + } + for (auto* obj : body->Group.getValues()) { + Gui::ViewProvider* vpBase = Gui::Application::Instance->getViewProvider(obj); + if (auto* vpPartDesign = dynamic_cast(vpBase)) { + vpPartDesign->updateOverlay(); + } + } } // TODO on activating the body switch to the "Through" mode (2015-09-05, Fat-Zer) @@ -335,7 +386,6 @@ void ViewProviderBody::onChanged(const App::Property* prop) } } - void ViewProviderBody::unifyVisualProperty(const App::Property* prop) { diff --git a/src/Mod/PartDesign/Gui/ViewProviderBody.h b/src/Mod/PartDesign/Gui/ViewProviderBody.h index 58447e9d48..c290f38b19 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderBody.h +++ b/src/Mod/PartDesign/Gui/ViewProviderBody.h @@ -30,6 +30,7 @@ #include #include #include +#include class SoGroup; class SoSeparator; @@ -109,7 +110,12 @@ protected: private: static const char* BodyModeEnum[]; + + void afterRecompute(const App::Document&, const std::vector& recomputedObjs); + fastsignals::scoped_connection m_RecomputedConn; + void onChangedObject(const Gui::ViewProvider& vp, const App::Property& prop); + fastsignals::scoped_connection m_ChangedConn; + void refreshOverlays(); }; - } // namespace PartDesignGui diff --git a/src/Mod/PartDesign/Gui/ViewProviderHole.cpp b/src/Mod/PartDesign/Gui/ViewProviderHole.cpp index 525d898fec..72c4210192 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderHole.cpp +++ b/src/Mod/PartDesign/Gui/ViewProviderHole.cpp @@ -26,13 +26,51 @@ #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 +#include +#include #include -#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "ViewProviderHole.h" #include "TaskHoleParameters.h" @@ -41,13 +79,39 @@ using namespace PartDesignGui; PROPERTY_SOURCE(PartDesignGui::ViewProviderHole, PartDesignGui::ViewProvider) + ViewProviderHole::ViewProviderHole() + : textureExtension(std::make_unique()) { sPixmap = "PartDesign_Hole.svg"; } ViewProviderHole::~ViewProviderHole() = default; +bool ViewProviderHole::onDelete(const std::vector& arg) +{ + clearThreadTextures(); + return PartDesignGui::ViewProvider::onDelete(arg); +} + +void ViewProviderHole::clearThreadTextures() +{ + if (m_threadOverlays.empty()) { + return; + } + + auto* bodyVp = getBodyViewProvider(); + SoGroup* root = bodyVp ? bodyVp->getRoot() : nullptr; + + for (auto const& [hole, sw] : m_threadOverlays) { + if (root && root->findChild(sw) >= 0) { + root->removeChild(sw); + } + sw->unref(); + } + m_threadOverlays.clear(); +} + std::vector ViewProviderHole::claimChildren() const { std::vector temp; @@ -70,3 +134,591 @@ TaskDlgFeatureParameters* ViewProviderHole::getEditDialog() { return new TaskDlgHoleParameters(this); } + +void ViewProviderHole::updateData(const App::Property* prop) +{ + PartDesignGui::ViewProvider::updateData(prop); + + auto* pcHole = getObject(); + if (!pcHole || !prop) { + return; + } + + if (prop == &pcHole->Threaded || prop == &pcHole->CosmeticThread || prop == &pcHole->ModelThread) { + if (pcHole->getParents().empty()) { + return; + } + updateOverlay(); + return; + } + if (prop == &pcHole->ThreadDepth || prop == &pcHole->ThreadDepthType) { + updateThreadClipper(pcHole); + return; + } + if (prop == &pcHole->ThreadDirection) { + updateThreadDirection(pcHole); + return; + } +} + +SoSeparator* ViewProviderHole::createThreadTextureSeparator() +{ + auto* pcHole = getObject(); + if (!pcHole) { + return nullptr; + } + + gp_Pnt holeOriginPnt; + auto holeOriginOpt = getHoleOrigin(pcHole); + if (!holeOriginOpt.has_value()) { + return nullptr; + } + holeOriginPnt = *holeOriginOpt; + + std::vector vertices; + std::vector normals; + std::vector indices; + std::vector uvs; + + if (!generateBoreMeshData(pcHole, holeOriginPnt, vertices, normals, indices, uvs) + || vertices.empty() || normals.empty() || indices.empty() || uvs.empty()) { + return nullptr; + } + + // Create subtree + auto* threadSep = new SoSeparator(); + threadSep->ref(); + + // The face is selectable but not the texture + auto* pickStyle = new SoPickStyle(); + pickStyle->style = SoPickStyle::UNPICKABLE; + threadSep->addChild(pickStyle); + + // Avoid flicker on transparent objects + auto* tt = new SoTransparencyType(); + tt->value = SoTransparencyType::DELAYED_BLEND; + threadSep->addChild(tt); + + // End Clipping plane + m_endThreadClipper = new SoClipPlane(); + threadSep->addChild(m_endThreadClipper); + + // Material + auto* mat = new SoMaterial(); + textureExtension->setCoinAppearance(mat, getGlobalMaterial()); + threadSep->addChild(mat); + + // Texture + auto* threadTexture = new SoTexture2(); + threadTexture->filename.setValue(":/images/ThreadOverlay.png"); + threadTexture->wrapS = SoTexture2::REPEAT; + threadTexture->wrapT = SoTexture2::REPEAT; + threadSep->addChild(threadTexture); + + // --- Texture transform for flipping --- + m_textureTransform = new SoTexture2Transform(); + updateThreadDirection(pcHole); // apply initial direction + threadSep->addChild(m_textureTransform); + + // Texcoords / normals / geometry + auto* tc = new SoTextureCoordinate2(); + tc->point.setValues(0, (int)uvs.size(), uvs.data()); + threadSep->addChild(tc); + + auto* nb = new SoNormalBinding(); + nb->value = SoNormalBinding::PER_VERTEX_INDEXED; + threadSep->addChild(nb); + + auto* ns = new SoNormal(); + ns->vector.setValues(0, (int)normals.size(), normals.data()); + threadSep->addChild(ns); + + auto* coords = new SoCoordinate3(); + coords->point.setValues(0, (int)vertices.size(), vertices.data()); + threadSep->addChild(coords); + + auto* faces = new SoIndexedFaceSet(); + faces->coordIndex.setValues(0, (int)indices.size(), indices.data()); + threadSep->addChild(faces); + + updateThreadClipper(pcHole); + applyThreadPhaseOffset(pcHole); + + return threadSep; +} + +void ViewProviderHole::updateThreadDirection(const PartDesign::Hole* pcHole) +{ + if (!pcHole || !m_textureTransform) { + return; + } + if (pcHole->ThreadDirection.getValue() == 0) { + m_textureTransform->scaleFactor.setValue(SbVec2f(-1.0F, 1.0F)); + } + else { + m_textureTransform->scaleFactor.setValue(SbVec2f(1.0F, 1.0F)); + } +} + +void ViewProviderHole::applyThreadPhaseOffset(const PartDesign::Hole* pcHole) +{ + if (!pcHole || !m_textureTransform) { + return; + } + // Applies a unique offset so overlapping threads can be shown as crossed + // Uses a stable hash of the hole name so it's deterministic between runs + const std::string key = pcHole->getNameInDocument(); + unsigned hash = std::hash {}(key); + // Map hash to 0..1 range for UV offset + constexpr float invMax = 1.0F / static_cast(std::numeric_limits::max()); + const float phase = static_cast(hash) * invMax; + // Apply only horizontal (U) offset + m_textureTransform->translation.setValue(SbVec2f(phase, 0.0F)); +} + +void ViewProviderHole::updateThreadClipper(const PartDesign::Hole* pcHole) +{ + if (!pcHole || pcHole->isRecomputing() || !m_endThreadClipper) { + return; + } + std::string theadDepthType = pcHole->ThreadDepthType.getValueAsString(); + if (theadDepthType == "Hole depth") { + m_endThreadClipper->on = FALSE; + return; + } + m_endThreadClipper->on = TRUE; + + auto holeNormalOpt = getHoleNormal(pcHole); + if (!holeNormalOpt.has_value()) { + return; + } + gp_Dir holeNormalAxis = *holeNormalOpt; + + auto holeOriginOpt = getHoleOrigin(pcHole); + if (!holeOriginOpt.has_value()) { + return; + } + gp_Pnt holeOriginPnt = *holeOriginOpt; + + // Compute clipping plane origin at the end of the threaded portion + gp_Pnt endPlanePnt = holeOriginPnt.Translated( + gp_Vec(holeNormalAxis) * -pcHole->ThreadDepth.getValue() + ); + + SbVec3f endPlanePoint = Base::convertTo(endPlanePnt); + SbVec3f endPlaneNormal = Base::convertTo(holeNormalAxis); + + // Update the end thread clipper plane + m_endThreadClipper->plane.setValue(SbPlane(endPlaneNormal, endPlanePoint)); +} + +std::optional ViewProviderHole::getHoleNormal(const PartDesign::Hole* pcHole) const +{ + if (!pcHole) { + return std::nullopt; + } + + Base::Vector3d normal = pcHole->guessNormalDirection(pcHole->getProfileShape()); + + // Reject if direction is mathematically zero (invalid for gp_Dir) + if (normal.IsNull()) { + return std::nullopt; + } + + return Base::convertTo(normal); +} + +std::optional ViewProviderHole::getHoleOrigin(const PartDesign::Hole* pcHole) const +{ + if (!pcHole) { + return std::nullopt; + } + + auto* sketch = freecad_cast(pcHole->Profile.getValue()); + + if (!sketch) { + return std::nullopt; + } + + const Base::Vector3d& pos = sketch->Placement.getValue().getPosition(); + return Base::convertTo(pos); +} + +std::vector ViewProviderHole::collectBoreFaces(const PartDesign::Hole* pcHole) const +{ + std::vector boreFaces; + if (!pcHole) { + return boreFaces; + } + + TopoDS_Shape bodyShape = getCurrentlyVisibleShape(pcHole); + if (bodyShape.IsNull()) { + return boreFaces; + } + + auto holeNormalOpt = getHoleNormal(pcHole); + if (!holeNormalOpt.has_value()) { + return boreFaces; + } + gp_Dir holeAxis = *holeNormalOpt; + + const double holeRadius = pcHole->Diameter.getValue() / 2.0; + const bool isTapered = pcHole->Tapered.getValue(); + const double taperSemiAngleRad = isTapered + ? Base::toRadians(90 - pcHole->TaperedAngle.getValue()) + : 0.0; + + for (TopExp_Explorer expl(bodyShape, TopAbs_FACE); expl.More(); expl.Next()) { + const TopoDS_Face& face = TopoDS::Face(expl.Current()); + Handle(Geom_Surface) surf = BRep_Tool::Surface(face); + if (surf.IsNull()) { + continue; + } + + // Unwrap trimmed surfaces + if (surf->IsKind(STANDARD_TYPE(Geom_RectangularTrimmedSurface))) { + surf = Handle(Geom_RectangularTrimmedSurface)::DownCast(surf)->BasisSurface(); + } + + gp_Ax1 axis; + if (!isTapered) { + if (!surf->IsKind(STANDARD_TYPE(Geom_CylindricalSurface))) { + continue; + } + auto cyl = Handle(Geom_CylindricalSurface)::DownCast(surf); + if (std::abs(cyl->Radius() - holeRadius) >= Precision::Confusion()) { + continue; + } + axis = cyl->Axis(); + } + else { + if (!surf->IsKind(STANDARD_TYPE(Geom_ConicalSurface))) { + continue; + } + auto con = Handle(Geom_ConicalSurface)::DownCast(surf); + double angle = std::abs(con->SemiAngle()); + if (std::abs(angle - taperSemiAngleRad) >= Precision::Angular()) { + continue; + } + axis = con->Axis(); + } + + if (!axis.Direction().IsParallel(holeAxis, Precision::Angular())) { + continue; + } + + boreFaces.push_back(face); + } + + return boreFaces; +} + +App::Material ViewProviderHole::getGlobalMaterial() +{ + if (auto* materialProp = dynamic_cast(getPropertyByName("Material"))) { + return materialProp->getValue(); + } + if (auto* bodyVp = getBodyViewProvider()) { + if (auto* materialProp + = freecad_cast(bodyVp->getPropertyByName("Material"))) { + return materialProp->getValue(); + } + } + + return App::Material::getDefaultAppearance(); +} + +TopoDS_Shape ViewProviderHole::getCurrentlyVisibleShape(const PartDesign::Hole* pcHole) const +{ + auto* body = PartDesign::Body::findBodyOf(pcHole); + if (!body) { + return {}; + } + const auto& features = body->Group.getValues(); + auto holeIt = std::ranges::find(features, pcHole); + if (holeIt == features.end()) { + return {}; + } + for (auto it = holeIt; it != features.end(); ++it) { + auto* posteriorFeature = dynamic_cast(*it); + if (posteriorFeature && posteriorFeature->Visibility.getValue()) { + return posteriorFeature->Shape.getValue(); + } + } + return body->Shape.getValue(); +} + +std::pair ViewProviderHole::buildOrthonormalFrame(const gp_Dir& axis) +{ + gp_Dir ref(0, 0, 1); + if (axis.IsParallel(ref, Precision::Angular())) { + ref = gp_Dir(0, 1, 0); + } + gp_Vec x_vec = axis.Crossed(ref); + if (x_vec.SquareMagnitude() < Precision::Confusion()) { + ref = gp_Dir(1, 0, 0); + x_vec = axis.Crossed(ref); + } + gp_Dir x_dir(x_vec); + gp_Dir y_dir(axis.Crossed(x_dir)); + return {x_dir, y_dir}; +} + +SbVec2f ViewProviderHole::addVertex( + std::vector& vertices, + std::vector& normals, + const gp_Pnt& pt, + const gp_Pnt& origin, + const gp_Dir& axis, + const gp_Dir& x_dir, + const gp_Dir& y_dir, + double minProj, + double initialRadius, + double threadPitch +) +{ + gp_Vec toPoint(origin, pt); + gp_Vec radialComp = toPoint - (toPoint.Dot(axis) * axis); + double axialDist = toPoint.Dot(axis) - minProj; + double currentRadius = radialComp.Magnitude(); + double radialOffset = currentRadius - initialRadius; + double lengthAlongTaper = std::sqrt((axialDist * axialDist) + (radialOffset * radialOffset)); + + float vCoord = static_cast(lengthAlongTaper / threadPitch); + double angleRad = std::atan2(radialComp.Dot(y_dir), radialComp.Dot(x_dir)); + float uCoord = static_cast(angleRad / (2 * M_PI)); + uCoord -= std::floor(uCoord); + + vertices.emplace_back(pt.X(), pt.Y(), pt.Z()); + gp_Dir normalDir = (radialComp.SquareMagnitude() > std::pow(Precision::Confusion(), 2)) + ? gp_Dir(radialComp) + : axis; + normals.emplace_back(normalDir.X(), normalDir.Y(), normalDir.Z()); + + return SbVec2f(uCoord, vCoord); +} + +namespace +{ +Handle(Geom_Surface) unwrapSurface(const TopoDS_Face& face) +{ + Handle(Geom_Surface) surf = BRep_Tool::Surface(face); + if (!surf.IsNull() && surf->IsKind(STANDARD_TYPE(Geom_RectangularTrimmedSurface))) { + surf = Handle(Geom_RectangularTrimmedSurface)::DownCast(surf)->BasisSurface(); + } + return surf; +} +} // namespace + +void ViewProviderHole::handleSeamTriangle( + std::vector& vertices, + std::vector& normals, + std::vector& uvs, + std::array& triIndices +) +{ + constexpr float seamThreshold = 0.5F; + + bool crossesSeam = std::abs(uvs[triIndices[0]][0] - uvs[triIndices[1]][0]) > seamThreshold + || std::abs(uvs[triIndices[1]][0] - uvs[triIndices[2]][0]) > seamThreshold + || std::abs(uvs[triIndices[2]][0] - uvs[triIndices[0]][0]) > seamThreshold; + + if (!crossesSeam) { + return; + } + + int idx0 = triIndices[0]; + int idx1 = triIndices[1]; + int idx2 = triIndices[2]; + + if (uvs[idx0][0] < seamThreshold) { + SbVec2f uv = uvs[idx0]; + uv[0] += 1.0F; + int newIdx = static_cast(vertices.size()); + vertices.push_back(vertices[idx0]); + normals.push_back(normals[idx0]); + uvs.push_back(uv); + triIndices[0] = newIdx; + } + + if (uvs[idx1][0] < seamThreshold) { + SbVec2f uv = uvs[idx1]; + uv[0] += 1.0F; + int newIdx = static_cast(vertices.size()); + vertices.push_back(vertices[idx1]); + normals.push_back(normals[idx1]); + uvs.push_back(uv); + triIndices[1] = newIdx; + } + + if (uvs[idx2][0] < seamThreshold) { + SbVec2f uv = uvs[idx2]; + uv[0] += 1.0F; + int newIdx = static_cast(vertices.size()); + vertices.push_back(vertices[idx2]); + normals.push_back(normals[idx2]); + uvs.push_back(uv); + triIndices[2] = newIdx; + } +} + +bool ViewProviderHole::generateBoreMeshData( + const PartDesign::Hole* pcHole, + const gp_Pnt& holeOriginPnt, + std::vector& vertices, + std::vector& normals, + std::vector& indices, + std::vector& uvs +) +{ + const double threadPitch = pcHole->getThreadPitch(); + if (threadPitch == 0.0) { + return false; + } + + vertices.clear(); + normals.clear(); + indices.clear(); + uvs.clear(); + + const auto& boreFaces = collectBoreFaces(pcHole); + if (boreFaces.empty()) { + return false; + } + + auto holeNormalOpt = getHoleNormal(pcHole); + if (!holeNormalOpt.has_value()) { + return false; + } + gp_Dir holeNormalAxis = *holeNormalOpt; + + double minProj = std::numeric_limits::max(); + double maxProj = std::numeric_limits::lowest(); + + // --- Compute projection bounds --- + for (const auto& face : boreFaces) { + std::vector meshPoints; + std::vector meshFacets; + if (Part::Tools::getTriangulation(face, meshPoints, meshFacets)) { + for (const auto& p : meshPoints) { + double proj = gp_Vec(holeOriginPnt, p).Dot(holeNormalAxis); + minProj = std::min(minProj, proj); + maxProj = std::max(maxProj, proj); + } + } + } + + const double holeRadius = pcHole->Diameter.getValue() / 2.0; + const double coneSemiAngleRad = pcHole->Tapered.getValue() + ? Base::toRadians(pcHole->TaperedAngle.getValue() * 0.5) + : 0.0; + const double initialRadius = (minProj * std::tan(coneSemiAngleRad)) + holeRadius; + + bool success = false; + + for (const auto& face : boreFaces) { + std::vector meshPoints; + std::vector meshFacets; + if (!Part::Tools::getTriangulation(face, meshPoints, meshFacets)) { + continue; + } + + Handle(Geom_Surface) surf = unwrapSurface(face); + gp_Ax3 surfPos; + if (auto cyl = Handle(Geom_CylindricalSurface)::DownCast(surf)) { + surfPos = cyl->Position(); + } + else if (auto cone = Handle(Geom_ConicalSurface)::DownCast(surf)) { + surfPos = cone->Position(); + } + else { + continue; + } + + auto [x_dir, y_dir] = buildOrthonormalFrame(surfPos.Direction()); + gp_Pnt localOrigin = surfPos.Location(); + + std::vector localToGlobalIndex(meshPoints.size()); + for (size_t i = 0; i < meshPoints.size(); ++i) { + localToGlobalIndex[i] = static_cast(vertices.size()), + uvs.push_back(addVertex( + vertices, + normals, + meshPoints[i], + localOrigin, + surfPos.Direction(), + x_dir, + y_dir, + minProj, + initialRadius, + threadPitch + )); + } + // --- Build indices --- + for (const auto& facet : meshFacets) { + std::array n = {1, 1, 1}; + facet.Get(n[0], n[1], n[2]); + std::array triIndices + = {localToGlobalIndex[n[0]], localToGlobalIndex[n[1]], localToGlobalIndex[n[2]]}; + handleSeamTriangle(vertices, normals, uvs, triIndices); + + indices.insert(indices.end(), {triIndices[0], triIndices[1], triIndices[2], -1}); + } + success = true; + } + + return success; +} + +bool ViewProviderHole::isHoleThreadVisible() const +{ + auto* hole = getObject(); + auto* body = PartDesign::Body::findBodyOf(hole); + if (!body || !body->Visibility.getValue() || hole->Suppressed.getValue() + || !hole->Threaded.getValue() || !hole->CosmeticThread.getValue() + || hole->ModelThread.getValue()) { + return false; + } + const auto& features = body->Group.getValues(); + auto holeIt = std::ranges::find(features, hole); + if (holeIt == features.end()) { + return false; + } + for (auto it = holeIt; it != features.end(); ++it) { + auto* posteriorFeature = dynamic_cast(*it); + if (posteriorFeature && posteriorFeature->Visibility.getValue()) { + return true; + } + } + // We've reached the end and no posterior feature is visible, + return false; +} + +void ViewProviderHole::updateOverlay() +{ + auto* hole = getObject(); + bool isThreadVisible = isHoleThreadVisible(); + auto* bodyVp = getBodyViewProvider(); + if (!bodyVp) { + return; + } + // Cleanup + auto it = m_threadOverlays.find(hole); + if (it != m_threadOverlays.end()) { + SoSwitch* existingSwitch = it->second; + bodyVp->getRoot()->removeChild(existingSwitch); + existingSwitch->unref(); + m_threadOverlays.erase(it); + } + // Add the thread + if (isThreadVisible) { + if (SoSeparator* newSep = createThreadTextureSeparator()) { + auto* threadSwitch = new SoSwitch(); + threadSwitch->ref(); + threadSwitch->addChild(newSep); + bodyVp->getRoot()->addChild(threadSwitch); + threadSwitch->whichChild = SO_SWITCH_ALL; + m_threadOverlays[hole] = threadSwitch; + } + } +} diff --git a/src/Mod/PartDesign/Gui/ViewProviderHole.h b/src/Mod/PartDesign/Gui/ViewProviderHole.h index 990731dac3..7af2993c5e 100644 --- a/src/Mod/PartDesign/Gui/ViewProviderHole.h +++ b/src/Mod/PartDesign/Gui/ViewProviderHole.h @@ -26,6 +26,33 @@ #pragma once #include "ViewProvider.h" +#include +#include + +#include +#include + +class SoTexture2Transform; +class SoSeparator; +class SoClipPlane; +class QMenu; + +namespace App +{ +class DocumentObject; +class Property; +} // namespace App + +namespace PartDesign +{ +class Hole; +} + +// Forward declarations for OpenCascade classes +class TopoDS_Face; +class TopoDS_Shape; +class gp_Dir; +class gp_Pnt; namespace PartDesignGui @@ -35,19 +62,68 @@ class PartDesignGuiExport ViewProviderHole: public ViewProvider { PROPERTY_HEADER_WITH_OVERRIDE(PartDesignGui::ViewProviderHole); + SoClipPlane* m_endThreadClipper {nullptr}; + SoTexture2Transform* m_textureTransform {nullptr}; + public: /// constructor ViewProviderHole(); /// destructor ~ViewProviderHole() override; + bool onDelete(const std::vector& arg) override; /// grouping handling std::vector claimChildren() const override; void setupContextMenu(QMenu* menu, QObject* receiver, const char* member) override; + SoSeparator* createThreadTextureSeparator(); + bool isHoleThreadVisible() const; + void updateOverlay() override; protected: TaskDlgFeatureParameters* getEditDialog() override; + void updateData(const App::Property* prop) override; + +private: + std::unique_ptr textureExtension; + std::optional getHoleNormal(const PartDesign::Hole* pcHole) const; + std::optional getHoleOrigin(const PartDesign::Hole* pcHole) const; + App::Material getGlobalMaterial(); + TopoDS_Shape getCurrentlyVisibleShape(const PartDesign::Hole* pcHole) const; + void updateThreadClipper(const PartDesign::Hole* pcHole); + void updateThreadDirection(const PartDesign::Hole* pcHole); + void applyThreadPhaseOffset(const PartDesign::Hole* pcHole); + + // meshing and UVs + std::vector collectBoreFaces(const PartDesign::Hole* pcHole) const; + bool generateBoreMeshData( + const PartDesign::Hole* pcHole, + const gp_Pnt& holeOriginPnt, + std::vector& vertices, + std::vector& normals, + std::vector& indices, + std::vector& uvs + ); + std::pair buildOrthonormalFrame(const gp_Dir& axis); + SbVec2f addVertex( + std::vector& vertices, + std::vector& normals, + const gp_Pnt& pt, + const gp_Pnt& origin, + const gp_Dir& axis, + const gp_Dir& x_dir, + const gp_Dir& y_dir, + double minProj, + double initialRadius, + double threadPitch + ); + void handleSeamTriangle( + std::vector& vertices, + std::vector& normals, + std::vector& uvs, + std::array& triIndices + ); + std::map m_threadOverlays; + void clearThreadTextures(); }; - } // namespace PartDesignGui