Files
PVPlant/Utils/gordon_profile_FP.py
2025-01-28 00:04:13 +01:00

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())