Thank you @kuboa.
Here my WIP customized parallel transport frame script if you want test it with all your paths…
To use:
copy the script text in blender text editor, select one or more curve/mesh paths and click on
text editor run script button (alt p).
import bpy, bpy_extras, bmesh, math
from mathutils import Vector, Matrix, Quaternion as Quat
from bpy import context
def planeProfile():
vtx = [ (0.1,0.1,0), (-0.1,0.1,0), (-0.1,-0.1,0), (0.1,-0.1,0) ]
edges = [ (0,1), (1,2), (2,3), (3,0) ]
mesh = bpy.data.meshes.new('plane_profile_mesh')
mesh.from_pydata(vtx, edges, [])
mesh.update()
return mesh
#clone profile and adjust location, rotation and shear
def setFrame_profile_byFrame(profile_mesh, frame, pos, scale, sclAxis, bank=True):
me = profile_mesh.copy() #copy profile mesh to me
up = Vector((0,0,1))
if bank:
q = up.rotation_difference(frame) #or -> axis = up.cross(frame).normalized(); angle = up.angle(frame)
else:
q = frame.to_track_quat('Z', 'Y')
mRot = Matrix.Rotation(q.angle, 4, q.axis)
me.transform(mRot)
if scale:
mScale = Matrix.Scale(scale, 4, sclAxis)
me.transform(mScale)
me.transform(Matrix.Translation(pos))
return me
def getPoints(obj):
if obj.type == 'CURVE':
obj_mesh = bpy.data.meshes.new_from_object(obj)
return [(obj.matrix_world @ v.co) for v in obj_mesh.vertices] #get path points/vertices list in world space coord
elif obj.type == 'MESH':
return [(obj.matrix_world @ v.co) for v in obj.data.vertices] #get path points/vertices list in world space coord
else:
return None
#my customized parallel transport frame
def createSweep_customPTF(path, profile):
bm_sweep = bmesh.new() #mesh buffer init
pathsize = len(path)
up = Vector((0,0,1))
frame = Vector((0,0,1))
frames = []
scales = []
sclAxis = []
for i in range(pathsize):
pos = path[i]
scale = None
axisScale = None
print('pos: ', i)
print('frame prev: ', frame)
if i < pathsize-1: #middle points
if i == 0: #start point
t1 = (path[i+1] - path[i]).normalized() #calc point forward tangent
axis = frame.cross(t1).normalized()
angle = frame.angle(t1)
q = Quat(axis, angle)
frame.rotate(q)
else:
t1 = (path[i+1] - path[i]).normalized()#catetoA #calc point forward tangent
t2 = (path[i-1] - path[i]).normalized()#catetoB #tangent backward
t3 = (t1 - t2).normalized()#hipotenusa
axisFrame = frame.cross(t3).normalized()
angleFrame = frame.angle(t3)
n = t2.cross(t1).normalized()
axisScale = t3.cross(n).normalized()
angleScale = t2.angle(t1)
beta = math.pi - angleScale
scale = math.fabs(1/math.cos(beta/2))
q = Quat(axisFrame, angleFrame)
frame.rotate(q)
#1.41217
#2 = 1.9465
print('frame: ', frame, ' scl: ', scale, ' sclAx: ', axisScale)
print('angleFrame: ', angleFrame, ' angleScale: ', angleScale, ' beta: ', beta, ' axisFrame: ', axisFrame)
else: #last point
axis = frame.cross(t2).normalized()
angle = frame.angle(t2)
q = Quat(-axis, angle)
frame.rotate(q)
frame_mesh = setFrame_profile_byFrame(profile, frame, pos, scale, axisScale, bank=False) #create new profile copy for new loc,rot,scale
bm_sweep.from_mesh( frame_mesh) #append current frame profile to mesh buffer
# end loop to create frames on each path point
#bridge profiles pairs (2 by 2)
n = len(profile.edges)
for i in range(pathsize-1):
e1 = bm_sweep.edges[i*n:i*n+n] #get first full profile
e2 = bm_sweep.edges[(i+1)*n:(i+1)*n+n] #get next full profile
bmesh.ops.bridge_loops(bm_sweep, edges=e1+e2) #exec brigde edges
return bm_sweep
###########################################################
##========= MAIN ==========##
##=========================##
import time
start_time = time.time()
paths = context.selected_objects[:]
profile = planeProfile()
if len(paths) >= 1:
for o in context.selected_objects: o.select_set(False) #deselect all
for p in paths:
########## sweep path ##########
path = getPoints(p)
bm_sweep = createSweep_customPTF(path, profile)
me_sweep = bpy.data.meshes.new('sweep_mesh') #new empty datamesh
bm_sweep.to_mesh(me_sweep) #transfer/convert BMESH sweep path to MESH data type
bm_sweep.free() #clear memory
sweep_obj = bpy.data.objects.new('sweep_obj', me_sweep) #new object
bpy.context.collection.objects.link(sweep_obj) #add to scene
context.view_layer.objects.active = sweep_obj #active
sweep_obj.select_set(True) #select
sweep_obj.show_wire = sweep_obj.show_all_edges = True #show wireframe over shader
print("--- %s seconds ---" % (time.time() - start_time))