Add mover support for draft wire and circle

* Draft wire points can be dragged individually.
* Draft wire individual edge can be moved and rotated.
* Draft circle/arc can be moved and rotated by dragging edge or face.
* Draft circle/arc radius can be changed by dragging vertex
* Draft arc first/last angle can be changed by dragging end points
This commit is contained in:
Zheng, Lei 2018-01-21 10:07:38 +08:00
parent b087200e50
commit c56a9d85d7
7 changed files with 318 additions and 242 deletions

View File

@ -796,7 +796,8 @@ class ViewProviderAsmElementLink(ViewProviderAsmOnTop):
return (1.0,60.0/255.0,60.0/255.0)
def doubleClicked(self,_vobj):
return movePart()
from . import mover
return mover.movePart()
def canDropObjectEx(self,_obj,owner,subname):
if logger.catchTrace('Cannot drop to AsmLink {}'.format(
@ -1554,227 +1555,6 @@ class Assembly(AsmGroup):
obj,subname,AsmConstraintGroup,False,relativeToChild)
class AsmMovingPart(object):
def __init__(self,hierarchy,info):
self.objs = [h.Assembly for h in reversed(hierarchy)]
self.assembly = resolveAssembly(info.Parent)
self.parent = info.Parent
self.subname = info.SubnameRef
self.undos = None
self.part = info.Part
self.partName = info.PartName
fixed = Constraint.getFixedTransform(self.assembly.getConstraints())
fixed = fixed.get(info.Part,None)
self.fixedTransform = fixed
if fixed and fixed.Shape:
shape = fixed.Shape
else:
shape = info.Shape
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 as the owner part.
rot = FreeCAD.Rotation()
hasBound = True
if not utils.isVertex(shape):
self.bbox = shape.BoundBox
else:
bbox = info.Object.ViewObject.getBoundingBox()
if bbox.isValid():
self.bbox = bbox
else:
logger.warn('empty bounding box of part {}'.format(
info.PartName))
self.bbox = FreeCAD.BoundBox(0,0,0,5,5,5)
hasBound = False
pos = utils.getElementPos(shape)
if not pos:
if hasBound:
pos = self.bbox.Center
else:
pos = shape.Placement.Base
pla = FreeCAD.Placement(pos,rot)
self.oldPlacement = info.Placement.copy()
self.offset = pla.copy()
self.offsetInv = pla.inverse()
self.draggerPlacement = info.Placement.multiply(pla)
self.tracePoint = self.draggerPlacement.Base
self.trace = None
@classmethod
def onRollback(cls):
doc = FreeCADGui.editDocument()
if not doc:
return
vobj = doc.getInEdit()
if vobj and isTypeOf(vobj,ViewProviderAssembly):
movingPart = getattr(vobj.Proxy,'_movingPart',None)
if movingPart:
vobj.Object.recompute(True)
movingPart.tracePoint = movingPart.draggerPlacement.Base
def update(self):
info = getElementInfo(self.parent,self.subname)
self.oldPlacement = info.Placement.copy()
self.part = info.Part
self.partName = info.PartName
pla = info.Placement.multiply(self.offset)
logger.trace('part move update {}: {}'.format(objName(self.parent),pla))
self.draggerPlacement = pla
return pla
def move(self):
obj = self.assembly.Object
pla = obj.ViewObject.DraggingPlacement
rollback = []
if self.fixedTransform:
fixed = self.fixedTransform
movement = self.draggerPlacement.inverse().multiply(pla)
if fixed.Shape:
# fixed position, so reset translation
movement.Base = FreeCAD.Vector()
if not utils.isVertex(fixed.Shape):
yaw,_,_ = movement.Rotation.toEuler()
# when dragging with a fixed axis, we align the dragger Z
# axis with that fixed axis. So we shall only keep the yaw
# among the euler angles
movement.Rotation = FreeCAD.Rotation(yaw,0,0)
pla = self.draggerPlacement.multiply(movement)
# obtain and update the part placement
pla = pla.multiply(self.offsetInv)
setPlacement(self.part,pla)
rollback.append((self.partName,self.part,self.oldPlacement.copy()))
if not gui.AsmCmdManager.AutoRecompute or \
QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier:
# AsmCmdManager.AutoRecompute means auto re-solve the system. The
# recompute() call below is only for updating linked element and
# stuff
obj.recompute(True)
return
# calls solver.solve(obj) and redirect all the exceptions message
# to logger only.
from . import solver
if not logger.catch('solver exception when moving part',
solver.solve, self.objs, dragPart=self.part, rollback=rollback):
obj.recompute(True)
if gui.AsmCmdManager.Trace and \
not self.tracePoint.isEqual(self.draggerPlacement.Base,1e-5):
try:
# check if the object is deleted
self.trace.Name
except Exception:
self.trace = None
mat = FreeCADGui.editDocument().EditingTransform
if not self.trace:
self.trace = FreeCAD.ActiveDocument.addObject(
'Part::Polygon','AsmTrace')
self.trace.Nodes = {-1:mat.multiply(self.tracePoint)}
self.tracePoint = self.draggerPlacement.Base
self.trace.Nodes = {-1:mat.multiply(self.draggerPlacement.Base)}
self.trace.recompute()
# self.draggerPlacement, which holds the intended dragger placement, is
# updated by the above solver call through the following chain,
# solver.solve() -> (triggers dependent objects recompute when done)
# Assembly.execute() ->
# ViewProviderAssembly.onExecute() ->
# AsmMovingPart.update()
return self.draggerPlacement
def getMovingElementInfo():
'''Extract information from current selection for part moving
It returns a tuple containing the selected assembly hierarchy (obtained from
Assembly.findChildren()), and AsmElementInfo of the selected child part
object.
If there is only one selection, then the moving part will be one belong to
the highest level assembly in selected hierarchy.
If there are two selections, then one selection must be a parent assembly
containing the other child object. The moving object will then be the
immediate child part object of the owner assembly. The actual selected sub
element, i.e. vertex, edge, face will determine the dragger placement
'''
sels = FreeCADGui.Selection.getSelectionEx('',False)
if not sels:
raise RuntimeError('no selection')
if not sels[0].SubElementNames:
raise RuntimeError('no sub object in selection')
if len(sels)>1 or len(sels[0].SubElementNames)>2:
raise RuntimeError('too many selection')
ret = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[0])
if not ret:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(sels[0].Object),sels[0].SubElementNames[0]))
if len(sels[0].SubElementNames)==1:
info = getElementInfo(ret[0].Assembly,ret[0].Subname)
if not info:
return
return (ret, info)
ret2 = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[1])
if not ret2:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(sels[0].Object),sels[0].SubElementNames[1]))
if len(ret) == len(ret2):
if not ret2[-1].Object:
ret,ret2 = ret2,ret
elif len(ret) > len(ret2):
ret,ret2 = ret2,ret
assembly = ret[-1].Assembly
for r in ret2:
if assembly == r.Assembly:
return (ret2, getElementInfo(r.Assembly,r.Subname))
raise RuntimeError('not child parent selection')
def canMovePart():
return logger.catchTrace('',getMovingElementInfo) is not None
def movePart(useCenterballDragger=None):
ret = logger.catch('exception when moving part', getMovingElementInfo)
if not ret:
return False
info = ret[1]
doc = FreeCADGui.editDocument()
if doc:
doc.resetEdit()
vobj = resolveAssembly(info.Parent).Object.ViewObject
doc = info.Parent.ViewObject.Document
if useCenterballDragger is not None:
vobj.UseCenterballDragger = useCenterballDragger
vobj.Proxy._movingPart = AsmMovingPart(*ret)
return doc.setEdit(vobj,1)
class AsmDocumentObserver:
def slotUndoDocument(self,_doc):
AsmMovingPart.onRollback()
def slotRedoDocument(self,_doc):
AsmMovingPart.onRollback()
def slotChangedObject(self,obj,prop):
Assembly.checkPartChange(obj,prop)
class ViewProviderAssembly(ViewProviderAsmGroup):
def __init__(self,vobj):
self._movingPart = None
@ -1811,7 +1591,8 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
return System.getIcon(self.ViewObject.Object)
def doubleClicked(self, _vobj):
return movePart()
from . import mover
return mover.movePart()
def onExecute(self):
if not getattr(self,'_movingPart',None):
@ -1844,14 +1625,15 @@ class ViewProviderAssembly(ViewProviderAsmGroup):
def onDragStart(self):
Assembly.cancelAutoSolve();
AsmMovingPart._Busy = True
FreeCADGui.Selection.clearSelection()
self.__class__._Busy = True
FreeCAD.setActiveTransaction('Assembly move')
def onDragMotion(self):
return self._movingPart.move()
def onDragEnd(self):
AsmMovingPart._Busy = False
self.__class__._Busy = False
FreeCAD.closeActiveTransaction()
def unsetEdit(self,_vobj,_mode):

