Merge pull request #22712 from PaddleStroke/asm_watcher

Assembly: TaskWatcher
This commit is contained in:
Kacper Donat
2025-07-28 00:13:28 +02:00
committed by GitHub
6 changed files with 640 additions and 3 deletions
+18 -1
View File
@@ -40,6 +40,7 @@
#include <Gui/Application.h>
#include <Gui/Document.h>
#include <Gui/MainWindow.h>
#include <Gui/ViewProviderDocumentObject.h>
#include "TaskView.h"
#include "TaskDialog.h"
@@ -317,6 +318,9 @@ TaskView::TaskView(QWidget *parent)
connectApplicationRedoDocument =
App::GetApplication().signalRedoDocument.connect
(std::bind(&Gui::TaskView::TaskView::slotRedoDocument, this, sp::_1));
connectApplicationInEdit =
Gui::Application::Instance->signalInEdit.connect(
std::bind(&Gui::TaskView::TaskView::slotInEdit, this, sp::_1));
//NOLINTEND
updateWatcher();
@@ -329,6 +333,7 @@ TaskView::~TaskView()
connectApplicationClosedView.disconnect();
connectApplicationUndoDocument.disconnect();
connectApplicationRedoDocument.disconnect();
connectApplicationInEdit.disconnect();
Gui::Selection().Detach(this);
for (QWidget* panel : contextualPanels) {
@@ -474,8 +479,20 @@ QSize TaskView::minimumSizeHint() const
void TaskView::slotActiveDocument(const App::Document& doc)
{
Q_UNUSED(doc);
if (!ActiveDialog)
if (!ActiveDialog) {
// at this point, active object of the active view returns None.
// which is a problem if shouldShow of a watcher rely on the presence
// of an active object (example Assembly).
QTimer::singleShot(100, this, &TaskView::updateWatcher);
}
}
void TaskView::slotInEdit(const Gui::ViewProviderDocumentObject& vp)
{
Q_UNUSED(vp);
if (!ActiveDialog) {
updateWatcher();
}
}
void TaskView::slotDeletedDocument(const App::Document& doc)
+3
View File
@@ -39,6 +39,7 @@ class Property;
namespace Gui {
class MDIView;
class ControlSingleton;
class ViewProviderDocumentObject;
namespace DockWnd{
class ComboView;
}
@@ -187,6 +188,7 @@ private:
void saveCurrentWidth();
void tryRestoreWidth();
void slotActiveDocument(const App::Document&);
void slotInEdit(const Gui::ViewProviderDocumentObject&);
void slotDeletedDocument(const App::Document&);
void slotViewClosed(const Gui::MDIView*);
void slotUndoDocument(const App::Document&);
@@ -224,6 +226,7 @@ protected:
Connection connectApplicationClosedView;
Connection connectApplicationUndoDocument;
Connection connectApplicationRedoDocument;
Connection connectApplicationInEdit;
};
} //namespace TaskView
+78 -1
View File
@@ -27,11 +27,12 @@ from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
import FreeCADGui as Gui
from PySide import QtCore, QtGui, QtWidgets
import UtilsAssembly
import Preferences
# translate = App.Qt.translate
translate = App.Qt.translate
__title__ = "Assembly Command Create Assembly"
__author__ = "Ondsel"
@@ -91,5 +92,81 @@ class CommandCreateAssembly:
App.closeActiveTransaction()
class ActivateAssemblyTaskPanel:
"""A basic TaskPanel to select an assembly to activate."""
def __init__(self, assemblies):
self.assemblies = assemblies
self.form = QtWidgets.QWidget()
self.form.setWindowTitle(translate("Assembly_ActivateAssembly", "Activate Assembly"))
layout = QtWidgets.QVBoxLayout(self.form)
label = QtWidgets.QLabel(
translate("Assembly_ActivateAssembly", "Select an assembly to activate:")
)
self.combo = QtWidgets.QComboBox()
for asm in self.assemblies:
# Store the user-friendly Label for display, and the internal Name for activation
self.combo.addItem(asm.Label, asm.Name)
layout.addWidget(label)
layout.addWidget(self.combo)
def accept(self):
"""Called when the user clicks OK."""
selected_name = self.combo.currentData()
if selected_name:
Gui.doCommand(f"Gui.ActiveDocument.setEdit('{selected_name}')")
return True
def reject(self):
"""Called when the user clicks Cancel or closes the panel."""
return True
class CommandActivateAssembly:
def __init__(self):
self.task_panel = None
def GetResources(self):
return {
"Pixmap": "Assembly_ActivateAssembly",
"MenuText": QT_TRANSLATE_NOOP("Assembly_ActivateAssembly", "Activate Assembly"),
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_ActivateAssembly", "Sets an assembly as the active one for editing."
),
"CmdType": "ForEdit",
}
def IsActive(self):
if Gui.Control.activeDialog() or App.ActiveDocument is None:
return False
# Command is only active if no assembly is currently active
if UtilsAssembly.activeAssembly() is not None:
return False
# And if there is at least one assembly in the document to activate
for obj in App.ActiveDocument.Objects:
if obj.isDerivedFrom("Assembly::AssemblyObject"):
return True
return False
def Activated(self):
doc = App.ActiveDocument
assemblies = [o for o in doc.Objects if o.isDerivedFrom("Assembly::AssemblyObject")]
if len(assemblies) == 1:
# If there's only one, activate it directly without showing a dialog
Gui.doCommand(f"Gui.ActiveDocument.setEdit('{assemblies[0].Name}')")
elif len(assemblies) > 1:
# If there are multiple, show a task panel to let the user choose
self.task_panel = ActivateAssemblyTaskPanel(assemblies)
Gui.Control.showDialog(self.task_panel)
if App.GuiUp:
Gui.addCommand("Assembly_CreateAssembly", CommandCreateAssembly())
Gui.addCommand("Assembly_ActivateAssembly", CommandActivateAssembly())
@@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/">
<file>icons/Assembly_ActivateAssembly.svg</file>
<file>icons/Assembly_AssemblyLink.svg</file>
<file>icons/Assembly_AssemblyLinkRigid.svg</file>
<file>icons/Assembly_InsertLink.svg</file>
@@ -0,0 +1,388 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="64"
height="64"
version="1.1"
viewBox="0 0 64 64"
id="svg96"
sodipodi:docname="Assembly_ActivateAssembly.svg"
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="1571"
id="namedview98"
showgrid="false"
inkscape:zoom="7.375"
inkscape:cx="19.186441"
inkscape:cy="33.152542"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg96"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs26">
<linearGradient
id="linearGradient4383">
<stop
stop-color="#3465a4"
offset="0"
id="stop2" />
<stop
stop-color="#729fcf"
offset="1"
id="stop4" />
</linearGradient>
<linearGradient
id="linearGradient4389"
x1="20.244"
x2="17.244"
y1="37.588"
y2="27.588"
gradientTransform="translate(-1.2435,-2.5881)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3774" />
<linearGradient
id="linearGradient4399"
x1="48.714"
x2="44.714"
y1="45.586"
y2="34.586"
gradientTransform="translate(1.2856,1.4142)"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#c4a000"
offset="0"
id="stop8" />
<stop
stop-color="#edd400"
offset="1"
id="stop10" />
</linearGradient>
<linearGradient
id="linearGradient69042"
x1="48.714"
x2="44.714"
y1="45.586"
y2="34.586"
gradientTransform="translate(-12.714,-17.586)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient3774" />
<linearGradient
id="linearGradient69056"
x1="27.244"
x2="22.244"
y1="54.588"
y2="40.588"
gradientTransform="translate(-1.2435,-2.5881)"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#c4a000"
offset="0"
id="stop14" />
<stop
stop-color="#fce94f"
offset="1"
id="stop16" />
</linearGradient>
<linearGradient
id="linearGradient69709"
x1="20.244"
x2="17.244"
y1="37.588"
y2="27.588"
gradientTransform="matrix(1,-0.026667,0,1,81.696,-5.3735)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4383" />
<linearGradient
id="linearGradient69717"
x1="50.714"
x2="48.714"
y1="25.586"
y2="20.586"
gradientTransform="translate(61.2256,1.0356)"
gradientUnits="userSpaceOnUse"
xlink:href="#linearGradient4383" />
<linearGradient
id="linearGradient3774">
<stop
stop-color="#4e9a06"
offset="0"
id="stop21" />
<stop
stop-color="#8ae234"
offset="1"
id="stop23" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1">
<path
style="fill:none;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
d="m 1128.2358,1230.7769 47.3569,-44.8156 90.1392,-7.1378 98.3518,-93.074 -122.4881,-129.43385 -196.8251,186.26295 z"
id="path2" />
</clipPath>
<linearGradient
xlink:href="#linearGradient3774"
id="linearGradient5137"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.51964051,0,0,0.50042191,1386.3532,982.21378)"
x1="288.11227"
y1="358.15811"
x2="372.52457"
y2="7.5416322" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4383"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,-0.026667,0,1,81.696,-5.3735)"
x1="20.244"
y1="37.588"
x2="17.244"
y2="27.588" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3774"
id="linearGradient2"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-1.2435,-2.5881)"
x1="20.244"
y1="37.588"
x2="17.244"
y2="27.588" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3774"
id="linearGradient3"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-12.714,-17.586)"
x1="48.714"
y1="45.586"
x2="44.714"
y2="34.586" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4399"
id="linearGradient4"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-52.714,-17.586)"
x1="48.714001"
y1="45.585999"
x2="44.714001"
y2="34.585999" />
</defs>
<metadata
id="metadata28">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
<dc:title>Path-Stock</dc:title>
<dc:date>2015-07-04</dc:date>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(0.19522715,0.27982587)">
<g
id="g1124"
transform="matrix(1.0964113,0,0,1.0964113,-13.226965,60.643745)"
style="opacity:1">
<g
id="g40-8"
style="stroke-width:2"
transform="translate(9.249999,-58.229485)">
<path
d="M 9,49 V 35 l 28,10 v 14 z"
id="path30-8"
style="fill:#babdb6;stroke:#0b1e23;stroke-linejoin:round;stroke-opacity:1" />
<path
d="M 37,59 V 45 L 55,28 v 13 z"
id="path32-2"
style="fill:#babdb6;stroke:#0b1e23;stroke-linejoin:round;stroke-opacity:1" />
<path
d="M 11.008,47.606 11,37.9997 l 24,8 0.0081,10.185 z"
id="path34-4"
style="fill:#888a85;stroke:#babdb6" />
<path
d="M 39.005,54.168 39,45.9998 l 14,-13 0.0021,7.1768 z"
id="path36-5"
style="fill:#888a85;stroke:#babdb6" />
<path
d="M 23,40 42,23 55,28 37,45 Z"
id="path38-5"
style="fill:#d3d7cf;stroke:#0b1e23;stroke-linejoin:round;stroke-opacity:1" />
</g>
<g
id="g943-1"
transform="translate(-50.750001,-58.229485)">
<path
d="m 91,33.5 -0.02739,-14.214 12.967,4.3352 v 14.5 z"
id="path54-7"
style="fill:url(#linearGradient1);stroke:#0b1e23;stroke-width:2;stroke-linejoin:round;stroke-opacity:1" />
<path
d="m 92.927,32.029 0.04731,-10.141 8.9272,3.29 0.0781,10.042 z"
id="path56-1"
style="fill:#888a85;stroke:#babdb6;stroke-width:2" />
<path
d="m 103.94,38.121 v -14.5 l 11,-9 L 115,28 Z"
id="path58-1"
style="fill:#edd400;stroke:#0b1e23;stroke-width:2;stroke-linejoin:round;stroke-opacity:1" />
<path
d="m 105.94,33.621 v -9 l 7,-6 -0.0122,8.5816 z"
id="path60-5"
style="fill:#888a85;stroke:#babdb6;stroke-width:2" />
<path
d="M 90.973,19.286 102,9.9998 l 12.94,4.6214 -11,9 z"
id="path62-2"
style="fill:#d3d7cf;stroke:#0b1e23;stroke-width:2;stroke-linejoin:round;stroke-opacity:1" />
</g>
<g
id="g963-7"
transform="translate(49.249999,-58.229485)">
<g
style="stroke:#172a04;stroke-width:2;stroke-linejoin:round"
transform="translate(-40)"
id="g48-6">
<path
style="fill:url(#linearGradient2)"
id="path42-1"
d="M 9,35 V 21 l 14,5 v 14 z" />
<path
style="fill:#8ae234"
id="path44-4"
d="M 9,21 28.585,5.209 42,10.0001 l -19,16 z" />
<path
style="fill:url(#linearGradient3)"
id="path46-2"
d="M 23,40 V 26 l 7.9726,-6.7138 0.02739,13.714 z" />
</g>
<path
style="fill:#888a85;fill-opacity:1;stroke:#0b1e23;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
id="path912-3"
d="M -31,35 V 21 l 14,5 v 14 z" />
<path
style="fill:#d3d7cf;fill-opacity:1;stroke:#0b1e23;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
id="path914-2"
d="M -31,21 -11.415,5.209 2,10.0001 l -19,16 z" />
<path
style="fill:url(#linearGradient4);fill-opacity:1;stroke:#0b1e23;stroke-width:2;stroke-linejoin:round;stroke-opacity:1"
id="path916-2"
d="M -17,40 V 26 l 7.9726,-6.7138 0.02739,13.714 z" />
<path
d="m -15,36 v -9 l 4,-3.5 v 9 z"
id="path50-1"
style="fill:#888a85;stroke:#babdb6;stroke-width:2;stroke-opacity:1" />
<path
d="m -29.049,33.746 0.08695,-9.9796 9.9568,3.5229 -0.02105,9.9613 z"
id="path52-6"
style="fill:none;stroke:#babdb6;stroke-width:2;stroke-opacity:1" />
</g>
</g>
</g>
<g
display="none"
fill="#ef2929"
fill-rule="evenodd"
opacity=".588"
stroke="#ef2929"
stroke-width="1px"
id="g94">
<path
d="m9 35v14"
id="path66" />
<path
d="m9 35 28 10"
id="path68" />
<path
d="m55 28v13"
id="path70" />
<path
d="m37 45 18-17"
id="path72" />
<path
d="m23 40v-14"
id="path74" />
<path
d="m29 5 13 5"
id="path76" />
<path
d="m23 26 19-16"
id="path78" />
<path
d="m19 13 10-8"
id="path80" />
<path
d="m55 15-9 8"
id="path82" />
<path
d="m42 23v-13"
id="path84" />
<path
d="m42 23 14 5"
id="path86" />
<path
d="m23 40 19-17"
id="path88" />
<path
d="m23 10h19"
id="path90" />
<path
d="m34 17v13"
id="path92" />
</g>
<g
transform="matrix(0.12325184,0.1166378,-0.1166378,0.12325184,11.596839,-245.60194)"
id="g4218"
clip-path="url(#clipPath1)">
<path
style="opacity:1;fill:#8ae234;fill-opacity:1;stroke:#172a04;stroke-width:14.7326;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1145.5648,1074.1543 c 18.5937,-18.383 48.2169,-28.9064 88.8688,-31.5737 v -37.4326 c 0,-4.0003 1.9294,-6.80966 5.7868,-8.43006 4.0547,-1.61639 7.5171,-0.95178 10.3849,2.00121 l 75.9581,73.15065 c 1.8799,1.8121 2.8184,3.9543 2.8184,6.4303 0,2.4767 -0.9389,4.6185 -2.8184,6.429 l -75.9602,73.1502 c -1.7833,1.81 -4.0064,2.7148 -6.6772,2.7148 -1.1865,0 -2.4222,-0.2382 -3.7083,-0.7141 -3.8572,-1.6204 -5.7882,-4.4273 -5.7882,-8.4296 v -35.8618 c -15.9228,1.2386 -29.1503,3.7387 -39.6881,7.5008 -10.5331,3.7627 -18.9637,9.0247 -25.2946,15.7879 -14.738,15.7158 -19.9806,42.6214 -15.7274,80.7237 0.1953,2.4775 -0.9389,4.0963 -3.4125,4.858 -0.2967,0.097 -0.7414,0.142 -1.3349,0.142 -1.9778,0 -3.361,-0.8095 -4.154,-2.4285 l -2.9667,-5.7148 c -1.3832,-2.6676 -3.3858,-6.9784 -6.007,-12.9313 -2.6194,-5.9556 -4.9969,-11.9071 -7.1237,-17.8612 -2.1269,-5.9515 -4.0298,-12.5235 -5.7119,-19.7147 -1.6805,-7.194 -2.5219,-13.5508 -2.5219,-19.074 0,-32.0039 8.3606,-56.2428 25.078,-72.7222 z"
id="path4228" />
<path
id="path5135"
d="m 1252.6459,1025.0625 c 18.4441,17.7617 37.1632,35.1962 55.3062,53.3235 -19.0906,18.0067 -37.542,36.7844 -56.7747,54.6184 -0.018,-12.2743 -0.087,-24.5491 0.032,-36.8231 -26.4463,1.6409 -53.9694,3.575 -77.8197,17.2303 -17.0095,9.5803 -28.4884,27.8106 -34.5388,47.088 -3.5743,-21.5969 -1.2897,-45.3408 10.607,-63.6609 11.0273,-16.9013 29.5358,-25.7267 47.629,-30.3404 14.7839,-4.3279 30.2561,-4.0127 45.1276,-7.7257 7.8392,-4.3982 10.4187,-14.9284 9.2176,-23.8416 -0.062,-3.7919 -0.2886,-7.6325 -0.4323,-11.451 0.5488,0.5275 1.0977,1.055 1.6465,1.5825 z"
style="opacity:1;fill:url(#linearGradient5137);fill-opacity:1;stroke:none;stroke-width:34.7533;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+152 -1
View File
@@ -118,11 +118,162 @@ class AssemblyWorkbench(Workbench):
# update the translation engine
FreeCADGui.updateLocale()
# Add task watchers to provide contextual tools in the task panel
self.setWatchers()
def Deactivated(self):
pass
FreeCADGui.Control.clearTaskWatcher()
def ContextMenu(self, recipient):
pass
def setWatchers(self):
import UtilsAssembly
translate = FreeCAD.Qt.translate
class AssemblyCreateWatcher:
"""Shows 'Create Assembly' when no assembly exists in the document."""
def __init__(self):
self.commands = ["Assembly_CreateAssembly"]
self.title = translate("Assembly", "Create")
def shouldShow(self):
doc = FreeCAD.ActiveDocument
if hasattr(doc, "RootObjects"):
for obj in doc.RootObjects:
if obj.isDerivedFrom("Assembly::AssemblyObject"):
return False
return True
class AssemblyActivateWatcher:
"""Shows 'Activate Assembly' when an assembly exists but is not active."""
def __init__(self):
self.commands = ["Assembly_ActivateAssembly"]
self.title = translate("Assembly", "Activate")
def shouldShow(self):
doc = FreeCAD.ActiveDocument
has_assembly = False
if hasattr(doc, "RootObjects"):
for obj in doc.RootObjects:
if obj.isDerivedFrom("Assembly::AssemblyObject"):
has_assembly = True
break
assembly = UtilsAssembly.activeAssembly()
return has_assembly and (assembly is None or assembly.Document != doc)
class AssemblyBaseWatcher:
"""Base class for watchers that require an active assembly."""
def __init__(self):
self.assembly = None
def shouldShow(self):
doc = FreeCAD.ActiveDocument
self.assembly = UtilsAssembly.activeAssembly()
return self.assembly is not None and self.assembly.Document == doc
class AssemblyInsertWatcher(AssemblyBaseWatcher):
"""Shows 'Insert Component' when an assembly is active."""
def __init__(self):
super().__init__()
self.commands = ["Assembly_Insert"]
self.title = translate("Assembly", "Insert")
def shouldShow(self):
return super().shouldShow()
class AssemblyGroundWatcher(AssemblyBaseWatcher):
"""Shows 'Ground' when the active assembly has no grounded parts."""
def __init__(self):
super().__init__()
self.commands = ["Assembly_ToggleGrounded"]
self.title = translate("Assembly", "Grounding")
def shouldShow(self):
if not super().shouldShow():
return False
return (
UtilsAssembly.assembly_has_at_least_n_parts(1)
and not UtilsAssembly.isAssemblyGrounded()
)
class AssemblyJointsWatcher(AssemblyBaseWatcher):
"""Shows Joint, View, and BOM tools when there are enough parts."""
def __init__(self):
super().__init__()
self.commands = [
"Assembly_CreateJointFixed",
"Assembly_CreateJointRevolute",
"Assembly_CreateJointCylindrical",
"Assembly_CreateJointSlider",
"Assembly_CreateJointBall",
"Separator",
"Assembly_CreateJointDistance",
"Assembly_CreateJointParallel",
"Assembly_CreateJointPerpendicular",
"Assembly_CreateJointAngle",
]
self.title = translate("Assembly", "Constraints")
def shouldShow(self):
if not super().shouldShow():
return False
return UtilsAssembly.assembly_has_at_least_n_parts(2)
class AssemblyToolsWatcher(AssemblyBaseWatcher):
"""Shows Joint, View, and BOM tools when there are enough parts."""
def __init__(self):
super().__init__()
self.commands = [
"Assembly_CreateView",
"Assembly_CreateBom",
]
self.title = translate("Assembly", "Tools")
def shouldShow(self):
if not super().shouldShow():
return False
return UtilsAssembly.assembly_has_at_least_n_parts(1)
class AssemblySimulationWatcher(AssemblyBaseWatcher):
"""Shows 'Create Simulation' when specific motional joints exist."""
def __init__(self):
super().__init__()
self.commands = ["Assembly_CreateSimulation"]
self.title = translate("Assembly", "Simulation")
def shouldShow(self):
if not super().shouldShow():
return False
joint_types = ["Revolute", "Slider", "Cylindrical"]
joints = UtilsAssembly.getJointsOfType(self.assembly, joint_types)
return len(joints) > 0
watchers = [
AssemblyCreateWatcher(),
AssemblyActivateWatcher(),
AssemblyInsertWatcher(),
AssemblyGroundWatcher(),
AssemblyJointsWatcher(),
AssemblyToolsWatcher(),
AssemblySimulationWatcher(),
]
FreeCADGui.Control.addTaskWatcher(watchers)
Gui.addWorkbench(AssemblyWorkbench())