diff --git a/assembly.py b/assembly.py index 15a1e18..51d0089 100644 --- a/assembly.py +++ b/assembly.py @@ -593,6 +593,7 @@ def getPartInfo(parent, subname): pla = part[1].Placement obj = part[0].getLinkedObject(False) partName = part[1].Name + part = part[1] else: # b) The elements are collapsed. Then the moveable Placement # is stored inside link object's PlacementList property. So, @@ -663,6 +664,12 @@ class AsmElementLink(AsmBase): self.getInfo(True) return False + def onChanged(self,_obj,prop): + if prop=='LinkedObject' and \ + getattr(self,'parent',None) and \ + not Constraint.isDisabled(self.parent.Object): + Assembly.autoSolve() + def getAssembly(self): return self.parent.parent.parent @@ -838,8 +845,9 @@ class AsmConstraint(AsmGroup): System.getTypeName(assembly))) def onChanged(self,obj,prop): - if Constraint.onChanged(obj,prop): - obj.recompute() + if prop != 'Visibility': + Constraint.onChanged(obj,prop) + Assembly.autoSolve() def linkSetup(self,obj): self.elements = None @@ -1007,10 +1015,6 @@ class AsmConstraint(AsmGroup): for e in sel.Elements: AsmElementLink.make(AsmElementLink.MakeInfo(cstr,*e)) cstr.Proxy._initializing = False - from . import solver - if cstr.recompute() and gui.AsmCmdManager.AutoRecompute: - logger.catch('solver exception when auto recompute', - solver.solve, sel.Assembly, undo=undo) if undo: FreeCAD.closeActiveTransaction() @@ -1174,17 +1178,90 @@ BuildShapeNames = (BuildShapeNone,BuildShapeCompound, BuildShapeFuse,BuildShapeCut) class Assembly(AsmGroup): + _Timer = QtCore.QTimer() + _PartMap = {} # maps part to assembly + _PartArrayMap = {} # maps array part to assembly + def __init__(self): + self.parts = set() + self.partArrays = set() self.constraints = None super(Assembly,self).__init__() + def _collectParts(self,oldParts,newParts,partMap): + for part in newParts: + try: + oldParts.remove(part) + except KeyError: + partMap[part] = self + for part in oldParts: + del partMap[part] + return newParts + def execute(self,obj): self.constraints = None self.buildShape() System.touch(obj) obj.ViewObject.Proxy.onExecute() + + parts = set() + partArrays = set() + for cstr in self.getConstraints(): + for element in cstr.Proxy.getElements(): + info = element.Proxy.getInfo() + if isinstance(info.Part,tuple): + partArrays.add(info.Part[0]) + else: + parts.add(info.Part) + parts = self._collectParts(self.parts,parts,Assembly._PartMap) + partArrays = self._collectParts( + self.partArrays,partArrays,Assembly._PartArrayMap) + return False # return False to call LinkBaseExtension::execute() + @classmethod + def canAutoSolve(cls): + from . import solver + return gui.AsmCmdManager.AutoRecompute and \ + not FreeCAD.ActiveDocument.Restoring and \ + not solver.isBusy() and \ + not ViewProviderAssembly.isBusy() + + @classmethod + def checkPartChange(cls, obj, prop): + if not cls.canAutoSolve(): + return + assembly = None + if prop == 'Placement': + partMap = cls._PartMap + assembly = partMap.get(obj,None) + elif prop == 'PlacementList': + partMap = cls._PartArrayMap + assembly = partMap.get(obj,None) + if assembly: + try: + # This will fail if assembly got deleted + assembly.Object.Name + except Exception: + del partMap[obj] + else: + cls.autoSolve(True) + + @classmethod + def autoSolve(cls,force=False): + if force or cls.canAutoSolve(): + if not cls._Timer.isSingleShot(): + cls._Timer.setSingleShot(True) + cls._Timer.timeout.connect(Assembly.onSolverTimer) + cls._Timer.start(300) + + @classmethod + def onSolverTimer(cls): + if cls.canAutoSolve(): + from . import solver + logger.catch('solver exception when auto recompute', + solver.solve, FreeCAD.ActiveDocument.Objects, True) + def onSolverChanged(self,setup=False): for obj in self.getConstraintGroup().Group: # setup==True usually means we are restoring, so try to restore the @@ -1238,6 +1315,8 @@ class Assembly(AsmGroup): super(Assembly,self).attach(obj) def linkSetup(self,obj): + self.parts = set() + self.partArrays = set() obj.configLinkProperty('Placement') super(Assembly,self).linkSetup(obj) obj.setPropertyStatus('VisibilityList','Output') @@ -1488,7 +1567,7 @@ class AsmMovingPart(object): rot = utils.getElementRotation(shape) if not rot: # in case the shape has no normal, like a vertex, just use an empty - # rotation, which means having the same rotation has the owner part. + # rotation, which means having the same rotation as the owner part. rot = FreeCAD.Rotation() hasBound = True @@ -1672,6 +1751,9 @@ class AsmDocumentObserver: def slotRedoDocument(self,_doc): self.checkMovingPart() + def slotChangedObject(self,obj,prop): + Assembly.checkPartChange(obj,prop) + class ViewProviderAssembly(ViewProviderAsmGroup): def __init__(self,vobj): @@ -1731,19 +1813,27 @@ class ViewProviderAssembly(ViewProviderAsmGroup): self._movingPart.draggerPlacement, self._movingPart.bbox) + _Busy = False + def onDragStart(self): + AsmMovingPart._Busy = True FreeCAD.setActiveTransaction('Assembly move') def onDragMotion(self): return self._movingPart.move() def onDragEnd(self): + AsmMovingPart._Busy = False FreeCAD.closeActiveTransaction() def unsetEdit(self,_vobj,_mode): self._movingPart = None return False + @classmethod + def isBusy(cls): + return cls._Busy + class AsmWorkPlane(object): def __init__(self,obj): diff --git a/solver.py b/solver.py index c40f29d..093e50e 100644 --- a/solver.py +++ b/solver.py @@ -18,7 +18,7 @@ PartInfo = namedtuple('SolverPartInfo', ('PartName','Placement','Params','Workplane','EntityMap','Group')) class Solver(object): - def __init__(self,assembly,reportFailed,undo,dragPart,recompute,rollback): + def __init__(self,assembly,reportFailed,dragPart,recompute,rollback): self.system = System.getSystem(assembly) cstrs = assembly.Proxy.getConstraints() if not cstrs: @@ -89,7 +89,6 @@ class Solver(object): objName(assembly),e.message)) self.system.log('done sloving') - undoDocs = set() if undo else None touched = False for part,partInfo in self._partMap.items(): if part in self._fixedParts: @@ -113,10 +112,6 @@ class Solver(object): if recompute and touched: assembly.recompute(True) - if undo: - for doc in undoDocs: - doc.commitTransaction() - def isFixedPart(self,info): return info.Part in self._fixedParts @@ -150,8 +145,8 @@ class Solver(object): self._partMap[info.Part] = partInfo return partInfo -def solve(objs=None,recursive=None,reportFailed=True, - recompute=True,undo=True,dragPart=None,rollback=None): +def _solve(objs=None,recursive=None,reportFailed=True, + recompute=True,dragPart=None,rollback=None): if not objs: sels = FreeCADGui.Selection.getSelectionEx('',False) if len(sels): @@ -207,7 +202,7 @@ def solve(objs=None,recursive=None,reportFailed=True, logger.debug('skip untouched assembly ' '{}'.format(objName(assembly))) continue - Solver(assembly,reportFailed,undo,dragPart,recompute,rollback) + Solver(assembly,reportFailed,dragPart,recompute,rollback) System.touch(assembly,False) except Exception: if rollback is not None: @@ -218,4 +213,18 @@ def solve(objs=None,recursive=None,reportFailed=True, return True +_SolverBusy = False + +def solve(*args, **kargs): + global _SolverBusy + if _SolverBusy: + raise RuntimeError("Recursive call of solve() is not allowed") + try: + _SolverBusy = True + return _solve(*args,**kargs) + finally: + _SolverBusy = False + +def isBusy(): + return _SolverBusy