
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.
281 lines
10 KiB
Python
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)
|
|
|