From 8fd15eaf28d6d03329a482eb23a1193e07ca5373 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Wed, 20 Mar 2024 16:14:11 +0100 Subject: [PATCH] Assembly: Exploded view refactoring --- src/Mod/Assembly/App/AssemblyObject.cpp | 17 ++ src/Mod/Assembly/App/AssemblyObject.h | 2 + src/Mod/Assembly/CommandCreateView.py | 295 +++++++++++++----------- src/Mod/Assembly/UtilsAssembly.py | 40 ++++ 4 files changed, 216 insertions(+), 138 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyObject.cpp b/src/Mod/Assembly/App/AssemblyObject.cpp index 2fba4b82ad..f5c498ee68 100644 --- a/src/Mod/Assembly/App/AssemblyObject.cpp +++ b/src/Mod/Assembly/App/AssemblyObject.cpp @@ -75,6 +75,7 @@ #include "AssemblyObject.h" #include "AssemblyObjectPy.h" #include "JointGroup.h" +#include "ViewGroup.h" namespace PartApp = Part; @@ -398,6 +399,22 @@ JointGroup* AssemblyObject::getJointGroup() return nullptr; } +ViewGroup* AssemblyObject::getExplodedViewGroup() +{ + App::Document* doc = getDocument(); + + std::vector viewGroups = doc->getObjectsOfType(ViewGroup::getClassTypeId()); + if (viewGroups.empty()) { + return nullptr; + } + for (auto viewGroup : viewGroups) { + if (hasObject(viewGroup)) { + return dynamic_cast(viewGroup); + } + } + return nullptr; +} + std::vector AssemblyObject::getJoints(bool updateJCS, bool delBadJoints) { std::vector joints = {}; diff --git a/src/Mod/Assembly/App/AssemblyObject.h b/src/Mod/Assembly/App/AssemblyObject.h index d9a4426e33..c82d165501 100644 --- a/src/Mod/Assembly/App/AssemblyObject.h +++ b/src/Mod/Assembly/App/AssemblyObject.h @@ -55,6 +55,7 @@ namespace Assembly { class JointGroup; +class ViewGroup; // This enum has to be the same as the one in JointObject.py enum class JointType @@ -167,6 +168,7 @@ public: void jointParts(std::vector joints); JointGroup* getJointGroup(); + ViewGroup* getExplodedViewGroup(); std::vector getJoints(bool updateJCS = true, bool delBadJoints = true); std::vector getGroundedJoints(); std::vector getJointsOfObj(App::DocumentObject* obj); diff --git a/src/Mod/Assembly/CommandCreateView.py b/src/Mod/Assembly/CommandCreateView.py index d871ad2681..9b4ac73940 100644 --- a/src/Mod/Assembly/CommandCreateView.py +++ b/src/Mod/Assembly/CommandCreateView.py @@ -26,6 +26,7 @@ import os import FreeCAD as App from pivy import coin +from Part import LineSegment, Compound from PySide.QtCore import QT_TRANSLATE_NOOP @@ -63,7 +64,10 @@ class CommandCreateView: } def IsActive(self): - return UtilsAssembly.isAssemblyCommandActive() + return ( + UtilsAssembly.isAssemblyCommandActive() + and UtilsAssembly.assembly_has_at_least_n_parts(2) + ) def Activated(self): assembly = UtilsAssembly.activeAssembly() @@ -78,7 +82,7 @@ class CommandCreateView: class ExplodedView: def __init__(self, expView): expView.addProperty( - "App::PropertyLinkList", "Steps", "Exploded View", "Step objects of the exploded view." + "App::PropertyLinkList", "Moves", "Exploded View", "Move objects of the exploded view." ) expView.Proxy = self @@ -94,10 +98,11 @@ class ExplodedView: return viewObj.InList[0] def onChanged(self, viewObj, prop): - if prop == "Steps" and self.stepsChangedCallback is not None: - self.stepsChangedCallback() + if prop == "Moves" and hasattr(self, "stepsChangedCallback"): + if self.stepsChangedCallback is not None: + self.stepsChangedCallback() - def setStepsChangedCallback(self, callback): + def setMovesChangedCallback(self, callback): self.stepsChangedCallback = callback def execute(self, fp): @@ -105,6 +110,39 @@ class ExplodedView: # App.Console.PrintMessage("Recompute Python Box feature\n") pass + def applyMoves(self, viewObj, com=None, size=None): + positions = [] # [[p1start, p1end], [p2start, p2end], ...] + if com is None: + com, size = UtilsAssembly.getComAndSize(self.getAssembly(viewObj)) + for move in viewObj.Moves: + positions = positions + move.Proxy.applyStep(move, com, size) + + return positions + + def getAssembly(self, viewObj): + return viewObj.InList[0] + + def saveAssemblyAndExplode(self, viewObj): + self.initialPlcs = UtilsAssembly.saveAssemblyPartsPlacements(self.getAssembly(viewObj)) + + self.positions = self.applyMoves(viewObj) + + lines = [] + + for startPos, endPos in self.positions: + line = LineSegment(startPos, endPos).toShape() + lines.append(line) + if lines: + return Compound(lines) + + return None + + def restoreAssembly(self, viewObj): + if self.initialPlcs is None: + return + + UtilsAssembly.restoreAssemblyPartsPlacements(self.getAssembly(viewObj), self.initialPlcs) + class ViewProviderExplodedView: def __init__(self, vobj): @@ -152,14 +190,21 @@ class ViewProviderExplodedView: return None def claimChildren(self): - return self.app_obj.Steps + return self.app_obj.Moves def doubleClicked(self, vobj): task = Gui.Control.activeTaskDialog() if task: task.reject() - assembly = vobj.Object.InList[0] + assembly = None + for obj in vobj.Object.InList: + if obj.isDerivedFrom("Assembly::AssemblyObject"): + assembly = obj + break + if assembly is None: + return False + if UtilsAssembly.activeAssembly() != assembly: Gui.ActiveDocument.setEdit(assembly) @@ -169,7 +214,7 @@ class ViewProviderExplodedView: return True -######### Exploded View Step ######### +######### Exploded View Move ######### ExplodedViewStepTypes = [ "Normal", "Radial", @@ -184,31 +229,31 @@ class ExplodedViewStep: evStep.addProperty( "App::PropertyStringList", "ObjNames", - "Exploded Step", + "Exploded Move", QT_TRANSLATE_NOOP("App::Property", "The object moved by the move"), ) evStep.addProperty( "App::PropertyLinkList", "Parts", - "Exploded Step", + "Exploded Move", QT_TRANSLATE_NOOP("App::Property", "The containing parts of objects moved by the move"), ) evStep.addProperty( "App::PropertyPlacement", - "Placement", - "Exploded Step", + "MovementTransform", + "Exploded Move", QT_TRANSLATE_NOOP( "App::Property", - "This is the movement of the step. The end placement is the result of the start placement * this placement.", + "This is the movement of the move. The end placement is the result of the start placement * this placement.", ), ) evStep.addProperty( "App::PropertyEnumeration", "MoveType", - "Exploded Step", + "Exploded Move", QT_TRANSLATE_NOOP("App::Property", "The type of the move"), ) evStep.MoveType = ExplodedViewStepTypes # sets the list @@ -229,6 +274,48 @@ class ExplodedViewStep: # App.Console.PrintMessage("Recompute Python Box feature\n") pass + def applyStep(self, move, com=App.Vector(), size=100): + positions = [] + if move.MoveType == "Radial": + distance = move.MovementTransform.Base.Length + factor = 1 + 4 * distance / size + + for objName, part in zip(move.ObjNames, move.Parts): + if not objName: + continue + obj = UtilsAssembly.getObjectInPart(objName, part) + if not obj: + continue + + if move.ViewObject: + startPos = UtilsAssembly.getCenterOfBoundingBox([obj], [part]) + + if move.MoveType == "Radial": + init_vec = obj.Placement.Base - com + obj.Placement.Base = com + init_vec * factor + else: + obj.Placement = move.MovementTransform * obj.Placement + + if move.ViewObject: + endPos = UtilsAssembly.getCenterOfBoundingBox([obj], [part]) + positions.append([startPos, endPos]) + + if move.ViewObject: + move.ViewObject.Proxy.redrawLines(move, positions) + + return positions + + def getAssembly(self, move): + return move.InList[0].InList[0] + + def getMovingobjects(self, move): + movingObjs = [] + for objName, part in zip(move.ObjNames, move.Parts): + obj = UtilsAssembly.getObjectInPart(objName, part) + if obj is not None: + movingObjs.append(obj) + return movingObjs + class ViewProviderExplodedViewStep: def __init__(self, vobj): @@ -259,61 +346,31 @@ class ViewProviderExplodedViewStep: self.display_mode.addChild(self.lineSetGroup) # Add the group to the display mode vobj.addDisplayMode(self.display_mode, "Wireframe") - if self.app_obj.MoveType == "Radial": - assembly = UtilsAssembly.activeAssembly() - self.assemblyCOM = UtilsAssembly.getCenterOfBoundingBox([assembly], [None]) - self.assemblyCOMSize = assembly.ViewObject.getBoundingBox().DiagonalLength - def updateData(self, stepObj, prop): """If a property of the handled feature has changed we have the chance to handle this here""" # stepObj is the handled feature, prop is the name of the property that has changed - if prop in ["Parts", "Placement"]: - self.redrawLines(stepObj) + pass - def redrawLines(self, stepObj): + def redrawLines(self, stepObj, positions): # Clear existing lines self.lineSetGroup.removeAllChildren() - if hasattr(stepObj, "Parts") and stepObj.Parts: - if stepObj.MoveType == "Radial": - distance = stepObj.Placement.Base.Length - factor = 1 + 4 * distance / self.assemblyCOMSize + for startPos, endPos in positions: + # Create the line + line = coin.SoLineSet() + line.numVertices.setValue(2) + coords = coin.SoCoordinate3() + coords.point.setValues(0, [startPos, endPos]) - for objName, part in zip(stepObj.ObjNames, stepObj.Parts): - if not objName: - return + # Create separator for this line to apply the style + line_sep = coin.SoSeparator() + line_sep.addChild(self.draw_style) + line_sep.addChild(self.so_color) + line_sep.addChild(coords) + line_sep.addChild(line) - obj = UtilsAssembly.getObjectInPart(objName, part) - - if not obj: - return - - plc2 = UtilsAssembly.getGlobalPlacement(obj, part) - plc2.Base = UtilsAssembly.getCenterOfBoundingBox([obj], [part]) - endPoint = plc2.Base - - if stepObj.MoveType == "Radial": - startPoint = (endPoint - self.assemblyCOM) / factor + self.assemblyCOM - - else: - plc1 = stepObj.Placement.inverse() * plc2 - startPoint = plc1.Base - - # Create the line - line = coin.SoLineSet() - line.numVertices.setValue(2) - coords = coin.SoCoordinate3() - coords.point.setValues(0, [startPoint, endPoint]) - - # Create separator for this line to apply the style - line_sep = coin.SoSeparator() - line_sep.addChild(self.draw_style) - line_sep.addChild(self.so_color) - line_sep.addChild(coords) - line_sep.addChild(line) - - # Add to the group - self.lineSetGroup.addChild(line_sep) + # Add to the group + self.lineSetGroup.addChild(line_sep) def getDisplayModes(self, obj): """Return a list of display modes.""" @@ -331,7 +388,7 @@ class ViewProviderExplodedViewStep: pass def getIcon(self): - return ":/icons/Assembly_ExplodedViewStep.svg" + return ":/icons/button_add_all.svg" def dumps(self): """When saving the document this object gets stored using Python's json module.\ @@ -355,7 +412,7 @@ class ExplodedViewSelGate: # Objects within the assembly. return True - if obj in self.viewObj.Steps: + if obj in self.viewObj.Moves: # Enable selection of steps object return True @@ -367,10 +424,15 @@ class TaskAssemblyCreateView(QtCore.QObject): def __init__(self, viewObj=None): super().__init__() + self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateView.ui") + self.form.stepList.installEventFilter(self) + self.form.stepList.itemClicked.connect(self.onItemClicked) + view = Gui.activeDocument().activeView() self.assembly = UtilsAssembly.activeAssembly() self.assembly.ViewObject.EnableMovement = False + self.com, self.size = UtilsAssembly.getComAndSize(self.assembly) self.asmDragger = self.assembly.ViewObject.getDragger() self.cbFin = view.addDraggerCallback( self.asmDragger, "addFinishCallback", self.draggerFinished @@ -379,17 +441,8 @@ class TaskAssemblyCreateView(QtCore.QObject): self.asmDragger, "addMotionCallback", self.draggerMoved ) - self.assemblyCOM = UtilsAssembly.getCenterOfBoundingBox([self.assembly], [None]) - self.assemblyCOMSize = self.assembly.ViewObject.getBoundingBox().DiagonalLength - - # self.doc = App.ActiveDocument - Gui.Selection.clearSelection() - self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateView.ui") - self.form.stepList.installEventFilter(self) - self.form.stepList.itemClicked.connect(self.onItemClicked) - self.form.btnAlignDragger.setMenu(QMenu(self.form.btnAlignDragger)) actionAlignTo = self.form.btnAlignDragger.menu().addAction("Align to...") actionAlignToCenter = self.form.btnAlignDragger.menu().addAction("Align to part center") @@ -406,14 +459,14 @@ class TaskAssemblyCreateView(QtCore.QObject): pref = Preferences.preferences() self.form.CheckBox_PartsAsSingleSolid.setChecked(pref.GetBool("PartsAsSingleSolid", True)) - self.saveAssemblyPartsPlacements(self.assembly) + self.initialPlcs = UtilsAssembly.saveAssemblyPartsPlacements(self.assembly) if viewObj: App.setActiveTransaction("Edit Exploded View") self.viewObj = viewObj - for step in self.viewObj.Steps: - step.Visibility = True - self.onStepsChanged() + for move in self.viewObj.Moves: + move.Visibility = True + self.onMovesChanged() else: App.setActiveTransaction("Create Exploded View") @@ -424,7 +477,7 @@ class TaskAssemblyCreateView(QtCore.QObject): ) Gui.Selection.addObserver(self, Gui.Selection.ResolveMode.NoResolve) - self.viewObj.Proxy.setStepsChangedCallback(self.onStepsChanged) + self.viewObj.Proxy.setMovesChangedCallback(self.onMovesChanged) self.callbackMove = view.addEventCallback("SoLocation2Event", self.moveMouse) self.callbackClick = view.addEventCallback("SoMouseButtonEvent", self.clickMouse) self.callbackKey = view.addEventCallback("SoKeyboardEvent", self.KeyboardEvent) @@ -439,9 +492,9 @@ class TaskAssemblyCreateView(QtCore.QObject): def accept(self): self.deactivate() - self.restoreAssemblyPartsPlacements(self.assembly) - for step in self.viewObj.Steps: - step.Visibility = False + UtilsAssembly.restoreAssemblyPartsPlacements(self.assembly, self.initialPlcs) + for move in self.viewObj.Moves: + move.Visibility = False App.closeActiveTransaction() return True @@ -465,7 +518,7 @@ class TaskAssemblyCreateView(QtCore.QObject): Gui.Selection.removeObserver(self) Gui.Selection.clearSelection() - self.viewObj.Proxy.setStepsChangedCallback(None) + self.viewObj.Proxy.setMovesChangedCallback(None) view.removeEventCallback("SoLocation2Event", self.callbackMove) view.removeEventCallback("SoMouseButtonEvent", self.callbackClick) view.removeEventCallback("SoKeyboardEvent", self.callbackKey) @@ -473,18 +526,6 @@ class TaskAssemblyCreateView(QtCore.QObject): if Gui.Control.activeDialog(): Gui.Control.closeDialog() - def saveAssemblyPartsPlacements(self, assembly): - self.initialPlcDict = {} - assemblyParts = UtilsAssembly.getMovablePartsWithin(assembly) - for part in assemblyParts: - self.initialPlcDict[part.Name] = part.Placement - - def restoreAssemblyPartsPlacements(self, assembly): - assemblyParts = UtilsAssembly.getMovablePartsWithin(assembly) - for part in assemblyParts: - if part.Name in self.initialPlcDict: - part.Placement = self.initialPlcDict[part.Name] - def setDragger(self): if self.blockSetDragger: return @@ -552,31 +593,15 @@ class TaskAssemblyCreateView(QtCore.QObject): self.assembly.ViewObject.DraggerVisibility = val self.form.btnAlignDragger.setVisible(val) - def onStepsChanged(self): + def onMovesChanged(self): # First reset positions - self.restoreAssemblyPartsPlacements(self.assembly) + UtilsAssembly.restoreAssemblyPartsPlacements(self.assembly, self.initialPlcs) + + self.viewObj.Proxy.applyMoves(self.viewObj, self.com, self.size) self.form.stepList.clear() - - for step in self.viewObj.Steps: - - if step.MoveType == "Radial": - distance = step.Placement.Base.Length - factor = 1 + 4 * distance / self.assemblyCOMSize - - for objName, part in zip(step.ObjNames, step.Parts): - obj = UtilsAssembly.getObjectInPart(objName, part) - if not obj: - continue - - if step.MoveType == "Radial": - init_vec = obj.Placement.Base - self.assemblyCOM - obj.Placement.Base = self.assemblyCOM + init_vec * factor - else: - obj.Placement = step.Placement * obj.Placement - - self.form.stepList.addItem(step.Name) - step.ViewObject.Proxy.redrawLines(step) + for move in self.viewObj.Moves: + self.form.stepList.addItem(move.Name) def onItemClicked(self, item): Gui.Selection.clearSelection() @@ -650,20 +675,20 @@ class TaskAssemblyCreateView(QtCore.QObject): ViewProviderExplodedView(self.viewObj.ViewObject) def createExplodedStepObject(self, moveType_index=0): - self.currentStep = App.ActiveDocument.addObject("App::FeaturePython", "Move") + self.currentStep = self.assembly.newObject("App::FeaturePython", "Move") ExplodedViewStep(self.currentStep, moveType_index) ViewProviderExplodedViewStep(self.currentStep.ViewObject) - # Note: self.viewObj.Steps.append(self.currentStep) does not work - listOfSteps = self.viewObj.Steps - listOfSteps.append(self.currentStep) - self.viewObj.Steps = listOfSteps + # Note: self.viewObj.Moves.append(self.currentStep) does not work + listOfMoves = self.viewObj.Moves + listOfMoves.append(self.currentStep) + self.viewObj.Moves = listOfMoves objNames = [] for obj in self.selectedObjs: objNames.append(obj.Name) - self.currentStep.Placement = App.Placement() + self.currentStep.MovementTransform = App.Placement() self.currentStep.ObjNames = objNames self.currentStep.Parts = self.selectedParts @@ -686,22 +711,16 @@ class TaskAssemblyCreateView(QtCore.QObject): if self.currentStep is None: self.createExplodedStepObject() + # reset the objects position to their position before the current move. + for obj, init_plc in zip(self.selectedObjs, self.selectedObjsInitPlc): + obj.Placement = init_plc + + # we update the move Placement. draggerPlc = self.assembly.ViewObject.DraggerPlacement - movePlc = draggerPlc * self.initialDraggerPlc.inverse() + self.currentStep.MovementTransform = draggerPlc * self.initialDraggerPlc.inverse() - if self.currentStep.MoveType == "Radial": - distance = movePlc.Base.Length - factor = 1 + 4 * distance / self.assemblyCOMSize - for obj, init_plc in zip(self.selectedObjs, self.selectedObjsInitPlc): - init_vec = init_plc.Base - self.assemblyCOM - obj.Placement.Base = self.assemblyCOM + init_vec * factor - - else: - for obj, init_plc in zip(self.selectedObjs, self.selectedObjsInitPlc): - obj.Placement = movePlc * init_plc - - # we update the step Placement after parts placement has updated. - self.currentStep.Placement = movePlc + # Apply the move + self.currentStep.Proxy.applyStep(self.currentStep, self.com, self.size) def draggerFinished(self, event): if self.currentStep.MoveType == "Radial": @@ -785,12 +804,12 @@ class TaskAssemblyCreateView(QtCore.QObject): sorted_indexes = sorted(selected_indexes, key=lambda x: x.row(), reverse=True) for index in sorted_indexes: row = index.row() - if row < len(self.viewObj.Steps): - step = self.viewObj.Steps[row] + if row < len(self.viewObj.Moves): + move = self.viewObj.Moves[row] # First remove the link from the viewObj - self.viewObj.Steps.remove(step) + self.viewObj.Moves.remove(move) # Delete the object - step.Document.removeObject(step.Name) + move.Document.removeObject(move.Name) return True # Consume the event diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 3495637e0f..28d90b02b1 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -949,3 +949,43 @@ def findPlacement(obj, part, elt, vtx, ignoreVertex=False): # plc = activeAssembly().Placement.inverse() * plc return plc + + +def saveAssemblyPartsPlacements(assembly): + initialPlcs = {} + assemblyParts = getMovablePartsWithin(assembly) + for part in assemblyParts: + initialPlcs[part.Name] = part.Placement + + return initialPlcs + + +def restoreAssemblyPartsPlacements(assembly, initialPlcs): + assemblyParts = getMovablePartsWithin(assembly) + for part in assemblyParts: + if part.Name in initialPlcs: + part.Placement = initialPlcs[part.Name] + + +def getComAndSize(assembly): + if assembly.ViewObject is None: + # these vars use the bounding box which is only available in gui... + # We could use the real center of mass, but it's too slow to compute it + return App.Vector(), 100 + + bbox = assembly.ViewObject.getBoundingBox() + if not bbox.isValid(): + return App.Vector(), 100 + + com = bbox.Center + size = bbox.DiagonalLength + return com, size + + +def getAssemblyShapes(assembly): + shapes = [] + assemblyParts = getMovablePartsWithin(assembly) + for part in assemblyParts: + shapes.append(part.Shape) + + return shapes