978 lines
41 KiB
Python
978 lines
41 KiB
Python
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2011 *
|
|
#* Yorik van Havre <yorik@uncreated.net> *
|
|
#* *
|
|
#* This program is free software; you can redistribute it and/or modify *
|
|
#* it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
#* as published by the Free Software Foundation; either version 2 of *
|
|
#* the License, or (at your option) any later version. *
|
|
#* for detail see the LICENCE text file. *
|
|
#* *
|
|
#* This program is distributed in the hope that it will be useful, *
|
|
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
#* GNU Library General Public License for more details. *
|
|
#* *
|
|
#* You should have received a copy of the GNU Library General Public *
|
|
#* License along with this program; if not, write to the Free Software *
|
|
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
#* USA *
|
|
#* *
|
|
#***************************************************************************
|
|
|
|
__title__="FreeCAD Draft Snap tools"
|
|
__author__ = "Yorik van Havre"
|
|
__url__ = "http://free-cad.sourceforge.net"
|
|
|
|
|
|
import FreeCAD, FreeCADGui, math, Draft, DraftGui, DraftTrackers, DraftVecUtils
|
|
from DraftGui import todo,getMainWindow
|
|
from FreeCAD import Vector
|
|
from pivy import coin
|
|
from PyQt4 import QtCore,QtGui
|
|
|
|
class Snapper:
|
|
"""The Snapper objects contains all the functionality used by draft
|
|
and arch module to manage object snapping. It is responsible for
|
|
finding snap points and displaying snap markers. Usually You
|
|
only need to invoke it's snap() function, all the rest is taken
|
|
care of.
|
|
|
|
3 functions are useful for the scriptwriter: snap(), constrain()
|
|
or getPoint() which is an all-in-one combo.
|
|
|
|
The indivudual snapToXXX() functions return a snap definition in
|
|
the form [real_point,marker_type,visual_point], and are not
|
|
meant to be used directly, they are all called when necessary by
|
|
the general snap() function.
|
|
|
|
The Snapper lives inside FreeCADGui once the Draft module has been
|
|
loaded.
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.lastObj = [None,None]
|
|
self.views = []
|
|
self.maxEdges = 0
|
|
self.radius = 0
|
|
self.constraintAxis = None
|
|
self.basepoint = None
|
|
self.affinity = None
|
|
self.mask = None
|
|
self.cursorMode = None
|
|
if Draft.getParam("maxSnap"):
|
|
self.maxEdges = Draft.getParam("maxSnapEdges")
|
|
|
|
# we still have no 3D view when the draft module initializes
|
|
self.tracker = None
|
|
self.extLine = None
|
|
self.grid = None
|
|
self.constrainLine = None
|
|
self.trackLine = None
|
|
self.radiusTracker = None
|
|
self.snapInfo = None
|
|
self.lastSnappedObject = None
|
|
self.active = True
|
|
self.forceGridOff = False
|
|
self.trackers = [[],[],[],[],[]] # view, grid, snap, extline, radius
|
|
|
|
self.polarAngles = [90,45]
|
|
|
|
# the snapmarker has "dot","circle" and "square" available styles
|
|
self.mk = {'passive':'circle',
|
|
'extension':'circle',
|
|
'parallel':'circle',
|
|
'grid':'circle',
|
|
'endpoint':'dot',
|
|
'midpoint':'dot',
|
|
'perpendicular':'dot',
|
|
'angle':'dot',
|
|
'center':'dot',
|
|
'ortho':'dot',
|
|
'intersection':'dot'}
|
|
self.cursors = {'passive':':/icons/Snap_Near.svg',
|
|
'extension':':/icons/Snap_Extension.svg',
|
|
'parallel':':/icons/Snap_Parallel.svg',
|
|
'grid':':/icons/Snap_Grid.svg',
|
|
'endpoint':':/icons/Snap_Endpoint.svg',
|
|
'midpoint':':/icons/Snap_Midpoint.svg',
|
|
'perpendicular':':/icons/Snap_Perpendicular.svg',
|
|
'angle':':/icons/Snap_Angle.svg',
|
|
'center':':/icons/Snap_Center.svg',
|
|
'ortho':':/icons/Snap_Ortho.svg',
|
|
'intersection':':/icons/Snap_Intersection.svg'}
|
|
|
|
def snap(self,screenpos,lastpoint=None,active=True,constrain=False,noTracker=False):
|
|
"""snap(screenpos,lastpoint=None,active=True,constrain=False,noTracker=False): returns a snapped
|
|
point from the given (x,y) screenpos (the position of the mouse cursor), active is to
|
|
activate active point snapping or not (passive), lastpoint is an optional
|
|
other point used to draw an imaginary segment and get additional snap locations. Constrain can
|
|
be True to constrain the point against the closest working plane axis.
|
|
Screenpos can be a list, a tuple or a coin.SbVec2s object. If noTracker is True,
|
|
the tracking line is not displayed."""
|
|
|
|
global Part, DraftGeomUtils
|
|
import Part, DraftGeomUtils
|
|
|
|
if not hasattr(self,"toolbar"):
|
|
self.makeSnapToolBar()
|
|
mw = getMainWindow()
|
|
bt = mw.findChild(QtGui.QToolBar,"Draft Snap")
|
|
if not bt:
|
|
mw.addToolBar(self.toolbar)
|
|
else:
|
|
if Draft.getParam("showSnapBar"):
|
|
bt.show()
|
|
|
|
def cstr(point):
|
|
"constrains if needed"
|
|
if constrain or self.mask:
|
|
fpt = self.constrain(point,lastpoint)
|
|
else:
|
|
self.unconstrain()
|
|
fpt = point
|
|
if self.radiusTracker:
|
|
self.radiusTracker.update(fpt)
|
|
return fpt
|
|
|
|
snaps = []
|
|
self.snapInfo = None
|
|
|
|
# type conversion if needed
|
|
if isinstance(screenpos,list):
|
|
screenpos = tuple(screenpos)
|
|
elif isinstance(screenpos,coin.SbVec2s):
|
|
screenpos = tuple(screenpos.getValue())
|
|
elif not isinstance(screenpos,tuple):
|
|
print "snap needs valid screen position (list, tuple or sbvec2s)"
|
|
return None
|
|
|
|
# setup trackers if needed
|
|
self.setTrackers()
|
|
|
|
# getting current snap Radius
|
|
self.radius = self.getScreenDist(Draft.getParam("snapRange"),screenpos)
|
|
if self.radiusTracker:
|
|
self.radiusTracker.update(self.radius)
|
|
self.radiusTracker.off()
|
|
|
|
# activate snap
|
|
oldActive = False
|
|
if Draft.getParam("alwaysSnap"):
|
|
oldActive = active
|
|
active = True
|
|
if not self.active:
|
|
active = False
|
|
|
|
self.setCursor('passive')
|
|
if self.tracker:
|
|
self.tracker.off()
|
|
if self.extLine:
|
|
self.extLine.off()
|
|
if self.trackLine:
|
|
self.trackLine.off()
|
|
|
|
point = self.getApparentPoint(screenpos[0],screenpos[1])
|
|
|
|
# setup a track line if we got a last point
|
|
if lastpoint:
|
|
if not self.trackLine:
|
|
self.trackLine = DraftTrackers.lineTracker()
|
|
self.trackLine.p1(lastpoint)
|
|
|
|
# check if we snapped to something
|
|
self.snapInfo = Draft.get3DView().getObjectInfo((screenpos[0],screenpos[1]))
|
|
|
|
# checking if parallel to one of the edges of the last objects or to a polar direction
|
|
if active:
|
|
eline = None
|
|
point,eline = self.snapToPolar(point,lastpoint)
|
|
point,eline = self.snapToExtensions(point,lastpoint,constrain,eline)
|
|
|
|
if not self.snapInfo:
|
|
|
|
# nothing has been snapped, check fro grid snap
|
|
if active:
|
|
point = self.snapToGrid(point)
|
|
fp = cstr(point)
|
|
if self.trackLine and lastpoint and (not noTracker):
|
|
self.trackLine.p2(fp)
|
|
self.trackLine.on()
|
|
return fp
|
|
|
|
else:
|
|
|
|
# we have an object to snap to
|
|
|
|
obj = FreeCAD.ActiveDocument.getObject(self.snapInfo['Object'])
|
|
if not obj:
|
|
return cstr(point)
|
|
|
|
self.lastSnappedObject = obj
|
|
|
|
if hasattr(obj.ViewObject,"Selectable"):
|
|
if not obj.ViewObject.Selectable:
|
|
return cstr(point)
|
|
|
|
if not active:
|
|
|
|
# passive snapping
|
|
snaps = [self.snapToVertex(self.snapInfo)]
|
|
|
|
else:
|
|
|
|
# first stick to the snapped object
|
|
s = self.snapToVertex(self.snapInfo)
|
|
if s:
|
|
point = s[0]
|
|
|
|
# active snapping
|
|
comp = self.snapInfo['Component']
|
|
|
|
if (Draft.getType(obj) == "Wall") and not oldActive:
|
|
edges = []
|
|
for o in [obj]+obj.Additions:
|
|
if Draft.getType(o) == "Wall":
|
|
if o.Base:
|
|
edges.extend(o.Base.Shape.Edges)
|
|
for edge in edges:
|
|
snaps.extend(self.snapToEndpoints(edge))
|
|
snaps.extend(self.snapToMidpoint(edge))
|
|
snaps.extend(self.snapToPerpendicular(edge,lastpoint))
|
|
snaps.extend(self.snapToIntersection(edge))
|
|
snaps.extend(self.snapToElines(edge,eline))
|
|
|
|
elif obj.isDerivedFrom("Part::Feature"):
|
|
if (not self.maxEdges) or (len(obj.Edges) <= self.maxEdges):
|
|
if "Edge" in comp:
|
|
# we are snapping to an edge
|
|
en = int(comp[4:])-1
|
|
if len(obj.Shape.Edges) > en:
|
|
edge = obj.Shape.Edges[en]
|
|
snaps.extend(self.snapToEndpoints(edge))
|
|
snaps.extend(self.snapToMidpoint(edge))
|
|
snaps.extend(self.snapToPerpendicular(edge,lastpoint))
|
|
#snaps.extend(self.snapToOrtho(edge,lastpoint,constrain)) # now part of snapToPolar
|
|
snaps.extend(self.snapToIntersection(edge))
|
|
snaps.extend(self.snapToElines(edge,eline))
|
|
|
|
if DraftGeomUtils.geomType(edge) == "Circle":
|
|
# the edge is an arc, we have extra options
|
|
snaps.extend(self.snapToAngles(edge))
|
|
snaps.extend(self.snapToCenter(edge))
|
|
|
|
elif "Vertex" in comp:
|
|
# directly snapped to a vertex
|
|
snaps.append(self.snapToVertex(self.snapInfo,active=True))
|
|
elif comp == '':
|
|
# workaround for the new view provider
|
|
snaps.append(self.snapToVertex(self.snapInfo,active=True))
|
|
else:
|
|
# all other cases (face, etc...) default to passive snap
|
|
snapArray = [self.snapToVertex(self.snapInfo)]
|
|
|
|
elif Draft.getType(obj) == "Dimension":
|
|
# for dimensions we snap to their 3 points
|
|
for pt in [obj.Start,obj.End,obj.Dimline]:
|
|
snaps.append([pt,'endpoint',pt])
|
|
|
|
elif Draft.getType(obj) == "Mesh":
|
|
# for meshes we only snap to vertices
|
|
snaps.extend(self.snapToEndpoints(obj.Mesh))
|
|
elif Draft.getType(obj) == "Points":
|
|
# for points we only snap to points
|
|
snaps.extend(self.snapToEndpoints(obj.Points))
|
|
|
|
# updating last objects list
|
|
if not self.lastObj[1]:
|
|
self.lastObj[1] = obj.Name
|
|
elif self.lastObj[1] != obj.Name:
|
|
self.lastObj[0] = self.lastObj[1]
|
|
self.lastObj[1] = obj.Name
|
|
|
|
if not snaps:
|
|
return cstr(point)
|
|
|
|
# calculating the nearest snap point
|
|
shortest = 1000000000000000000
|
|
origin = Vector(self.snapInfo['x'],self.snapInfo['y'],self.snapInfo['z'])
|
|
winner = [Vector(0,0,0),None,Vector(0,0,0)]
|
|
for snap in snaps:
|
|
if (not snap) or (snap[0] == None):
|
|
print "debug: Snapper: invalid snap point: ",snaps
|
|
else:
|
|
delta = snap[0].sub(origin)
|
|
if delta.Length < shortest:
|
|
shortest = delta.Length
|
|
winner = snap
|
|
|
|
# see if we are out of the max radius, if any
|
|
if self.radius:
|
|
dv = point.sub(winner[2])
|
|
if (dv.Length > self.radius):
|
|
if (not oldActive) and self.isEnabled("passive"):
|
|
winner = self.snapToVertex(self.snapInfo)
|
|
|
|
# setting the cursors
|
|
if self.tracker:
|
|
self.tracker.setCoords(winner[2])
|
|
self.tracker.setMarker(self.mk[winner[1]])
|
|
self.tracker.on()
|
|
# setting the trackline
|
|
fp = cstr(winner[2])
|
|
if self.trackLine and lastpoint:
|
|
self.trackLine.p2(fp)
|
|
self.trackLine.on()
|
|
# set the cursor
|
|
self.setCursor(winner[1])
|
|
|
|
# return the final point
|
|
return fp
|
|
|
|
def getApparentPoint(self,x,y):
|
|
"returns a 3D point, projected on the current working plane"
|
|
view = Draft.get3DView()
|
|
pt = view.getPoint(x,y)
|
|
if self.mask != "z":
|
|
if hasattr(FreeCAD,"DraftWorkingPlane"):
|
|
if view.getCameraType() == "Perspective":
|
|
camera = view.getCameraNode()
|
|
p = camera.getField("position").getValue()
|
|
dv = pt.sub(Vector(p[0],p[1],p[2]))
|
|
else:
|
|
dv = view.getViewDirection()
|
|
return FreeCAD.DraftWorkingPlane.projectPoint(pt,dv)
|
|
return pt
|
|
|
|
def snapToExtensions(self,point,last,constrain,eline):
|
|
"returns a point snapped to extension or parallel line to last object, if any"
|
|
|
|
if self.isEnabled("extension"):
|
|
tsnap = self.snapToExtOrtho(last,constrain,eline)
|
|
if tsnap:
|
|
if (tsnap[0].sub(point)).Length < self.radius:
|
|
if self.tracker:
|
|
self.tracker.setCoords(tsnap[2])
|
|
self.tracker.setMarker(self.mk[tsnap[1]])
|
|
self.tracker.on()
|
|
if self.extLine:
|
|
self.extLine.p2(tsnap[2])
|
|
self.extLine.on()
|
|
self.setCursor(tsnap[1])
|
|
return tsnap[2],eline
|
|
else:
|
|
tsnap = self.snapToExtPerpendicular(last)
|
|
if tsnap:
|
|
if (tsnap[0].sub(point)).Length < self.radius:
|
|
if self.tracker:
|
|
self.tracker.setCoords(tsnap[2])
|
|
self.tracker.setMarker(self.mk[tsnap[1]])
|
|
self.tracker.on()
|
|
if self.extLine:
|
|
self.extLine.p2(tsnap[2])
|
|
self.extLine.on()
|
|
self.setCursor(tsnap[1])
|
|
return tsnap[2],eline
|
|
|
|
for o in [self.lastObj[1],self.lastObj[0]]:
|
|
if o:
|
|
ob = FreeCAD.ActiveDocument.getObject(o)
|
|
if ob:
|
|
if ob.isDerivedFrom("Part::Feature"):
|
|
edges = ob.Shape.Edges
|
|
if (not self.maxEdges) or (len(edges) <= self.maxEdges):
|
|
for e in edges:
|
|
if DraftGeomUtils.geomType(e) == "Line":
|
|
np = self.getPerpendicular(e,point)
|
|
if not DraftGeomUtils.isPtOnEdge(np,e):
|
|
if (np.sub(point)).Length < self.radius:
|
|
if self.isEnabled('extension'):
|
|
if np != e.Vertexes[0].Point:
|
|
if self.tracker:
|
|
self.tracker.setCoords(np)
|
|
self.tracker.setMarker(self.mk['extension'])
|
|
self.tracker.on()
|
|
if self.extLine:
|
|
self.extLine.p1(e.Vertexes[0].Point)
|
|
self.extLine.p2(np)
|
|
self.extLine.on()
|
|
self.setCursor('extension')
|
|
return np,Part.Line(e.Vertexes[0].Point,np).toShape()
|
|
else:
|
|
if self.isEnabled('parallel'):
|
|
if last:
|
|
ve = DraftGeomUtils.vec(e)
|
|
if not DraftVecUtils.isNull(ve):
|
|
de = Part.Line(last,last.add(ve)).toShape()
|
|
np = self.getPerpendicular(de,point)
|
|
if (np.sub(point)).Length < self.radius:
|
|
if self.tracker:
|
|
self.tracker.setCoords(np)
|
|
self.tracker.setMarker(self.mk['parallel'])
|
|
self.tracker.on()
|
|
self.setCursor('parallel')
|
|
return np,de
|
|
return point,eline
|
|
|
|
def snapToPolar(self,point,last):
|
|
"snaps to polar lines from the given point"
|
|
if self.isEnabled('ortho') and (not self.mask):
|
|
if last:
|
|
vecs = []
|
|
if hasattr(FreeCAD,"DraftWorkingPlane"):
|
|
ax = [FreeCAD.DraftWorkingPlane.u,
|
|
FreeCAD.DraftWorkingPlane.v,
|
|
FreeCAD.DraftWorkingPlane.axis]
|
|
else:
|
|
ax = [FreeCAD.Vector(1,0,0),
|
|
FreeCAD.Vector(0,1,0),
|
|
FreeCAD.Vector(0,0,1)]
|
|
for a in self.polarAngles:
|
|
if a == 90:
|
|
vecs.extend([ax[0],DraftVecUtils.neg(ax[0])])
|
|
vecs.extend([ax[1],DraftVecUtils.neg(ax[1])])
|
|
else:
|
|
v = DraftVecUtils.rotate(ax[0],math.radians(a),ax[2])
|
|
vecs.extend([v,DraftVecUtils.neg(v)])
|
|
v = DraftVecUtils.rotate(ax[1],math.radians(a),ax[2])
|
|
vecs.extend([v,DraftVecUtils.neg(v)])
|
|
for v in vecs:
|
|
de = Part.Line(last,last.add(v)).toShape()
|
|
np = self.getPerpendicular(de,point)
|
|
if ((self.radius == 0) and (point.sub(last).getAngle(v) < 0.087)) \
|
|
or ((np.sub(point)).Length < self.radius):
|
|
if self.tracker:
|
|
self.tracker.setCoords(np)
|
|
self.tracker.setMarker(self.mk['parallel'])
|
|
self.tracker.on()
|
|
self.setCursor('ortho')
|
|
return np,de
|
|
return point,None
|
|
|
|
def snapToGrid(self,point):
|
|
"returns a grid snap point if available"
|
|
if self.grid:
|
|
if self.grid.Visible:
|
|
if self.isEnabled("grid"):
|
|
np = self.grid.getClosestNode(point)
|
|
if np:
|
|
dv = point.sub(np)
|
|
if (self.radius == 0) or (dv.Length <= self.radius):
|
|
if self.tracker:
|
|
self.tracker.setCoords(np)
|
|
self.tracker.setMarker(self.mk['grid'])
|
|
self.tracker.on()
|
|
self.setCursor('grid')
|
|
return np
|
|
return point
|
|
|
|
def snapToEndpoints(self,shape):
|
|
"returns a list of enpoints snap locations"
|
|
snaps = []
|
|
if self.isEnabled("endpoint"):
|
|
if hasattr(shape,"Vertexes"):
|
|
for v in shape.Vertexes:
|
|
snaps.append([v.Point,'endpoint',v.Point])
|
|
elif hasattr(shape,"Point"):
|
|
snaps.append([shape.Point,'endpoint',shape.Point])
|
|
elif hasattr(shape,"Points"):
|
|
if len(shape.Points) and hasattr(shape.Points[0],"Vector"):
|
|
for v in shape.Points:
|
|
snaps.append([v.Vector,'endpoint',v.Vector])
|
|
else:
|
|
for v in shape.Points:
|
|
snaps.append([v,'endpoint',v])
|
|
return snaps
|
|
|
|
def snapToMidpoint(self,shape):
|
|
"returns a list of midpoints snap locations"
|
|
snaps = []
|
|
if self.isEnabled("midpoint"):
|
|
if isinstance(shape,Part.Edge):
|
|
mp = DraftGeomUtils.findMidpoint(shape)
|
|
if mp:
|
|
snaps.append([mp,'midpoint',mp])
|
|
return snaps
|
|
|
|
def snapToPerpendicular(self,shape,last):
|
|
"returns a list of perpendicular snap locations"
|
|
snaps = []
|
|
if self.isEnabled("perpendicular"):
|
|
if last:
|
|
if isinstance(shape,Part.Edge):
|
|
if DraftGeomUtils.geomType(shape) == "Line":
|
|
np = self.getPerpendicular(shape,last)
|
|
elif DraftGeomUtils.geomType(shape) == "Circle":
|
|
dv = last.sub(shape.Curve.Center)
|
|
dv = DraftVecUtils.scaleTo(dv,shape.Curve.Radius)
|
|
np = (shape.Curve.Center).add(dv)
|
|
elif DraftGeomUtils.geomType(shape) == "BSplineCurve":
|
|
pr = shape.Curve.parameter(last)
|
|
np = shape.Curve.value(pr)
|
|
else:
|
|
return snaps
|
|
snaps.append([np,'perpendicular',np])
|
|
return snaps
|
|
|
|
def snapToOrtho(self,shape,last,constrain):
|
|
"returns a list of ortho snap locations"
|
|
snaps = []
|
|
if self.isEnabled("ortho"):
|
|
if constrain:
|
|
if isinstance(shape,Part.Edge):
|
|
if last:
|
|
if DraftGeomUtils(shape) == "Line":
|
|
if self.constraintAxis:
|
|
tmpEdge = Part.Line(last,last.add(self.constraintAxis)).toShape()
|
|
# get the intersection points
|
|
pt = DraftGeomUtils.findIntersection(tmpEdge,shape,True,True)
|
|
if pt:
|
|
for p in pt:
|
|
snaps.append([p,'ortho',p])
|
|
return snaps
|
|
|
|
def snapToExtOrtho(self,last,constrain,eline):
|
|
"returns an ortho X extension snap location"
|
|
if self.isEnabled("extension") and self.isEnabled("ortho"):
|
|
if constrain and last and self.constraintAxis and self.extLine:
|
|
tmpEdge1 = Part.Line(last,last.add(self.constraintAxis)).toShape()
|
|
tmpEdge2 = Part.Line(self.extLine.p1(),self.extLine.p2()).toShape()
|
|
# get the intersection points
|
|
pt = DraftGeomUtils.findIntersection(tmpEdge1,tmpEdge2,True,True)
|
|
if pt:
|
|
return [pt[0],'ortho',pt[0]]
|
|
if eline:
|
|
try:
|
|
tmpEdge2 = Part.Line(self.extLine.p1(),self.extLine.p2()).toShape()
|
|
# get the intersection points
|
|
pt = DraftGeomUtils.findIntersection(eline,tmpEdge2,True,True)
|
|
if pt:
|
|
return [pt[0],'ortho',pt[0]]
|
|
except:
|
|
return None
|
|
return None
|
|
|
|
def snapToExtPerpendicular(self,last):
|
|
"returns a perpendicular X extension snap location"
|
|
if self.isEnabled("extension") and self.isEnabled("perpendicular"):
|
|
if last and self.extLine:
|
|
if self.extLine.p1() != self.extLine.p2():
|
|
tmpEdge = Part.Line(self.extLine.p1(),self.extLine.p2()).toShape()
|
|
np = self.getPerpendicular(tmpEdge,last)
|
|
return [np,'perpendicular',np]
|
|
return None
|
|
|
|
def snapToElines(self,e1,e2):
|
|
"returns a snap location at the infinite intersection of the given edges"
|
|
snaps = []
|
|
if self.isEnabled("intersection") and self.isEnabled("extension"):
|
|
if e1 and e2:
|
|
# get the intersection points
|
|
pts = DraftGeomUtils.findIntersection(e1,e2,True,True)
|
|
if pts:
|
|
for p in pts:
|
|
snaps.append([p,'intersection',p])
|
|
return snaps
|
|
|
|
|
|
def snapToAngles(self,shape):
|
|
"returns a list of angle snap locations"
|
|
snaps = []
|
|
if self.isEnabled("angle"):
|
|
rad = shape.Curve.Radius
|
|
pos = shape.Curve.Center
|
|
for i in [0,30,45,60,90,120,135,150,180,210,225,240,270,300,315,330]:
|
|
ang = math.radians(i)
|
|
cur = Vector(math.sin(ang)*rad+pos.x,math.cos(ang)*rad+pos.y,pos.z)
|
|
snaps.append([cur,'angle',cur])
|
|
return snaps
|
|
|
|
def snapToCenter(self,shape):
|
|
"returns a list of center snap locations"
|
|
snaps = []
|
|
if self.isEnabled("center"):
|
|
rad = shape.Curve.Radius
|
|
pos = shape.Curve.Center
|
|
for i in [15,37.5,52.5,75,105,127.5,142.5,165,195,217.5,232.5,255,285,307.5,322.5,345]:
|
|
ang = math.radians(i)
|
|
cur = Vector(math.sin(ang)*rad+pos.x,math.cos(ang)*rad+pos.y,pos.z)
|
|
snaps.append([cur,'center',pos])
|
|
return snaps
|
|
|
|
def snapToIntersection(self,shape):
|
|
"returns a list of intersection snap locations"
|
|
snaps = []
|
|
if self.isEnabled("intersection"):
|
|
# get the stored objects to calculate intersections
|
|
if self.lastObj[0]:
|
|
obj = FreeCAD.ActiveDocument.getObject(self.lastObj[0])
|
|
if obj:
|
|
if obj.isDerivedFrom("Part::Feature"):
|
|
if (not self.maxEdges) or (len(obj.Shape.Edges) <= self.maxEdges):
|
|
for e in obj.Shape.Edges:
|
|
# get the intersection points
|
|
pt = DraftGeomUtils.findIntersection(e,shape)
|
|
if pt:
|
|
for p in pt:
|
|
snaps.append([p,'intersection',p])
|
|
return snaps
|
|
|
|
def snapToVertex(self,info,active=False):
|
|
p = Vector(info['x'],info['y'],info['z'])
|
|
if active:
|
|
if self.isEnabled("endpoint"):
|
|
return [p,'endpoint',p]
|
|
else:
|
|
return []
|
|
elif self.isEnabled("passive"):
|
|
return [p,'passive',p]
|
|
else:
|
|
return []
|
|
|
|
def getScreenDist(self,dist,cursor):
|
|
"returns a distance in 3D space from a screen pixels distance"
|
|
view = Draft.get3DView()
|
|
p1 = view.getPoint(cursor)
|
|
p2 = view.getPoint((cursor[0]+dist,cursor[1]))
|
|
return (p2.sub(p1)).Length
|
|
|
|
def getPerpendicular(self,edge,pt):
|
|
"returns a point on an edge, perpendicular to the given point"
|
|
dv = pt.sub(edge.Vertexes[0].Point)
|
|
nv = DraftVecUtils.project(dv,DraftGeomUtils.vec(edge))
|
|
np = (edge.Vertexes[0].Point).add(nv)
|
|
return np
|
|
|
|
def setCursor(self,mode=None):
|
|
"setCursor(self,mode=None): sets or resets the cursor to the given mode or resets"
|
|
|
|
if not mode:
|
|
for v in self.views:
|
|
v.unsetCursor()
|
|
self.views = []
|
|
self.cursorMode = None
|
|
else:
|
|
if mode != self.cursorMode:
|
|
if not self.views:
|
|
mw = DraftGui.getMainWindow()
|
|
self.views = mw.findChildren(QtGui.QWidget,"QtGLArea")
|
|
baseicon = QtGui.QPixmap(":/icons/Draft_Cursor.svg")
|
|
newicon = QtGui.QPixmap(32,24)
|
|
newicon.fill(QtCore.Qt.transparent)
|
|
qp = QtGui.QPainter()
|
|
qp.begin(newicon)
|
|
qp.drawPixmap(0,0,baseicon)
|
|
if not (mode == 'passive'):
|
|
tp = QtGui.QPixmap(self.cursors[mode]).scaledToWidth(16)
|
|
qp.drawPixmap(QtCore.QPoint(16, 8), tp);
|
|
qp.end()
|
|
cur = QtGui.QCursor(newicon,8,8)
|
|
for v in self.views:
|
|
v.setCursor(cur)
|
|
self.cursorMode = mode
|
|
|
|
def restack(self):
|
|
if self.grid:
|
|
self.grid.lowerTracker()
|
|
|
|
def off(self):
|
|
"finishes snapping"
|
|
if self.tracker:
|
|
self.tracker.off()
|
|
if self.trackLine:
|
|
self.trackLine.off()
|
|
if self.extLine:
|
|
self.extLine.off()
|
|
if self.radiusTracker:
|
|
self.radiusTracker.off()
|
|
if self.grid:
|
|
if not Draft.getParam("alwaysShowGrid"):
|
|
self.grid.off()
|
|
self.unconstrain()
|
|
self.radius = 0
|
|
self.setCursor()
|
|
if Draft.getParam("hideSnapBar"):
|
|
self.toolbar.hide()
|
|
self.mask = None
|
|
|
|
def constrain(self,point,basepoint=None,axis=None):
|
|
'''constrain(point,basepoint=None,axis=None: Returns a
|
|
constrained point. Axis can be "x","y" or "z" or a custom vector. If None,
|
|
the closest working plane axis will be picked.
|
|
Basepoint is the base point used to figure out from where the point
|
|
must be constrained. If no basepoint is given, the current point is
|
|
used as basepoint.'''
|
|
|
|
# without the Draft module fully loaded, no axes system!"
|
|
if not hasattr(FreeCAD,"DraftWorkingPlane"):
|
|
return point
|
|
|
|
point = Vector(point)
|
|
|
|
# setup trackers if needed
|
|
if not self.constrainLine:
|
|
self.constrainLine = DraftTrackers.lineTracker(dotted=True)
|
|
|
|
# setting basepoint
|
|
if not basepoint:
|
|
if not self.basepoint:
|
|
self.basepoint = point
|
|
else:
|
|
self.basepoint = basepoint
|
|
delta = point.sub(self.basepoint)
|
|
|
|
# setting constraint axis
|
|
if self.mask:
|
|
self.affinity = self.mask
|
|
if not self.affinity:
|
|
self.affinity = FreeCAD.DraftWorkingPlane.getClosestAxis(delta)
|
|
if isinstance(axis,FreeCAD.Vector):
|
|
self.constraintAxis = axis
|
|
elif axis == "x":
|
|
self.constraintAxis = FreeCAD.DraftWorkingPlane.u
|
|
elif axis == "y":
|
|
self.constraintAxis = FreeCAD.DraftWorkingPlane.v
|
|
elif axis == "z":
|
|
self.constraintAxis = FreeCAD.DraftWorkingPlane.axis
|
|
else:
|
|
if self.affinity == "x":
|
|
self.constraintAxis = FreeCAD.DraftWorkingPlane.u
|
|
elif self.affinity == "y":
|
|
self.constraintAxis = FreeCAD.DraftWorkingPlane.v
|
|
else:
|
|
self.constraintAxis = FreeCAD.DraftWorkingPlane.axis
|
|
|
|
# calculating constrained point
|
|
cdelta = DraftVecUtils.project(delta,self.constraintAxis)
|
|
npoint = self.basepoint.add(cdelta)
|
|
|
|
# setting constrain line
|
|
if self.constrainLine:
|
|
if point != npoint:
|
|
self.constrainLine.p1(point)
|
|
self.constrainLine.p2(npoint)
|
|
self.constrainLine.on()
|
|
else:
|
|
self.constrainLine.off()
|
|
|
|
return npoint
|
|
|
|
def unconstrain(self):
|
|
self.basepoint = None
|
|
self.affinity = None
|
|
if self.constrainLine:
|
|
self.constrainLine.off()
|
|
|
|
def getPoint(self,last=None,callback=None,movecallback=None,extradlg=None):
|
|
|
|
"""
|
|
getPoint([last],[callback],[movecallback],[extradlg]) : gets a 3D point
|
|
from the screen. You can provide an existing point, in that case additional
|
|
snap options and a tracker are available.
|
|
You can also pass a function as callback, which will get called
|
|
with the resulting point as argument, when a point is clicked, and optionally
|
|
another callback which gets called when the mouse is moved.
|
|
|
|
If the operation gets cancelled (the user pressed Escape), no point is returned.
|
|
|
|
Example:
|
|
|
|
def cb(point):
|
|
if point:
|
|
print "got a 3D point: ",point
|
|
|
|
FreeCADGui.Snapper.getPoint(callback=cb)
|
|
|
|
If the callback function accepts more than one argument, it will also receive
|
|
the last snapped object. Finally, a pyqt dialog can be passed as extra taskbox.
|
|
|
|
"""
|
|
|
|
import inspect
|
|
|
|
self.pt = None
|
|
self.lastSnappedObject = None
|
|
self.ui = FreeCADGui.draftToolBar
|
|
self.view = Draft.get3DView()
|
|
|
|
def move(event_cb):
|
|
event = event_cb.getEvent()
|
|
mousepos = event.getPosition()
|
|
ctrl = event.wasCtrlDown()
|
|
shift = event.wasShiftDown()
|
|
self.pt = FreeCADGui.Snapper.snap(mousepos,lastpoint=last,active=ctrl,constrain=shift)
|
|
if hasattr(FreeCAD,"DraftWorkingPlane"):
|
|
self.ui.displayPoint(self.pt,last,plane=FreeCAD.DraftWorkingPlane,mask=FreeCADGui.Snapper.affinity)
|
|
if movecallback:
|
|
movecallback(self.pt)
|
|
|
|
def getcoords(point,relative=False):
|
|
self.pt = point
|
|
if relative and last and hasattr(FreeCAD,"DraftWorkingPlane"):
|
|
v = FreeCAD.DraftWorkingPlane.getGlobalCoords(point)
|
|
self.pt = last.add(v)
|
|
accept()
|
|
|
|
def click(event_cb):
|
|
event = event_cb.getEvent()
|
|
if event.getButton() == 1:
|
|
if event.getState() == coin.SoMouseButtonEvent.DOWN:
|
|
accept()
|
|
|
|
def accept():
|
|
self.view.removeEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(),self.callbackClick)
|
|
self.view.removeEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(),self.callbackMove)
|
|
obj = FreeCADGui.Snapper.lastSnappedObject
|
|
FreeCADGui.Snapper.off()
|
|
self.ui.offUi()
|
|
if callback:
|
|
if len(inspect.getargspec(callback).args) > 2:
|
|
callback(self.pt,obj)
|
|
else:
|
|
callback(self.pt)
|
|
self.pt = None
|
|
|
|
def cancel():
|
|
self.view.removeEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(),self.callbackClick)
|
|
self.view.removeEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(),self.callbackMove)
|
|
FreeCADGui.Snapper.off()
|
|
self.ui.offUi()
|
|
if callback:
|
|
callback(None)
|
|
|
|
# adding callback functions
|
|
self.ui.pointUi(cancel=cancel,getcoords=getcoords,extra=extradlg,rel=bool(last))
|
|
self.callbackClick = self.view.addEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(),click)
|
|
self.callbackMove = self.view.addEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(),move)
|
|
|
|
def makeSnapToolBar(self):
|
|
"builds the Snap toolbar"
|
|
self.toolbar = QtGui.QToolBar(None)
|
|
self.toolbar.setObjectName("Draft Snap")
|
|
self.toolbar.setWindowTitle("Draft Snap")
|
|
self.toolbarButtons = []
|
|
self.masterbutton = QtGui.QPushButton(None)
|
|
self.masterbutton.setIcon(QtGui.QIcon(":/icons/Snap_Lock.svg"))
|
|
self.masterbutton.setIconSize(QtCore.QSize(16, 16))
|
|
self.masterbutton.setMaximumSize(QtCore.QSize(26,26))
|
|
self.masterbutton.setToolTip("Snap On/Off")
|
|
self.masterbutton.setObjectName("SnapButtonMain")
|
|
self.masterbutton.setCheckable(True)
|
|
self.masterbutton.setChecked(True)
|
|
QtCore.QObject.connect(self.masterbutton,QtCore.SIGNAL("toggled(bool)"),self.toggle)
|
|
self.toolbar.addWidget(self.masterbutton)
|
|
for c,i in self.cursors.iteritems():
|
|
if i:
|
|
b = QtGui.QPushButton(None)
|
|
b.setIcon(QtGui.QIcon(i))
|
|
b.setIconSize(QtCore.QSize(16, 16))
|
|
b.setMaximumSize(QtCore.QSize(26,26))
|
|
b.setToolTip(c)
|
|
b.setObjectName("SnapButton"+c)
|
|
b.setCheckable(True)
|
|
b.setChecked(True)
|
|
self.toolbar.addWidget(b)
|
|
self.toolbarButtons.append(b)
|
|
QtCore.QObject.connect(b,QtCore.SIGNAL("toggled(bool)"),self.saveSnapModes)
|
|
# restoring states
|
|
t = Draft.getParam("snapModes")
|
|
if t:
|
|
c = 0
|
|
for b in [self.masterbutton]+self.toolbarButtons:
|
|
if len(t) > c:
|
|
b.setChecked(bool(int(t[c])))
|
|
c += 1
|
|
if not Draft.getParam("showSnapBar"):
|
|
self.toolbar.hide()
|
|
|
|
def saveSnapModes(self):
|
|
"saves the snap modes for next sessions"
|
|
t = ''
|
|
for b in [self.masterbutton]+self.toolbarButtons:
|
|
t += str(int(b.isChecked()))
|
|
Draft.setParam("snapModes",t)
|
|
|
|
def toggle(self,checked=None):
|
|
"toggles the snap mode"
|
|
if hasattr(self,"toolbarButtons"):
|
|
if checked == None:
|
|
self.masterbutton.toggle()
|
|
elif checked:
|
|
if hasattr(self,"savedButtonStates"):
|
|
for i in range(len(self.toolbarButtons)):
|
|
self.toolbarButtons[i].setEnabled(True)
|
|
self.toolbarButtons[i].setChecked(self.savedButtonStates[i])
|
|
else:
|
|
self.savedButtonStates = []
|
|
for i in range(len(self.toolbarButtons)):
|
|
self.savedButtonStates.append(self.toolbarButtons[i].isChecked())
|
|
self.toolbarButtons[i].setEnabled(False)
|
|
self.saveSnapModes()
|
|
|
|
def showradius(self):
|
|
"shows the snap radius indicator"
|
|
self.radius = self.getScreenDist(Draft.getParam("snapRange"),(400,300))
|
|
if self.radiusTracker:
|
|
self.radiusTracker.update(self.radius)
|
|
self.radiusTracker.on()
|
|
|
|
def isEnabled(self,but):
|
|
"returns true if the given button is turned on"
|
|
for b in self.toolbarButtons:
|
|
if str(b.objectName()) == "SnapButton" + but:
|
|
return (b.isEnabled() and b.isChecked())
|
|
return False
|
|
|
|
def show(self):
|
|
"shows the toolbar and the grid"
|
|
if not hasattr(self,"toolbar"):
|
|
self.makeSnapToolBar()
|
|
mw = getMainWindow()
|
|
bt = mw.findChild(QtGui.QToolBar,"Draft Snap")
|
|
if not bt:
|
|
mw.addToolBar(self.toolbar)
|
|
self.toolbar.show()
|
|
if FreeCADGui.ActiveDocument:
|
|
self.setTrackers()
|
|
|
|
def setGrid(self):
|
|
"sets the grid, if visible"
|
|
if self.grid and (not self.forceGridOff):
|
|
if self.grid.Visible:
|
|
self.grid.set()
|
|
self.setTrackers()
|
|
|
|
def setTrackers(self):
|
|
v = Draft.get3DView()
|
|
if v in self.trackers[0]:
|
|
i = self.trackers[0].index(v)
|
|
self.grid = self.trackers[1][i]
|
|
self.tracker = self.trackers[2][i]
|
|
self.extLine = self.trackers[3][i]
|
|
self.radiusTracker = self.trackers[4][i]
|
|
else:
|
|
if Draft.getParam("grid"):
|
|
self.grid = DraftTrackers.gridTracker()
|
|
else:
|
|
self.grid = None
|
|
self.tracker = DraftTrackers.snapTracker()
|
|
self.extLine = DraftTrackers.lineTracker(dotted=True)
|
|
self.radiusTracker = DraftTrackers.radiusTracker()
|
|
self.trackers[0].append(v)
|
|
self.trackers[1].append(self.grid)
|
|
self.trackers[2].append(self.tracker)
|
|
self.trackers[3].append(self.extLine)
|
|
self.trackers[4].append(self.radiusTracker)
|
|
if self.grid and (not self.forceGridOff):
|
|
self.grid.set()
|
|
|
|
if not hasattr(FreeCADGui,"Snapper"):
|
|
FreeCADGui.Snapper = Snapper()
|
|
if not hasattr(FreeCAD,"DraftWorkingPlane"):
|
|
import WorkingPlane, Draft_rc
|
|
FreeCAD.DraftWorkingPlane = WorkingPlane.plane()
|
|
print FreeCAD.DraftWorkingPlane
|
|
FreeCADGui.addIconPath(":/icons")
|