FreeCAD_assembly3/mover.py
Zheng, Lei cc412bdc22 Add SketchPlane constraint for easy 2D sketch
The SketchPlane expects the first element to be a planar face/edge to
define a work plane for any following draft wire/circle/arc elements.
The 2D constrainted elements can either be inside the defining
SketchPlane constraint, or other constrains following the SketchPlane
in the constraint group.

Add a new SketchPlane to define a new work plane for any following
draft elements. Add an empty SketchPlane to undefine the work plane,
effectively making any following draft elements free in 3D space.
2018-01-23 17:32:20 +08:00

281 lines
10 KiB
Python

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.Part):
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.edge2VertexIndex(part,info.Subname,True)
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)