View File

@ -423,7 +423,7 @@ class Constraint(ProxyType):
if elements:
info = elements[0].Proxy.getInfo()
firstPart = info.Part
if not found and firstPart:
if not found and firstPart and not utils.isDraftObject(firstPart):
ret[firstPart] = False
return ret
@ -594,9 +594,10 @@ class Locked(Base):
ret = []
for e in obj.Proxy.getElements():
info = e.Proxy.getInfo()
if utils.isDraftObject(info):
continue
shape = None
if utils.isVertex(info.Shape) or \
utils.isDraftCircle(info) or \
utils.isLinearEdge(info.Shape):
shape = info.Shape
ret.append(cls.Info(Part=info.Part,Shape=shape))

19
gui.py
View File

@ -106,6 +106,7 @@ class AsmCmdBase(object):
_toolbarName = 'Assembly3'
_menuGroupName = ''
_contextMenuName = 'Assembly'
_accel = None
@classmethod
def checkActive(cls):
@ -113,17 +114,20 @@ class AsmCmdBase(object):
@classmethod
def GetResources(cls):
return {
ret = {
'Pixmap':addIconToFCAD(cls._iconName),
'MenuText':cls.getMenuText(),
'ToolTip':cls.getToolTip()
}
if cls._accel:
ret['Accel'] = cls._accel
return ret
class AsmCmdNew(AsmCmdBase):
_id = 0
_menuText = 'Create assembly'
_iconName = 'Assembly_New_Assembly.svg'
_accel = 'A, N'
@classmethod
def Activated(cls):
@ -134,6 +138,7 @@ class AsmCmdSolve(AsmCmdBase):
_id = 1
_menuText = 'Solve constraints'
_iconName = 'AssemblyWorkbench.svg'
_accel = 'A, S'
@classmethod
def Activated(cls):
@ -149,16 +154,17 @@ class AsmCmdMove(AsmCmdBase):
_menuText = 'Move part'
_iconName = 'Assembly_Move.svg'
_useCenterballDragger = True
_accel = 'A, M'
@classmethod
def Activated(cls):
from . import assembly
assembly.movePart(cls._useCenterballDragger)
from . import mover
mover.movePart(cls._useCenterballDragger)
@classmethod
def checkActive(cls):
from . import assembly
cls._active = assembly.canMovePart()
from . import mover
cls._active = mover.canMovePart()
@classmethod
def onClearSelection(cls):
@ -169,6 +175,7 @@ class AsmCmdAxialMove(AsmCmdMove):
_menuText = 'Axial move part'
_iconName = 'Assembly_AxialMove.svg'
_useCenterballDragger = False
_accel = 'A, A'
class AsmCmdCheckable(AsmCmdBase):
_id = -2

View File

@ -34,7 +34,7 @@ class Assembly3Workbench(FreeCADGui.Workbench):
cmd.workbenchDeactivated()
def Initialize(self):
from .assembly import AsmDocumentObserver
from .mover import AsmDocumentObserver
from .gui import AsmCmdManager,SelectionObserver
cmdSet = set()
for name,cmds in AsmCmdManager.Toolbars.items():
@ -60,7 +60,6 @@ class Assembly3Workbench(FreeCADGui.Workbench):
self.appendContextMenu(name,cmds)
def ContextMenu(self, _recipient):
from .utils import logger
logger.catch('',self._contextMenu)
FreeCADGui.addWorkbench(Assembly3Workbench)

280
mover.py Normal file
View File

@ -0,0 +1,280 @@
import math
import FreeCAD, FreeCADGui
from PySide import QtCore, QtGui
from . import utils, gui
from .assembly import isTypeOf, Assembly, ViewProviderAssembly, \
resolveAssembly, getElementInfo, setPlacement
from .utils import logger, objName
from .constraint import Constraint
class AsmMovingPart(object):
def __init__(self,hierarchy,info):
self.objs = [h.Assembly for h in reversed(hierarchy)]
self.assembly = resolveAssembly(info.Parent)
self.viewObject = self.assembly.Object.ViewObject
self.info = info
self.undos = None
fixed = Constraint.getFixedTransform(self.assembly.getConstraints())
fixed = fixed.get(info.Part,None)
self.fixedTransform = fixed
if fixed and fixed.Shape:
shape = fixed.Shape
else:
shape = info.Shape
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 as the owner part.
rot = FreeCAD.Rotation()
hasBound = True
if not utils.isVertex(shape):
self.bbox = shape.BoundBox
else:
bbox = info.Object.ViewObject.getBoundingBox()
if bbox.isValid():
self.bbox = bbox
else:
logger.warn('empty bounding box of part {}'.format(
info.PartName))
self.bbox = FreeCAD.BoundBox(0,0,0,5,5,5)
hasBound = False
pos = utils.getElementPos(shape)
if not pos:
if hasBound:
pos = self.bbox.Center
else:
pos = shape.Placement.Base
pla = FreeCAD.Placement(pos,rot)
self.offset = pla.copy()
self.offsetInv = pla.inverse()
self.draggerPlacement = info.Placement.multiply(pla)
self.tracePoint = self.draggerPlacement.Base
self.trace = None
@classmethod
def onRollback(cls):
doc = FreeCADGui.editDocument()
if not doc:
return
vobj = doc.getInEdit()
if vobj and isTypeOf(vobj,ViewProviderAssembly):
movingPart = getattr(vobj.Proxy,'_movingPart',None)
if movingPart:
vobj.Object.recompute(True)
movingPart.tracePoint = movingPart.draggerPlacement.Base
def update(self):
info = getElementInfo(self.info.Parent,self.info.SubnameRef)
self.info = info
if utils.isDraftObject(info):
pos = utils.getElementPos(info.Shape)
rot = utils.getElementRotation(info.Shape)
pla = info.Placement.multiply(FreeCAD.Placement(pos,rot))
else:
pla = info.Placement.multiply(self.offset)
logger.trace('part move update {}: {}'.format(objName(info.Parent),pla))
self.draggerPlacement = pla
return pla
@property
def Movement(self):
pla = self.viewObject.DraggingPlacement.multiply(
self.draggerPlacement.inverse())
return utils.roundPlacement(pla)
def move(self):
info = self.info
part = info.Part
obj = self.assembly.Object
pla = self.viewObject.DraggingPlacement
updatePla = True
rollback = []
if utils.isDraftWire(part):
updatePla = False
if info.Subname.startswith('Vertex'):
idx = utils.draftWireVertex2PointIndex(part,info.Subname)
if idx is None:
logger.error('Invalid draft wire vertex {} {}'.format(
info.Subname, info.PartName))
return
change = [idx]
else:
change = utils.draftWireEdge2PointIndex(part,info.Subname)
if change[0] is None or change[1] is None:
logger.error('Invalid draft wire edge {} {}'.format(
info.Subname, info.PartName))
return
movement = self.Movement
points = part.Points
for idx in change:
pt = points[idx]
rollback.append((info.PartName, part, (idx,pt)))
points[idx] = movement.multVec(pt)
part.Points = points
elif info.Subname.startswith('Vertex') and \
utils.isDraftCircle(part):
updatePla = False
a1 = part.FirstAngle
a2 = part.LastAngle
r = part.Radius
rollback.append((info.PartName, part, (r,a1,a2)))
pt = info.Placement.inverse().multVec(pla.Base)
part.Radius = pt.Length
if a1 != a2:
pt.z = 0
a = math.degrees(FreeCAD.Vector(1,0,0).getAngle(pt))
if info.Subname.endswith('1'):
part.FirstAngle = a
else:
part.LastAngle = a
elif self.fixedTransform:
fixed = self.fixedTransform
movement = self.Movement
if fixed.Shape:
# fixed position, so reset translation
movement.Base = FreeCAD.Vector()
if not utils.isVertex(fixed.Shape):
yaw,_,_ = movement.Rotation.toEuler()
# when dragging with a fixed axis, we align the dragger Z
# axis with that fixed axis. So we shall only keep the yaw
# among the euler angles
movement.Rotation = FreeCAD.Rotation(yaw,0,0)
pla = self.draggerPlacement.multiply(movement)
if updatePla:
# obtain and update the part placement
pla = pla.multiply(self.offsetInv)
setPlacement(info.Part,pla)
rollback.append((info.PartName,info.Part,info.Placement.copy()))
if not gui.AsmCmdManager.AutoRecompute or \
QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier:
# AsmCmdManager.AutoRecompute means auto re-solve the system. The
# recompute() call below is only for updating linked element and
# stuff
obj.recompute(True)
return
# calls solver.solve(obj) and redirect all the exceptions message
# to logger only.
from . import solver
if not logger.catch('solver exception when moving part',
solver.solve, self.objs, dragPart=info.Part, rollback=rollback):
obj.recompute(True)
if gui.AsmCmdManager.Trace and \
not self.tracePoint.isEqual(self.draggerPlacement.Base,1e-5):
try:
# check if the object is deleted
self.trace.Name
except Exception:
self.trace = None
mat = FreeCADGui.editDocument().EditingTransform
if not self.trace:
self.trace = FreeCAD.ActiveDocument.addObject(
'Part::Polygon','AsmTrace')
self.trace.Nodes = {-1:mat.multiply(self.tracePoint)}
self.tracePoint = self.draggerPlacement.Base
self.trace.Nodes = {-1:mat.multiply(self.draggerPlacement.Base)}
self.trace.recompute()
# self.draggerPlacement, which holds the intended dragger placement, is
# updated by the above solver call through the following chain,
# solver.solve() -> (triggers dependent objects recompute when done)
# Assembly.execute() ->
# ViewProviderAssembly.onExecute() ->
# AsmMovingPart.update()
return self.draggerPlacement
def getMovingElementInfo():
'''Extract information from current selection for part moving
It returns a tuple containing the selected assembly hierarchy (obtained from
Assembly.findChildren()), and AsmElementInfo of the selected child part
object.
If there is only one selection, then the moving part will be one belong to
the highest level assembly in selected hierarchy.
If there are two selections, then one selection must be a parent assembly
containing the other child object. The moving object will then be the
immediate child part object of the owner assembly. The actual selected sub
element, i.e. vertex, edge, face will determine the dragger placement
'''
sels = FreeCADGui.Selection.getSelectionEx('',False)
if not sels:
raise RuntimeError('no selection')
if not sels[0].SubElementNames:
raise RuntimeError('no sub object in selection')
if len(sels)>1 or len(sels[0].SubElementNames)>2:
raise RuntimeError('too many selection')
ret = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[0])
if not ret:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(sels[0].Object),sels[0].SubElementNames[0]))
if len(sels[0].SubElementNames)==1:
info = getElementInfo(ret[0].Assembly,ret[0].Subname)
if not info:
return
return (ret, info)
ret2 = Assembly.findChildren(sels[0].Object,sels[0].SubElementNames[1])
if not ret2:
raise RuntimeError('invalid selection {}, subname {}'.format(
objName(sels[0].Object),sels[0].SubElementNames[1]))
if len(ret) == len(ret2):
if not ret2[-1].Object:
ret,ret2 = ret2,ret
elif len(ret) > len(ret2):
ret,ret2 = ret2,ret
assembly = ret[-1].Assembly
for r in ret2:
if assembly == r.Assembly:
return (ret2, getElementInfo(r.Assembly,r.Subname))
raise RuntimeError('not child parent selection')
def canMovePart():
return logger.catchTrace('',getMovingElementInfo) is not None
def movePart(useCenterballDragger=None):
ret = logger.catch('exception when moving part', getMovingElementInfo)
if not ret:
return False
info = ret[1]
doc = FreeCADGui.editDocument()
if doc:
doc.resetEdit()
vobj = resolveAssembly(info.Parent).Object.ViewObject
doc = info.Parent.ViewObject.Document
if useCenterballDragger is not None:
vobj.UseCenterballDragger = useCenterballDragger
vobj.Proxy._movingPart = AsmMovingPart(*ret)
return doc.setEdit(vobj,1)
class AsmDocumentObserver:
def slotUndoDocument(self,_doc):
AsmMovingPart.onRollback()
def slotRedoDocument(self,_doc):
AsmMovingPart.onRollback()
def slotChangedObject(self,obj,prop):
Assembly.checkPartChange(obj,prop)

