349 lines
13 KiB
Python
349 lines
13 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
|
||
|
|
__title__ = "Freehand BSpline"
|
||
|
|
__author__ = "Christophe Grellier (Chris_G)"
|
||
|
|
__license__ = "LGPL 2.1"
|
||
|
|
__doc__ = "Creates an freehand BSpline curve"
|
||
|
|
__usage__ = """*** Interpolation curve control keys :
|
||
|
|
|
||
|
|
a - Select all / Deselect
|
||
|
|
i - Insert point in selected segments
|
||
|
|
t - Set / unset tangent (view direction)
|
||
|
|
p - Align selected objects
|
||
|
|
s - Snap points on shape / Unsnap
|
||
|
|
l - Set/unset a linear interpolation
|
||
|
|
x,y,z - Axis constraints during grab
|
||
|
|
q - Apply changes and quit editing"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
|
||
|
|
import FreeCAD
|
||
|
|
import FreeCADGui
|
||
|
|
import Part
|
||
|
|
|
||
|
|
from . import ICONPATH
|
||
|
|
from . import _utils
|
||
|
|
from . import profile_editor
|
||
|
|
|
||
|
|
TOOL_ICON = os.path.join(ICONPATH, 'editableSpline.svg')
|
||
|
|
# debug = _utils.debug
|
||
|
|
debug = _utils.doNothing
|
||
|
|
|
||
|
|
|
||
|
|
def check_pivy():
|
||
|
|
try:
|
||
|
|
profile_editor.MarkerOnShape([FreeCAD.Vector()])
|
||
|
|
return True
|
||
|
|
except Exception as exc:
|
||
|
|
FreeCAD.Console.PrintWarning(str(exc) + "\nPivy interaction library failure\n")
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def midpoint(e):
|
||
|
|
p = e.FirstParameter + 0.5 * (e.LastParameter - e.FirstParameter)
|
||
|
|
return e.valueAt(p)
|
||
|
|
|
||
|
|
|
||
|
|
class GordonProfileFP:
|
||
|
|
"""Creates an editable interpolation curve"""
|
||
|
|
def __init__(self, obj, s, d, t):
|
||
|
|
"""Add the properties"""
|
||
|
|
obj.addProperty("App::PropertyLinkSubList", "Support", "Profile", "Constraint shapes").Support = s
|
||
|
|
obj.addProperty("App::PropertyFloatConstraint", "Parametrization", "Profile", "Parametrization factor")
|
||
|
|
obj.addProperty("App::PropertyFloat", "Tolerance", "Profile", "Tolerance").Tolerance = 1e-5
|
||
|
|
obj.addProperty("App::PropertyBool", "Periodic", "Profile", "Periodic curve").Periodic = False
|
||
|
|
obj.addProperty("App::PropertyVectorList", "Data", "Profile", "Data list").Data = d
|
||
|
|
obj.addProperty("App::PropertyVectorList", "Tangents", "Profile", "Tangents list")
|
||
|
|
obj.addProperty("App::PropertyBoolList", "Flags", "Profile", "Tangent flags")
|
||
|
|
obj.addProperty("App::PropertyIntegerList", "DataType", "Profile", "Types of interpolated points").DataType = t
|
||
|
|
obj.addProperty("App::PropertyBoolList", "LinearSegments", "Profile", "Linear segment flags")
|
||
|
|
obj.Parametrization = (1.0, 0.0, 1.0, 0.05)
|
||
|
|
obj.Proxy = self
|
||
|
|
|
||
|
|
def get_shapes(self, fp):
|
||
|
|
if hasattr(fp, 'Support'):
|
||
|
|
sl = list()
|
||
|
|
for ob, names in fp.Support:
|
||
|
|
for name in names:
|
||
|
|
if "Vertex" in name:
|
||
|
|
n = eval(name.lstrip("Vertex"))
|
||
|
|
if len(ob.Shape.Vertexes) >= n:
|
||
|
|
sl.append(ob.Shape.Vertexes[n - 1])
|
||
|
|
elif ("Point" in name):
|
||
|
|
sl.append(Part.Vertex(ob.Shape.Point))
|
||
|
|
elif ("Edge" in name):
|
||
|
|
n = eval(name.lstrip("Edge"))
|
||
|
|
if len(ob.Shape.Edges) >= n:
|
||
|
|
sl.append(ob.Shape.Edges[n - 1])
|
||
|
|
elif ("Face" in name):
|
||
|
|
n = eval(name.lstrip("Face"))
|
||
|
|
if len(ob.Shape.Faces) >= n:
|
||
|
|
sl.append(ob.Shape.Faces[n - 1])
|
||
|
|
return sl
|
||
|
|
|
||
|
|
def get_points(self, fp, stretch=True):
|
||
|
|
touched = False
|
||
|
|
shapes = self.get_shapes(fp)
|
||
|
|
if not len(fp.Data) == len(fp.DataType):
|
||
|
|
FreeCAD.Console.PrintError("Gordon Profile : Data and DataType mismatch\n")
|
||
|
|
return(None)
|
||
|
|
pts = list()
|
||
|
|
shape_idx = 0
|
||
|
|
for i in range(len(fp.Data)):
|
||
|
|
if fp.DataType[i] == 0: # Free point
|
||
|
|
pts.append(fp.Data[i])
|
||
|
|
elif (fp.DataType[i] == 1):
|
||
|
|
if (shape_idx < len(shapes)): # project on shape
|
||
|
|
d, p, i = Part.Vertex(fp.Data[i]).distToShape(shapes[shape_idx])
|
||
|
|
if d > fp.Tolerance:
|
||
|
|
touched = True
|
||
|
|
pts.append(p[0][1]) # shapes[shape_idx].valueAt(fp.Data[i].x))
|
||
|
|
shape_idx += 1
|
||
|
|
else:
|
||
|
|
pts.append(fp.Data[i])
|
||
|
|
if stretch and touched:
|
||
|
|
params = [0]
|
||
|
|
knots = [0]
|
||
|
|
moves = [pts[0] - fp.Data[0]]
|
||
|
|
lsum = 0
|
||
|
|
mults = [2]
|
||
|
|
for i in range(1, len(pts)):
|
||
|
|
lsum += fp.Data[i - 1].distanceToPoint(fp.Data[i])
|
||
|
|
params.append(lsum)
|
||
|
|
if fp.DataType[i] == 1:
|
||
|
|
knots.append(lsum)
|
||
|
|
moves.append(pts[i] - fp.Data[i])
|
||
|
|
mults.insert(1, 1)
|
||
|
|
mults[-1] = 2
|
||
|
|
if len(moves) < 2:
|
||
|
|
return(pts)
|
||
|
|
# FreeCAD.Console.PrintMessage("%s\n%s\n%s\n"%(moves,mults,knots))
|
||
|
|
curve = Part.BSplineCurve()
|
||
|
|
curve.buildFromPolesMultsKnots(moves, mults, knots, False, 1)
|
||
|
|
for i in range(1, len(pts)):
|
||
|
|
if fp.DataType[i] == 0:
|
||
|
|
# FreeCAD.Console.PrintMessage("Stretch %s #%d: %s to %s\n"%(fp.Label,i,pts[i],curve.value(params[i])))
|
||
|
|
pts[i] += curve.value(params[i])
|
||
|
|
if touched:
|
||
|
|
return pts
|
||
|
|
else:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def execute(self, obj):
|
||
|
|
try:
|
||
|
|
o = FreeCADGui.ActiveDocument.getInEdit().Object
|
||
|
|
if o == obj:
|
||
|
|
return
|
||
|
|
except:
|
||
|
|
FreeCAD.Console.PrintWarning("execute is disabled during editing\n")
|
||
|
|
pts = self.get_points(obj)
|
||
|
|
if pts:
|
||
|
|
if len(pts) < 2:
|
||
|
|
FreeCAD.Console.PrintError("{} : Not enough points\n".format(obj.Label))
|
||
|
|
return False
|
||
|
|
else:
|
||
|
|
obj.Data = pts
|
||
|
|
else:
|
||
|
|
pts = obj.Data
|
||
|
|
|
||
|
|
tans = [FreeCAD.Vector()] * len(pts)
|
||
|
|
flags = [False] * len(pts)
|
||
|
|
for i in range(len(obj.Tangents)):
|
||
|
|
tans[i] = obj.Tangents[i]
|
||
|
|
for i in range(len(obj.Flags)):
|
||
|
|
flags[i] = obj.Flags[i]
|
||
|
|
# if not (len(obj.LinearSegments) == len(pts)-1):
|
||
|
|
# FreeCAD.Console.PrintError("%s : Points and LinearSegments mismatch\n"%obj.Label)
|
||
|
|
if len(obj.LinearSegments) > 0:
|
||
|
|
for i, b in enumerate(obj.LinearSegments):
|
||
|
|
if b:
|
||
|
|
tans[i] = pts[i + 1] - pts[i]
|
||
|
|
tans[i + 1] = tans[i]
|
||
|
|
flags[i] = True
|
||
|
|
flags[i + 1] = True
|
||
|
|
params = profile_editor.parameterization(pts, obj.Parametrization, obj.Periodic)
|
||
|
|
|
||
|
|
curve = Part.BSplineCurve()
|
||
|
|
if len(pts) == 2:
|
||
|
|
curve.buildFromPoles(pts)
|
||
|
|
elif obj.Periodic and pts[0].distanceToPoint(pts[-1]) < 1e-7:
|
||
|
|
curve.interpolate(Points=pts[:-1], Parameters=params, PeriodicFlag=obj.Periodic, Tolerance=obj.Tolerance, Tangents=tans[:-1], TangentFlags=flags[:-1])
|
||
|
|
else:
|
||
|
|
curve.interpolate(Points=pts, Parameters=params, PeriodicFlag=obj.Periodic, Tolerance=obj.Tolerance, Tangents=tans, TangentFlags=flags)
|
||
|
|
obj.Shape = curve.toShape()
|
||
|
|
|
||
|
|
def onChanged(self, fp, prop):
|
||
|
|
if prop in ("Support", "Data", "DataType", "Periodic"):
|
||
|
|
# FreeCAD.Console.PrintMessage("%s : %s changed\n"%(fp.Label,prop))
|
||
|
|
if (len(fp.Data) == len(fp.DataType)) and (sum(fp.DataType) == len(fp.Support)):
|
||
|
|
new_pts = self.get_points(fp, True)
|
||
|
|
if new_pts:
|
||
|
|
fp.Data = new_pts
|
||
|
|
if prop == "Parametrization":
|
||
|
|
self.execute(fp)
|
||
|
|
|
||
|
|
def onDocumentRestored(self, fp):
|
||
|
|
fp.setEditorMode("Data", 2)
|
||
|
|
fp.setEditorMode("DataType", 2)
|
||
|
|
|
||
|
|
|
||
|
|
class GordonProfileVP:
|
||
|
|
def __init__(self, vobj):
|
||
|
|
vobj.Proxy = self
|
||
|
|
self.select_state = True
|
||
|
|
self.active = False
|
||
|
|
|
||
|
|
def getIcon(self):
|
||
|
|
return TOOL_ICON
|
||
|
|
|
||
|
|
def attach(self, vobj):
|
||
|
|
self.Object = vobj.Object
|
||
|
|
self.active = False
|
||
|
|
self.select_state = vobj.Selectable
|
||
|
|
self.ip = None
|
||
|
|
|
||
|
|
def setEdit(self, vobj, mode=0):
|
||
|
|
if mode == 0 and check_pivy():
|
||
|
|
if vobj.Selectable:
|
||
|
|
self.select_state = True
|
||
|
|
vobj.Selectable = False
|
||
|
|
pts = list()
|
||
|
|
sl = list()
|
||
|
|
for ob, names in self.Object.Support:
|
||
|
|
for name in names:
|
||
|
|
sl.append((ob, (name,)))
|
||
|
|
shape_idx = 0
|
||
|
|
for i in range(len(self.Object.Data)):
|
||
|
|
p = self.Object.Data[i]
|
||
|
|
t = self.Object.DataType[i]
|
||
|
|
if t == 0:
|
||
|
|
pts.append(profile_editor.MarkerOnShape([p]))
|
||
|
|
elif t == 1:
|
||
|
|
pts.append(profile_editor.MarkerOnShape([p], sl[shape_idx]))
|
||
|
|
shape_idx += 1
|
||
|
|
for i in range(len(pts)): # p,t,f in zip(pts, self.Object.Tangents, self.Object.Flags):
|
||
|
|
if i < min(len(self.Object.Flags), len(self.Object.Tangents)):
|
||
|
|
if self.Object.Flags[i]:
|
||
|
|
pts[i].tangent = self.Object.Tangents[i]
|
||
|
|
self.ip = profile_editor.InterpoCurveEditor(pts, self.Object)
|
||
|
|
self.ip.periodic = self.Object.Periodic
|
||
|
|
self.ip.param_factor = self.Object.Parametrization
|
||
|
|
for i in range(min(len(self.Object.LinearSegments), len(self.ip.lines))):
|
||
|
|
self.ip.lines[i].tangent = self.Object.LinearSegments[i]
|
||
|
|
self.ip.lines[i].updateLine()
|
||
|
|
self.active = True
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def unsetEdit(self, vobj, mode=0):
|
||
|
|
if isinstance(self.ip, profile_editor.InterpoCurveEditor) and check_pivy():
|
||
|
|
pts = list()
|
||
|
|
typ = list()
|
||
|
|
tans = list()
|
||
|
|
flags = list()
|
||
|
|
# original_links = self.Object.Support
|
||
|
|
new_links = list()
|
||
|
|
for p in self.ip.points:
|
||
|
|
if isinstance(p, profile_editor.MarkerOnShape):
|
||
|
|
pt = p.points[0]
|
||
|
|
pts.append(FreeCAD.Vector(pt[0], pt[1], pt[2]))
|
||
|
|
if p.sublink:
|
||
|
|
new_links.append(p.sublink)
|
||
|
|
typ.append(1)
|
||
|
|
else:
|
||
|
|
typ.append(0)
|
||
|
|
if p.tangent:
|
||
|
|
tans.append(p.tangent)
|
||
|
|
flags.append(True)
|
||
|
|
else:
|
||
|
|
tans.append(FreeCAD.Vector())
|
||
|
|
flags.append(False)
|
||
|
|
self.Object.Tangents = tans
|
||
|
|
self.Object.Flags = flags
|
||
|
|
self.Object.LinearSegments = [li.linear for li in self.ip.lines]
|
||
|
|
self.Object.DataType = typ
|
||
|
|
self.Object.Data = pts
|
||
|
|
self.Object.Support = new_links
|
||
|
|
vobj.Selectable = self.select_state
|
||
|
|
self.ip.quit()
|
||
|
|
self.ip = None
|
||
|
|
self.active = False
|
||
|
|
self.Object.Document.recompute()
|
||
|
|
return True
|
||
|
|
|
||
|
|
def doubleClicked(self, vobj):
|
||
|
|
if not hasattr(self, 'active'):
|
||
|
|
self.active = False
|
||
|
|
if not self.active:
|
||
|
|
self.active = True
|
||
|
|
# self.setEdit(vobj)
|
||
|
|
vobj.Document.setEdit(vobj)
|
||
|
|
else:
|
||
|
|
vobj.Document.resetEdit()
|
||
|
|
self.active = False
|
||
|
|
return True
|
||
|
|
|
||
|
|
def __getstate__(self):
|
||
|
|
return {"name": self.Object.Name}
|
||
|
|
|
||
|
|
def __setstate__(self, state):
|
||
|
|
self.Object = FreeCAD.ActiveDocument.getObject(state["name"])
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
class GordonProfileCommand:
|
||
|
|
"""Creates a editable interpolation curve"""
|
||
|
|
|
||
|
|
def makeFeature(self, sub, pts, typ):
|
||
|
|
fp = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Freehand BSpline")
|
||
|
|
GordonProfileFP(fp, sub, pts, typ)
|
||
|
|
GordonProfileVP(fp.ViewObject)
|
||
|
|
FreeCAD.Console.PrintMessage(__usage__)
|
||
|
|
FreeCAD.ActiveDocument.recompute()
|
||
|
|
FreeCADGui.SendMsgToActiveView("ViewFit")
|
||
|
|
fp.ViewObject.Document.setEdit(fp.ViewObject)
|
||
|
|
|
||
|
|
def Activated(self):
|
||
|
|
s = FreeCADGui.Selection.getSelectionEx()
|
||
|
|
try:
|
||
|
|
ordered = FreeCADGui.activeWorkbench().Selection
|
||
|
|
if ordered:
|
||
|
|
s = ordered
|
||
|
|
except AttributeError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
sub = list()
|
||
|
|
pts = list()
|
||
|
|
for obj in s:
|
||
|
|
if obj.HasSubObjects:
|
||
|
|
# FreeCAD.Console.PrintMessage("object has subobjects %s\n"%str(obj.SubElementNames))
|
||
|
|
for n in obj.SubElementNames:
|
||
|
|
sub.append((obj.Object, [n]))
|
||
|
|
for p in obj.PickedPoints:
|
||
|
|
pts.append(p)
|
||
|
|
|
||
|
|
if len(pts) == 0:
|
||
|
|
pts = [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(5, 0, 0), FreeCAD.Vector(10, 0, 0)]
|
||
|
|
typ = [0, 0, 0]
|
||
|
|
elif len(pts) == 1:
|
||
|
|
pts.append(pts[0] + FreeCAD.Vector(5, 0, 0))
|
||
|
|
pts.append(pts[0] + FreeCAD.Vector(10, 0, 0))
|
||
|
|
typ = [1, 0, 0]
|
||
|
|
else:
|
||
|
|
typ = [1] * len(pts)
|
||
|
|
self.makeFeature(sub, pts, typ)
|
||
|
|
|
||
|
|
def IsActive(self):
|
||
|
|
if FreeCAD.ActiveDocument:
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def GetResources(self):
|
||
|
|
return {'Pixmap': TOOL_ICON,
|
||
|
|
'MenuText': __title__,
|
||
|
|
'ToolTip': "{}<br><br><b>Usage :</b><br>{}".format(__doc__, "<br>".join(__usage__.splitlines()))}
|
||
|
|
|
||
|
|
|
||
|
|
FreeCADGui.addCommand('gordon_profile', GordonProfileCommand())
|