534 lines
20 KiB
Python
534 lines
20 KiB
Python
import FreeCAD
|
|
import FreeCADGui
|
|
import Part
|
|
from freecad.Curves import graphics
|
|
from pivy import coin
|
|
|
|
|
|
# from graphics import COLORS
|
|
# FreeCAD.Console.PrintMessage("Using local Pivy.graphics library\n")
|
|
|
|
|
|
def parameterization(points, a, closed):
|
|
"""Computes a knot Sequence for a set of points
|
|
fac (0-1) : parameterization factor
|
|
fac=0 -> Uniform / fac=0.5 -> Centripetal / fac=1.0 -> Chord-Length"""
|
|
pts = points.copy()
|
|
if closed and pts[0].distanceToPoint(pts[-1]) > 1e-7: # we need to add the first point as the end point
|
|
pts.append(pts[0])
|
|
params = [0]
|
|
for i in range(1, len(pts)):
|
|
p = pts[i] - pts[i - 1]
|
|
if isinstance(p, FreeCAD.Vector):
|
|
le = p.Length
|
|
else:
|
|
le = p.length()
|
|
pl = pow(le, a)
|
|
params.append(params[-1] + pl)
|
|
return params
|
|
|
|
|
|
class ConnectionMarker(graphics.Marker):
|
|
def __init__(self, points):
|
|
super(ConnectionMarker, self).__init__(points, True)
|
|
|
|
|
|
class MarkerOnShape(graphics.Marker):
|
|
def __init__(self, points, sh=None):
|
|
super(MarkerOnShape, self).__init__(points, True)
|
|
self._shape = None
|
|
self._sublink = None
|
|
self._tangent = None
|
|
self._translate = coin.SoTranslation()
|
|
self._text_font = coin.SoFont()
|
|
self._text_font.name = "Arial:Bold"
|
|
self._text_font.size = 13.0
|
|
self._text = coin.SoText2()
|
|
self._text_switch = coin.SoSwitch()
|
|
self._text_switch.addChild(self._translate)
|
|
self._text_switch.addChild(self._text_font)
|
|
self._text_switch.addChild(self._text)
|
|
self.on_drag_start.append(self.add_text)
|
|
self.on_drag_release.append(self.remove_text)
|
|
self.addChild(self._text_switch)
|
|
if isinstance(sh, Part.Shape):
|
|
self.snap_shape = sh
|
|
elif isinstance(sh, (tuple, list)):
|
|
self.sublink = sh
|
|
|
|
def subshape_from_sublink(self, o):
|
|
name = o[1][0]
|
|
if 'Vertex' in name:
|
|
n = eval(name.lstrip('Vertex'))
|
|
return(o[0].Shape.Vertexes[n - 1])
|
|
elif 'Edge' in name:
|
|
n = eval(name.lstrip('Edge'))
|
|
return(o[0].Shape.Edges[n - 1])
|
|
elif 'Face' in name:
|
|
n = eval(name.lstrip('Face'))
|
|
return(o[0].Shape.Faces[n - 1])
|
|
|
|
def add_text(self):
|
|
self._text_switch.whichChild = coin.SO_SWITCH_ALL
|
|
self.on_drag.append(self.update_text)
|
|
|
|
def remove_text(self):
|
|
self._text_switch.whichChild = coin.SO_SWITCH_NONE
|
|
self.on_drag.remove(self.update_text)
|
|
|
|
def update_text(self):
|
|
p = self.points[0]
|
|
coords = ['{: 9.3f}'.format(p[0]), '{: 9.3f}'.format(p[1]), '{: 9.3f}'.format(p[2])]
|
|
self._translate.translation = p
|
|
self._text.string.setValues(0, 3, coords)
|
|
|
|
@property
|
|
def tangent(self):
|
|
return self._tangent
|
|
|
|
@tangent.setter
|
|
def tangent(self, t):
|
|
if isinstance(t, FreeCAD.Vector):
|
|
if t.Length > 1e-7:
|
|
self._tangent = t
|
|
self._tangent.normalize()
|
|
self.marker.markerIndex = coin.SoMarkerSet.DIAMOND_FILLED_9_9
|
|
else:
|
|
self._tangent = None
|
|
self.marker.markerIndex = coin.SoMarkerSet.CIRCLE_FILLED_9_9
|
|
else:
|
|
self._tangent = None
|
|
self.marker.markerIndex = coin.SoMarkerSet.CIRCLE_FILLED_9_9
|
|
|
|
@property
|
|
def snap_shape(self):
|
|
return self._shape
|
|
|
|
@snap_shape.setter
|
|
def snap_shape(self, sh):
|
|
if isinstance(sh, Part.Shape):
|
|
self._shape = sh
|
|
else:
|
|
self._shape = None
|
|
self.alter_color()
|
|
|
|
@property
|
|
def sublink(self):
|
|
return self._sublink
|
|
|
|
@sublink.setter
|
|
def sublink(self, sl):
|
|
if isinstance(sl, (tuple, list)) and not (sl == self._sublink):
|
|
self._shape = self.subshape_from_sublink(sl)
|
|
self._sublink = sl
|
|
else:
|
|
self._shape = None
|
|
self._sublink = None
|
|
self.alter_color()
|
|
|
|
def alter_color(self):
|
|
if isinstance(self._shape, Part.Vertex):
|
|
self.set_color("white")
|
|
elif isinstance(self._shape, Part.Edge):
|
|
self.set_color("cyan")
|
|
elif isinstance(self._shape, Part.Face):
|
|
self.set_color("magenta")
|
|
else:
|
|
self.set_color("black")
|
|
|
|
def __repr__(self):
|
|
return("MarkerOnShape({})".format(self._shape))
|
|
|
|
def drag(self, mouse_coords, fact=1.):
|
|
if self.enabled:
|
|
pts = self.points
|
|
for i, p in enumerate(pts):
|
|
p[0] = mouse_coords[0] * fact + self._tmp_points[i][0]
|
|
p[1] = mouse_coords[1] * fact + self._tmp_points[i][1]
|
|
p[2] = mouse_coords[2] * fact + self._tmp_points[i][2]
|
|
if self._shape:
|
|
v = Part.Vertex(p[0], p[1], p[2])
|
|
proj = v.distToShape(self._shape)[1][0][1]
|
|
# FreeCAD.Console.PrintMessage("%s -> %s\n"%(p.getValue(), proj))
|
|
p[0] = proj.x
|
|
p[1] = proj.y
|
|
p[2] = proj.z
|
|
self.points = pts
|
|
for foo in self.on_drag:
|
|
foo()
|
|
|
|
|
|
class ConnectionPolygon(graphics.Polygon):
|
|
std_col = "green"
|
|
|
|
def __init__(self, markers):
|
|
super(ConnectionPolygon, self).__init__(
|
|
sum([m.points for m in markers], []), True)
|
|
self.markers = markers
|
|
|
|
for m in self.markers:
|
|
m.on_drag.append(self.updatePolygon)
|
|
|
|
def updatePolygon(self):
|
|
self.points = sum([m.points for m in self.markers], [])
|
|
|
|
@property
|
|
def drag_objects(self):
|
|
return self.markers
|
|
|
|
def check_dependency(self):
|
|
if any([m._delete for m in self.markers]):
|
|
self.delete()
|
|
|
|
|
|
class ConnectionLine(graphics.Line):
|
|
def __init__(self, markers):
|
|
super(ConnectionLine, self).__init__(
|
|
sum([m.points for m in markers], []), True)
|
|
self.markers = markers
|
|
self._linear = False
|
|
for m in self.markers:
|
|
m.on_drag.append(self.updateLine)
|
|
|
|
def updateLine(self):
|
|
self.points = sum([m.points for m in self.markers], [])
|
|
if self._linear:
|
|
p1 = self.markers[0].points[0]
|
|
p2 = self.markers[-1].points[0]
|
|
t = p2 - p1
|
|
tan = FreeCAD.Vector(t[0], t[1], t[2])
|
|
for m in self.markers:
|
|
m.tangent = tan
|
|
|
|
@property
|
|
def linear(self):
|
|
return self._linear
|
|
|
|
@linear.setter
|
|
def linear(self, b):
|
|
self._linear = bool(b)
|
|
|
|
@property
|
|
def drag_objects(self):
|
|
return self.markers
|
|
|
|
def check_dependency(self):
|
|
if any([m._delete for m in self.markers]):
|
|
self.delete()
|
|
|
|
|
|
class InterpoCurveEditor(object):
|
|
"""Interpolation curve free-hand editor
|
|
my_editor = InterpoCurveEditor([points], obj)
|
|
obj is the FreeCAD object that will receive
|
|
the curve shape at the end of editing.
|
|
points can be :
|
|
- Vector (free point)
|
|
- (Vector, shape) (point on shape)"""
|
|
def __init__(self, points=[], fp=None):
|
|
self.points = list()
|
|
self.curve = Part.BSplineCurve()
|
|
self.fp = fp
|
|
self.root_inserted = False
|
|
self.periodic = False
|
|
self.param_factor = 1.0
|
|
# self.support = None # Not yet implemented
|
|
for p in points:
|
|
if isinstance(p, FreeCAD.Vector):
|
|
self.points.append(MarkerOnShape([p]))
|
|
elif isinstance(p, (tuple, list)):
|
|
self.points.append(MarkerOnShape([p[0]], p[1]))
|
|
elif isinstance(p, (MarkerOnShape, ConnectionMarker)):
|
|
self.points.append(p)
|
|
else:
|
|
FreeCAD.Console.PrintError("InterpoCurveEditor : bad input")
|
|
# Setup coin objects
|
|
if self.fp:
|
|
self.guidoc = self.fp.ViewObject.Document
|
|
else:
|
|
if not FreeCADGui.ActiveDocument:
|
|
FreeCAD.newDocument("New")
|
|
self.guidoc = FreeCADGui.ActiveDocument
|
|
self.view = self.guidoc.ActiveView
|
|
self.rm = self.view.getViewer().getSoRenderManager()
|
|
self.sg = self.view.getSceneGraph()
|
|
self.setup_InteractionSeparator()
|
|
self.update_curve()
|
|
|
|
def setup_InteractionSeparator(self):
|
|
if self.root_inserted:
|
|
self.sg.removeChild(self.root)
|
|
self.root = graphics.InteractionSeparator(self.rm)
|
|
self.root.setName("InteractionSeparator")
|
|
# self.root.ovr_col = "yellow"
|
|
# self.root.sel_col = "green"
|
|
self.root.pick_radius = 40
|
|
self.root.on_drag.append(self.update_curve)
|
|
# Keyboard callback
|
|
# self.events = coin.SoEventCallback()
|
|
self._controlCB = self.root.events.addEventCallback(coin.SoKeyboardEvent.getClassTypeId(), self.controlCB)
|
|
# populate root node
|
|
# self.root.addChild(self.events)
|
|
self.root += self.points
|
|
self.build_lines()
|
|
self.root += self.lines
|
|
# set FreeCAD color scheme
|
|
for o in self.points + self.lines:
|
|
o.ovr_col = "yellow"
|
|
o.sel_col = "green"
|
|
self.root.register()
|
|
self.sg.addChild(self.root)
|
|
self.root_inserted = True
|
|
self.root.selected_objects = list()
|
|
|
|
def compute_tangents(self):
|
|
tans = list()
|
|
flags = list()
|
|
for i in range(len(self.points)):
|
|
if isinstance(self.points[i].snap_shape, Part.Face):
|
|
for vec in self.points[i].points:
|
|
u, v = self.points[i].snap_shape.Surface.parameter(FreeCAD.Vector(vec))
|
|
norm = self.points[i].snap_shape.normalAt(u, v)
|
|
cp = self.curve.parameter(FreeCAD.Vector(vec))
|
|
t = self.curve.tangent(cp)[0]
|
|
pl = Part.Plane(FreeCAD.Vector(), norm)
|
|
ci = Part.Geom2d.Circle2d()
|
|
ci.Radius = t.Length * 2
|
|
w = Part.Wire([ci.toShape(pl)])
|
|
f = Part.Face(w)
|
|
# proj = f.project([Part.Vertex(t)])
|
|
proj = Part.Vertex(t).distToShape(f)[1][0][1]
|
|
# pt = proj.Vertexes[0].Point
|
|
# FreeCAD.Console.PrintMessage("Projection %s -> %s\n"%(t, proj))
|
|
if proj.Length > 1e-7:
|
|
tans.append(proj)
|
|
flags.append(True)
|
|
else:
|
|
tans.append(FreeCAD.Vector(1, 0, 0))
|
|
flags.append(False)
|
|
elif self.points[i].tangent:
|
|
for j in range(len(self.points[i].points)):
|
|
tans.append(self.points[i].tangent)
|
|
flags.append(True)
|
|
else:
|
|
for j in range(len(self.points[i].points)):
|
|
tans.append(FreeCAD.Vector(0, 0, 0))
|
|
flags.append(False)
|
|
return(tans, flags)
|
|
|
|
def update_curve(self):
|
|
pts = list()
|
|
for p in self.points:
|
|
pts += p.points
|
|
# FreeCAD.Console.PrintMessage("pts :\n%s\n"%str(pts))
|
|
if len(pts) > 1:
|
|
fac = self.param_factor
|
|
if self.fp:
|
|
fac = self.fp.Parametrization
|
|
params = parameterization(pts, fac, self.periodic)
|
|
self.curve.interpolate(Points=pts, Parameters=params, PeriodicFlag=self.periodic)
|
|
tans, flags = self.compute_tangents()
|
|
if any(flags):
|
|
if (len(tans) == len(pts)) and (len(flags) == len(pts)):
|
|
self.curve.interpolate(Points=pts, Parameters=params, PeriodicFlag=self.periodic, Tangents=tans, TangentFlags=flags)
|
|
if self.fp:
|
|
self.fp.Shape = self.curve.toShape()
|
|
|
|
def build_lines(self):
|
|
self.lines = list()
|
|
for i in range(len(self.points) - 1):
|
|
line = ConnectionLine([self.points[i], self.points[i + 1]])
|
|
line.set_color("blue")
|
|
self.lines.append(line)
|
|
|
|
def controlCB(self, attr, event_callback):
|
|
event = event_callback.getEvent()
|
|
if event.getState() == event.UP:
|
|
# FreeCAD.Console.PrintMessage("Key pressed : %s\n"%event.getKey())
|
|
if event.getKey() == ord("i"):
|
|
self.subdivide()
|
|
elif event.getKey() == ord("p"):
|
|
self.set_planar()
|
|
elif event.getKey() == ord("t"):
|
|
self.set_tangents()
|
|
elif event.getKey() == ord("q"):
|
|
if self.fp:
|
|
self.fp.ViewObject.Proxy.doubleClicked(self.fp.ViewObject)
|
|
else:
|
|
self.quit()
|
|
elif event.getKey() == ord("s"):
|
|
sel = FreeCADGui.Selection.getSelectionEx()
|
|
tup = None
|
|
if len(sel) == 1:
|
|
tup = (sel[0].Object, sel[0].SubElementNames)
|
|
for i in range(len(self.root.selected_objects)):
|
|
if isinstance(self.root.selected_objects[i], MarkerOnShape):
|
|
self.root.selected_objects[i].sublink = tup
|
|
FreeCAD.Console.PrintMessage("Snapped to {}\n".format(str(self.root.selected_objects[i].sublink)))
|
|
self.root.selected_objects[i].drag_start()
|
|
self.root.selected_objects[i].drag((0, 0, 0.))
|
|
self.root.selected_objects[i].drag_release()
|
|
self.update_curve()
|
|
elif event.getKey() == ord("l"):
|
|
self.toggle_linear()
|
|
elif (event.getKey() == 65535) or (event.getKey() == 65288): # Suppr or Backspace
|
|
# FreeCAD.Console.PrintMessage("Some objects have been deleted\n")
|
|
pts = list()
|
|
for o in self.root.dynamic_objects:
|
|
if isinstance(o, MarkerOnShape):
|
|
pts.append(o)
|
|
self.points = pts
|
|
self.setup_InteractionSeparator()
|
|
self.update_curve()
|
|
|
|
def toggle_linear(self):
|
|
for o in self.root.selected_objects:
|
|
if isinstance(o, ConnectionLine):
|
|
o.linear = not o.linear
|
|
i = self.lines.index(o)
|
|
if i > 0:
|
|
self.lines[i - 1].linear = False
|
|
if i < len(self.lines) - 1:
|
|
self.lines[i + 1].linear = False
|
|
o.updateLine()
|
|
o.drag_start()
|
|
o.drag((0, 0, 0.00001))
|
|
o.drag_release()
|
|
self.update_curve()
|
|
|
|
def set_tangents(self):
|
|
# view_dir = FreeCAD.Vector(0, 0, 1)
|
|
view_dir = FreeCADGui.ActiveDocument.ActiveView.getViewDirection()
|
|
markers = list()
|
|
for o in self.root.selected_objects:
|
|
if isinstance(o, MarkerOnShape):
|
|
markers.append(o)
|
|
elif isinstance(o, ConnectionLine):
|
|
markers.extend(o.markers)
|
|
if len(markers) > 0:
|
|
for m in markers:
|
|
if m.tangent:
|
|
m.tangent = None
|
|
else:
|
|
i = self.points.index(m)
|
|
if i == 0:
|
|
m.tangent = -view_dir
|
|
else:
|
|
m.tangent = view_dir
|
|
self.update_curve()
|
|
|
|
def set_planar(self):
|
|
# view_dir = FreeCAD.Vector(0, 0, 1)
|
|
view_dir = FreeCADGui.ActiveDocument.ActiveView.getViewDirection()
|
|
markers = list()
|
|
for o in self.root.selected_objects:
|
|
if isinstance(o, MarkerOnShape):
|
|
markers.append(o)
|
|
elif isinstance(o, ConnectionLine):
|
|
markers.extend(o.markers)
|
|
if len(markers) > 2:
|
|
vec0 = markers[0].points[0]
|
|
vec1 = markers[-1].points[0]
|
|
p0 = FreeCAD.Vector(vec0[0], vec0[1], vec0[2])
|
|
p1 = FreeCAD.Vector(vec1[0], vec1[1], vec1[2])
|
|
pl = Part.Plane(p0, p1, p1 + view_dir)
|
|
for o in markers:
|
|
if isinstance(o.snap_shape, Part.Vertex):
|
|
FreeCAD.Console.PrintMessage("Snapped to Vertex\n")
|
|
elif isinstance(o.snap_shape, Part.Edge):
|
|
FreeCAD.Console.PrintMessage("Snapped to Edge\n")
|
|
c = o.snap_shape.Curve
|
|
pts = pl.intersect(c)[0]
|
|
new_pts = list()
|
|
for ip in o.points:
|
|
iv = FreeCAD.Vector(ip[0], ip[1], ip[2])
|
|
dmin = 1e50
|
|
new = None
|
|
for op in pts:
|
|
ov = FreeCAD.Vector(op.X, op.Y, op.Z)
|
|
if iv.distanceToPoint(ov) < dmin:
|
|
dmin = iv.distanceToPoint(ov)
|
|
new = ov
|
|
new_pts.append(new)
|
|
o.points = new_pts
|
|
elif isinstance(o.snap_shape, Part.Face):
|
|
FreeCAD.Console.PrintMessage("Snapped to Face\n")
|
|
s = o.snap_shape.Surface
|
|
cvs = pl.intersect(s)
|
|
new_pts = list()
|
|
for ip in o.points:
|
|
iv = Part.Vertex(FreeCAD.Vector(ip[0], ip[1], ip[2]))
|
|
dmin = 1e50
|
|
new = None
|
|
for c in cvs:
|
|
e = c.toShape()
|
|
d, pts, info = iv.distToShape(e)
|
|
if d < dmin:
|
|
dmin = d
|
|
new = pts[0][1]
|
|
new_pts.append(new)
|
|
o.points = new_pts
|
|
else:
|
|
FreeCAD.Console.PrintMessage("Not snapped\n")
|
|
new_pts = list()
|
|
for ip in o.points:
|
|
iv = FreeCAD.Vector(ip[0], ip[1], ip[2])
|
|
u, v = pl.parameter(iv)
|
|
new_pts.append(pl.value(u, v))
|
|
o.points = new_pts
|
|
for li in self.lines:
|
|
li.updateLine()
|
|
self.update_curve()
|
|
|
|
def subdivide(self):
|
|
# get selected lines and subdivide them
|
|
pts = list()
|
|
new_select = list()
|
|
for o in self.lines:
|
|
# FreeCAD.Console.PrintMessage("object %s\n"%str(o))
|
|
if isinstance(o, ConnectionLine):
|
|
pts.append(o.markers[0])
|
|
if o in self.root.selected_objects:
|
|
idx = self.lines.index(o)
|
|
FreeCAD.Console.PrintMessage("Subdividing line #{}\n".format(idx))
|
|
p1 = o.markers[0].points[0]
|
|
p2 = o.markers[1].points[0]
|
|
par1 = self.curve.parameter(FreeCAD.Vector(p1))
|
|
par2 = self.curve.parameter(FreeCAD.Vector(p2))
|
|
midpar = (par1 + par2) / 2.0
|
|
mark = MarkerOnShape([self.curve.value(midpar)])
|
|
pts.append(mark)
|
|
new_select.append(mark)
|
|
pts.append(self.points[-1])
|
|
self.points = pts
|
|
self.setup_InteractionSeparator()
|
|
self.root.selected_objects = new_select
|
|
self.update_curve()
|
|
return(True)
|
|
|
|
def quit(self):
|
|
self.root.events.removeEventCallback(coin.SoKeyboardEvent.getClassTypeId(), self._controlCB)
|
|
self.root.unregister()
|
|
# self.root.removeAllChildren()
|
|
self.sg.removeChild(self.root)
|
|
self.root_inserted = False
|
|
|
|
|
|
def get_guide_params():
|
|
sel = FreeCADGui.Selection.getSelectionEx()
|
|
pts = list()
|
|
for s in sel:
|
|
pts.extend(list(zip(s.PickedPoints, s.SubObjects)))
|
|
return(pts)
|
|
|
|
|
|
def main():
|
|
obj = FreeCAD.ActiveDocument.addObject("Part::Spline", "profile")
|
|
tups = get_guide_params()
|
|
InterpoCurveEditor(tups, obj)
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|