View File

@ -59,7 +59,7 @@ class Solver(object):
addDragPoint = getattr(self.system,'addWhereDragged',None)
if addDragPoint:
info = self._partMap.get(dragPart,None)
if info:
if info and info.Workplane:
# add dragging point
self.system.log('add drag point '
'{}'.format(info.Workplane[1]))

View File

@ -469,18 +469,18 @@ def fit_rotation_axis_to_surface1( surface, n_u=3, n_v=3 ):
_tol = 10e-7
def roundPlacement(pla):
pos = [ 0.0 if abs(v)<_tol else v for v in pla.Base ]
q = [ 0.0 if abs(v)<_tol else v for v in pla.Rotation.Q ]
return FreeCAD.Placement(FreeCAD.Vector(*pos),FreeCAD.Rotation(*q))
def isSameValue(v1,v2):
if isinstance(v1,(tuple,list)):
assert(len(v1)==len(v2))
vs = zip(v1,v2)
else:
vs = (v1,v2),
for v1,v2 in vs:
v = v1-v2
if v>=_tol or v<=-_tol:
return False
return True
return all([abs(v1-v2)<_tol for v1,v2 in vs])
def isSamePos(p1,p2):
return p1.distanceToPoint(p2) < _tol
@ -511,6 +511,13 @@ def draftWireVertex2PointIndex(obj,name):
if idx < len(obj.Points):
return idx
def draftWireEdge2PointIndex(obj,name):
vname1,vname2 = edge2VertexIndex(name)
if not vname1:
return None,None
return (draftWireVertex2PointIndex(obj,vname1),
draftWireVertex2PointIndex(obj,vname2))
def edge2VertexIndex(name):
'deduct the vertex index from the edge index'
idx = getElementIndex(name,'Edge')