#
#
# Blender add-on with tools to draw and edit Bezier curves along with other utility ops
#
# Supported Blender Version: 2.8x
#
# Copyright (C) 2019 Shrinivas Kulkarni
# License: GPL (https://github.com/Shriinivas/blenderbezierutils/blob/master/LICENSE)
#
import os, bpy, bmesh, blf, gpu
from bpy.props import BoolProperty, IntProperty, EnumProperty, \
FloatProperty, StringProperty, CollectionProperty, FloatVectorProperty, PointerProperty
from bpy.types import Panel, Operator, WorkSpaceTool, AddonPreferences, Menu
from mathutils import Vector, Matrix, geometry, kdtree
from math import e, pi, log, sin, cos, tan, radians, degrees, sqrt, asin, acos, atan, floor, \
ceil, pow, exp
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_location_3d, \
region_2d_to_origin_3d
from bpy_extras.view3d_utils import location_3d_to_region_2d
from bpy_extras.object_utils import world_to_camera_view
from gpu_extras.batch import batch_for_shader
import random, time, math
from bpy.app.handlers import persistent
from xml.dom import minidom
from shutil import copyfile
bl_info = {
"name": "Bezier Utilities",
"author": "Shrinivas Kulkarni",
"version": (0, 9, 96),
"location": "Properties > Active Tool and Workspace Settings > Bezier Utilities",
"description": "Collection of Bezier curve utility ops",
"category": "Object",
"wiki_url": "https://github.com/Shriinivas/blenderbezierutils/blob/master/README.md",
"blender": (2, 80, 0),
}
DEF_ERR_MARGIN = 0.0001
# Markers for invalid data
LARGE_NO = 9e+9 # Both float and int
LARGE_VECT = Vector((LARGE_NO, LARGE_NO, LARGE_NO))
INVAL = '~'
###################### Common functions ######################
def floatCmpWithMargin(float1, float2, margin = DEF_ERR_MARGIN):
return abs(float1 - float2) < margin
def vectCmpWithMargin(v1, v2, margin = DEF_ERR_MARGIN):
return all(floatCmpWithMargin(v1[i], v2[i], margin) for i in range(0, len(v1)))
def isBezier(bObj):
return bObj.type == 'CURVE' and len(bObj.data.splines) > 0 \
and bObj.data.splines[0].type == 'BEZIER' and \
len(bObj.data.splines[0].bezier_points) > 0
def safeRemoveObj(obj):
try:
collections = obj.users_collection
for c in collections:
c.objects.unlink(obj)
if(obj.data.users == 1):
if(obj.type == 'MESH'):
bpy.data.meshes.remove(obj.data)
elif(obj.type == 'CURVE'):
bpy.data.curves.remove(obj.data)
#else? TODO
bpy.data.objects.remove(obj)
except:
pass
#TODO combine with copyObjAttr
def copyBezierPt(src, target, freeHandles = None, srcMw = Matrix(), invDestMW = Matrix()):
target.handle_left_type = 'FREE'
target.handle_right_type = 'FREE'
target.co = invDestMW @ (srcMw @ src.co)
target.handle_left = invDestMW @ (srcMw @ src.handle_left)
target.handle_right = invDestMW @ (srcMw @ src.handle_right)
if(freeHandles == None or freeHandles[0] == False):
target.handle_left_type = src.handle_left_type
if(freeHandles == None or freeHandles[1] == False):
target.handle_right_type = src.handle_right_type
def createSplineForSeg(curveData, bezierPts):
spline = curveData.splines.new('BEZIER')
spline.bezier_points.add(len(bezierPts)-1)
spline.use_cyclic_u = False
for i, pt in enumerate(bezierPts):
if(i == 0): freeHandles = [False, True]
elif(i == len(bezierPts) - 1): freeHandles = [True, False]
else: freeHandles = None
copyBezierPt(pt, spline.bezier_points[i], freeHandles = freeHandles)
return spline
def createSpline(curveData, srcSpline, excludePtIdxs = {}):
spline = curveData.splines.new('BEZIER')
spline.bezier_points.add(len(srcSpline.bezier_points) - len(excludePtIdxs) - 1)
spline.use_cyclic_u = srcSpline.use_cyclic_u
ptIdx = 0
for i in range(0, len(srcSpline.bezier_points)):
if(i not in excludePtIdxs):
copyBezierPt(srcSpline.bezier_points[i], \
spline.bezier_points[ptIdx], freeHandles = None)
ptIdx += 1
return spline
def createSkeletalCurve(obj, collections):
objCopy = obj.copy()
objCopy.name = obj.name
dataCopy = obj.data.copy()
dataCopy.splines.clear()
objCopy.data = dataCopy
# Duplicate the action (animation data)
if obj.animation_data and obj.animation_data.action:
objCopy.animation_data_clear() # Clear any existing animation data links
objCopy.animation_data_create() # Create a new animation data block
action_copy = obj.animation_data.action.copy() # Copy the action
objCopy.animation_data.action = action_copy # Assign the copied action
for coll in collections:
coll.objects.link(objCopy)
return objCopy
def createObjFromPts(curvePts, dimensions = '3D', collection = None, \
closed = False, calcHdlTypes = True):
data = bpy.data.curves.new('BezierCurve', 'CURVE')
data.dimensions = dimensions
obj = bpy.data.objects.new('BezierCurve', data)
# ~ collection = context.collection
if(collection == None):
collection = bpy.context.scene.collection
collection.objects.link(obj)
# ~ obj.location = context.scene.cursor.location
# ~ depsgraph = context.evaluated_depsgraph_get()
# ~ depsgraph.update()
# ~ invM = obj.matrix_world.inverted_safe()
spline = data.splines.new('BEZIER')
spline.use_cyclic_u = False
if(vectCmpWithMargin(curvePts[0][1], curvePts[-1][1])):
curvePts[0][0] = curvePts[-1][0]
spline.use_cyclic_u = True
curvePts.pop()
if(closed): spline.use_cyclic_u = True
spline.bezier_points.add(len(curvePts) - 1)
prevPt = None
for i, pt in enumerate(curvePts):
currPt = spline.bezier_points[i]
currPt.co = pt[1]
currPt.handle_right = pt[2]
if(not calcHdlTypes and len(pt) > 3):
currPt.handle_right_type = pt[3]
currPt.handle_left_type = pt[4]
elif(prevPt != None and prevPt.handle_right == prevPt.co \
and pt[0] == pt[1] and currPt.co != prevPt.co): # straight line
if(prevPt.handle_left_type != 'VECTOR'):
prevPt.handle_left_type = 'FREE'
prevPt.handle_right_type = 'VECTOR'
currPt.handle_right_type = 'FREE'
currPt.handle_left_type = 'VECTOR'
else:
currPt.handle_left_type = 'FREE'
currPt.handle_right_type = 'FREE'
currPt.handle_left = pt[0]
ldiffV = (pt[1] - pt[0])
rdiffV = (pt[2] - pt[1])
if(vectCmpWithMargin(ldiffV, rdiffV) and \
not floatCmpWithMargin(ldiffV.length, 0)):
currPt.handle_left_type = 'ALIGNED'
currPt.handle_right_type = 'ALIGNED'
prevPt = currPt
bpts = spline.bezier_points
if(spline.use_cyclic_u and vectCmpWithMargin(bpts[-1].handle_right, bpts[-1].co) \
and vectCmpWithMargin(bpts[0].handle_left, bpts[0].co)):
if(bpts[-1].handle_left_type != 'VECTOR'):
bpts[-1].handle_left_type = 'FREE'
bpts[-1].handle_right_type = 'VECTOR'
if(bpts[0].handle_right_type != 'VECTOR'):
bpts[0].handle_right_type = 'FREE'
bpts[0].handle_left_type = 'VECTOR'
return obj
def removeShapeKeys(obj):
if(obj.data.shape_keys == None):
return
keyblocks = reversed(obj.data.shape_keys.key_blocks)
for sk in keyblocks:
obj.shape_key_remove(sk)
def getShapeKeyInfo(obj):
keyData = []
keyNames = []
if(obj.data.shape_keys != None):
keyblocks = obj.data.shape_keys.key_blocks
for key in keyblocks:
keyData.append([[d.handle_left.copy(), d.co.copy(), d.handle_right.copy()] \
for d in key.data])
keyNames.append(key.name)
return keyNames, keyData
def updateShapeKeyData(obj, keyData, keyNames, startIdx, cnt = None, add = False):
if(obj.data.shape_keys == None and not add):
return
currIdx = obj.active_shape_key_index
if(not add): removeShapeKeys(obj)
if(cnt == None): cnt = len(keyData[0])
for i, name in enumerate(keyNames):
key = obj.shape_key_add(name = name)
for j in range(0, cnt):
keyIdx = j + startIdx
key.data[j].handle_left = keyData[i][keyIdx][0].copy()
key.data[j].co = keyData[i][keyIdx][1].copy()
key.data[j].handle_right = keyData[i][keyIdx][2].copy()
obj.active_shape_key_index = currIdx
#TODO: Fix this hack if possible
def copyObjAttr(src, dest, invDestMW = Matrix(), mw = Matrix()):
for att in dir(src):
try:
if(att not in ['co', 'handle_left', 'handle_right', \
'handle_left_type', 'handle_right_type']):
setattr(dest, att, getattr(src, att))
except Exception as e:
pass
try:
lt = src.handle_left_type
rt = src.handle_right_type
dest.handle_left_type = 'FREE'
dest.handle_right_type = 'FREE'
dest.co = invDestMW @ (mw @ src.co)
dest.handle_left = invDestMW @ (mw @ src.handle_left)
dest.handle_right = invDestMW @ (mw @ src.handle_right)
dest.handle_left_type = lt
dest.handle_right_type = rt
pass
except Exception as e:
pass
def getLastSegIdx(obj, splineIdx):
spline = obj.data.splines[splineIdx]
ptCnt = len(spline.bezier_points)
return ptCnt - 1 if(spline.use_cyclic_u) else ptCnt - 2
def addLastSeg(spline):
if(spline.use_cyclic_u):
lt0 = spline.bezier_points[0].handle_left_type
rt0 = spline.bezier_points[0].handle_right_type
pt = spline.bezier_points[0]
pt.handle_left_type = 'FREE'
pt.handle_right_type = 'FREE'
spline.use_cyclic_u = False
spline.bezier_points.add(1)
copyObjAttr(spline.bezier_points[0], spline.bezier_points[-1])
spline.bezier_points[0].handle_left_type = lt0
spline.bezier_points[-1].handle_right_type = rt0
def moveSplineStart(obj, splineIdx, idx):
pts = obj.data.splines[splineIdx].bezier_points
cnt = len(pts)
ptCopy = [[p.co.copy(), p.handle_right.copy(), \
p.handle_left.copy(), p.handle_right_type, \
p.handle_left_type] for p in pts]
for i, pt in enumerate(pts):
srcIdx = (idx + i) % cnt
p = ptCopy[srcIdx]
pt.handle_left_type = 'FREE'
pt.handle_right_type = 'FREE'
pt.co = p[0]
pt.handle_right = p[1]
pt.handle_left = p[2]
pt.handle_right_type = p[3]
pt.handle_left_type = p[4]
def joinCurves(curves):
obj = curves[0]
invMW = obj.matrix_world.inverted_safe()
for curve in curves[1:]:
mw = curve.matrix_world
for spline in curve.data.splines:
newSpline = obj.data.splines.new('BEZIER')
copyObjAttr(spline, newSpline)
newSpline.bezier_points.add(len(spline.bezier_points)-1)
for i, pt in enumerate(spline.bezier_points):
copyObjAttr(pt, newSpline.bezier_points[i], \
invDestMW = invMW, mw = mw)
safeRemoveObj(curve)
return obj
def getObjBBoxCenter(obj):
bbox = obj.bound_box
return obj.matrix_world @ Vector(((bbox[0][0] + bbox[4][0]) / 2, \
(bbox[0][1] + bbox[3][1]) / 2, (bbox[0][2] + bbox[1][2]) / 2))
# Only mesh and Bezier curve
def shiftOrigin(obj, origin):
oLoc = obj.location.copy()
mw = obj.matrix_world
invMw = mw.inverted_safe()
if(obj.type == 'MESH'):
for vert in obj.data.vertices:
vert.co += invMw @ oLoc - invMw @ origin
elif(obj.type == 'CURVE'):
for s in obj.data.splines:
bpts = s.bezier_points
for bpt in bpts:
lht = bpt.handle_left_type
rht = bpt.handle_right_type
bpt.handle_left_type = 'FREE'
bpt.handle_right_type = 'FREE'
bpt.co += invMw @ oLoc - invMw @ origin
bpt.handle_left += invMw @ oLoc - invMw @ origin
bpt.handle_right += invMw @ oLoc - invMw @ origin
bpt.handle_left_type = lht
bpt.handle_right_type = rht
obj.location = origin
# Only mesh and Bezier curve; depsgraph not updated
def shiftMatrixWorld(obj, mw):
invMw = mw.inverted_safe()
omw = obj.matrix_world
if(obj.type == 'MESH'):
for vert in obj.data.vertices:
vert.co = invMw @ (omw @ vert.co)
elif(obj.type == 'CURVE'):
for s in obj.data.splines:
bpts = s.bezier_points
for bpt in bpts:
lht = bpt.handle_left_type
rht = bpt.handle_right_type
bpt.handle_left_type = 'FREE'
bpt.handle_right_type = 'FREE'
bpt.co = invMw @ (omw @ bpt.co)
bpt.handle_left = invMw @ (omw @ bpt.handle_left)
bpt.handle_right = invMw @ (omw @ bpt.handle_right)
bpt.handle_left_type = lht
bpt.handle_right_type = rht
obj.matrix_world = mw
# Also shifts origin; depsgraph not updated
def alignToNormal(curve):
depsgraph = bpy.context.evaluated_depsgraph_get()
depsgraph.update()
mw = curve.matrix_world.copy()
normals = []
for spline in curve.data.splines:
bpts = spline.bezier_points
bptCnt = len(bpts)
if(bptCnt > 2):
normals.append(geometry.normal(mw @ bpts[i].co for i in range(bptCnt)))
cnt = len(normals)
if(cnt > 0):
normal = Vector([sum(normals[i][j] for i in range(cnt)) \
for j in range(3)]) / cnt
quatMat = normal.to_track_quat('Z', 'X').to_matrix().to_4x4()
shiftMatrixWorld(curve, quatMat)
def copyProperties(srcObj, destCurve):
if(srcObj == None or destCurve == None):
return
destData = destCurve.data
srcData = srcObj.data
# If object is bezier curve copy curve properties and material
if(isBezier(srcObj)):
# Copying just a few attributes
destData.dimensions = srcData.dimensions
destData.resolution_u = srcData.resolution_u
destData.render_resolution_u = srcData.render_resolution_u
destData.fill_mode = srcData.fill_mode
destData.use_fill_deform = srcData.use_fill_deform
destData.use_radius = srcData.use_radius
destData.use_stretch = srcData.use_stretch
destData.use_deform_bounds = srcData.use_deform_bounds
destData.twist_smooth = srcData.twist_smooth
destData.twist_mode = srcData.twist_mode
destData.offset = srcData.offset
destData.extrude = srcData.extrude
destData.bevel_depth = srcData.bevel_depth
destData.bevel_resolution = srcData.bevel_resolution
destData.bevel_object = srcData.bevel_object
destData.taper_object = srcData.taper_object
destData.use_fill_caps = srcData.use_fill_caps
if(hasattr(srcData, 'materials') and len(srcData.materials) > 0):
mat = srcData.materials[srcObj.active_material_index]
if(len(destData.materials) == 0 or mat.name not in destData.materials):
destData.materials.append(mat)
activeIdx = -1 #Last
else:
activeIdx = destData.materials.find(mat.name)
destCurve.active_material_index = activeIdx
def reverseCurve(curve):
cp = curve.data.copy()
curve.data.splines.clear()
for s in reversed(cp.splines):
ns = curve.data.splines.new('BEZIER')
copyObjAttr(s, ns)
ns.bezier_points.add(len(s.bezier_points) - 1)
for i, p in enumerate(reversed(s.bezier_points)):
copyObjAttr(p, ns.bezier_points[i])
ns.bezier_points[i].handle_left_type = 'FREE'
ns.bezier_points[i].handle_right_type = 'FREE'
ns.bezier_points[i].handle_left = p.handle_right
ns.bezier_points[i].handle_right = p.handle_left
ns.bezier_points[i].handle_left_type = p.handle_right_type
ns.bezier_points[i].handle_right_type = p.handle_left_type
bpy.data.curves.remove(cp)
# Insert spline at location insertIdx, duplicated from existing spline at
# location srcSplineIdx and remove points with indices in removePtIdxs from new spline
def insertSpline(obj, srcSplineIdx, insertIdx, removePtIdxs):
srcSpline = obj.data.splines[srcSplineIdx]
# Appended at end
createSpline(obj.data, srcSpline, removePtIdxs)
splineCnt = len(obj.data.splines)
nextIdx = insertIdx
for idx in range(nextIdx, splineCnt - 1):
srcSpline = obj.data.splines[nextIdx]
createSpline(obj.data, srcSpline)
obj.data.splines.remove(srcSpline)
def removeBezierPts(obj, splineIdx, removePtIdxs):
oldSpline = obj.data.splines[splineIdx]
bpts = oldSpline.bezier_points
if(min(removePtIdxs) >= len(bpts)):
return
if(len(set(range(len(bpts))) - set(removePtIdxs)) == 0) :
obj.data.splines.remove(oldSpline)
if(len(obj.data.splines) == 0):
safeRemoveObj(obj)
return
insertSpline(obj, splineIdx, splineIdx, removePtIdxs)
obj.data.splines.remove(obj.data.splines[splineIdx + 1])
# Returns a tuple with first value indicating change in spline index (-1, 0, 1)
# and second indicating shift in seg index (negative) due to removal
def removeBezierSeg(obj, splineIdx, segIdx):
nextIdx = getAdjIdx(obj, splineIdx, segIdx)
if(nextIdx == None): return
spline = obj.data.splines[splineIdx]
bpts = spline.bezier_points
ptCnt = len(bpts)
lastSegIdx = getLastSegIdx(obj, splineIdx)
splineIdxIncr = 0
segIdxIncr = 0
if(ptCnt <= 2):
removeBezierPts(obj, splineIdx, {segIdx, nextIdx})
# Spline removed by above call
splineIdxIncr = -1
else:
bpt = obj.data.splines[splineIdx].bezier_points[segIdx]
bpt.handle_right_type = 'FREE'
bpt.handle_left_type = 'FREE'
nextIdx = getAdjIdx(obj, splineIdx, segIdx)
bpt = obj.data.splines[splineIdx].bezier_points[nextIdx]
bpt.handle_right_type = 'FREE'
bpt.handle_left_type = 'FREE'
if(spline.use_cyclic_u):
spline.use_cyclic_u = False
if(segIdx != lastSegIdx):
moveSplineStart(obj, splineIdx, getAdjIdx(obj, splineIdx, segIdx))
segIdxIncr = - (segIdx + 1)
else:
if(segIdx == lastSegIdx):
removeBezierPts(obj, splineIdx, {lastSegIdx + 1})
elif(segIdx == 0):
removeBezierPts(obj, splineIdx, {0})
segIdxIncr = -1
else:
insertSpline(obj, splineIdx, splineIdx, set(range(segIdx + 1, ptCnt)))
removeBezierPts(obj, splineIdx + 1, range(segIdx + 1))
splineIdxIncr = 1
segIdxIncr = - (segIdx + 1)
return splineIdxIncr, segIdxIncr
def insertBezierPts(obj, splineIdx, startIdx, cos, handleType, margin = DEF_ERR_MARGIN):
spline = obj.data.splines[splineIdx]
bpts = spline.bezier_points
nextIdx = getAdjIdx(obj, splineIdx, startIdx)
firstPt = bpts[startIdx]
nextPt = bpts[nextIdx]
if(firstPt.handle_right_type == 'AUTO'):
firstPt.handle_left_type = 'ALIGNED'
firstPt.handle_right_type = 'ALIGNED'
if(nextPt.handle_left_type == 'AUTO'):
nextPt.handle_left_type = 'ALIGNED'
nextPt.handle_right_type = 'ALIGNED'
fhdl = firstPt.handle_right_type
nhdl = nextPt.handle_left_type
firstPt.handle_right_type = 'FREE'
nextPt.handle_left_type = 'FREE'
ptCnt = len(bpts)
addCnt = len(cos)
bpts.add(addCnt)
nextIdx = startIdx + 1
for i in range(0, (ptCnt - nextIdx)):
idx = ptCnt - i - 1# reversed
offsetIdx = idx + addCnt
copyObjAttr(bpts[idx], bpts[offsetIdx])
endIdx = getAdjIdx(obj, splineIdx, nextIdx, addCnt)
firstPt = bpts[startIdx]
nextPt = bpts[endIdx]
prevPt = firstPt
for i, pt in enumerate(bpts[nextIdx:nextIdx + addCnt]):
pt.handle_left_type = 'FREE'
pt.handle_right_type = 'FREE'
co = cos[i]
seg = [prevPt.co, prevPt.handle_right, nextPt.handle_left, nextPt.co]
t = getTForPt(seg, co, margin)
ctrlPts0 = getPartialSeg(seg, 0, t)
ctrlPts1 = getPartialSeg(seg, t, 1)
segPt = [ctrlPts0[2], ctrlPts1[0], ctrlPts1[1]]
prevRight = ctrlPts0[1]
nextLeft = ctrlPts1[2]
pt.handle_left = segPt[0]
pt.co = segPt[1]
pt.handle_right = segPt[2]
pt.handle_left_type = handleType
pt.handle_right_type = handleType
prevPt.handle_right = prevRight
prevPt = pt
nextPt.handle_left = nextLeft
firstPt.handle_right_type = fhdl
nextPt.handle_left_type = nhdl
# https://devtalk.blender.org/t/get-hex-gamma-corrected-color/2422/2
def toHexStr(rgba):
ch = []
for c in rgba[:3]:
if c < 0.0031308:
cc = 0.0 if c < 0.0 else c * 12.92
else:
cc = 1.055 * pow(c, 1.0 / 2.4) - 0.055
ch.append(hex(max(min(int(cc * 255 + 0.5), 255), 0))[2:])
return ''.join(ch), str(rgba[-1])
# Change position of bezier points according to new matrix_world
def changeMW(obj, newMW):
invMW = newMW.inverted_safe()
for spline in obj.data.splines:
for pt in spline.bezier_points:
pt.co = invMW @ (obj.mw @ pt.co)
pt.handle_left = invMW @ (obj.mw @ pt.handle_left)
pt.handle_right = invMW @ (obj.mw @ pt.handle_right)
obj.matrix_world = newMW
# Return map in the form of objName->[splineIdx, [startPt, endPt]]
# Remove the invalid keys (if any) from it.
def updateCurveEndPtMap(endPtMap, addObjNames = None, removeObjNames = None):
invalOs = set()
if(addObjNames == None):
addObjNames = [o.name for o in bpy.context.scene.objects]
invalOs = endPtMap.keys() - set(addObjNames) # In case of redo
if(removeObjNames != None):
invalOs.union(set(removeObjNames))
for o in invalOs:
del endPtMap[o]
for objName in addObjNames:
obj = bpy.context.scene.objects.get(objName)
if(obj != None and isBezier(obj) and obj.visible_get()):
endPtMap[objName] = []
mw = obj.matrix_world
for i, s in enumerate(obj.data.splines):
pts = [mw @ pt.co for pt in s.bezier_points]
endPtMap[objName].append([i, pts])
elif(endPtMap.get(objName) != None):
del endPtMap[objName]
return endPtMap
#Round to logarithmic scale .1, 0, 10, 100 etc.
#(47.538, -1) -> 47.5; (47.538, 0) -> 48.0; (47.538, 1) -> 50.0; (47.538, 2) -> 0,
# TODO: Rework after grid subdiv is enabled (in a version later than 2.8)
def roundedVect(space3d, vect, rounding, axes):
rounding += 1
subdiv = getGridSubdiv(space3d)
# TODO: Separate logic for 1
if(subdiv == 1): subdiv = 10
fact = ((subdiv ** rounding) / subdiv) / getUnitScale()
retVect = vect.copy()
# ~ Vector([round(vect[i] / fact) * fact for i in axes])
for i in axes: retVect[i] = round(vect[i] / fact) * fact
return retVect
###################### Screen functions ######################
def getGridSubdiv(space3d):
return space3d.overlay.grid_subdivisions
def getUnit():
return bpy.context.scene.unit_settings.length_unit
def getUnitSystem():
return bpy.context.scene.unit_settings.system
def getUnitScale():
fact = 3.28084 if(getUnitSystem() == 'IMPERIAL') else 1
return fact * bpy.context.scene.unit_settings.scale_length
def get3dLoc(region, rv3d, xy, vec = None):
if(vec == None):
vec = region_2d_to_vector_3d(region, rv3d, xy)
return region_2d_to_location_3d(region, rv3d, xy, vec)
# TODO: Rework after grid subdiv is enabled (in a version later than 2.8)
def getViewDistRounding(space3d, rv3d):
viewDist = rv3d.view_distance * getUnitScale()
gridDiv = getGridSubdiv(space3d)
subFact = 1
# TODO: Separate logic for 1
if(gridDiv == 1): gridDiv = 10
elif(gridDiv == 2): subFact = 5
elif(viewDist < 0.5): subFact = 2
return int(log(viewDist, gridDiv)) - subFact
# Return axis-indices (x:0, y:1, z:2) of plane with closest orientation
# to view
def getClosestPlaneToView(rv3d):
viewmat = rv3d.view_matrix
trans, quat, scale = viewmat.decompose()
tm = quat.to_matrix().to_4x4()
viewnormal = tm.inverted() @ Vector((0, 0, 1))
xynormal = [Vector((0, 0, 1)), [0, 1]]
yznormal = [Vector((1, 0, 0)), [1, 2]]
xznormal = [Vector((0, 1, 0)), [0, 2]]
normals = [xynormal, yznormal, xznormal]
minAngle = pi
minIdx = None
for idx, normal in enumerate(normals):
rotDiff = viewnormal.rotation_difference(normal[0]).angle
if(rotDiff > pi / 2):
rotDiff = pi - rotDiff
if(rotDiff < minAngle):
minIdx = idx
minAngle = rotDiff
return normals[minIdx][1]
def getCoordFromLoc(region, rv3d, loc):
coord = location_3d_to_region_2d(region, rv3d, loc)
# return a unlocatable pt if None to avoid errors
return coord if(coord != None) else Vector((9000, 9000))
# To be called only from 3d view
def getCurrAreaRegion(context):
a, r = [(a, r) for a in bpy.context.screen.areas if a.type == 'VIEW_3D' \
for r in a.regions if(r == context.region)][0]
return a, r
def isOutside(context, event, exclInRgns = True):
x = event.mouse_region_x
y = event.mouse_region_y
region = context.region
if(x < 0 or x > region.width or y < 0 or y > region.height):
return True
elif(not exclInRgns):
return False
area, r = getCurrAreaRegion(context)
for r in area.regions:
if(r == region):
continue
xR = r.x - region.x
yR = r.y - region.y
if(x >= xR and y >= yR and x <= (xR + r.width) and y <= (yR + r.height)):
return True
return False
def getPtProjOnPlane(region, rv3d, xy, p1, p2, p3, p4 = None):
vec = region_2d_to_vector_3d(region, rv3d, xy)
orig = region_2d_to_origin_3d(region, rv3d, xy)
pt = geometry.intersect_ray_tri(p1, p2, p3, vec, orig, False)#p4 != None)
# ~ if(not pt and p4):
# ~ pt = geometry.intersect_ray_tri(p2, p4, p3, vec, orig, True)
return pt
# find the location on 3d line p1-p2 if xy is already on 2d projection (in rv3d) of p1-p2
def getPtProjOnLine(region, rv3d, xy, p1, p2):
# Just find a non-linear point (TODO: simpler way)
pd1 = p2 - p1
pd2 = Vector(sorted(pd1, key=lambda x: abs(x), reverse = True))
maxIdx0 = [i for i in range(3) if abs(pd1[i]) == abs(pd2[0])][0]
maxIdx1 = [i for i in range(3) if abs(pd1[i]) == abs(pd2[1])][0]
pd = Vector()
pd[maxIdx0] = -pd2[1]
pd[maxIdx1] = pd2[0]
p3 = p2 + pd
# Raycast from 2d point onto the plane
return getPtProjOnPlane(region, rv3d, xy[:2], p1, p2, p3)
def getLineTransMatrices(pt0, pt1):
diffV = (pt1 - pt0)
invTm = diffV.to_track_quat('X', 'Z').to_matrix().to_4x4()
tm = invTm.inverted_safe()
return tm, invTm
def getWindowRegionIdx(area, regionIdx): # For finding quad view index
idx = 0
for j, r in enumerate(area.regions):
if(j == regionIdx): return idx
if(r.type == 'WINDOW'): idx += 1
return None
def getAreaRegionIdxs(xy, exclInRgns = True):
x, y = xy
areas = [a for a in bpy.context.screen.areas]
idxs = None
for i, a in enumerate(areas):
if(a.type != 'VIEW_3D'): continue
regions = [r for r in a.regions]
for j, r in enumerate(regions):
if(x > r.x and x < r.x + r.width and y > r.y and y < r.y + r.height):
if(r.type == 'WINDOW'):
if(not exclInRgns):
return [i, j]
idxs = [i, j]
elif(exclInRgns):
return None
return idxs
# ~ def getMinViewDistRegion():
# ~ viewDist = 9e+99
# ~ rv3d = None
# ~ area = None
# ~ region = None
# ~ areas = [a for a in bpy.context.screen.areas if(a.type == 'VIEW_3D')]
# ~ for a in areas:
# ~ regions = [r for r in a.regions if r.type == 'WINDOW']
# ~ if(len(a.spaces[0].region_quadviews) > 0):
# ~ for i, r in enumerate(a.spaces[0].region_quadviews):
# ~ if(r.view_distance < viewDist):
# ~ viewDist = r.view_distance
# ~ rv3d = r
# ~ area = a
# ~ region = regions[i]
# ~ else:
# ~ r = a.spaces[0].region_3d
# ~ if(r.view_distance < viewDist):
# ~ viewDist = r.view_distance
# ~ rv3d = r
# ~ area = a
# ~ region = regions[0]
# ~ return a, region, rv3d#, viewDist
def getAllAreaRegions():
info = []
areas = []
i = 0
areas = [a for a in bpy.context.screen.areas if(a.type == 'VIEW_3D')]
# bpy.context.screen doesn't work in case of Add-on Config window
while(len(areas) == 0 and i < len(bpy.data.screens)):
areas = [a for a in bpy.data.screens[i].areas if(a.type == 'VIEW_3D')]
i += 1
for a in areas:
regions = [r for r in a.regions if r.type == 'WINDOW']
if(len(a.spaces[0].region_quadviews) > 0):
for i, r in enumerate(a.spaces[0].region_quadviews):
info.append([a, regions[i], r])
else:
r = a.spaces[0].region_3d
info.append([a, regions[0], r])
return info
def getResetBatch(shader, btype): # "LINES" or "POINTS"
return batch_for_shader(shader, btype, {"pos": [], "color": []})
# From python template
def getFaceUnderMouse(obj, region, rv3d, xy, maxFaceCnt):
if(obj == None or obj.type != 'MESH' \
or len(obj.data.polygons) > maxFaceCnt):
return None, None, None
viewVect = region_2d_to_vector_3d(region, rv3d, xy)
rayOrig = region_2d_to_origin_3d(region, rv3d, xy)
mw = obj.matrix_world
invMw = mw.inverted_safe()
rayTarget = rayOrig + viewVect
rayOrigObj = invMw @ rayOrig
rayTargetObj = invMw @ rayTarget
rayDirObj = rayTargetObj - rayOrigObj
success, location, normal, faceIdx = obj.ray_cast(rayOrigObj, rayDirObj)
if(success):
return mw @ location, normal, faceIdx
else:
return None, None, None
def getSnappableObjs(region, rv3d, xy):
objs = bpy.context.selected_objects
if(bpy.context.object != None):
objs.append(bpy.context.object)
return [o for o in objs if(o.type == 'MESH' and len(o.modifiers) == 0 and \
isPtIn2dBBox(o, region, rv3d, xy))]
# precise can be pretty expensive with large vert count
def get2dBBox(obj, region, rv3d, precise = False):
mw = obj.matrix_world
if(precise):
co2ds = [getCoordFromLoc(region, rv3d, mw @ Vector(v.co)) \
for v in obj.data.vertices]
else:
co2ds = [getCoordFromLoc(region, rv3d, mw @ Vector(b)) for b in obj.bound_box]
minX = min(c[0] for c in co2ds)
maxX = max(c[0] for c in co2ds)
minY = min(c[1] for c in co2ds)
maxY = max(c[1] for c in co2ds)
return minX, minY, maxX, maxY
def isPtIn2dBBox(obj, region, rv3d, xy, extendBy = 0, precise = False):
minX, minY, maxX, maxY = get2dBBox(obj, region, rv3d, precise)
if(xy[0] > (minX - extendBy) and xy[0] < (maxX + extendBy) \
and xy[1] > (minY - extendBy) and xy[1] < (maxY + extendBy)):
return True
else: return False
# ~ def isLocIn2dBBox(obj, region, rv3d, loc, extendBy = 0, precise = False):
# ~ xy = getCoordFromLoc(region, rv3d, loc)
# ~ return isPtIn2dBBox(obj, region, rv3d, xy, extendBy, precise)
def getClosestEdgeLoc2d(obj, region, rv3d, xy, faceIdx = None):
mw = obj.matrix_world
minDist = LARGE_NO
closestLoc = None
pt = Vector(xy).to_3d()
edgeWSCos = None
edgeIdx = None
closestIntersect = None
vertPairs = obj.data.polygons[faceIdx].edge_keys if faceIdx != None \
else [e.vertices for e in obj.data.edges]
for i, vertPair in enumerate(vertPairs):
co0 = mw @ obj.data.vertices[vertPair[0]].co
co1 = mw @ obj.data.vertices[vertPair[1]].co
pt0 = getCoordFromLoc(region, rv3d, co0).to_3d()
pt1 = getCoordFromLoc(region, rv3d, co1).to_3d()
intersect, percDist = geometry.intersect_point_line(pt, pt0, pt1)
if(percDist < 0):
intersect = pt0
percDist = 0
elif(percDist > 1):
intersect = pt1
percDist = 1
dist = (intersect - pt).length
if(dist < minDist):
minDist = dist
closestIntersect = intersect
edgeWSCos = [co0, co1]
edgeIdx = i
if(edgeWSCos != None):
closestLoc = getPtProjOnLine(region, rv3d, closestIntersect, \
edgeWSCos[0], edgeWSCos[1])
return edgeIdx, edgeWSCos, closestLoc, minDist
# TODO: Fix the signature
def getSelFaceLoc(region, rv3d, xy, maxFaceCnt, objs = None, checkEdge = False):
if(objs == None): objs = getSnappableObjs(region, rv3d, xy)
if(len(objs) > maxFaceCnt): return None, None, None, None
for obj in objs:
loc, normal, faceIdx = getFaceUnderMouse(obj, region, rv3d, xy, maxFaceCnt)
if(loc != None):
if(checkEdge):
edgeIdx, edgeWSCos, closestLoc, minDist = \
getClosestEdgeLoc2d(obj, region, rv3d, xy, faceIdx)
if(closestLoc != None and minDist < FTProps.snapDist):
return obj, closestLoc, normal, faceIdx
return obj, loc, normal, faceIdx
return None, None, None, None
# TODO: Fix the signature
# ~ def getSelEdgeLoc(region, rv3d, xy, maxFaceCnt, objs = None):
# ~ if(objs == None): objs = getSnappableObjs()
# ~ if(len(objs) > maxFaceCnt): return None, None, None, None
# ~ minDist = LARGE_NO
# ~ closestLoc = None
# ~ closestEdgeIdx = None
# ~ closestObj = None
# ~ objCnt = 0
# ~ objs = bpy.context.selected_objects
# ~ if(bpy.context.object != None): objs.append(bpy.context.object)
# ~ for obj in objs:
# ~ edgeIdx, edgeWSCos, loc, dist = \
# ~ getClosestEdgeLoc2d(obj, region, rv3d, xy)
# ~ if(dist < minDist):
# ~ minDist = dist
# ~ closestLoc = loc
# ~ closestEdgeIdx = edgeIdx
# ~ closestObj = obj
# ~ return closestObj, closestLoc, minDist, closestEdgeIdx
###################### Op Specific functions ######################
def closeSplines(curve, htype = None):
for spline in curve.data.splines:
if(htype != None):
spline.bezier_points[0].handle_left_type = htype
spline.bezier_points[-1].handle_right_type = htype
spline.use_cyclic_u = True
# TODO: Update shapekey (not working due to moving of start pt in cyclic)
def splitCurveSelPts(selPtMap, newColl = True):
changeCnt = 0
newObjs = []
if(len(selPtMap) == 0): return newObjs, changeCnt
for obj in selPtMap.keys():
splinePtMap = selPtMap.get(obj)
if((len(obj.data.splines) == 1 and \
len(obj.data.splines[0].bezier_points) <= 2 and \
not obj.data.splines[0].use_cyclic_u) or len(splinePtMap) == 0):
continue
keyNames, keyData = getShapeKeyInfo(obj)
collections = obj.users_collection
if(newColl):
objGrp = bpy.data.collections.new(obj.name)
parentColls = [objGrp]
else:
parentColls = collections
splineCnt = len(obj.data.splines)
endSplineIdx = splineCnt- 1
if(endSplineIdx not in splinePtMap.keys()):
splinePtMap[endSplineIdx] = \
[len(obj.data.splines[endSplineIdx].bezier_points) - 1]
splineIdxs = sorted(splinePtMap.keys())
lastSplineIdx = -1
objCopy = createSkeletalCurve(obj, parentColls)
newObjs.append(objCopy)
for i in splineIdxs:
for j in range(lastSplineIdx + 1, i):
srcSpline = obj.data.splines[j]
createSpline(objCopy.data, srcSpline)
# ~ updateShapeKeyData(objCopy, keyData, keyNames, skStart, ptCnt)
srcSpline = obj.data.splines[i]
selPtIdxs = sorted(splinePtMap[i])
if(len(selPtIdxs) == 0):
createSpline(objCopy.data, srcSpline)
else:
bpts = srcSpline.bezier_points
cyclic = srcSpline.use_cyclic_u
if(cyclic):
firstIdx = selPtIdxs[0]
moveSplineStart(obj, i, firstIdx)
selPtIdxs = [getAdjIdx(obj, i, s, -firstIdx) for s in selPtIdxs]
addLastSeg(srcSpline)
if(len(selPtIdxs) > 0 and selPtIdxs[0] == 0):
selPtIdxs.pop(0)
if(len(selPtIdxs) > 0 and selPtIdxs[-1] == len(bpts) - 1):
selPtIdxs.pop(-1)
bpts = srcSpline.bezier_points
if(len(selPtIdxs) == 0):
segBpts = bpts[:len(bpts)]
createSplineForSeg(objCopy.data, segBpts)
else:
lastSegIdx = 0
bpts = srcSpline.bezier_points
for j in selPtIdxs:
segBpts = bpts[lastSegIdx:j + 1]
createSplineForSeg(objCopy.data, segBpts)
# ~ updateShapeKeyData(objCopy, keyData, keyNames, \
# ~ len(newObjs), 2)
objCopy = createSkeletalCurve(obj, parentColls)
newObjs.append(objCopy)
lastSegIdx = j
if(j != len(bpts) - 1): createSplineForSeg(objCopy.data, bpts[j:])
lastSplineIdx = i
if(len(objCopy.data.splines) == 0):
newObjs.remove(objCopy)
safeRemoveObj(objCopy)
if(newColl):
for collection in collections:
collection.children.link(objGrp)
safeRemoveObj(obj)
changeCnt += 1
for obj in newObjs:
obj.data.splines.active = obj.data.splines[0]
return newObjs, changeCnt
#split value is one of {'spline', 'seg', 'point'} (TODO: Enum)
def splitCurve(selObjs, split, newColl = True):
changeCnt = 0
newObjs = []
if(len(selObjs) == 0):
return newObjs, changeCnt
for obj in selObjs:
if(not isBezier(obj) or len(obj.data.splines) == 0):
continue
if(len(obj.data.splines) == 1):
if(split == 'spline'):
newObjs.append(obj)
continue
if(split == 'seg' and len(obj.data.splines[0].bezier_points) <= 2):
newObjs.append(obj)
continue
if(split == 'point' and len(obj.data.splines[0].bezier_points) == 1):
newObjs.append(obj)
continue
keyNames, keyData = getShapeKeyInfo(obj)
collections = obj.users_collection
if(newColl):
objGrp = bpy.data.collections.new(obj.name)
parentColls = [objGrp]
else:
parentColls = collections
segCnt = 0
for i, spline in enumerate(obj.data.splines):
if(split == 'seg' or split == 'point'):
ptLen = len(spline.bezier_points)
if(split == 'seg'):
ptLen -= 1
for j in range(0, ptLen):
objCopy = createSkeletalCurve(obj, parentColls)
if(split == 'seg'):
createSplineForSeg(objCopy.data, \
spline.bezier_points[j:j+2])
updateShapeKeyData(objCopy, keyData, keyNames, \
len(newObjs), 2)
else: #(split == 'point')
mw = obj.matrix_world.copy()
newSpline = objCopy.data.splines.new('BEZIER')
newPtCo = mw @ spline.bezier_points[j].co.copy()
newWM = Matrix()
newWM.translation = newPtCo
objCopy.matrix_world = newWM
copyObjAttr(spline.bezier_points[j], \
newSpline.bezier_points[0], newWM.inverted_safe(), mw)
# No point having shapekeys (pun intended :)
removeShapeKeys(objCopy)
newObjs.append(objCopy)
if(split == 'seg' and spline.use_cyclic_u):
objCopy = createSkeletalCurve(obj, parentColls)
createSplineForSeg(objCopy.data, \
[spline.bezier_points[-1], spline.bezier_points[0]])
updateShapeKeyData(objCopy, keyData, keyNames, -1, 2)
newObjs.append(objCopy)
else: #split == 'spline'
objCopy = createSkeletalCurve(obj, parentColls)
createSpline(objCopy.data, spline)
currSegCnt = len(objCopy.data.splines[0].bezier_points)
updateShapeKeyData(objCopy, keyData, keyNames, segCnt, currSegCnt)
newObjs.append(objCopy)
segCnt += currSegCnt
if(newColl):
for collection in collections:
collection.children.link(objGrp)
safeRemoveObj(obj)
changeCnt += 1
for obj in newObjs:
obj.data.splines.active = obj.data.splines[0]
return newObjs, changeCnt
def getClosestCurve(srcMW, pt, curves, minDist = 9e+99):
closestCurve = None
for i, curve in enumerate(curves):
mw = curve.matrix_world
addLastSeg(curve.data.splines[0])
start = curve.data.splines[0].bezier_points[0]
end = curve.data.splines[-1].bezier_points[-1]
dist = ((mw @ start.co) - (srcMW @ pt)).length
if(dist < minDist):
minDist = dist
closestCurve = curve
dist = ((mw @ end.co) - (srcMW @ pt)).length
if(dist < minDist):
minDist = dist
reverseCurve(curve)
closestCurve = curve
return closestCurve, minDist
def getCurvesArrangedByDist(curves):
idMap = {c.name:c for c in curves}
orderedCurves = [curves[0].name]
nextCurve = curves[0]
remainingCurves = curves[1:]
#Arrange in order
while(len(remainingCurves) > 0):
addLastSeg(nextCurve.data.splines[-1])
srcMW = nextCurve.matrix_world
ncEnd = nextCurve.data.splines[-1].bezier_points[-1]
closestCurve, dist = getClosestCurve(srcMW, ncEnd.co, remainingCurves)
#Check the start also for the first curve
if(len(orderedCurves) == 1):
ncStart = nextCurve.data.splines[0].bezier_points[0]
closestCurve2, dist2 = getClosestCurve(srcMW, ncStart.co, \
remainingCurves, dist)
if(closestCurve2 != None):
reverseCurve(nextCurve)
closestCurve = closestCurve2
orderedCurves.append(closestCurve.name)
nextCurve = closestCurve
remainingCurves.remove(closestCurve)
return [idMap[cn] for cn in orderedCurves]
def joinSegs(curves, optimized, straight, srcCurve = None, margin = DEF_ERR_MARGIN):
if(len(curves) == 0):
return None
if(len(curves) == 1):
return curves[0]
if(optimized):
curves = getCurvesArrangedByDist(curves)
firstCurve = curves[0]
if(srcCurve == None):
srcCurve = firstCurve
elif(srcCurve != firstCurve):
srcCurveData = srcCurve.data.copy()
changeMW(firstCurve, srcCurve.matrix_world)
srcCurve.data = firstCurve.data
srcMW = srcCurve.matrix_world
invSrcMW = srcMW.inverted_safe()
newCurveData = srcCurve.data
for curve in curves[1:]:
if(curve == srcCurve):
curveData = srcCurveData
else:
curveData = curve.data
mw = curve.matrix_world
currSpline = newCurveData.splines[-1]
nextSpline = curveData.splines[0]
addLastSeg(currSpline)
addLastSeg(nextSpline)
currBezierPt = currSpline.bezier_points[-1]
nextBezierPt = nextSpline.bezier_points[0]
#Don't add new point if the last one and the current one are the 'same'
if(vectCmpWithMargin(srcMW @ currBezierPt.co, mw @ nextBezierPt.co, margin)):
currBezierPt.handle_right_type = nextBezierPt.handle_right_type
if(currBezierPt.handle_right_type != 'VECTOR'):
currBezierPt.handle_right_type = 'FREE'
currBezierPt.handle_right = invSrcMW @ (mw @ nextBezierPt.handle_right)
ptIdx = 1
else:
ptIdx = 0
if(straight and ptIdx == 0):
currBezierPt.handle_left_type = 'FREE'
currBezierPt.handle_right_type = 'VECTOR'
# ~ currBezierPt.handle_right = currBezierPt.co
for i in range(ptIdx, len(nextSpline.bezier_points)):
if((i == len(nextSpline.bezier_points) - 1) and
vectCmpWithMargin(mw @ nextSpline.bezier_points[i].co, \
srcMW @ currSpline.bezier_points[0].co, margin)):
currSpline.bezier_points[0].handle_left_type = 'FREE'
currSpline.bezier_points[0].handle_left = \
invSrcMW @ (mw @ nextSpline.bezier_points[i].handle_left)
currSpline.use_cyclic_u = True
break
currSpline.bezier_points.add(1)
currBezierPt = currSpline.bezier_points[-1]
copyObjAttr(nextSpline.bezier_points[i], currBezierPt, invSrcMW, mw)
if(straight and i == 0):
currBezierPt.handle_right_type = 'FREE'
currBezierPt.handle_left_type = 'VECTOR'
# ~ currBezierPt.handle_left = currBezierPt.co
#Simply add the remaining splines
for spline in curveData.splines[1:]:
newSpline = newCurveData.splines.new('BEZIER')
copyObjAttr(spline, newSpline)
for i, pt in enumerate(spline.bezier_points):
if(i > 0):
newSpline.bezier_points.add(1)
copyObjAttr(pt, newSpline.bezier_points[-1], invSrcMW, mw)
if(curve != srcCurve):
safeRemoveObj(curve)
if(firstCurve != srcCurve):
safeRemoveObj(firstCurve)
return srcCurve
def removeDupliVert(curve, margin):
newCurveData = curve.data.copy()
newCurveData.splines.clear()
dupliFound = False
for spline in curve.data.splines:
newCurveData.splines.new('BEZIER')
currSpline = newCurveData.splines[-1]
copyObjAttr(spline, currSpline)
if(len(spline.bezier_points) == 1):
copyObjAttr(spline.bezier_points[0], currSpline.bezier_points[0])
continue
cmpPts = spline.bezier_points[:]
pt0 = cmpPts[0]
while(vectCmpWithMargin(cmpPts[-1].co, pt0.co, margin) and
len(cmpPts) > 1):
endPt = cmpPts.pop()
pt0.handle_left_type = 'FREE'
pt0.handle_right_type = 'FREE'
pt0.handle_left = endPt.handle_left
currSpline.use_cyclic_u = True
dupliFound = True
prevPt = None
for pt in cmpPts:
if(prevPt != None and vectCmpWithMargin(prevPt.co, pt.co, margin)):
copyObjAttr(pt, currSpline.bezier_points[-1])
dupliFound = True
else:
if(prevPt != None): currSpline.bezier_points.add(1)
copyObjAttr(pt, currSpline.bezier_points[-1])
prevPt = pt
if(dupliFound):
curve.data = newCurveData
else:
bpy.data.curves.remove(newCurveData)
def convertToFace(curve, remeshRes, perSeg, fillType, optimized):
bptData = getBptData(curve, local = True)
bm = bmesh.new()
splineLens = [spline.calc_length() for spline in curve.data.splines]
maxSplineLen = max(splineLens)
centers = []
normals = []
for splineIdx, spline in enumerate(curve.data.splines):
bpts = spline.bezier_points
verts = []
addLastVert = spline.use_cyclic_u or fillType != 'NONE'
if(not perSeg and remeshRes > 0):
segPts = [bptData[splineIdx][x] for x in range(len(bpts))]
if(addLastVert): segPts.append(segPts[0])
numSegs = int(remeshRes * splineLens[splineIdx] / maxSplineLen)
if(numSegs <= 2):
vertCos = [bpts[0].co, bpts[-1].co]
else:
pts = getInterpBezierPts(segPts, subdivPerUnit = 100, segLens = None)
vertCos = getInterpolatedVertsCo(pts, numSegs)
for co in vertCos:
verts.append(bm.verts.new(co))
else:
if(remeshRes > 0):
segPtPairs = [getSegPtsInSpline(bptData, splineIdx, \
ptIdx, addLastVert) for ptIdx in range(len(bpts))]
if(not addLastVert): segPtPairs.pop()
segs = [[segPts[0][1], segPts[0][2], segPts[1][0], segPts[1][1]] \
for segPts in segPtPairs]
segLens = [getSegLen(seg) for seg in segs]
maxLen = max(segLens)
for i, segPts in enumerate(segPtPairs):
if(optimized and isStraightSeg(segPts)):
verts.append(bm.verts.new(segPts[0][1]))
verts.append(bm.verts.new(segPts[1][1]))
else:
pts = getInterpBezierPts(segPts, subdivPerUnit = 100, \
segLens = [segLens[i]])
numSegs = ceil(remeshRes * segLens[i] / maxLen)
vertCos = getInterpolatedVertsCo(pts, numSegs)
for j, co in enumerate(vertCos):
if(j == 0 and i > 0): continue
verts.append(bm.verts.new(co))
else:
for ptIdx, bpt in enumerate(bpts):
verts.append(bm.verts.new(bpts[ptIdx].co))
if(addLastVert):
verts.append(bm.verts.new(bpts[0].co))
if(len(verts) < 2):
pass
elif(len(verts) == 2):
bm.edges.new(verts)
else:
if(spline.use_cyclic_u):
bm.verts.remove(verts[-1])
verts.pop()
vertCos = [v.co for v in verts]
center = Vector([sum(vertCos[i][j] for i in range(len(vertCos))) \
for j in range(3)]) / len(vertCos)
normal = geometry.normal(vertCos)
centers.append(center)
normals.append(normal)
if(fillType == 'NGON'):
bm.faces.new(verts)
elif(fillType == 'NONE'):
for i in range(1, len(verts)):
bm.edges.new([verts[i-1], verts[i]])
if(spline.use_cyclic_u): bm.edges.new([verts[-1], verts[0]])
elif(fillType == 'FAN'):
centerVert = bm.verts.new(center)
for i in range(1, len(verts)):
bm.faces.new([centerVert, verts[i-1], verts[i]])
if(spline.use_cyclic_u): bm.faces.new([centerVert, verts[-1], verts[0]])
cnt = len(centers)
if(cnt > 0):
center = Vector([sum(centers[i][j] for i in range(cnt)) \
for j in range(3)]) / cnt
normal = Vector([sum(normals[i][j] for i in range(cnt)) \
for j in range(3)]) / cnt
else:
center = None
normal = None
m = bpy.data.meshes.new(curve.data.name)
bm.to_mesh(m)
meshObj = bpy.data.objects.new(curve.name, m)
collections = curve.users_collection
for c in collections:
c.objects.link(meshObj)
return meshObj, center, normal
def convertToMesh(curve):
mt = curve.to_mesh()#Can't be used directly
bm = bmesh.new()
bm.from_mesh(mt)
m = bpy.data.meshes.new(curve.data.name)
bm.to_mesh(m)
meshObj = bpy.data.objects.new(curve.name, m)
collections = curve.users_collection
for c in collections:
c.objects.link(meshObj)
return meshObj
def applyMeshModifiers(meshObj, remeshDepth):
bpy.context.view_layer.objects.active = meshObj
normal = geometry.normal([v.co for v in meshObj.data.vertices])
normal = Vector([round(c, 5) for c in normal])
if(vectCmpWithMargin(normal, Vector())):
return meshObj
planeVert = Vector([round(c, 5) for c in meshObj.data.vertices[0].co])
mod = meshObj.modifiers.new('mod', type='SOLIDIFY')
mod.thickness = 20
bpy.ops.object.modifier_apply(modifier = mod.name)
mod = meshObj.modifiers.new('mod', type='REMESH')
mod.octree_depth = remeshDepth
mod.use_remove_disconnected = False
bpy.ops.object.modifier_apply(modifier = mod.name)
bm = bmesh.new()
bm.from_mesh(meshObj.data)
bm.verts.ensure_lookup_table()
toRemove = []
for i, v in enumerate(bm.verts):
co = Vector([round(c, 5) for c in v.co])
if (abs(geometry.distance_point_to_plane(co, planeVert, normal)) > DEF_ERR_MARGIN):
toRemove.append(v)
for v in toRemove:
bm.verts.remove(v)
bm.to_mesh(meshObj.data)
def unsubdivideObj(meshObj):
bm = bmesh.new()
bm.from_object(meshObj, bpy.context.evaluated_depsgraph_get())
bmesh.ops.unsubdivide(bm, verts = bm.verts)
bm.to_mesh(meshObj.data)
def pasteLength(src, dests):
tmp = bpy.data.curves.new('t', 'CURVE')
ts = tmp.splines.new('BEZIER')
ts.bezier_points.add(1)
mw = src.matrix_world
srcLen = sum(getSplineLenTmpObj(ts, s, mw) for s in src.data.splines)
for c in dests:
mw = c.matrix_world
destLen = sum(getSplineLenTmpObj(ts, s, mw) for s in c.data.splines)
fact = (srcLen / destLen)
for s in c.data.splines:
lts = []
rts = []
for pt in s.bezier_points:
lts.append(pt.handle_left_type)
rts.append(pt.handle_right_type)
pt.handle_left_type = 'FREE'
pt.handle_right_type = 'FREE'
for pt in s.bezier_points:
pt.co = fact * pt.co
pt.handle_left = fact * pt.handle_left
pt.handle_right = fact * pt.handle_right
for i, pt in enumerate(s.bezier_points):
pt.handle_left_type = lts[i]
pt.handle_right_type = rts[i]
bpy.data.curves.remove(tmp)
def intersectCurves(curves, action, firstActive, margin, rounding):
allIntersectCos, intersectMap = getCurveIntersectPts(curves, firstActive, margin, \
rounding)
if(action in {'MARK_EMPTY', 'MARK_POINT'}):
newObjs = []
collection = bpy.data.collections.new('Intersect Markers')
bpy.context.scene.collection.children.link(collection)
objName = 'Marker'
for co in allIntersectCos:
if(action == 'MARK_EMPTY'):
obj = bpy.data.objects.new(objName, None)
elif(action == 'MARK_POINT'):
curveData = bpy.data.curves.new(objName, 'CURVE')
curveData.splines.new('BEZIER')
obj = bpy.data.objects.new(objName, curveData)
obj.location = co
newObjs.append(obj)
collection.objects.link(obj)
for obj in newObjs:
obj.select_set(True)
if(action in {'INSERT_PT', 'CUT'}):
mapKeys = sorted(intersectMap.keys(), key = lambda x:(x[0], x[1], x[2]))
selPtMap = {}
prevCnt = 0
prevCurveIdx = None
prevSplineIdx = None
for i, key in enumerate(mapKeys):
intersectPts = intersectMap[key]
curveIdx, splineIdx, segIdx = key
if(prevCurveIdx == curveIdx and prevSplineIdx == splineIdx):
segIdx += prevCnt
else:
prevCnt = 0
curve = curves[curveIdx]
if(firstActive and curve == curves[0]):
continue
mw = curve.matrix_world
pts = curve.data.splines[splineIdx].bezier_points
nextIdx = getAdjIdx(curve, splineIdx, segIdx)
seg = [mw @ pts[segIdx].co, mw @ pts[segIdx].handle_right, \
mw @ pts[nextIdx].handle_left, mw @ pts[nextIdx].co]
sortedCos = getCosSortedByT(seg, intersectPts, margin)
sortedCos = removeDupliCos(sortedCos, margin)
insertCos = sortedCos.copy()
start = seg[0].freeze()
end = seg[1].freeze()
startIdxIncr = 1
endIdxIncr = 1
if(start in insertCos):
startIdxIncr = 0 # Keep in split list
insertCos.remove(start) # Remove from insert list
if(end in insertCos):
insertCos.remove(end) # Remove from insert list
endIdxIncr = 2 # Keep in split list
for j, co in enumerate(insertCos):
insertCos[j] = mw.inverted_safe() @ co
insertBezierPts(curve, splineIdx, segIdx, insertCos, 'FREE', margin)
prevCurveIdx = curveIdx
prevSplineIdx = splineIdx
prevCnt += len(insertCos)
if(action == 'CUT'):
if(selPtMap.get(curve) == None):
selPtMap[curve] = {}
if(selPtMap[curve].get(splineIdx) == None):
selPtMap[curve][splineIdx] = []
selPtMap[curve][splineIdx] += \
list(range(segIdx + startIdxIncr, \
segIdx + len(sortedCos) + endIdxIncr))
if(action == 'CUT'):
for curve in list(selPtMap.keys()):
splineIdxs = sorted(selPtMap[curve].keys())
if(len(curve.data.splines) > 1):
newObjs, changeCnt = splitCurve([curve], 'spline', \
curve.users_collection)
splineIdxs = list(selPtMap[curve].keys())
for idx in splineIdxs:
newCurve = newObjs[idx]
selPtMap[newCurve] = {}
selPtMap[newCurve][0] = selPtMap[curve][idx]
selPtMap[curve].pop(idx)
if(len(selPtMap[curve]) == 0):
selPtMap.pop(curve)
splitCurveSelPts(selPtMap)
def getSVGPt(co, docW, docH, camera = None, region = None, rv3d = None):
if(camera != None):
scene = bpy.context.scene
xy = world_to_camera_view(scene, camera, co)
return complex(xy[0] * docW, docH - (xy[1] * docH))
elif(region != None and rv3d != None):
xy = getCoordFromLoc(region, rv3d, co)
return complex(xy[0], docH - xy[1])
def getPathD(path):
curve = ''
for i, part in enumerate(path):
comps = []
for j, segment in enumerate(part):
if(j == 0):
comps.append('M {},{} C'.format(segment[0].real, segment[0].imag))
args = (segment[1].real, segment[1].imag,
segment[2].real, segment[2].imag,
segment[3].real, segment[3].imag)
comps.append('{},{} {},{} {},{}'.format(*args))
curve += ' ' .join(comps)
return curve
def getPathBBox(path):
minX, minY, maxX, maxY = [None, None, None, None]
for part in path:
for seg in part:
seg3d = [(seg[i].real, seg[i].imag, 0) for i in range(len(seg))]
leftBotFront, rgtTopBack = getBBox(seg3d)
if(minX == None or leftBotFront[0] < minX):
minX = leftBotFront[0]
if(minY == None or leftBotFront[1] < minY):
minY = leftBotFront[1]
if(maxX == None or rgtTopBack[0] > maxX):
maxX = rgtTopBack[0]
if(maxY == None or rgtTopBack[1] > maxY):
maxY = rgtTopBack[1]
return minX, minY, maxX, maxY
def createClipElem(doc, svgElem, docW, docH, clipElemId):
elem = doc.createElement('defs')
svgElem.appendChild(elem)
clipElem = doc.createElement('clipPath')
clipElem.setAttribute('clipPathUnits', 'userSpaceOnUse')
clipElem.setAttribute('id', clipElemId)
elem.appendChild(clipElem)
rectElem = doc.createElement('rect')
rectElem.setAttribute('x', '0')
rectElem.setAttribute('y', '0')
rectElem.setAttribute('width', str(docW))
rectElem.setAttribute('height', str(docH))
clipElem.appendChild(rectElem)
def getSVGPathElem(doc, docW, docH, path, idx, lineWidth, lineCol, lineAlpha, \
fillCol, fillAlpha, clipView, clipElemId):
idPrefix = 'id'
style= {'opacity':'1', 'stroke':'#000000', 'stroke-width':'1', \
'fill':'none', 'stroke-linecap':'round', 'stroke-linejoin':'miter', \
'stroke-miterlimit':'4'}
clipped = False
if(clipView):
minX, minY, maxX, maxY = getPathBBox(path)
if(maxX < 0 or maxY < 0 or minX > docW or minY > docH):
return None
if(minX < 0 or minY < 0 or maxX > docW or maxY > docH):
clipped = True
elem = doc.createElement('path')
elem.setAttribute('id', idPrefix + str(idx).zfill(3))
elem.setAttribute('d', getPathD(path))
style['stroke-width'] = str(lineWidth)
style['stroke'] = '#' + lineCol
style['opacity'] = lineAlpha
if(fillCol != None):
style['fill'] = '#' + fillCol
style['opacity'] = fillAlpha # Overwrite
styleStr = ';'.join([k + ':' + style[k] for k in style])
elem.setAttribute('style', styleStr)
if(clipped):
elem.setAttribute('clip-path', 'url(#' + clipElemId + ')')
return elem
def exportSVG(context, filepath, exportView, clipView, lineWidth, lineColorOpts, \
lineColor, fillColorOpts, fillColor):
svgXML = ''
clipElemId = 'BBoxClipElem'
if(lineColorOpts == 'PICK'):
lineCol, lineAlpha = toHexStr(lineColor)
if(fillColorOpts == 'PICK'):
fillCol, fillAlpha = toHexStr(fillColor)
if exportView == 'ACTIVE_VIEW':
area = context.area
if(area.type != 'VIEW_3D'):
area = [a for a in bpy.context.screen.areas if a.type == 'VIEW_3D'][0]
region = [r for r in area.regions if r.type == 'WINDOW'][0]
space3d = area.spaces[0]
if(len(space3d.region_quadviews) > 0):
rv3d = space3d.region_quadviews[3]
else:
rv3d = space3d.region_3d
camera = None
docW = region.width
docH = region.height
else:
region = None
rv3d = None
camera = bpy.data.objects[exportView]
docW = bpy.context.scene.render.resolution_x
docH = bpy.context.scene.render.resolution_y
doc = minidom.parseString(svgXML)
svgElem = doc.documentElement
svgElem.setAttribute('width', str(docW))
svgElem.setAttribute('height', str(docH))
if(clipView):
createClipElem(doc, svgElem, docW, docH, clipElemId)
idx = 0
for o in bpy.context.scene.objects:
mw = o.matrix_world
if(isBezier(o) and o.visible_get()):
path = []
filledPath = []
for spline in o.data.splines:
part = []
bpts = spline.bezier_points
for i in range(1, len(bpts)):
prevBezierPt = bpts[i-1]
pt = bpts[i]
seg = [prevBezierPt.co, prevBezierPt.handle_right, pt.handle_left, pt.co]
part.append([getSVGPt(mw @ co, docW, docH, camera, region, rv3d) for co in seg])
if(spline.use_cyclic_u):
seg = [bpts[-1].co, bpts[-1].handle_right, bpts[0].handle_left, bpts[0].co]
part.append([getSVGPt(mw @ co, docW, docH, camera, region, rv3d) for co in seg])
if(len(part) > 0):
if (spline.use_cyclic_u and o.data.dimensions == '2D' \
and o.data.fill_mode != 'NONE'):
filledPath.append(part)
else:
path.append(part)
for p in [path, filledPath]:
if(len(p) == 0): continue
if(lineColorOpts == 'RANDOM'):
lineColor = [random.random() for i in range(3)] + [1]
lineCol, lineAlpha = toHexStr(lineColor)
if(p == path):
fc, fa = None, None
elif(fillColorOpts == 'RANDOM'):
fillColor = [random.random() for i in range(3)] + [1]
fc, fa = toHexStr(fillColor)
else:
fc, fa = fillCol, fillAlpha
svgPathElem = getSVGPathElem(doc, docW, docH, p, idx, lineWidth, \
lineCol, lineAlpha, fc, fa, clipView, clipElemId)
if(svgPathElem != None):
svgElem.appendChild(svgPathElem)
idx += 1
doc.writexml(open(filepath,"w"))
###################### Operators ######################
class SeparateSplinesObjsOp(Operator):
bl_idname = "object.separate_splines"
bl_label = "Separate Splines"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Separate splines of selected Bezier curves as new objects"
def execute(self, context):
selObjs = bpy.context.selected_objects
newObjs, changeCnt = splitCurve(selObjs, split = 'spline')
if(changeCnt > 0):
self.report({'INFO'}, "Separated "+ str(changeCnt) + " curve object" + \
("s" if(changeCnt > 1) else "") + \
" into " +str(len(newObjs)) + " new ones")
return {'FINISHED'}
class SplitBezierObjsOp(Operator):
bl_idname = "object.separate_segments"
bl_label = "Separate Segments"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Separate segments of selected Bezier curves as new objects"
def execute(self, context):
selObjs = [o for o in bpy.context.selected_objects if(isBezier(o))]
if(context.mode == 'EDIT_CURVE'):
selPtMap = {}
for o in selObjs:
selPtMap[o] = {}
for i, s in enumerate(o.data.splines):
pts = s.bezier_points
ptIdxs = [x for x in range(0, len(pts)) if pts[x].select_control_point]
if(len(ptIdxs) > 0): selPtMap[o][i] = ptIdxs
newObjs, changeCnt = splitCurveSelPts(selPtMap)
else:
newObjs, changeCnt = splitCurve(selObjs, split = 'seg')
if(changeCnt > 0):
bpy.context.view_layer.objects.active = newObjs[-1]
self.report({'INFO'}, "Split "+ str(changeCnt) + " curve object" + \
("s" if(changeCnt > 1) else "") + \
" into " + str(len(newObjs)) + " new objects")
return {'FINISHED'}
class splitBezierObjsPtsOp(Operator):
bl_idname = "object.separate_points"
bl_label = "Separate Points"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Separate bezier points of selected curves as new objects"
def execute(self, context):
selObjs = bpy.context.selected_objects
newObjs, changeCnt = splitCurve(selObjs, split = 'point')
if(changeCnt > 0):
bpy.context.view_layer.objects.active = newObjs[-1]
self.report({'INFO'}, "Split "+ str(changeCnt) + " curve object" + \
("s" if(changeCnt > 1) else "") + \
" into " + str(len(newObjs)) + " new objects")
return {'FINISHED'}
class JoinBezierSegsOp(Operator):
bl_idname = "object.join_curves"
bl_label = "Join"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Join selected curves (with new segments if required)"
def execute(self, context):
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o)]
straight = bpy.context.window_manager.bezierToolkitParams.straight
optimized = bpy.context.window_manager.bezierToolkitParams.optimized
mergeDist = bpy.context.window_manager.bezierToolkitParams.joinMergeDist
newCurve = joinSegs(curves, optimized = optimized, straight = straight, \
margin = mergeDist)
# ~ removeShapeKeys(newCurve)
bpy.context.view_layer.objects.active = newCurve
return {'FINISHED'}
class InvertSelOp(Operator):
bl_idname = "object.invert_sel_in_collection"
bl_label = "Invert Selection"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Invert selection within collection of active object"
def execute(self, context):
if(bpy.context.active_object != None):
collections = bpy.context.active_object.users_collection
for collection in collections:
for o in collection.objects:
o.select_set(not o.select_get())
return {'FINISHED'}
class SelectInCollOp(Operator):
bl_idname = "object.select_in_collection"
bl_label = "Select"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Select objects within collection of active object"
def execute(self, context):
if(bpy.context.active_object != None):
selectIntrvl = bpy.context.window_manager.bezierToolkitParams.selectIntrvl
for obj in bpy.context.selected_objects:
collections = obj.users_collection
for collection in collections:
objs = [o for o in collection.objects]
idx = objs.index(obj)
for i, o in enumerate(objs[idx:]):
if( i % (selectIntrvl + 1) == 0): o.select_set(True)
else: o.select_set(False)
for i, o in enumerate(reversed(objs[:idx+1])):
if( i % (selectIntrvl + 1) == 0): o.select_set(True)
else: o.select_set(False)
return {'FINISHED'}
class CloseStraightOp(Operator):
bl_idname = "object.close_straight"
bl_label = "Close Splines With Straight Segment"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Close all splines in selected curves with straight segmennt"
def execute(self, context):
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o)]
for curve in curves:
for spline in curve.data.splines:
spline.bezier_points[0].handle_right_type = 'FREE'
spline.bezier_points[0].handle_left_type = 'VECTOR'
spline.bezier_points[-1].handle_left_type = 'FREE'
spline.bezier_points[-1].handle_right_type = 'VECTOR'
spline.use_cyclic_u = True
return {'FINISHED'}
class CloseSplinesOp(Operator):
bl_idname = "object.close_splines"
bl_label = "Close Splines"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Close all splines in selected curves"
def execute(self, context):
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o)]
for curve in curves:
for spline in curve.data.splines:
# ~ spline.bezier_points[0].handle_left_type = 'ALIGNED'
# ~ spline.bezier_points[-1].handle_right_type = 'ALIGNED'
spline.use_cyclic_u = True
return {'FINISHED'}
class OpenSplinesOp(Operator):
bl_idname = "object.open_splines"
bl_label = "Open up Splines"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Open up all splines in selected curves"
def execute(self, context):
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o)]
for curve in curves:
for spline in curve.data.splines:
spline.use_cyclic_u = False
return {'FINISHED'}
class SetHandleTypesOp(Operator):
bl_idname = "object.set_handle_types"
bl_label = "Set Handle Type of All Points"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Set the handle type of all the points of the selected curves"
def execute(self, context):
ht = bpy.context.window_manager.bezierToolkitParams.handleType
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o)]
for curve in curves:
for spline in curve.data.splines:
for pt in spline.bezier_points:
pt.handle_left_type = ht
pt.handle_right_type = ht
return {'FINISHED'}
class RemoveDupliVertCurveOp(Operator):
bl_idname = "object.remove_dupli_vert_curve"
bl_label = "Remove Duplicate Curve Vertices"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Remove duplicate vertices and mark splines as cyclic if applicable"
def execute(self, context):
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o)]
for curve in curves:
removeDupliVert(curve, \
bpy.context.window_manager.bezierToolkitParams.dupliVertMargin)
return {'FINISHED'}
class convertToMeshOp(Operator):
bl_idname = "object.convert_2d_mesh"
bl_label = "Convert"
bl_description = "Convert 2D curve to mesh with quad faces"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o)]
params = bpy.context.window_manager.bezierToolkitParams
remeshDepth = params.remeshDepth
unsubdivide = params.unsubdivide
fillType = params.fillType
optimized = params.remeshOptimized
remeshRes = params.remeshRes
perSeg = (params.remeshApplyTo == 'PERSEG')
for curve in curves:
center, normal = None, None
if(fillType == 'QUAD'):
for spline in curve.data.splines:
spline.use_cyclic_u = True
curve.data.dimensions = '2D'
curve.data.fill_mode = 'BOTH'
meshObj = convertToMesh(curve)
applyMeshModifiers(meshObj, remeshDepth)
if(unsubdivide):
unsubdivideObj(meshObj)
else:
meshObj, center, normal = \
convertToFace(curve, remeshRes, perSeg, fillType, optimized)
meshObj.matrix_world = curve.matrix_world.copy()
meshObj.select_set(True)
if(center != None and normal != None):
newOrig = meshObj.matrix_world @ center
shiftOrigin(meshObj, newOrig)
quatMat = normal.to_track_quat('Z', 'X').to_matrix().to_4x4()
tm = meshObj.matrix_world @ quatMat
shiftMatrixWorld(meshObj, tm)
meshObj.location = newOrig # Location not from tm
safeRemoveObj(curve)
return {'FINISHED'}
class AlignToFaceOp(Operator):
bl_idname = "object.align_to_face"
bl_label = "Align to Face"
bl_description = "Align all points of selected curves with " + \
"nearest face of selected meshes"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
alignLoc = bpy.context.window_manager.bezierToolkitParams.alignToFaceLoc
alignOrig = bpy.context.window_manager.bezierToolkitParams.alignToFaceOrig
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o) and o.visible_get()]
if(len(curves) == 0): return {'FINISHED'}
mesheObjs = [o for o in bpy.data.objects if o in bpy.context.selected_objects \
and o.type == 'MESH' and o.visible_get()]
if(len(mesheObjs) == 0): return {'FINISHED'}
depsgraph = bpy.context.evaluated_depsgraph_get()
medianLists = []
for o in mesheObjs:
medianLists.append([o.matrix_world @ f.center for f in o.data.polygons])
searchTree = NestedListSearch(medianLists)
for curve in curves:
center = getObjBBoxCenter(curve)
oLoc = curve.location.copy()
srs = searchTree.findInLists(center, searchRange = None)
if(len(srs) != 1): continue
objIdx, faceIdx, median, dist = srs[0]
meshObj = mesheObjs[objIdx]
faceCenter = meshObj.matrix_world @ meshObj.data.polygons[faceIdx].center
normal = meshObj.data.polygons[faceIdx].normal
quatMat = normal.to_track_quat('Z', 'X').to_matrix().to_4x4()
tm = meshObj.matrix_world @ quatMat
shiftMatrixWorld(curve, tm)
invTm = tm.inverted_safe()
centerLocal = invTm @ center
for spline in curve.data.splines:
for bpt in spline.bezier_points:
lht = bpt.handle_left_type
rht = bpt.handle_right_type
bpt.handle_left_type = 'FREE'
bpt.handle_right_type = 'FREE'
bpt.co[2] = centerLocal[2]
bpt.handle_right[2] = centerLocal[2]
bpt.handle_left[2] = centerLocal[2]
bpt.handle_left_type = lht
bpt.handle_right_type = rht
if(alignOrig == 'FACE'):
shiftOrigin(curve, faceCenter)
elif(alignOrig == 'BBOX'):
depsgraph.update()
center = getObjBBoxCenter(curve) # Recalculate after alignment
shiftOrigin(curve, center)
else:
shiftOrigin(curve, oLoc)
if(alignLoc): curve.location = faceCenter
return {'FINISHED'}
class SetCurveColorOp(bpy.types.Operator):
bl_idname = "object.set_curve_color"
bl_label = "Set Color"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Set color of selected curves"
def execute(self, context):
curves = bpy.context.selected_objects
for curve in curves:
curve.data['curveColor'] = \
bpy.context.window_manager.bezierToolkitParams.curveColorPick
return {'FINISHED'}
class RemoveCurveColorOp(bpy.types.Operator):
bl_idname = "object.remove_curve_color"
bl_label = "Remove Color"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Remove color of selected curves"
def execute(self, context):
curves = bpy.context.selected_objects
for curve in curves:
if(curve.data.get('curveColor')):
del curve.data['curveColor']
return {'FINISHED'}
class PasteLengthOp(Operator):
bl_idname = "object.paste_length"
bl_label = "Paste Length"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Make the selected curves the same length as the active one"
def execute(self, context):
src = bpy.context.object
if(src != None and isBezier(src)):
dests = [o for o in bpy.context.selected_objects if(isBezier(o) and o != src)]
if(len(dests) > 0):
pasteLength(src, dests)
return {'FINISHED'}
class IntersectCurvesOp(Operator):
bl_idname = "object.intersect_curves"
bl_label = "Intersect Curves"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Intersect the selected curves"
def execute(self, context):
params = bpy.context.window_manager.bezierToolkitParams
rounding = 2
actCurve = bpy.context.active_object
if(params.intersectNonactive):
if(not isBezier(actCurve)):
self.report({'WARNING'}, "Active Object Not A Curve")
return {'FINISHED'}
curves = [o for o in bpy.data.objects \
if o in bpy.context.selected_objects and isBezier(o) and o != actCurve]
if(actCurve != None and isBezier(actCurve)):
curves = [actCurve] + curves
if(len(curves) < 2):
self.report({'INFO'}, "Please select at least two Bezier curve objects")
else:
intersectCurves(curves, params.intersectOp, params.intersectNonactive, \
params.intersectMargin, rounding)
return {'FINISHED'}
class ExportSVGOp(Operator):
bl_idname = "object.export_svg"
bl_label = "Export to SVG"
bl_options = {'REGISTER', 'UNDO'}
def getExportViewList(scene = None, context = None):
cameras = [o for o in bpy.data.objects if o.type == 'CAMERA']
vlist = [('ACTIVE_VIEW', 'Viewport View', "Export Viewport View")]
for c in cameras:
vlist.append((c.name, c.name, 'Export view from ' + c.name))
return vlist
filepath : StringProperty(subtype='FILE_PATH')
#User input
clipView : BoolProperty(name="Clip View", \
description = "Clip objects to view boundary", \
default = True)
exportView: EnumProperty(name = 'Export View',
items = getExportViewList,
description='View to export')
lineWidth: FloatProperty(name="Line Width", \
description='Line width in exported SVG', default = 3, min = 0)
lineColorOpts: EnumProperty(name = 'Line Color',
items = (('RANDOM', 'Random', 'Use random color for curves'),
('PICK', 'Pick', 'Pick color'),
),
description='Color to draw curve lines')
lineColor: bpy.props.FloatVectorProperty(
name="Line Color",
subtype="COLOR",
size=4,
min=0.0,
max=1.0,
default=(0.5, 0.5, 0.5, 1.0)
)
fillColorOpts: EnumProperty(name = 'Fill Color',
items = (('RANDOM', 'Random', 'Use random fill color'),
('PICK', 'Pick', 'Pick color'),
),
description='Color to fill solid curves')
fillColor: bpy.props.FloatVectorProperty(
name="Fill Color",
subtype="COLOR",
size=4,
min=0.0,
max=1.0,
default=(0, 0.3, 0.5, 1.0)
)
def execute(self, context):
exportSVG(context, self.filepath, self.exportView, self.clipView, self.lineWidth, \
self.lineColorOpts, self.lineColor, self.fillColorOpts, self.fillColor)
return {'FINISHED'}
def draw(self, context):
layout = self.layout
col = layout.column()
row = col.row()
row.prop(self, "exportView")
col = layout.column()
row = col.row()
row.prop(self, "clipView")
col = layout.column()
row = col.row()
row.prop(self, "lineWidth")
col = layout.column()
row = col.row()
row.prop(self, "lineColorOpts")
if(self.lineColorOpts == 'PICK'):
row = row.split()
row.prop(self, "lineColor", text = '')
col = layout.column()
row = col.row()
row.prop(self, "fillColorOpts")
if(self.fillColorOpts == 'PICK'):
row = row.split()
row.prop(self, "fillColor", text = '')
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def markVertHandler(self, context):
if(self.markVertex):
bpy.ops.wm.bb_mark_vertex()
class MarkerController:
drawHandlerRef = None
defPointSize = 6
ptColor = (0, .8, .8, 1)
def createSMMap(self, context):
objs = context.selected_objects
smMap = {}
for curve in objs:
if(not isBezier(curve)):
continue
smMap[curve.name] = {}
mw = curve.matrix_world
for splineIdx, spline in enumerate(curve.data.splines):
if(not spline.use_cyclic_u):
continue
#initialize to the curr start vert co and idx
smMap[curve.name][splineIdx] = \
[mw @ curve.data.splines[splineIdx].bezier_points[0].co, 0]
for pt in spline.bezier_points:
pt.select_control_point = False
if(len(smMap[curve.name]) == 0):
del smMap[curve.name]
return smMap
def createBatch(self, context):
positions = [s[0] for cn in self.smMap.values() for s in cn.values()]
colors = [MarkerController.ptColor for i in range(0, len(positions))]
self.batch = batch_for_shader(self.shader, \
"POINTS", {"pos": positions, "color": colors})
if context.area:
context.area.tag_redraw()
def drawHandler(self):
# bgl.glPointSize(MarkerController.defPointSize)
gpu.state.point_size_set(MarkerController.defPointSize)
self.batch.draw(self.shader)
def removeMarkers(self, context):
if(MarkerController.drawHandlerRef != None):
bpy.types.SpaceView3D.draw_handler_remove(MarkerController.drawHandlerRef, \
"WINDOW")
if(context.area and hasattr(context.space_data, 'region_3d')):
context.area.tag_redraw()
MarkerController.drawHandlerRef = None
self.deselectAll()
def __init__(self, context):
self.smMap = self.createSMMap(context)
self.shader = gpu.shader.from_builtin('FLAT_COLOR')
# ~ self.shader.bind()
try:
MarkerController.defPointSize = \
context.preferences.addons[__name__].preferences.markerSize
except Exception as e:
# ~ print("BezierUtils: Fetching marker size", e)
MarkerController.defPointSize = 6
MarkerController.drawHandlerRef = \
bpy.types.SpaceView3D.draw_handler_add(self.drawHandler, \
(), "WINDOW", "POST_VIEW")
self.createBatch(context)
def saveStartVerts(self):
for curveName in self.smMap.keys():
curve = bpy.data.objects[curveName]
spMap = self.smMap[curveName]
for splineIdx in spMap.keys():
markerInfo = spMap[splineIdx]
if(markerInfo[1] != 0):
loc, idx = markerInfo[0], markerInfo[1]
moveSplineStart(curve, splineIdx, idx)
def updateSMMap(self):
for curveName in self.smMap.keys():
curve = bpy.data.objects[curveName]
spMap = self.smMap[curveName]
mw = curve.matrix_world
for splineIdx in spMap.keys():
markerInfo = spMap[splineIdx]
loc, idx = markerInfo[0], markerInfo[1]
pts = curve.data.splines[splineIdx].bezier_points
selIdxs = [x for x in range(0, len(pts)) \
if pts[x].select_control_point == True]
selIdx = selIdxs[0] if(len(selIdxs) > 0 ) else idx
co = mw @ pts[selIdx].co
self.smMap[curveName][splineIdx] = [co, selIdx]
def deselectAll(self):
for curveName in self.smMap.keys():
curve = bpy.data.objects[curveName]
for spline in curve.data.splines:
for pt in spline.bezier_points:
pt.select_control_point = False
def getSpaces3D(context):
areas3d = [area for area in context.window.screen.areas \
if area.type == 'VIEW_3D']
return [s for a in areas3d for s in a.spaces if s.type == 'VIEW_3D']
def hideHandles(context):
states = []
spaces = MarkerController.getSpaces3D(context)
for s in spaces:
if(hasattr(s.overlay, 'show_curve_handles')):
states.append(s.overlay.show_curve_handles)
s.overlay.show_curve_handles = False
elif(hasattr(s.overlay, 'display_handle')): # 2.90
states.append(s.overlay.display_handle)
s.overlay.display_handle = 'NONE'
return states
def resetShowHandleState(context, handleStates):
spaces = MarkerController.getSpaces3D(context)
for i, s in enumerate(spaces):
if(hasattr(s.overlay, 'show_curve_handles')):
s.overlay.show_curve_handles = handleStates[i]
elif(hasattr(s.overlay, 'display_handle')): # 2.90
s.overlay.display_handle = handleStates[i]
class ModalMarkSegStartOp(bpy.types.Operator):
bl_description = "Mark Vertex"
bl_idname = "wm.bb_mark_vertex"
bl_label = "Mark Start Vertex"
def cleanup(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
self.markerState.removeMarkers(context)
MarkerController.resetShowHandleState(context, self.handleStates)
bpy.context.window_manager.bezierToolkitParams.markVertex = False
def modal (self, context, event):
if(context.mode == 'OBJECT' or event.type == "ESC" or \
not bpy.context.window_manager.bezierToolkitParams.markVertex):
self.cleanup(context)
return {'CANCELLED'}
elif(event.type == "RET"):
self.markerState.saveStartVerts()
self.cleanup(context)
return {'FINISHED'}
if(event.type == 'TIMER'):
self.markerState.updateSMMap()
self.markerState.createBatch(context)
return {"PASS_THROUGH"}
def execute(self, context):
#TODO: Why such small step?
self._timer = context.window_manager.event_timer_add(time_step = 0.0001, \
window = context.window)
context.window_manager.modal_handler_add(self)
self.markerState = MarkerController(context)
#Hide so that users don't accidentally select handles instead of points
self.handleStates = MarkerController.hideHandles(context)
return {"RUNNING_MODAL"}
###################### Single Panel for All Ops ######################
class BezierUtilsPanel(Panel):
bl_label = "Bezier Utilities"
bl_idname = "CURVE_PT_bezierutils"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Tool'
@classmethod
def poll(cls, context):
return context.mode in {'OBJECT', 'EDIT_CURVE'}
def draw(self, context):
params = bpy.context.window_manager.bezierToolkitParams
layout = self.layout
layout.use_property_decorate = False
if(context.mode == 'OBJECT'):
row = layout.row()
row.prop(params, "intersectExpanded",
icon="TRIA_DOWN" if params.intersectExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Intersect Curves', icon='GRAPH')
if params.intersectExpanded:
box = layout.box()
col = box.column().split()
row = col.row()
row.prop(params, 'intersectMargin', text = 'Proximity')
col = box.column().split()
row = col.row()
row.prop(params, 'intersectOp', text = 'Action')
if(params.intersectOp in {'INSERT_PT', 'CUT'}):
row = col.row()
row.prop(params, 'intersectNonactive', text = 'Only Non-active')
col = box.column().split()
col.operator('object.intersect_curves')
row = layout.row()
row.prop(params, "splitExpanded",
icon="TRIA_DOWN" if params.splitExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False)
row.label(text="Split Curves", icon = 'UNLINKED')
if params.splitExpanded:
box = layout.box()
col = box.column().split()
col.operator('object.separate_splines')
col = box.column().split()
col.operator('object.separate_segments')
col = box.column().split()
col.operator('object.separate_points')
row = layout.row()
row.prop(params, "joinExpanded",
icon="TRIA_DOWN" if params.joinExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text="Join Curves", icon = 'LINKED')
if params.joinExpanded:
box = layout.box()
col = box.column().split()
col.prop(params, 'straight')
col = box.column().split()
col.prop(params, 'optimized')
col = box.column().split()
col.prop(params, 'joinMergeDist')
col = box.column().split()
col.operator('object.join_curves')
row = layout.row()
row.prop(params, "alignToFaceExpanded",
icon="TRIA_DOWN" if params.alignToFaceExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text="Align to Face", icon = 'FCURVE')
if params.alignToFaceExpanded:
box = layout.box()
col = box.column().split()
col.prop(params, 'alignToFaceOrig')
col = box.column().split()
col.prop(params, 'alignToFaceLoc')
col = box.column().split()
col.operator('object.align_to_face')
row = layout.row()
row.prop(params, "selectExpanded",
icon="TRIA_DOWN" if params.selectExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Select Objects In Collection', icon='RESTRICT_SELECT_OFF')
if params.selectExpanded:
box = layout.box()
col = box.column().split()
row = col.row()
row.prop(params, 'selectIntrvl')
row.operator('object.select_in_collection')
col = box.column().split()
col.operator('object.invert_sel_in_collection')
row = layout.row()
row.prop(params, "convertExpanded",
icon="TRIA_DOWN" if params.convertExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Convert Curve to Mesh', icon='MESH_DATA')
if params.convertExpanded:
box = layout.box()
col = box.column().split()
col.prop(params, 'fillType')
if(params.fillType == 'QUAD'):
col = box.column().split()
row = col.row()
row.prop(params, 'remeshDepth')
row.prop(params, 'unsubdivide')
else:
col = box.column().split()
row = col.row()
row.prop(params, 'remeshRes')
row.prop(params, 'remeshApplyTo')
if(params.remeshApplyTo == 'PERSEG'):
col = box.column().split()
row = col.row()
row.prop(params, 'remeshOptimized')
col = box.column().split()
col.operator('object.convert_2d_mesh')
row = layout.row()
row.prop(params, "handleTypesExpanded",
icon="TRIA_DOWN" if params.handleTypesExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Set Handle Type', icon='MOD_CURVE')
if params.handleTypesExpanded:
box = layout.box()
col = box.column().split()
row = col.row()
col = row.column()
col.prop(params, 'handleType')
col = box.column().split()
col.operator('object.set_handle_types')
row = layout.row()
row.prop(params, "removeDupliExpanded",
icon="TRIA_DOWN" if params.removeDupliExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Remove Duplicate Vertices', icon='X')
if params.removeDupliExpanded:
box = layout.box()
col = box.column().split()
row = col.row()
row.prop(params, 'dupliVertMargin', text = 'Proximity')
col = box.column().split()
col.operator('object.remove_dupli_vert_curve')
######## Curve Color #########
row = layout.row()
row.prop(params, "curveColorExpanded",
icon="TRIA_DOWN" if params.curveColorExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Set Curve Colors', icon='MATERIAL')
if params.curveColorExpanded:
box = layout.box()
col = box.column().split()
row = col.row()
row.prop(params, "curveColorPick", text = 'Curve Color')
row.operator('object.set_curve_color')
row.operator('object.remove_curve_color')
col = box.column().split()
row = col.row()
row.prop(params, 'applyCurveColor', toggle = True)
######## Other Tools #########
row = layout.row()
row.prop(params, "otherExpanded",
icon="TRIA_DOWN" if params.otherExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Other Tools', icon='TOOL_SETTINGS')
if params.otherExpanded:
box = layout.box()
col = box.column().split()
col.operator('object.export_svg')
col = box.column().split()
col.operator('object.paste_length')
col = box.column().split()
col.operator('object.close_splines')
col = box.column().split()
col.operator('object.close_straight')
col = box.column().split()
col.operator('object.open_splines')
tool = context.workspace.tools.from_space_view3d_mode('OBJECT', \
create = False)
if(tool.idname == FlexiDrawBezierTool.bl_idname and \
params.drawObjType == 'MATH'):
row = layout.row()
row.prop(params, "mathExtraExpanded",
icon="TRIA_DOWN" if params.mathExtraExpanded else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text='Flexi Draw Math Function', icon='GRAPH')
if params.mathExtraExpanded:
# Equation params are duplicated in the toolbar...
# any changes here should also reflect there
box = layout.box()
col = box.column().split()
col.prop(params, "mathFnList")
col = box.column().split()
col.prop(params, "mathFnName")
col = box.column().split()
col.prop(params, "mathFnDescr")
col = box.column().split()
col.prop(params, "mathFnType")
col = box.column().split()
col.prop(params, "mathFnResolution")
if(params.mathFnType == 'PARAMETRIC'):
col = box.column().split()
col.prop(params, "drawMathFnParametric1")
col = box.column().split()
col.prop(params, "drawMathFnParametric2")
col = box.column().split()
col.prop(params, "drawMathTMapTo")
col = box.column().split()
col.prop(params, "drawMathTScaleFact")
col = box.column().split()
col.prop(params, "drawMathTStart")
else:
col = box.column().split()
col.prop(params, "drawMathFn")
col = box.column().split()
col.prop(params, "mathFnclipVal")
paramCol = box.column()
for i in range(Primitive2DDraw.getParamCnt()):
char = chr(ord('A') + i)
innerBox = paramCol.box()
col = innerBox.column().split()
row = col.row()
row.label(text = char)
row.prop(params, MathFnDraw.startPrefix + str(i), \
text = '') # Value
row.prop(params, MathFnDraw.incrPrefix + str(i), \
text = '') # Step
col = box.column().split()
row = col.row()
row.operator('object.save_math_fn')
row.operator('object.reset_math_fn')
row.operator('object.load_math_fn', text = 'Import')
row.operator('object.delete_math_fn', text = 'Delete')
else:
col = layout.column()
col.operator('object.separate_segments', text = 'Split At Selected Points')
col = layout.column()
col.prop(params, 'markVertex', toggle = True)
################ Stand-alone handler for changing curve colors #################
drawHandlerRef = None
shader = None
lineBatch = None
lineWidth = 1.5
@persistent
def colorCurves(scene = None, add = False, remove = False):
def ccDrawHandler():
if(bpy.context.window_manager.bezierToolkitParams.applyCurveColor):
# bgl.glLineWidth(BezierUtilsPanel.lineWidth)
gpu.state.line_width_set(BezierUtilsPanel.lineWidth)
if(BezierUtilsPanel.lineBatch != None):
BezierUtilsPanel.lineBatch.draw(BezierUtilsPanel.shader)
if(add and BezierUtilsPanel.drawHandlerRef == None):
try:
BezierUtilsPanel.lineWidth = \
bpy.context.preferences.addons[__name__].preferences.lineWidth
except Exception as e:
print("BezierUtils: Error fetching line width in ColorCurves: ", e)
BezierUtilsPanel.lineWidth = 1.5
BezierUtilsPanel.drawHandlerRef = \
bpy.types.SpaceView3D.draw_handler_add(ccDrawHandler, \
(), "WINDOW", "POST_VIEW")
BezierUtilsPanel.shader = gpu.shader.from_builtin('FLAT_COLOR')
return
elif(remove):
if(BezierUtilsPanel.drawHandlerRef != None):
bpy.types.SpaceView3D.draw_handler_remove(BezierUtilsPanel.drawHandlerRef, \
"WINDOW")
BezierUtilsPanel.drawHandlerRef = None
return
if(bpy.context.screen == None):
return
if(bpy.context.window_manager.bezierToolkitParams.applyCurveColor):
objs = [o for o in bpy.context.scene.objects if(isBezier(o) and \
o.visible_get() and len(o.modifiers) == 0 and not o.select_get())]
lineCos = []
lineColors = []
for o in objs:
colorVal = o.data.get('curveColor')
if(colorVal != None):
for i, spline in enumerate(o.data.splines):
for j in range(0, len(spline.bezier_points)):
segPts = getBezierDataForSeg(o, i, j, withShapeKey = True, \
shapeKeyIdx = None, fromMix = True)
if(segPts == None):
continue
pts = getPtsAlongBezier2D(segPts, getAllAreaRegions(), \
FTProps.dispCurveRes, maxRes = MAX_NONSEL_CURVE_RES)
linePts = getLinesFromPts(pts)
lineCos += linePts
lineColors += [colorVal for i in range(0, len(linePts))]
BezierUtilsPanel.lineBatch = batch_for_shader(BezierUtilsPanel.shader, \
"LINES", {"pos": lineCos, "color": lineColors})
# ~ else:
# ~ BezierUtilsPanel.lineBatch = batch_for_shader(BezierUtilsPanel.shader, \
# ~ "LINES", {"pos": [], "color": []})
areas = [a for a in bpy.context.screen.areas if a.type == 'VIEW_3D']
for a in areas:
a.tag_redraw()
################### Common Bezier Functions & Classes ###################
def getPtFromT(p0, p1, p2, p3, t):
c = (1 - t)
pt = (c ** 3) * p0 + 3 * (c ** 2) * t * p1 + \
3 * c * (t ** 2) * p2 + (t ** 3) * p3
return pt
def getTangentAtT(p0, p1, p2, p3, t):
c = (1 - t)
tangent = -3 * (c * c) * p0 + 3 * c * c * p1 - 6 * t * c * p1 - \
3 * t * t * p2 + 6 * t * c * p2 + 3 * t * t * p3
return tangent
# iterative brute force, not optimized, some iterations maybe redundant
def getTsForPt(p0, p1, p2, p3, co, coIdx, tolerance = 0.000001, maxItr = 1000):
ts = set()
# check t from start to end and end to start
for T in [1., 0.]:
# check clockwise as well as anticlockwise
for dirn in [1, -1]:
t = T
t2 = 1
rhs = getPtFromT(p0, p1, p2, p3, t)[coIdx]
error = rhs - co
i = 0
while(abs(error) > tolerance and i < maxItr):
t2 /= 2
if(dirn * error < 0):
t += t2
else:
t -= t2
rhs = getPtFromT(p0, p1, p2, p3, t)[coIdx]
error = rhs - co
i += 1
if(i < maxItr and t >= 0 and t <= 1):
ts.add(round(t, 3))
return ts
#TODO: There may be a more efficient approach, but this seems foolproof
def getTForPt(curve, testPt, tolerance = .000001):
minLen = LARGE_NO
retT = None
for coIdx in range(0, 3):
ts = getTsForPt(curve[0], curve[1], curve[2], curve[3], \
testPt[coIdx], coIdx, tolerance)
for t in ts:
pt = getPtFromT(curve[0], curve[1], curve[2], curve[3], t)
pLen = (testPt - pt).length
if(pLen < minLen):
minLen = pLen
retT = t
return retT
def getCosSortedByT(seg, cos, margin):
coInfo = set()
for co in cos:
t = getTForPt(seg, co, margin)
# ~ if(all(abs(co[i] - seg[3][i]) < margin for i in range(3)) or t >= 1):
if(t >= 1):
coInfo.add(((seg[3]).freeze(), 1))
# ~ elif(all(abs(co[i] - seg[0][i]) < margin for i in range(3)) or t <= 0):
elif(t <= 0):
coInfo.add(((seg[0]).freeze(), 0))
else:
coInfo.add((co.freeze(), t))
return [inf[0] for inf in sorted(coInfo, key = lambda x: x[1])]
# TODO: Check for initial and end points of the segment (here or in getCosSortedByT)
# Currently inserting points more than once if intersect is invoked repeatedly
def removeDupliCos(sortedCos, margin):
prevCo = sortedCos[0]
newCos = [prevCo]
for i in range(1, len(sortedCos)):
co = sortedCos[i]
if(not vectCmpWithMargin(co, prevCo, margin)):
newCos.append(co)
prevCo = co
return newCos
# https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
#(3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
def getBBox(seg):
A = seg[0]
B = seg[1]
C = seg[2]
D = seg[3]
leftBotFront = Vector([min([A[i], D[i]]) for i in range(0, 3)])
rgtTopBack = Vector([max([A[i], D[i]]) for i in range(0, 3)])
a = [3 * D[i] - 9 * C[i] + 9 * B[i] - 3 * A[i] for i in range(0, 3)]
b = [6 * A[i] - 12 * B[i] + 6 * C[i] for i in range(0, 3)]
c = [3 * (B[i] - A[i]) for i in range(0, 3)]
solnsxyz = []
for i in range(0, 3):
solns = []
if(a[i] == 0):
if(b[i] == 0):
solns.append(0)#Independent of t so lets take the starting pt
else:
solns.append(c[i] / b[i])
else:
rootFact = b[i] * b[i] - 4 * a[i] * c[i]
if(rootFact >=0 ):
#Two solutions with + and - sqrt
solns.append((-b[i] + sqrt(rootFact)) / (2 * a[i]))
solns.append((-b[i] - sqrt(rootFact)) / (2 * a[i]))
solnsxyz.append(solns)
for i, soln in enumerate(solnsxyz):
for j, t in enumerate(soln):
if(t <= 1 and t >= 0):
co = getPtFromT(A[i], B[i], C[i], D[i], t)
if(co < leftBotFront[i]): leftBotFront[i] = co
if(co > rgtTopBack[i]): rgtTopBack[i] = co
return leftBotFront, rgtTopBack
def getBBoxOverlapInfo(seg0, seg1):
bbox0 = getBBox(seg0)
max0 = [max(bbox0[i][axis] for i in range(2)) for axis in range(3)]
min0 = [min(bbox0[i][axis] for i in range(2)) for axis in range(3)]
bbox1 = getBBox(seg1)
overlap = True
if(any(all(bbox1[i][j] < min0[j] for i in range(2)) for j in range(3)) or \
any(all(bbox1[i][j] > max0[j] for i in range(2)) for j in range(3))):
overlap = False
return overlap, bbox0, bbox1
def getBBoxCenter(bbox): # bbox -> [leftBotFront, rightTopBack]
return Vector(((bbox[0][i] + bbox[1][i]) / 2 for i in range(3)))
def getIntersectPts(seg0, seg1, soln, solnRounded, recurs, margin, rounding, \
maxRecurs = 100):
overlap, bbox0, bbox1 = getBBoxOverlapInfo(seg0, seg1)
if(overlap):
if(recurs == maxRecurs):
print('Maximum recursions in getIntersectPts!')
return False
if(all(abs(bbox0[0][i] - bbox0[1][i]) < margin for i in range(3))):
center = getBBoxCenter(bbox0)
roundedVect = Vector([round(x, rounding) for x in center]).freeze()
if(roundedVect not in solnRounded):
soln.append(center)
solnRounded.add(roundedVect)
return True
elif(all(abs(bbox1[0][i] - bbox1[1][i]) < margin for i in range(3))):
center = getBBoxCenter(bbox1)
roundedVect = Vector([round(x, rounding) for x in center]).freeze()
if(roundedVect not in solnRounded):
soln.append(center)
solnRounded.add(roundedVect)
return True
else:
seg01 = getPartialSeg(seg0, t0 = 0, t1 = 0.5)
seg02 = getPartialSeg(seg0, t0 = 0.5, t1 = 1)
seg11 = getPartialSeg(seg1, t0 = 0, t1 = 0.5)
seg12 = getPartialSeg(seg1, t0 = 0.5, t1 = 1)
r0 = getIntersectPts(seg01, seg11, soln, solnRounded, \
recurs + 1, margin, rounding)
r1 = getIntersectPts(seg01, seg12, soln, solnRounded, \
recurs + 1, margin, rounding)
r2 = getIntersectPts(seg02, seg11, soln, solnRounded, \
recurs + 1, margin, rounding)
r3 = getIntersectPts(seg02, seg12, soln, solnRounded, \
recurs + 1, margin, rounding)
return any((r0, r1, r2, r3))
return False
# splineInfos: [(curveIdx0, splineIdx0), (curveIdx0, splineIdx1),...]
# First active means intersections only with the first curve (otherwise all combinations)
def getSplineIntersectPts(curves, splineInfos, firstActive, margin, rounding):
segPairMap = {}
areas = [a for a in bpy.context.screen.areas if a.type == 'VIEW_3D']
for i, c0Info in enumerate(splineInfos):
idxCurve0, idxSpline0 = c0Info
if(firstActive and idxCurve0 != splineInfos[0][0]):
break
c0 = curves[idxCurve0]
spline0 = c0.data.splines[idxSpline0]
for j in range(i + 1, len(splineInfos)):
idxCurve1, idxSpline1 = splineInfos[j]
c1 = curves[idxCurve1]
spline1 = c1.data.splines[idxSpline1]
segPairMap[((idxCurve0, idxSpline0), (idxCurve1, idxSpline1))] = \
[((idxSeg0, s0), (idxSeg1, s1)) for idxSeg0, s0 in \
enumerate(getRoundedSplineSegs(c0.matrix_world, spline0)) for \
idxSeg1, s1 in enumerate(getRoundedSplineSegs(c1.matrix_world, \
spline1))]
intersectMap = {}
allIntersectCos = []
for key in segPairMap:
(idxCurve0, idxSpline0), (idxCurve1, idxSpline1) = key
segPairInfo = segPairMap[key]
for info in segPairInfo:
solnRounded = set()
soln = []
(idxSeg0, seg0), (idxSeg1, seg1) = info
ret = getIntersectPts(seg0, seg1, soln, solnRounded, recurs = 0, \
margin = margin, rounding = rounding)
if(ret):
extKey = (idxCurve0, idxSpline0, idxSeg0)
if(intersectMap.get(extKey) == None):
intersectMap[extKey] = []
intersectMap[extKey] += soln
extKey = (idxCurve1, idxSpline1, idxSeg1)
if(intersectMap.get(extKey) == None):
intersectMap[extKey] = []
intersectMap[extKey] += soln
allIntersectCos += soln
return allIntersectCos, intersectMap
def getCurveIntersectPts(curves, firstActive, margin, rounding):
splineInfos = [(x, y) for x in range(len(curves)) \
for y in range(len(curves[x].data.splines))]
return getSplineIntersectPts(curves, splineInfos, firstActive, margin, rounding)
def getRoundedSplineSegs(mw, spline, reverse = False, rounding = 5):
def getRoundedVect(mw, co, rounding):
return Vector((round(x, rounding) for x in (mw @ co)))
bpts = spline.bezier_points
if(reverse): bpts = reversed(bpts)
segs = []
for i in range(1, len(bpts)):
segPts = [bpts[i-1].co, bpts[i-1].handle_right, bpts[i].handle_left, bpts[i].co]
segs.append([getRoundedVect(mw, pt, rounding) for pt in segPts])
if(spline.use_cyclic_u):
segPts = [bpts[-1].co, bpts[-1].handle_right, bpts[0].handle_left, bpts[0].co]
segs.append([getRoundedVect(mw, pt, rounding) for pt in segPts])
return segs
# Because there is some discrepancy between this and getSegLen
# This seems to be more accurate
def getSegLenTmpObj(tmpSpline, bpts, mw = Matrix()):
tmpSpline.bezier_points[0].co = mw @ bpts[0].co
tmpSpline.bezier_points[0].handle_right = mw @ bpts[0].handle_right
tmpSpline.bezier_points[1].handle_left = mw @ bpts[1].handle_left
tmpSpline.bezier_points[1].co = mw @ bpts[1].co
return tmpSpline.calc_length()
def getSplineLenTmpObj(tmpSpline, spline, mw):
l = 0
bpts = spline.bezier_points
l += sum(getSegLenTmpObj(tmpSpline, bpts[i:i+2], mw) for i in range(len(bpts) -1))
if(spline.use_cyclic_u): l += getSegLenTmpObj(tmpSpline, [bpts[-1], bpts[0]], mw)
return l
def getSegLen(pts, error = DEF_ERR_MARGIN, start = None, end = None, t1 = 0, t2 = 1):
if(start == None): start = pts[0]
if(end == None): end = pts[-1]
t1_5 = (t1 + t2)/2
mid = getPtFromT(*pts, t1_5)
l = (end - start).length
l2 = (mid - start).length + (end - mid).length
if (l2 - l > error):
return (getSegLen(pts, error, start, mid, t1, t1_5) +
getSegLen(pts, error, mid, end, t1_5, t2))
return l2
def hasAlignedHandles(pt):
if(len(pt) == 5 and 'ALIGNED' in {pt[3], pt[4]} and 'FREE' not in {pt[3], pt[4]}):
return True
diffV1 = pt[1] - pt[0]
diffV2 = pt[2] - pt[1]
if(vectCmpWithMargin(diffV1.normalized(), diffV2.normalized())):
return True
return False
def isStraightSeg(segPts):
if(len(segPts) != 2): return False
if((len(segPts[0]) == 5 or len(segPts[1]) == 5) and \
segPts[0][4] == 'VECTOR' and segPts[1][3] == 'VECTOR'):
return True
if(vectCmpWithMargin((segPts[0][2]-segPts[0][1]).normalized(), \
(segPts[1][1] - segPts[1][0]).normalized())):
return True
return False
# Get pt coords along curve defined by the four control pts (segPts)
# subdivPerUnit: No of subdivisions per unit length
# (which is the same as no of pts excluding the end pts)
def getInterpBezierPts(segPts, subdivPerUnit, segLens = None, maxRes = None):
if(len(segPts) < 2):
return []
curvePts = []
for i in range(1, len(segPts)):
seg = [segPts[i-1][1], segPts[i-1][2], segPts[i][0], segPts[i][1]]
if(segLens != None and len(segLens) > (i-1)):
res = int(segLens[i-1] * subdivPerUnit)
else:
res = int(getSegLen(seg) * subdivPerUnit)
if(res < 2): res = 2
if(maxRes != None and res > maxRes): res = maxRes
curvePts += geometry.interpolate_bezier(*seg, res)
return curvePts
# Used in functions where actual locs of pts on curve matter (like subdiv Bezier)
# (... kind of expensive)
def getPtsAlongBezier3D(segPts, rv3d, curveRes, minRes = 200):
viewDist = rv3d.view_distance
# The smaller the view dist (higher zoom level),
# the higher the num of subdivisions
curveRes = curveRes / viewDist
if(curveRes < minRes): curveRes = minRes
return getInterpBezierPts(segPts, subdivPerUnit = curveRes)
# Used in functions where only visual resolution of curve matters (like draw Bezier)
# (... not so expensive)
# TODO: Calculate maxRes dynamically
def getPtsAlongBezier2D(segPts, areaRegionInfo, curveRes, maxRes = None):
segLens = []
for i in range(1, len(segPts)):
seg = [segPts[i-1][1], segPts[i-1][2], segPts[i][0], segPts[i][1]]
#TODO: A more optimized solution... (Called very frequently)
segLen = 0
for info in areaRegionInfo:
seg2D = [getCoordFromLoc(info[1], info[2], loc) for loc in seg]
sl = getSegLen(seg2D)
if(sl > segLen):
segLen = sl
segLens.append(segLen)
return getInterpBezierPts(segPts, subdivPerUnit = curveRes, \
segLens = segLens, maxRes = maxRes)
def getLinesFromPts(pts):
positions = []
for i, pt in enumerate(pts):
positions.append(pt)
if(i > 0 and i < (len(pts)-1)):
positions.append(pt)
return positions
#see https://stackoverflow.com/questions/878862/drawing-part-of-a-b%c3%a9zier-curve-by-reusing-a-basic-b%c3%a9zier-curve-function/879213#879213
def getPartialSeg(seg, t0, t1):
pts = [seg[0], seg[1], seg[2], seg[3]]
if(t0 > t1):
tt = t1
t1 = t0
t0 = tt
u0 = 1.0 - t0
u1 = 1.0 - t1
qa = [pts[0][i]*u0*u0 + pts[1][i]*2*t0*u0 + pts[2][i]*t0*t0 for i in range(0, 3)]
qb = [pts[0][i]*u1*u1 + pts[1][i]*2*t1*u1 + pts[2][i]*t1*t1 for i in range(0, 3)]
qc = [pts[1][i]*u0*u0 + pts[2][i]*2*t0*u0 + pts[3][i]*t0*t0 for i in range(0, 3)]
qd = [pts[1][i]*u1*u1 + pts[2][i]*2*t1*u1 + pts[3][i]*t1*t1 for i in range(0, 3)]
pta = Vector([qa[i]*u0 + qc[i]*t0 for i in range(0, 3)])
ptb = Vector([qa[i]*u1 + qc[i]*t1 for i in range(0, 3)])
ptc = Vector([qb[i]*u0 + qd[i]*t0 for i in range(0, 3)])
ptd = Vector([qb[i]*u1 + qd[i]*t1 for i in range(0, 3)])
return [pta, ptb, ptc, ptd]
def getInterpolatedVertsCo(curvePts, numDivs):
# Can be calculated only once
curveLength = sum((curvePts[i] - curvePts[i-1]).length
for i in range(1, len(curvePts)))
if(floatCmpWithMargin(curveLength, 0)):
return [curvePts[0]] * numDivs
segLen = curveLength / numDivs
vertCos = [curvePts[0]]
actualLen = 0
vertIdx = 0
for i in range(1, numDivs):
co = None
targetLen = i * segLen
while(not floatCmpWithMargin(actualLen, targetLen)
and actualLen < targetLen):
vertCo = curvePts[vertIdx]
vertIdx += 1
nextVertCo = curvePts[vertIdx]
actualLen += (nextVertCo - vertCo).length
if(floatCmpWithMargin(actualLen, targetLen)):
co = curvePts[vertIdx]
else: #interpolate
diff = actualLen - targetLen
co = (nextVertCo - (nextVertCo - vertCo) * \
(diff/(nextVertCo - vertCo).length))
#Revert to last pt
vertIdx -= 1
actualLen -= (nextVertCo - vertCo).length
vertCos.append(co)
# ~ if(not vectCmpWithMargin(curvePts[0], curvePts[-1])):
vertCos.append(curvePts[-1])
return vertCos
#
# The following section is a Python conversion of the javascript
# a2c function at: https://github.com/fontello/svgpath
# (Copyright (C) 2013-2015 by Vitaly Puzrin)
#
# Note: Most of the comments are retained
######################## a2c start #######################
TAU = pi * 2
# eslint-disable space-infix-ops
# Calculate an angle between two unit vectors
#
# Since we measure angle between radii of circular arcs,
# we can use simplified math (without length normalization)
#
def unit_vector_angle(ux, uy, vx, vy):
if(ux * vy - uy * vx < 0):
sign = -1
else:
sign = 1
dot = ux * vx + uy * vy
# Add this to work with arbitrary vectors:
# dot /= sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy)
# rounding errors, e.g. -1.0000000000000002 can screw up this
if (round(dot, 3) >= 1.0):
dot = 1.0
if (round(dot, 3) <= -1.0):
dot = -1.0
return sign * acos(dot)
# Convert from endpoint to center parameterization,
# see http:#www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
#
# Return [cx, cy, theta1, delta_theta]
#
def get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi):
# Step 1.
#
# Moving an ellipse so origin will be the middlepoint between our two
# points. After that, rotate it to line up ellipse axes with coordinate
# axes.
#
x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2
y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2
rx_sq = rx * rx
ry_sq = ry * ry
x1p_sq = x1p * x1p
y1p_sq = y1p * y1p
# Step 2.
#
# Compute coordinates of the centre of this ellipse (cx', cy')
# in the new coordinate system.
#
radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq)
if (radicant < 0):
# due to rounding errors it might be e.g. -1.3877787807814457e-17
radicant = 0
radicant /= (rx_sq * y1p_sq) + (ry_sq * x1p_sq)
factor = 1
if(fa == fs):# Migration Note: note ===
factor = -1
radicant = sqrt(radicant) * factor #(fa === fs ? -1 : 1)
cxp = radicant * rx/ry * y1p
cyp = radicant * -ry/rx * x1p
# Step 3.
#
# Transform back to get centre coordinates (cx, cy) in the original
# coordinate system.
#
cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2
cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2
# Step 4.
#
# Compute angles (theta1, delta_theta).
#
v1x = (x1p - cxp) / rx
v1y = (y1p - cyp) / ry
v2x = (-x1p - cxp) / rx
v2y = (-y1p - cyp) / ry
theta1 = unit_vector_angle(1, 0, v1x, v1y)
delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y)
if (fs == 0 and delta_theta > 0):#Migration Note: note ===
delta_theta -= TAU
if (fs == 1 and delta_theta < 0):#Migration Note: note ===
delta_theta += TAU
return [ cx, cy, theta1, delta_theta ]
#
# Approximate one unit arc segment with bezier curves,
# see http:#math.stackexchange.com/questions/873224
#
def approximate_unit_arc(theta1, delta_theta):
alpha = 4.0/3 * tan(delta_theta/4)
x1 = cos(theta1)
y1 = sin(theta1)
x2 = cos(theta1 + delta_theta)
y2 = sin(theta1 + delta_theta)
return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ]
def a2c(x1, y1, x2, y2, fa, fs, rx, ry, phi, noSegs):
sin_phi = sin(phi * TAU / 360)
cos_phi = cos(phi * TAU / 360)
# Make sure radii are valid
#
x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2
y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2
if (x1p == 0 and y1p == 0): # Migration Note: note ===
# we're asked to draw line to itself
return []
if (rx == 0 or ry == 0): # Migration Note: note ===
# one of the radii is zero
return []
# Compensate out-of-range radii
#
rx = abs(rx)
ry = abs(ry)
lmbd = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry)
if (lmbd > 1):
rx *= sqrt(lmbd)
ry *= sqrt(lmbd)
# Get center parameters (cx, cy, theta1, delta_theta)
#
cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi)
result = []
theta1 = cc[2]
delta_theta = cc[3]
# Split an arc to multiple segments, so each segment
# will be less than 90
#
segments = noSegs # int(max(ceil(abs(delta_theta) / (TAU / 4)), 1))
delta_theta /= segments
for i in range(0, segments):
result.append(approximate_unit_arc(theta1, delta_theta))
theta1 += delta_theta
# We have a bezier approximation of a unit circle,
# now need to transform back to the original ellipse
#
return getMappedList(result, rx, ry, sin_phi, cos_phi, cc)
def getMappedList(result, rx, ry, sin_phi, cos_phi, cc):
mappedList = []
for elem in result:
curve = []
for i in range(0, len(elem), 2):
x = elem[i + 0]
y = elem[i + 1]
# scale
x *= rx
y *= ry
# rotate
xp = cos_phi*x - sin_phi*y
yp = sin_phi*x + cos_phi*y
# translate
elem[i + 0] = xp + cc[0]
elem[i + 1] = yp + cc[1]
curve.append(complex(elem[i + 0], elem[i + 1]))
mappedList.append(curve)
return mappedList
######################## a2c end #######################
def get3DVector(cmplx, axisIdxs, z):
vElems = [None] * 3
vElems[axisIdxs[0]] = cmplx.real
vElems[axisIdxs[1]] = cmplx.imag
vElems[axisIdxs[2]] = z
return Vector(vElems)
def getSegsForArc(start, radius, sweep, end, noSegs, axisIdxs, z):
x1, y1 = start.real, start.imag
x2, y2 = end.real, end.imag
fa = 0
fs = sweep
rx, ry = radius.real, radius.imag
phi = 0
curvesPts = a2c(x1, y1, x2, y2, fa, fs, rx, ry, phi, noSegs)
newSegs = []
for curvePts in curvesPts:
newSegs.append([get3DVector(curvePts[0], axisIdxs, z), get3DVector(curvePts[1], axisIdxs, z), \
get3DVector(curvePts[2], axisIdxs, z), get3DVector(curvePts[3], axisIdxs, z)])
return newSegs
def getWSDataForSegs(segs):
prevSeg = None
wsData = []
for j, seg in enumerate(segs):
pt = seg[0]
handleRight = seg[1]
if(j == 0): handleLeft = pt
else: handleLeft = prevSeg[2]
ht = 'ALIGNED' if(vectCmpWithMargin(pt - handleLeft, handleRight - pt)) else 'FREE'
wsData.append([handleLeft, pt, handleRight, ht, ht])
prevSeg = seg
if(prevSeg != None): wsData.append([seg[2], seg[3], seg[3], 'FREE', 'FREE'])
else: return []
return wsData
################### Common to Draw and Edit Flexi Bezier Ops ###################
# Some global constants
SEARCH_CURVE_RES = .5 # Per pixel seg divisions (.5 is one div per 2 pixel units)
DBL_CLK_DURN = 0.25
SNGL_CLK_DURN = 0.3
MAX_SEL_CURVE_RES = 1000
MAX_NONSEL_CURVE_RES = 100
EVT_NOT_CONS = 0
EVT_CONS = 1
EVT_META_OR_SNAP = 2
TOOL_TYPE_FLEXI_DRAW = 'Flexi Draw'
TOOL_TYPE_FLEXI_GREASE = 'Flexi Grease'
TOOL_TYPE_FLEXI_EDIT = 'Flexi Edit'
TOOL_TYPES_FLEXI_DRAW_COMMON = {TOOL_TYPE_FLEXI_DRAW, TOOL_TYPE_FLEXI_GREASE}
TOOL_TYPES_FLEXI_ALL = {TOOL_TYPE_FLEXI_DRAW, \
TOOL_TYPE_FLEXI_GREASE, TOOL_TYPE_FLEXI_EDIT}
class BptDisplayInfo:
# handleNos: 0: seg1-left, 1: seg1-right
# tipColors: leftHdl, pt, rightHdl
# Caller to make sure there are no tips without handle
def __init__(self, pt, tipColors, handleNos = None):
self.pt = pt
if(len(tipColors) == 1):
self.tipColors = [None, tipColors[0], None]
elif(len(tipColors) == 3):
self.tipColors = tipColors
if(handleNos == None): self.handleNos = []
else: self.handleNos = handleNos
class SegDisplayInfo:
def __init__(self, segPts, segColor):
self.segPts = segPts
self.segColor = segColor
class RegionMouseXYInfo:
def getRegionMouseXYInfo(event, exclInRgns):
xyScreen = [event.mouse_x, event.mouse_y]
idxs = getAreaRegionIdxs(xyScreen, exclInRgns)
if(idxs == None):
return None
else:
i, j = idxs
area = bpy.context.screen.areas[i]
region = area.regions[j]
space3d = area.spaces[0]
if(len(space3d.region_quadviews) > 0):
qIdx = getWindowRegionIdx(area, j)
rv3d = space3d.region_quadviews[qIdx]
else:
rv3d = space3d.region_3d
xy = [xyScreen[0] - region.x, xyScreen[1] - region.y]
return RegionMouseXYInfo(area, space3d, region, rv3d, xy, xyScreen)
def __init__(self, area, space3d, region, rv3d, xy, xyScreen):
self.area = area
self.space3d = space3d
self.region = region
self.rv3d = rv3d
self.xy = xy
self.xyScreen = xyScreen
def __eq__(self, other):
if(other == None): return False
return self.area == other.area and self.region == other.region and \
self.rv3d == other.rv3d
def getLineShades(lineCos, baseColor, start, end, mid = True):
if(len(lineCos) == 0 ): return [], []
if(len(lineCos) == 1 ): return lineCos[0], [baseColor]
if(mid): midPt = lineCos[0] + (lineCos[1] - lineCos[0]) / 2
col1 = [start * c for c in baseColor]
col2 = [end * c for c in baseColor]
if(mid): return [lineCos[0], midPt, midPt, lineCos[1]], [col1, col2, col2, col1]
else: return [lineCos[0], lineCos[1]], [col1, col2]
class BGLDrawInfo:
def __init__(self, size, color, pts):
self.size = size
self.color = color
self.pts = pts
class BGLDrawInfoLine(BGLDrawInfo):
def __init__(self, size, color, pts, \
gradientStart = None, gradientEnd = None, mid = True):
super(BGLDrawInfoLine, self).__init__(size, color, pts)
self.gradientStart = gradientStart
self.gradientEnd = gradientEnd
self.mid = mid
class BGLDrawMgr:
def __init__(self, shader):
self.lineInfoMap = {}
self.ptInfoMap = {}
self.shader = shader
def addLineInfo(self, infoId, size, color, pts, \
gradientStart = None, gradientEnd = None, mid = True):
self.lineInfoMap[infoId] = BGLDrawInfoLine(size, color, pts, \
gradientStart, gradientEnd, mid)
def addPtInfo(self, infoId, size, color, pts):
self.ptInfoMap[infoId] = BGLDrawInfo(size, color, pts)
def redraw(self):
lineInfos = sorted(self.lineInfoMap.values(), key = lambda x: (x.size))
pos = []
col = []
batches = []
for i, info in enumerate(lineInfos):
if(i == 0 or info.size != lineInfos[i-1].size):
if(i > 0):
# bgl.glLineWidth(lineInfos[i-1].size)
gpu.state.line_width_set(lineInfos[i-1].size)
batch = batch_for_shader(self.shader, \
'LINES', {"pos": pos, "color": col})
batch.draw(self.shader)
pos = []
col = []
if(info.gradientEnd != None and info.gradientStart != None):
if(len(info.pts) != 2 and len(info.color) != 1):
raise ValueError('Exactly two ' + \
'coordinates and one color for gradient line')
linePos, lineCols = getLineShades(info.pts, info.color[0], \
info.gradientStart, info.gradientEnd, info.mid)
else:
if(len(info.pts) == 0): continue
linePos = info.pts[:]
lineCols = info.color[:]
diff = len(linePos) - len(lineCols)
if(diff >= 0):
for j in range(diff):
lineCols.append(info.color[-1])
else:
for j in range(-diff):
lineCols.pop()
pos += linePos
col += lineCols
if(len(pos) > 0):
# bgl.glLineWidth(lineInfos[-1].size)
gpu.state.line_width_set(lineInfos[-1].size)
batch = batch_for_shader(self.shader, \
'LINES', {"pos": pos, "color": col})
batch.draw(self.shader)
ptInfos = sorted(self.ptInfoMap.values(), key = lambda x: (x.size))
for i, info in enumerate(ptInfos):
if(i == 0 or info.size != ptInfos[i-1].size):
if(i > 0):
# bgl.glPointSize(ptInfos[i-1].size)
gpu.state.point_size_set(ptInfos[i-1].size)
batch = batch_for_shader(self.shader, \
'POINTS', {"pos": pos, "color": col})
batch.draw(self.shader)
pos = []
col = []
if(len(info.pts) == 0): continue
ptCols = info.color[:]
diff = len(info.pts) - len(info.color)
if(diff >= 0):
for j in range(diff):
ptCols.append(info.color[-1])
else:
for j in range(-diff):
ptCols.pop()
pos += info.pts[:]
col += ptCols
if(len(pos) > 0):
# bgl.glPointSize(ptInfos[-1].size)
gpu.state.point_size_set(ptInfos[-1].size)
batch = batch_for_shader(self.shader, \
'POINTS', {"pos": pos, "color": col})
batch.draw(self.shader)
def resetLineInfo(self, infoId):
drawInfo = self.lineInfoMap.get(infoId)
if(drawInfo != None):
drawInfo.pts = []
def resetPtInfo(self, infoId):
drawInfo = self.ptInfoMap.get(infoId)
if(drawInfo != None):
drawInfo.pts = []
def reset(self):
for key in list(self.ptInfoMap.keys()):
self.ptInfoMap[key].pts = []
for key in list(self.lineInfoMap.keys()):
self.lineInfoMap[key].pts = []
# Return line batch for bezier line segments and handles and point batch for handle tips
def updateBezierBatches(bglDrawMgr, segDispInfos, bptDispInfos, areaRegionInfo, \
defHdlType = 'ALIGNED'):
lineCos = [] #segment is also made up of lines
lineColors = []
for i, info in enumerate(segDispInfos):
segPts = info.segPts
if(isStraightSeg(segPts)):
lineCos += [segPts[0][1], segPts[1][1]]
lineColors += [info.segColor, info.segColor]
else:
pts = getPtsAlongBezier2D(segPts, areaRegionInfo, \
FTProps.dispCurveRes, maxRes = MAX_NONSEL_CURVE_RES)
segLineCos = getLinesFromPts(pts)
lineCos += segLineCos
lineColors += [info.segColor for j in range(0, len(segLineCos))]
tipCos = []
tipColors = []
for i, info in enumerate(bptDispInfos):
pt = info.pt
for hn in info.handleNos:
lineCos += [pt[hn], pt[hn + 1]]
if(len(pt) < 5): htype = defHdlType # For Draw
else: htype = pt[3 + hn]
lineColors += [ModalBaseFlexiOp.hdlColMap[htype], \
ModalBaseFlexiOp.hdlColMap[htype]]
# Re-arrange tips so handles are on top of Bezier point
tc = info.tipColors
tc = [tc[1], tc[0], tc[2]]
pt = [pt[1], pt[0], pt[2]]
for j, tipColor in enumerate(tc):
if(tipColor != None):
tipCos.append(pt[j])
tipColors.append(tipColor)
bglDrawMgr.addLineInfo('bezLineBatch', FTProps.lineWidth, lineColors, lineCos)
bglDrawMgr.addPtInfo('bezTipBatch', FTProps.drawPtSize, tipColors, tipCos)
def resetToolbarTool():
win = bpy.context.window
scr = win.screen
areas3d = [area for area in scr.areas if area.type == 'VIEW_3D']
override = {'window':win,'screen':scr, 'scene' :bpy.context.scene}
for a in areas3d:
override['area'] = a
regions = [region for region in a.regions if region.type == 'WINDOW']
for r in regions:
override['region'] = r
with bpy.context.temp_override(**override):
bpy.ops.wm.tool_set_by_index()
def updateMetaBtns(caller, event, keymap = None):
if(keymap == None):
keymap = {'LEFT_SHIFT': 'shift', 'RIGHT_SHIFT': 'shift',
'LEFT_CTRL':'ctrl', 'RIGHT_CTRL':'ctrl',
'LEFT_ALT': 'alt', 'RIGHT_ALT': 'alt'}
var = keymap.get(event.type)
if(var != None):
expr = 'caller.' + var + ' = '
if(event.value == 'PRESS'): exec(expr +'True')
if(event.value == 'RELEASE'): exec(expr +'False')
return True
return False
unitMap = {'FEET': "'", 'METERS':'m'}
class FTHotKeyData:
def __init__(self, id, key, label, description, isExclusive = False, \
inclTools = None, exclTools = None):
if(isExclusive and '+' in key):
raise ValueError('Exclusive keys cannot be combined with meta keys')
self.id = id
self.key = key
self.label = label
self.description = description
self.default = key
# isExclusive means the hot key function will be invoked even if there are
# meta keys (that are not part of the hot key combination) are
# held down together with this key. This way user can have both
# meta key related functionality (e.g. snapping) amd hot key functionality
# (e. g. tweak position) simultaneously (Tweak from a snapped point)
self.isExclusive = isExclusive
self.exclTools = exclTools if(exclTools != None) else set()
self.inclTools = inclTools if(inclTools != None) else TOOL_TYPES_FLEXI_ALL
self.inclTools = self.inclTools - self.exclTools
class FTHotKeys:
##################### Key IDs #####################
# Draw
hkGrabRepos = 'hkGrabRepos'
hkUndoLastSeg = 'hkUndoLastSeg'
hkDissociateHdl = 'hkDissociateHdl'
hkResetLastHdl = 'hkResetLastHdl'
drawHotkeys = []
drawHotkeys.append(FTHotKeyData(hkGrabRepos, 'G', 'Grab Bezier Point', \
'Grab Bezier point while drawing', inclTools = TOOL_TYPES_FLEXI_DRAW_COMMON, \
isExclusive = True))
drawHotkeys.append(FTHotKeyData(hkDissociateHdl, 'V', 'Dissociate Draw Hendle', \
'Dissociate right handle from left while drawing', \
inclTools = TOOL_TYPES_FLEXI_DRAW_COMMON, isExclusive = True))
drawHotkeys.append(FTHotKeyData(hkResetLastHdl, 'Shift+R', 'Reset Last Handle', \
'Reset last handle so that new segment starts as straight line', \
inclTools = TOOL_TYPES_FLEXI_DRAW_COMMON))
drawHotkeys.append(FTHotKeyData(hkUndoLastSeg, 'BACK_SPACE', 'Undo Last Segment', \
'Undo drawing of last segment', inclTools = TOOL_TYPES_FLEXI_DRAW_COMMON))
# Edit
hkUniSubdiv = 'hkUniSubdiv'
hkBevelPt = 'hkBevelPt'
hkAlignHdl = 'hkAlignHdl'
hkDelPtSeg = 'hkDelPtSeg'
hkToggleHdl = 'hkToggleHdl'
hkSplitAtSel = 'hkSplitAtSel'
hkMnHdlType = 'hkMnHdlType'
hkMnSelect = 'hkMnSelect'
hkMnDeselect = 'hkMnDeselect'
editHotkeys = []
editHotkeys.append(FTHotKeyData(hkUniSubdiv, 'W', 'Segment Uniform Subdivide', \
'Hotkey to initiate Segment Uniform Subdiv op', \
inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkBevelPt, 'Ctrl+B', 'Bevel Selected Points', \
'Hotkey to initiate Bevel op', inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkAlignHdl, 'K', 'Align Handle', \
'Hotkey to align one handle with the other', \
inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkDelPtSeg, 'DEL', 'Delete Point / Seg', \
'Delete selected Point / Segment, align selected handle with other point', \
inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkToggleHdl, 'H', 'Hide / Unhide Handles', \
'Toggle handle visibility', inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkSplitAtSel, 'L', 'Split At Selected Points', \
'Split curve at selected Bezier points', inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkMnHdlType, 'S', 'Set Handle Type', \
'Set type of selected handles', inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkMnSelect, 'A', 'Select', \
'Select elements from existing spline selection', \
inclTools = {TOOL_TYPE_FLEXI_EDIT}))
editHotkeys.append(FTHotKeyData(hkMnDeselect, 'Alt+A', 'Deselect', \
'Deselect elements from existing spline selection', \
inclTools = {TOOL_TYPE_FLEXI_EDIT}))
# Common
hkToggleKeyMap = 'hkToggleKeyMap'
hkSwitchOut = 'hkSwitchOut'
hkTweakPos = 'hkTweakPos'
hkToggleDrwEd = 'hkToggleDrwEd'
hkReorient = 'hkReorient'
commonHotkeys = []
commonHotkeys.append(FTHotKeyData(hkToggleKeyMap, 'Ctrl+Shift+H', \
'Hide / Unhide Keymap', \
'Hide / Unhide Keymap Displayed When Flexi Tool Is Active'))
commonHotkeys.append(FTHotKeyData(hkSwitchOut, 'F1', 'Exit Flexi Tool', \
'Switch out of the Flexi Tool mode'))
commonHotkeys.append(FTHotKeyData(hkTweakPos, 'P', 'Tweak Position', \
'Tweak position or enter polar coordinates of the draw / edit point', \
isExclusive = True))
commonHotkeys.append(FTHotKeyData(hkToggleDrwEd, 'E', 'Toggle Draw / Edit', \
'Toggle between Draw & Edit Flexi Tools', \
exclTools = {TOOL_TYPE_FLEXI_GREASE}))
commonHotkeys.append(FTHotKeyData(hkReorient, 'U', \
'Origin to Active Face', \
'Move origin / orientation to face under mouse cursor \" + \
"if the origin / orientation is object face'))
# Snapping
hkSnapVert = 'hkSnapVert'
hkSnapGrid = 'hkSnapGrid'
hkSnapAngle = 'hkSnapAngle'
# Snapping (Suffix important)
hkSnapVertMeta = 'hkSnapVertMeta'
hkSnapGridMeta = 'hkSnapGridMeta'
hkSnapAngleMeta = 'hkSnapAngleMeta'
snapHotkeys = [] # Order important
snapHotkeys.append(FTHotKeyData(hkSnapVert, 'F5', 'Snap to Vert / Face', \
'Key pressed with mouse click for snapping to Vertex or Face'))
snapHotkeys.append(FTHotKeyData(hkSnapGrid, 'F6', 'Snap to Grid', \
'Key pressed with mouse click for snapping to Grid'))
snapHotkeys.append(FTHotKeyData(hkSnapAngle, 'F7', 'Snap to Angle', \
'Key pressed with mouse click for snapping to Angle Increment'))
snapHotkeysMeta = [] # Order should be same as snapHotkeys
# These keys are not event.type (they can have an entry 'KEY')
snapHotkeysMeta.append(FTHotKeyData(hkSnapVertMeta, 'ALT', 'Snap to Vert / Face', \
'Key pressed with mouse click for snapping to Vertex or Face'))
snapHotkeysMeta.append(FTHotKeyData(hkSnapGridMeta, 'CTRL', 'Snap to Grid', \
'Key pressed with mouse click for snapping to Grid'))
snapHotkeysMeta.append(FTHotKeyData(hkSnapAngleMeta, 'SHIFT', 'Snap to Angle', \
'Key pressed with mouse click for snapping to Angle Increment'))
# Metakeys not part of the map
idDataMap = {h.id: h for h in \
drawHotkeys + editHotkeys + commonHotkeys + snapHotkeys}
# There can be multiple keydata with same keys (e.g. same key for draw and edit)
# Not creating a separate map for now. May require later.
# ~ keyDataMap = {h.key: h for h in \
# ~ drawHotkeys + editHotkeys + commonHotkeys + snapHotkeys}
exclKeys = {'RET', 'SPACE', 'ESC', 'X', 'Y', 'Z', \
'ACCENT_GRAVE', 'COMMA', 'PERIOD', 'F2', 'F3'}
metas = ['Alt', 'Ctrl', 'Shift']
def getSnapHotKeys(kId):
metaId = kId + 'Meta'
keydatas = [k for k in FTHotKeys.snapHotkeysMeta if k.id == metaId]
if(len(keydatas) > 0):
keydata = keydatas[0]
if(keydata.key != 'KEY'):
return ['LEFT_' + keydata.key, 'RIGHT_' + keydata.key]
keydata = FTHotKeys.idDataMap.get(kId)
return [keydata.key]
def getKey(key, metas):
keyVal = ''
for i, meta in enumerate(metas):
if(meta): keyVal += FTHotKeys.metas[i] + '+'
keyVal += key
return keyVal
def getHotKeyData(toolType, key, metas):
# Map will be more efficient, but not so many keys... So ok for now
kds = [kd for kd in FTHotKeys.idDataMap.values() \
if(kd.key == FTHotKeys.getKey(key, metas) and toolType in kd.inclTools)]
return kds[0] if len(kds) > 0 else None
def isHotKey(id, key, metas):
currKeyData = FTHotKeys.idDataMap[id]
# Only compare part without meta for exclusive keys
if(currKeyData.isExclusive):
return currKeyData.key == key
else:
return currKeyData.key == FTHotKeys.getKey(key, metas)
def haveCommonTool(keyData1, keyData2):
return len(keyData1.inclTools.intersection(keyData2.inclTools)) > 0
# The regular part of the snap keys is validated against assigned key without meta
# So that if e.g. Ctrl+B is already assigned, B is not available as reg part
def isAssignedWithoutMeta(kId, key):
if(key.endswith(INVAL)): return True
return any([kd.key.split('+')[-1] == key for kd in FTHotKeys.idDataMap.values() \
if kd.id != kId and FTHotKeys.haveCommonTool(kd, FTHotKeys.idDataMap[kId])])
def isAssigned(kId, key):
if(key.endswith(INVAL)): return True
currKeyData = FTHotKeys.idDataMap.get(kId)
exclKeys = [kd for kd in FTHotKeys.idDataMap.values() \
if(FTHotKeys.haveCommonTool(kd, currKeyData) and ((key == kd.key) or \
(kd.isExclusive and (key.split('+')[-1] == kd.key)) or \
(currKeyData.isExclusive and (key == kd.key.split('+')[-1]))))]
return len(exclKeys) > 0
updating = False
# Validation for single key text field (format: meta key + regular key)
# UI Format: Key and 3 toggle buttons for meta
# TODO: Separate update for each id?
# TODO: Reset on any exception?
def updateHotkeys(dummy, context):
try:
FTHotKeys.updateHKPropPrefs(context)
# ~ FTHotKeys.keyDataMap = {h.key: h for h in \
# ~ [k for k in (FTHotKeys.drawHotkeys + FTHotKeys.editHotkeys + \
# ~ FTHotKeys.commonHotkeys + FTHotKeys.snapHotkeys)]}
except Exception as e:
FTHotKeys.updateHKPropPrefs(context, reset = True)
print("BezierUtils: Error fetching keymap preferences", e)
# TODO: This method combines two rather different functionality based on reset flag
# TODO: Metakey setting in updateSnapMetaKeys and reset here also
def updateHKPropPrefs(context, reset = False):
if(FTHotKeys.updating): return
FTHotKeys.updating = True
prefs = context.preferences.addons[__name__].preferences
hmap = FTHotKeys.idDataMap
# Checking entire map even if one key changed (TODO)
for kId in hmap:
if(reset): hmap[kId].key = hmap[kId].default
# User pressed key
prefKey = getattr(prefs, kId)
combKey = ''
for meta in FTHotKeys.metas:
if(hasattr(prefs, kId + meta) and \
getattr(prefs, kId + meta) == True):
combKey += meta + '+'
# key in map has format - meta1 + met2 ... + (event.type)
prefKey = combKey + prefKey
if(hmap[kId].key == prefKey): continue
valid = (prefKey != INVAL and not reset)
if(valid):
if(kId in [k.id for k in FTHotKeys.snapHotkeys]):
valid = not FTHotKeys.isAssignedWithoutMeta(kId, prefKey)
else:
valid = not FTHotKeys.isAssigned(kId, prefKey)
# Revert to key from map if invalid
if(not valid):
comps = hmap[kId].key.split('+')
setattr(prefs, kId, comps[-1])
for meta in FTHotKeys.metas:
setattr(prefs, kId + meta, meta in comps[:-1])
else:
hmap[kId].key = prefKey
if(reset):
for i, metakeyData in enumerate(FTHotKeys.snapHotkeysMeta):
metakeyData.key = metakeyData.default
metaKeyId = metakeyData.id
setattr(prefs, metaKeyId, metakeyData.key)
FTHotKeys.updating = False
ModalBaseFlexiOp.propsChanged()
def getAvailKey(prefs):
availKey = None
oldKeys = set([m.upper() for m in FTHotKeys.metas])
newKeys = set([getattr(prefs, kd.id) \
for kd in FTHotKeys.snapHotkeysMeta])
for availKey in (oldKeys - newKeys): break # Only one entry
return availKey
def initSnapMetaFromPref(context):
prefs = context.preferences.addons[__name__].preferences
for i, metakeyData in enumerate(FTHotKeys.snapHotkeysMeta):
metaKeyId = metakeyData.id
prefKeyMeta = getattr(prefs, metaKeyId)
metakeyData.key = prefKeyMeta
if(prefKeyMeta == 'KEY'):
regKeyId = metaKeyId[0:metaKeyId.index('Meta')]
regKeyData = FTHotKeys.idDataMap[regKeyId]
prefKeyReg = getattr(prefs, regKeyId)
regKeyData.key = prefKeyReg
ModalBaseFlexiOp.propsChanged()
# Validation for snap keys (format: EITHER meta key OR regular key)
# (regular part validated by updateHotkeys)
# UI Format: Drop-down with entries Ctrl, Alt, Shift, Keyboard and ...
# a separate single key field activated with selection of 'Keyboard' in the dropdown
def updateSnapMetaKeys(dummy, context):
if(FTHotKeys.updating): return
FTHotKeys.updating = True
prefs = context.preferences.addons[__name__].preferences
changedKeyIdx = -1
prefKeyMeta = None
for i, metakeyData in enumerate(FTHotKeys.snapHotkeysMeta):
metaKeyId = metakeyData.id
prefKeyMeta = getattr(prefs, metaKeyId)
if(prefKeyMeta != metakeyData.key): # Changed entry
changedKeyIdx = i
break
if(changedKeyIdx != -1):
metaKeyId = metakeyData.id # loop broke at metakeyData
metakeyData.key = prefKeyMeta
if(prefKeyMeta == 'KEY'):
regKeyId = metaKeyId[0:metaKeyId.index('Meta')]
regKeyData = FTHotKeys.idDataMap[regKeyId]
prefKeyReg = getattr(prefs, regKeyId)
# Invalid regular key->assign available meta key to the dropdown entry
if(prefKeyReg == INVAL or \
FTHotKeys.isAssignedWithoutMeta(regKeyId, prefKeyReg)):
setattr(prefs, regKeyId, regKeyData.key)
else:
regKeyData.key = prefKeyReg
else:
# Change the other drop-down with the same key
availKey = FTHotKeys.getAvailKey(prefs)
for i, kd in enumerate(FTHotKeys.snapHotkeysMeta):
if(kd.key == prefKeyMeta and i != changedKeyIdx):
kd.key = availKey
setattr(prefs, kd.id, availKey)
break
FTHotKeys.updating = False
ModalBaseFlexiOp.propsChanged()
def keyCodeMap():
kcMap = {}
digits = ['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', \
'SIX', 'SEVEN', 'EIGHT', 'NINE']#, 'PERIOD']
numpadDigits = ['NUMPAD_' + d for d in digits]
# ~ 199: 'NUMPAD_PERIOD'
for i in range(26):
kcMap[i + 97] = chr(ord('A') + i)
# Digits not available, used for keyboard input
# ~ for i in range(10):
# ~ kcMap[i + 48] = digits[i]
for i in range(10):
kcMap[i + 150] = numpadDigits[i]
for i in range(12):
kcMap[i + 300] = 'F' + str(i + 1)
kcMap[161] = 'NUMPAD_SLASH'
kcMap[160] = 'NUMPAD_ASTERIX'
kcMap[162] = 'NUMPAD_MINUS'
kcMap[163] = 'NUMPAD_ENTER'
kcMap[164] = 'NUMPAD_PLUS'
kcMap[165] = 'PAUSE'
kcMap[166] = 'INSERT'
kcMap[167] = 'HOME'
kcMap[168] = 'PAGE_UP'
kcMap[169] = 'PAGE_DOWN'
kcMap[170] = 'END'
kcMap[199] = 'NUMPAD_PERIOD'
kcMap[219] = 'TAB'
kcMap[220] = 'RET'
kcMap[221] = 'SPACE'
kcMap[223] = 'BACK_SPACE'
kcMap[224] = 'DEL'
kcMap[225] = 'SEMI_COLON'
kcMap[226] = 'PERIOD'
kcMap[227] = 'COMMA'
kcMap[228] = "QUOTE"
kcMap[229] = 'ACCENT_GRAVE'
# ~ kcMap[230] = 'MINUS' # Reserved for keyboard input
kcMap[232] = 'SLASH'
kcMap[233] = 'BACK_SLASH'
kcMap[234] = 'EQUAL'
kcMap[235] = 'LEFT_BRACKET'
kcMap[236] = 'RIGHT_BRACKET'
return kcMap
# Drop down item tuples with 400 entries
# INVAL in all 3 fields for unavailable keycodes
# TODO: Very big drop-down for every hotkey (because of default)
def getKeyMapTupleStr():
kcMap = FTHotKeys.keyCodeMap()
tuples = []
exclKeys = FTHotKeys.exclKeys
for i in range(0, 400):
if(kcMap.get(i) != None and kcMap[i] not in exclKeys):
char = kcMap[i]
else:
char = INVAL
tuples.append((char, char, char))
return ''.join(["('" + t[0] + "','" + t[1] + "','" + t[2] +"')," \
for t in tuples])
def getHKFieldStr(keydata, addMeta):
propName = keydata.id
text = keydata.label
description = keydata.description
updateFn = 'FTHotKeys.updateHotkeys'
default = keydata.default
keys = default.split('+')
key = keys[-1]
# ~ "items = (('A', 'A','A'),('B', 'B', 'B')), " + \
retVal = propName + ": EnumProperty(name = '" + text + "', " + \
"items = (" + FTHotKeys.getKeyMapTupleStr() + "), " + \
(("default = '"+ key + "', ") if default != INVAL else '') + \
"update = " + updateFn + ", " + \
"description = '"+ description +"') \n"
if(addMeta):
for meta in FTHotKeys.metas:
retVal += propName + meta + ": BoolProperty(name='"+ meta +"', " + \
"description='"+ meta +" Key', " + \
"update = " + updateFn + ", " + \
"default = "+ ('True' if meta in keys[:-1] else 'False') +") \n"
return retVal
def getMetaHKFieldStr(keydataMeta):
propName = keydataMeta.id
text = keydataMeta.label
description = keydataMeta.description
updateFn = 'FTHotKeys.updateSnapMetaKeys'
default = keydataMeta.default
itemsStr = "('CTRL', 'Ctrl', 'Ctrl'), ('ALT', 'Alt', 'Alt'), \
('SHIFT', 'Shift', 'Shift'), ('KEY', 'Keyboard', 'Other keyboard key')"
retVal = propName + ": EnumProperty(name = '" + text + "', " + \
"items = (" + itemsStr + "), " + \
"default = '"+ default + "', " + \
"update = " + updateFn + ", " + \
"description = '"+ description +"') \n"
return retVal
def getHKDispLines(toolType):
hkData = [k for k in FTHotKeys.commonHotkeys if toolType not in k.exclTools]
for i, hk in enumerate(FTHotKeys.snapHotkeysMeta):
if(hk.key == 'KEY' and toolType not in hk.exclTools):
hkData.append(FTHotKeys.snapHotkeys[i])
else:
hkData.append(hk)
if(toolType in TOOL_TYPES_FLEXI_DRAW_COMMON):
hkData += [k for k in FTHotKeys.drawHotkeys if toolType not in k.exclTools]
if(toolType == TOOL_TYPE_FLEXI_EDIT):
hkData += [k for k in FTHotKeys.editHotkeys if toolType not in k.exclTools]
labels = []
config = []
keys = []
stdKeylabels = []
stdKeylabels.append(['Lock to YZ Plane', 'Shift+X'])
stdKeylabels.append(['Lock to XZ Plane', 'Shift+Y'])
stdKeylabels.append(['Lock to XY Plane', 'Shift+Z'])
stdKeylabels.append(['Lock to Z-Axis', 'Z'])
stdKeylabels.append(['Lock to Y-Axis', 'Y'])
stdKeylabels.append(['Lock to X-Axis', 'X'])
stdKeylabels.append(['Confirm Operation', 'Space / Enter'])
for k in hkData:
labels.append(k.label)
config.append(True)
keys.append(k.key)
for k in stdKeylabels:
labels.append(k[0])
config.append(False)
keys.append(k[1])
return config, labels, keys
# Flexi Tool Constants
class FTProps:
propUpdating = False
def updateProps(dummy, context):
FTProps.updatePropsPrefs(context)
def updatePropsPrefs(context, resetPrefs = False):
if(FTProps.propUpdating): return
FTProps.propUpdating = True
try:
prefs = context.preferences.addons[__name__].preferences
props = ['drawPtSize', 'lineWidth', 'axisLineWidth', 'editSubdivPtSize', \
'greaseSubdivPtSize', 'colDrawSelSeg', 'colDrawNonHltSeg', 'colDrawHltSeg', \
'colDrawMarker', 'colGreaseSelSeg', 'colGreaseNonHltSeg', 'colGreaseMarker', \
'colHdlFree', 'colHdlVector', 'colHdlAligned', 'colHdlAuto', 'colSelTip', \
'colHltTip', 'colBezPt', 'colHdlPtTip', 'colAdjBezTip', 'colEditSubdiv', \
'colGreaseSubdiv', 'colGreaseBezPt', 'colKeymapText', 'colKeymapKey', 'snapDist', \
'dispSnapInd', 'dispAxes', 'snapPtSize', 'liveUpdate', 'dispCurveRes', \
'showKeyMap', 'keyMapFontSize', 'keyMapLocX', 'keyMapLocY', 'keyMapNextToTool', \
'defBevelFact', 'maxBevelFact', 'minBevelFact', 'bevelIncr', 'numpadEntry', \
'mathFnTxtFontSize', 'colMathFnTxt']
if(resetPrefs):
FTProps.initDefault()
for prop in props:
exec('prefs.' + prop +' = FTProps.' + prop)
# ~ setattr(prefs, prop, getattr(FTProps, prop))
else:
for prop in props:
exec('FTProps.' + prop +' = prefs.' + prop)
# ~ setattr(FTProps, prop, getattr(prefs, prop))
except Exception as e:
print("BezierUtils: Error fetching default sizes in Draw Bezier", e)
FTProps.initDefault()
ModalBaseFlexiOp.propsChanged()
try: ModalBaseFlexiOp.opObj.refreshDisplaySelCurves()
except: pass
FTProps.propUpdating = False
def initDefault():
FTProps.drawPtSize = 5
FTProps.lineWidth = 1.5
FTProps.axisLineWidth = .25
FTProps.editSubdivPtSize = 6
FTProps.greaseSubdivPtSize = 4
FTProps.colDrawSelSeg = (.6, .8, 1, 1)
FTProps.colDrawNonHltSeg = (.1, .4, .6, 1)
FTProps.colDrawHltSeg = (.2, .6, .9, 1)
FTProps.colGreaseSelSeg = (0.2, .8, 0.2, 1)
FTProps.colGreaseNonHltSeg = (0.2, .6, 0.2, 1)
FTProps.colHdlFree = (.6, .05, .05, 1)
FTProps.colHdlVector = (.4, .5, .2, 1)
FTProps.colHdlAligned = (1, .3, .3, 1)
FTProps.colHdlAuto = (.8, .5, .2, 1)
FTProps.colDrawMarker = (.6, .8, 1, 1)
FTProps.colGreaseMarker = (0.2, .8, 0.2, 1)
FTProps.colSelTip = (.2, .7, .3, 1)
FTProps.colHltTip = (.8, 1, .8, 1)
FTProps.colBezPt = (1, 1, 0, 1)
FTProps.colHdlPtTip = (.7, .7, 0, 1)
FTProps.colAdjBezTip = (.1, .1, .1, 1)
FTProps.colEditSubdiv = (.3, 0, 0, 1)
FTProps.colGreaseSubdiv = (1, .3, 1, 1)
FTProps.colGreaseBezPt = (1, .3, 1, 1)
FTProps.colKeymapText = (1.0, 1.0, 1.0, 1.0)
FTProps.colKeymapKey = (0.0, 1.0, 1.0, 1.0)
FTProps.snapDist = 20
FTProps.dispSnapInd = False
FTProps.dispAxes = True
FTProps.showKeyMap = False
FTProps.keyMapFontSize = 10
FTProps.keyMapLocX = 10
FTProps.keyMapLocY = 10
FTProps.keyMapNextToTool = True
FTProps.snapPtSize = 3
FTProps.liveUpdate = False
FTProps.dispCurveRes = .4
FTProps.defBevelFact = 4
FTProps.maxBevelFact = 15
FTProps.minBevelFact = -15
FTProps.bevelIncr = .5
FTProps.numpadEntry = False
FTProps.mathFnTxtFontSize = 20
FTProps.colMathFnTxt = (0.6, 1.0, 0.03, 1.0)
class FTMenuData:
def __init__(self, hotkeyId, options, menuClassName, menuClassLabel, handler):
self.hotkeyId = hotkeyId
self.options = options
self.menuClassName = menuClassName
self.menuClassLabel = menuClassLabel
self.handler = handler
# TODO: Move to end to avoid eval/getattr due to forward referencing
class FTMenu:
propSuffix = 'ftMenuOpt'
# Edit
editMenus = []
editMenus.append(FTMenuData(FTHotKeys.hkMnHdlType, \
[['miHdlAuto', 'Auto', 'HANDLETYPE_AUTO_VEC'], \
['miHdlAligned', 'Aligned', 'HANDLETYPE_ALIGNED_VEC'], \
['miHdlFree', 'Free', 'HANDLETYPE_FREE_VEC'], \
['miHdlVector', 'Vector', 'HANDLETYPE_VECTOR_VEC']], \
'VIEW3D_MT_FlexiEditHdlMenu', 'Set Handle Type', 'mnSetHdlType'))
editMenus.append(FTMenuData(FTHotKeys.hkMnSelect, \
[['miSelSegs', 'Segments', 'GP_SELECT_BETWEEN_STROKES'], \
['miSelAllSplines', 'All Splines', 'GP_MULTIFRAME_EDITING'], \
['miSelBezPts', 'Bezier Points', 'GP_ONLY_SELECTED'], \
['miSelHdls', 'Handles', 'CURVE_BEZCURVE'], \
['miSelObj', 'Curve Object', 'GP_SELECT_STROKES'], \
['miSelAll', 'Everything', 'SELECT_EXTEND']], \
'VIEW3D_MT_FlexiEditSelMenu', 'Select', 'mnSelect'))
editMenus.append(FTMenuData(FTHotKeys.hkMnDeselect, \
[['miDeselSegs', 'Segments', 'GP_SELECT_BETWEEN_STROKES'], \
['miDeselBezPts', 'Bezier Points', 'GP_SELECT_POINTS'], \
['miDeselHdls', 'Handles', 'CURVE_BEZCURVE'], \
['miDeselObj', 'Curve Object', 'GP_SELECT_STROKES'], \
['miDeselInvert', 'Invert Selection', 'SELECT_SUBTRACT']], \
'VIEW3D_MT_FlexiEditDeselMenu', 'Deselect', 'mnDeselect'))
idDataMap = {m.hotkeyId: m for m in editMenus}
toolClassMap = {'ModalFlexiEditBezierOp': set([m.hotkeyId for m in editMenus])}
toolTypeMap = {'ModalFlexiEditBezierOp': TOOL_TYPE_FLEXI_EDIT}
currMenuId = None
abandoned = False
def getMenuData(caller, hotkeyId):
keyIds = FTMenu.toolClassMap.get(caller.__class__.__name__)
if(keyIds != None and hotkeyId in keyIds):
return FTMenu.idDataMap.get(hotkeyId)
return None
def procMenu(parent, context, event, outside):
metakeys = parent.snapper.getMetakeys()
evtType = event.type
if(FTMenu.abandoned == True and evtType == 'ESC'):
FTMenu.abandoned = False
return True
if(FTMenu.currMenuId != None):
params = bpy.context.window_manager.bezierToolkitParams
opt = FTMenu.getCurrMenuSel()
if(opt != None or not evtType.startswith('TIMER')): # What's TIMER_REPORT?
context.window_manager.event_timer_remove(parent.menuTimer)
menuData = FTMenu.idDataMap.get(FTMenu.currMenuId)
FTMenu.currMenuId = None
if(evtType == 'TIMER'):
fn = getattr(parent, menuData.handler)
fn(opt)
else:
FTMenu.abandoned = True
return True
if(outside): return False
parentClassName = parent.__class__.__name__
hkData = FTHotKeys.getHotKeyData(FTMenu.toolTypeMap.get(parentClassName), \
evtType, metakeys)
if(hkData == None): return False
menuData = FTMenu.getMenuData(parent, hkData.id)
if(menuData == None): return False
if(event.value == 'RELEASE'):
FTMenu.resetMenuOptions()
FTMenu.currMenuId = menuData.hotkeyId
parent.menuTimer = \
context.window_manager.event_timer_add(time_step = 0.0001, \
window = context.window)
ret = bpy.ops.wm.call_menu_pie(name = menuData.menuClassName)
return True
def getCurrMenuSel():
menuData = FTMenu.idDataMap.get(FTMenu.currMenuId)
if(menuData != None):
params = bpy.context.window_manager.bezierToolkitParams
for i, propName in enumerate(FTMenu.getAllOptPropNames()):
if(getattr(params, propName)): return menuData.options[i]
return None
def resetMenuOptions():
params = bpy.context.window_manager.bezierToolkitParams
for propName in FTMenu.getAllOptPropNames():
setattr(params, propName, False)
def getAllOptPropNames():
propNames = []
maxMenuOpts = max(len(m.options) for m in FTMenu.idDataMap.values())
for i in range(maxMenuOpts):
propNames.append(FTMenu.propSuffix + str(i))
return propNames
def getMNClassDefStr(menuData):
retStr = 'class ' + menuData.menuClassName + '(Menu):\n' + \
'\tbl_label = "' + menuData.menuClassLabel + '"\n'+ \
'\tdef draw(self, context): \n' + \
'\t\tparams = bpy.context.window_manager.bezierToolkitParams\n' + \
'\t\tlayout = self.layout\n' + \
'\t\tpie = layout.menu_pie()\n'
for i, opt in enumerate(menuData.options):
retStr += '\t\top = pie.operator("object.ft_menu_options", text = "'+ \
opt[1] + '"' + \
((', icon = "'+ opt[2] +'"') if(opt[2] != '') else '') + ')\n'
retStr += '\t\top.optIdx = ' + str(i) + '\n'
return retStr
def getMNPropDefStr(menuData):
retStr = ''
for propName in FTMenu.getAllOptPropNames():
retStr += propName +': BoolProperty(default = False)\n'
return retStr
class FTMenuOptionOp(Operator):
bl_idname = "object.ft_menu_options"
bl_label = "Set FT Menu Options"
bl_description = "Set option"
optIdx : IntProperty()
def execute(self, context):
FTMenu.resetMenuOptions()
params = bpy.context.window_manager.bezierToolkitParams
setattr(params, FTMenu.propSuffix + str(self.optIdx), True)
return {'FINISHED'}
class SnapDigits:
digitMap = {'ZERO':'0', 'ONE':'1', 'TWO':'2', 'THREE':'3', 'FOUR':'4', 'FIVE':'5', \
'SIX':'6', 'SEVEN':'7', 'EIGHT':'8', 'NINE':'9', 'PERIOD':'.'}
numpadDigitMap = {'NUMPAD_0':'0', 'NUMPAD_1':'1', 'NUMPAD_2':'2', 'NUMPAD_3':'3', \
'NUMPAD_4':'4', 'NUMPAD_5':'5', 'NUMPAD_6':'6', 'NUMPAD_7':'7', 'NUMPAD_8':'8', \
'NUMPAD_9':'9', 'NUMPAD_PERIOD': '.'}
def getValidFloat(sign, digits):
delta = 0
valid = True
for i in range(len(digits), 0, -1):
td = digits[0:i]
try:
delta = float(sign + ''.join(td))
return delta, valid
except:
valid = False
return delta, valid
def __init__(self, getFreeAxes, getEditCoPair):
self.getFreeAxes = getFreeAxes
self.getEditCoPair = getEditCoPair
self.initialize()
def initialize(self):
self.axes = None # [0, 1, 2] etc.
self.deltaVec = None # Always cartesian
self.axisIdx = 0
self.digitChars = []
self.signChar = ''
self.polar = False
self.pDataIdx = 0 # Corresponding to axisIdx
def hasVal(self):
return self.axes != None
# Theta in degrees
def getPolarCos(self):
axis0, axis1 = self.getFreeAxes()[0], self.getFreeAxes()[1]
val0, val1 = self.deltaVec[axis0], self.deltaVec[axis1]
if(val0 == 0):
if(val1 == 0): return 0, 0
theta = (val1 / abs(val1)) * 90
else:
theta = degrees(atan(abs(val1) / abs(val0)))
if(val0 < 0): theta = 180 - theta
if(val1 < 0): theta = -theta
r = sqrt(val1 * val1 + val0 * val0)
return [r, theta]
# Theta in degrees
def getDeltaFromPolar(self, polCos):
r, theta = polCos
return r * cos(radians(theta)), r * sin(radians(theta))
def addToPolar(self, delta):
polCos = list(self.getPolarCos())
polCos[self.pDataIdx] += delta
val0, val1 = self.getDeltaFromPolar(polCos)
return val0, val1
def digitsToVec(self):
delta, valid = SnapDigits.getValidFloat(self.signChar, self.digitChars)
if(not self.polar or self.pDataIdx == 0): delta /= getUnitScale()
if(self.polar):
axis0, axis1 = self.getFreeAxes()[0], self.getFreeAxes()[1]
self.deltaVec[axis0], self.deltaVec[axis1] = self.addToPolar(delta)
else:
self.deltaVec[self.axisIdx] += delta
def vecToDigits(self):
if(self.polar):
vals = self.getPolarCos()
v = round(vals[self.pDataIdx], 4)
else:
v = round(self.deltaVec[self.axisIdx], 4)
self.digitChars = list(str(abs(v))) if v != 0 else ''
while(len(self.digitChars) > 0 and self.digitChars[-1] == '0'):
self.digitChars.pop()
if(len(self.digitChars) > 0 and self.digitChars[-1] == '.'):
self.digitChars.pop()
self.signChar = '-' if v < 0 else ''
def updateDeltaFromEditCos(self):
editCos = self.getEditCoPair()
self.deltaVec = editCos[1] - editCos[0]
def procEvent(self, context, event, metakeys):
if(FTHotKeys.isHotKey(FTHotKeys.hkTweakPos, event.type, metakeys)):
editCos = self.getEditCoPair()
if(len(editCos) == 2):
if(event.value == 'RELEASE'):
if(self.axes == None):
self.axes = self.getFreeAxes() # TODO: Always dynamic?
self.polar = False
# Only possible within a plane right now
elif(len(self.getFreeAxes()) == 2):
self.polar = not self.polar
self.pDataIdx = 0
self.updateDeltaFromEditCos()
self.digitChars = []
self.axisIdx = self.axes[0]
return True
dmap = self.digitMap.copy()
if(FTProps.numpadEntry):
dmap.update(self.numpadDigitMap)
dval = dmap.get(event.type)
if(dval != None):
if(event.value == 'RELEASE'):
self.digitChars.append(dval)
if(self.axes == None):
self.axes = self.getFreeAxes() # TODO: Always dynamic?
self.deltaVec = Vector()
self.axisIdx = self.axes[0]
return True
if(not self.hasVal()): # No further processing if nothing to process
return False
retVal = True
if(event.type == 'MINUS'):
if(event.value == 'RELEASE'):
self.signChar = '' if(self.signChar == '-') else '-'
elif(event.type == 'ESC'):
if(event.value == 'RELEASE'):
self.initialize()
elif(event.type == 'BACK_SPACE'):
if(event.value == 'RELEASE'):
if(len(self.digitChars) > 0): self.digitChars.pop()
else:
if(self.polar):
polarCos = self.getPolarCos()
if(not floatCmpWithMargin(polarCos[self.pDataIdx], 0)):
self.vecToDigits()
# TODO: Retain theta without this workaround
polarCos[self.pDataIdx] = 0.00001
delta = self.getDeltaFromPolar(polarCos)
axis0, axis1 = self.getFreeAxes()[0], self.getFreeAxes()[1]
self.deltaVec[axis0] = delta[0]
self.deltaVec[axis1] = delta[1]
else:
if(self.deltaVec[self.axisIdx] != 0):
self.vecToDigits()
self.deltaVec[self.axisIdx] = 0
elif(event.type == 'TAB'):
if(event.value == 'RELEASE'):
self.digitsToVec()
if(self.polar):
self.pDataIdx = (self.pDataIdx + 1) % 2
else:
self.axisIdx = \
self.axes[(self.axes.index(self.axisIdx) + 1) % len(self.axes)]
self.digitChars = []
self.signChar = ''
else:
retVal = False
return retVal
def getCurrDelta(self):
if(self.axes == None): return Vector()
val = self.deltaVec.copy()
delta, valid = SnapDigits.getValidFloat(self.signChar, self.digitChars)
if(not self.polar or self.pDataIdx == 0): delta /= getUnitScale()
if(self.polar):
axis0, axis1 = self.getFreeAxes()[0], self.getFreeAxes()[1]
val[axis0], val[axis1] = self.addToPolar(delta)
else:
val[self.axisIdx] += delta
return val
def getDeltaStrPolar(self):
delta, valid = SnapDigits.getValidFloat(self.signChar, self.digitChars)
polCos = self.getPolarCos()
polCos[0] *= getUnitScale()
strs = ['['] * 2
idx0 = self.pDataIdx
idx1 = 1 - self.pDataIdx
d = polCos[idx0]
if(d != 0):
strs[idx0] += str(round(d, 4)) + ('+' if (self.signChar == '') else '')
strs[idx0] += self.signChar + ''.join(self.digitChars) + '] = ' \
+ str(round((delta + d), 4)) + ('' if valid else ' ')
strs[idx1] = '[' + str(round(polCos[1 - self.pDataIdx], 4)) + ']'
return 'r: '+strs[0] + ' theta: '+ strs[1]
def getCurrDeltaStr(self):
delta, valid = SnapDigits.getValidFloat(self.signChar, self.digitChars)
retStr = '['
d = self.deltaVec[self.axisIdx] * getUnitScale()
if(d != 0):
retStr += str(round(d, 4)) + ('+' if (self.signChar == '') else '')
retStr += self.signChar + ''.join(self.digitChars) + '] = ' \
+ str(round((delta + d), 4)) + ('' if valid else ' ')
return retStr
class CustomAxis:
def __init__(self):
#TODO: What's better?
if(bpy.data.scenes[0].get('btk_co1') == None):
bpy.data.scenes[0]['btk_co1'] = LARGE_VECT
if(bpy.data.scenes[0].get('btk_co2') == None):
bpy.data.scenes[0]['btk_co2'] = LARGE_VECT
self.axisPts = [Vector(bpy.data.scenes[0]['btk_co1']), \
Vector(bpy.data.scenes[0]['btk_co2'])]
if(bpy.data.scenes[0].get('btk_snapPtCnt') == None):
bpy.data.scenes[0]['btk_snapPtCnt'] = 3
self.snapCnt = bpy.data.scenes[0]['btk_snapPtCnt']
self.inDrawAxis = False # User drawing the custom axis
def length(self):
pts = self.axisPts
# Strange floating points!
if(all(pt < (LARGE_NO - 1000) for pt in pts[0] + pts[1])):
return (pts[1] - pts[0]).length
else:
return 0
def set(self, idx, co):
self.axisPts[idx] = co
if(idx == 0): bpy.data.scenes[0]['btk_co1'] = [c for c in co]
if(idx == 1): bpy.data.scenes[0]['btk_co2'] = [c for c in co]
bpy.data.scenes[0]['btk_snapPtCnt'] = self.snapCnt
def getSnapPts(self): # ptCnt excluding end points
pts = self.axisPts
if(self.length() == 0): return [pts[0], pts[1]]
snapPts = [pts[0]]
interval = self.snapCnt + 1
incr = self.length() / interval
diffV = pts[1] - pts[0]
for i in range(1, interval):
snapPts.append(pts[0] + (diffV * (incr * i) / self.length()) )
snapPts.append(pts[1])
return snapPts
def procDrawEvent(self, context, event, snapper, rmInfo):
if(event.type == 'RIGHTMOUSE'):
snapOrigin = bpy.context.window_manager.bezierToolkitParams.snapOrigin
if(event.value == 'RELEASE' and snapOrigin == 'AXIS'):
loc = snapper.get3dLocSnap(rmInfo, \
SnapParams(snapper, snapToAxisLine = False))
if(not self.inDrawAxis): self.set(0, loc)
else: self.set(1, loc)
self.inDrawAxis = not self.inDrawAxis
return True
if(self.inDrawAxis):
if(event.type in {'WHEELDOWNMOUSE', 'WHEELUPMOUSE', 'NUMPAD_PLUS', \
'NUMPAD_MINUS','PLUS', 'MINUS'}):
if(event.type in {'NUMPAD_PLUS', 'NUMPAD_MINUS', 'PLUS', 'MINUS'} \
and event.value == 'PRESS'):
return True
elif(event.type =='WHEELUPMOUSE' or event.type.endswith('PLUS')):
if(self.snapCnt < 20): self.snapCnt += 1
elif(event.type =='WHEELDOWNMOUSE' or event.type.endswith('MINUS')):
if(self.snapCnt > 0): self.snapCnt -= 1
if(event.type == 'MOUSEMOVE'):
loc = snapper.get3dLocSnap(rmInfo, \
SnapParams(snapper, snapToAxisLine = False))
self.set(1, loc)
if(event.type == 'ESC'):
self.set(0, LARGE_VECT)
self.set(1, LARGE_VECT)
self.inDrawAxis = False
return True
return False
# TODO: Make independent of snapper
class SnapParams:
def __init__(self, \
snapper, \
xyDelta = [0, 0], \
vec = None, \
refreshStatus = True, \
snapToAxisLine = True, \
lastCo1Axis = False, \
enableSnap = True, \
vertSnap = None, \
gridSnap = None, \
angleSnap = None, \
refLine = None, \
refLineOrig = None, \
selCo = None, \
inEdit = None, \
hasSel = None, \
transType = None, \
origType = None, \
axisScale = None, \
freeAxesN = None, \
dispAxes = True,
snapToPlane = None):
self.xyDelta = xyDelta
self.vec = vec
self.refreshStatus = refreshStatus
self.snapToAxisLine = snapToAxisLine
self.lastCo1Axis = lastCo1Axis
if(enableSnap):
self.vertSnap = snapper.vertSnap if(vertSnap == None) else vertSnap
self.gridSnap = snapper.gridSnap if(gridSnap == None) else gridSnap
self.angleSnap = snapper.angleSnap if(angleSnap == None) else angleSnap
else:
self.vertSnap = False
self.gridSnap = False
self.angleSnap = False
self.refLine = snapper.getRefLine() if(refLine == None) else refLine
self.refLineOrig = snapper.getRefLineOrig() if(refLineOrig == None) \
else refLineOrig
self.selCo = snapper.getSelCo() if(selCo == None) else selCo
self.inEdit = snapper.isEditing() if(inEdit == None) else inEdit
self.hasSel = snapper.hasSelection() if(hasSel == None) else hasSel
params = bpy.context.window_manager.bezierToolkitParams
self.transType = params.snapOrient if(transType == None) else transType
self.origType = params.snapOrigin if(origType == None) else origType
self.axisScale = params.axisScale if(axisScale == None) else axisScale
self.freeAxesN = snapper.getFreeAxesNormalized() \
if(freeAxesN == None) else freeAxesN
self.dispAxes = dispAxes
if(snapToPlane != None):
self.snapToPlane = snapToPlane
else:
if(showSnapToPlane(params)): # Only if Snap to Plane option is visible
self.snapToPlane = params.snapToPlane
else: self.snapToPlane = False
class Snapper:
DEFAULT_ANGLE_SNAP_STEPS = 3
MAX_SNAP_VERT_CNT = 1000
MAX_SNAP_FACE_CNT = 1000
def __init__(self, context, getSnapLocs, getRefLine, getRefLineOrig, getSelCo, \
getCurrLine, hasSelection, isEditing):
self.getSnapLocs = getSnapLocs
self.getRefLine = getRefLine
self.getCurrLine = getCurrLine
self.getRefLineOrig = getRefLineOrig
self.getSelCo = getSelCo
self.hasSelection = hasSelection
self.isEditing = isEditing
self.angleSnapSteps = Snapper.DEFAULT_ANGLE_SNAP_STEPS
self.customAxis = CustomAxis()
self.snapDigits = SnapDigits(self.getSnapParamsFreeAxes, self.getEditCoPair)
self.initialize()
self.updateSnapKeyMap()
# ~ bpy.context.space_data.overlay.show_axis_x = False
# ~ bpy.context.space_data.overlay.show_axis_y = False
def resetMetakeys(self):
# metakeys for all functions
self.shift = False
self.ctrl = False
self.alt = False
def resetSnapKeys(self):
self.angleSnap = False
self.gridSnap = False
self.vertSnap = False
def initialize(self):
self.resetMetakeys()
self.resetSnapKeys()
self.tm = None
self.orig = None
self.snapCo = None
self.freezeOrient = False
self.lastSnapTypes = set()
self.resetSnap()
def updateSnapKeyMap(self):
self.snapKeyMap = {}
kIds = [FTHotKeys.hkSnapVert, FTHotKeys.hkSnapAngle, FTHotKeys.hkSnapGrid]
varMap = {FTHotKeys.hkSnapVert: 'vertSnap', \
FTHotKeys.hkSnapAngle: 'angleSnap', FTHotKeys.hkSnapGrid: 'gridSnap'}
for kId in kIds:
keys = FTHotKeys.getSnapHotKeys(kId)
for key in keys:
self.snapKeyMap[key] = varMap[kId]
def resetSnap(self): # Called even during isEditing
self.freeAxes = [] # All axes free
self.snapDigits.initialize()
self.rmInfo = None
self.snapParams = None
# This variable lets caller know that return was pressed after digits were entered
# Caller can reset snapper as per convenience
self.digitsConfirmed = False
self.lastSelCo = None
# ~ self.snapStack = [] # TODO
# Return user selection in header menu, [] for None
# (Tightly coupled with strings in BezierToolkitParams)
def getFreeAxesGlobal(self):
constrAxes = bpy.context.window_manager.bezierToolkitParams.constrAxes
if(constrAxes != 'NONE'):
idx = constrAxes.find('-')
axis = constrAxes[idx + 1]
freeAxes = [ord(axis) - ord('X')]
if(idx > 0): freeAxes = sorted(list({0, 1, 2} - set(freeAxes)))
return freeAxes
else: return []
# Return actual free axes (menu + hotkey combined), [] for None
def getFreeAxesCombined(self):
constrAxes = self.getFreeAxesGlobal()
if(len(constrAxes) == 0): return self.freeAxes
if(len(self.freeAxes) == 0): return constrAxes
freeAxes = sorted(list(set(constrAxes).intersection(set(self.freeAxes))))
if(len(freeAxes) == 0): return constrAxes
else: return freeAxes
# Return actual free axes (menu + hotkey combined), [0, 1, 2] for None
def getFreeAxesNormalized(self):
if(len(self.getFreeAxesCombined()) == 0): return [0, 1, 2]
else: return self.getFreeAxesCombined()
def getSnapParamsFreeAxes(self):
if(self.snapParams != None):
return self.snapParams.freeAxesN
else:
return self.getFreeAxesNormalized()
def getCurrOrig(self, rmInfo, obj, origType, refLineOrig, selCo):
if(origType == 'AXIS'):
if(self.customAxis.length() != 0): return self.customAxis.axisPts[0]
elif(origType == 'REFERENCE'):
if(refLineOrig != None): return refLineOrig
elif(origType == 'CURR_POS'):
if(selCo != None): return selCo
elif(origType == 'OBJECT' and obj != None): return obj.location
elif(origType == 'FACE' and rmInfo != None):
selObj, location, normal, faceIdx = getSelFaceLoc(rmInfo.region, \
rmInfo.rv3d, rmInfo.xy, self.MAX_SNAP_FACE_CNT)
if(faceIdx != None):
return selObj.matrix_world @ selObj.data.polygons[faceIdx].center
elif(origType == 'CURSOR'): return bpy.context.scene.cursor.location
return Vector((0, 0, 0))
def getTransMatsForOrient(self, rmInfo, obj, transType, axisScale):
custAxis = self.customAxis
if(abs(custAxis.length()) <= DEF_ERR_MARGIN): custAxis = None
refLine = self.getRefLine()
currLine = self.getCurrLine()
if(refLine != None and len(refLine) < 2): refLine = None
if(currLine != None and len(currLine) < 2): currLine = None
tmScale = Matrix()
if(custAxis != None and axisScale == 'AXIS'):
unitD = custAxis.length() / (custAxis.snapCnt + 1)
tmScale = Matrix.Scale(1 / unitD, 4)
elif(refLine != None and axisScale == 'REFERENCE'):
unitD = (refLine[1] - refLine[0]).length / 10
if(unitD > DEF_ERR_MARGIN):
tmScale = Matrix.Scale(1 / unitD, 4)
tm = None
if(transType == 'AXIS' and custAxis != None):
tm, invTm = getLineTransMatrices(custAxis.axisPts[0], custAxis.axisPts[1])
elif(transType == 'REFERENCE' and refLine != None):
tm, invTm = getLineTransMatrices(refLine[0], refLine[1])
elif(transType == 'CURR_POS' and currLine != None):
tm, invTm = getLineTransMatrices(currLine[0], currLine[1])
elif(transType == 'VIEW'):
tm = rmInfo.rv3d.view_matrix
elif(obj != None and transType == 'OBJECT'):
tm = (obj.matrix_world).inverted_safe()
elif(transType == 'FACE'):
selObj, location, normal, faceIdx = getSelFaceLoc(rmInfo.region, \
rmInfo.rv3d, rmInfo.xy, self.MAX_SNAP_FACE_CNT)
if(faceIdx != None):
normal = selObj.data.polygons[faceIdx].normal
quat = normal.to_track_quat('Z', 'X').to_matrix().to_4x4()
tm = (selObj.matrix_world @ quat).inverted_safe()
if(tm != None):
trans, quat, scale = tm.decompose()
tm = quat.to_matrix().to_4x4() @ tmScale
else:
tm = tmScale
return tm, tm.inverted_safe()
def isLocked(self):
return len(self.freeAxes) > 0 or \
(self.snapDigits.hasVal() and not self.digitsConfirmed)
def getMetakeys(self):
return [self.alt, self.ctrl, self.shift]
# To be called in modal method of parent
def procEvent(self, context, event):
# update ctrl etc.
retValMeta = updateMetaBtns(self, event)
retValSnap = updateMetaBtns(self, event, self.snapKeyMap)
if(retValMeta or retValSnap): return EVT_META_OR_SNAP
metakeys = self.getMetakeys()
refLineOrig = self.snapParams.refLineOrig if self.snapParams != None \
else self.getRefLineOrig()
if(refLineOrig != None):
snapDProc = self.snapDigits.procEvent(context, event, metakeys)
if(snapDProc):
self.digitsConfirmed = False # Always reset if there was any digit entered
return EVT_CONS
if(FTHotKeys.isHotKey(FTHotKeys.hkReorient, event.type, metakeys)):
if(event.value == 'RELEASE'):
self.freezeOrient = not self.freezeOrient
return EVT_CONS
if(not self.ctrl and event.type in {'X', 'Y', 'Z'}):
self.digitsConfirmed = False # Always reset if there is any lock axis
if(event.value == 'RELEASE'):
self.freeAxes = [ord(event.type) - ord('X')]
if(self.shift):
self.freeAxes = sorted(list({0, 1, 2} - set(self.freeAxes)))
# if already part of global axes, don't store (no escape needed)
if(self.getFreeAxesCombined() == self.getFreeAxesGlobal()):
self.freeAxes = []
return EVT_CONS
retVal = EVT_NOT_CONS
# Consume escape or return / space only if there's something to process
if(self.isLocked()):
retVal = EVT_CONS
if(event.type == 'RET' or event.type == 'SPACE'):
if(event.value == 'RELEASE'):
# ~ self.resetSnap() # This is the responsibility of the caller
self.digitsConfirmed = True # Confirm first time
elif(event.type == 'ESC'):
if(event.value == 'RELEASE'): self.resetSnap()
else:
retVal = EVT_NOT_CONS
return retVal
def getStatusStr(self, unit, invTm, refPt, newPt):
manualEntry = self.snapDigits.hasVal()
if(manualEntry and self.snapDigits.polar):
return self.snapDigits.getDeltaStrPolar()
axes = self.getSnapParamsFreeAxes()
diffV = self.snapDigits.getCurrDelta() \
if manualEntry else (newPt - refPt)
diffV *= getUnitScale()
diffVActual = invTm @ diffV
retStr = ''
transformed = invTm != Matrix()
axisDeltaFormat = 'D{axis}: {axisDelta}'
axisDiffFormat = '{{{axisDiff}}} '
for i, d in enumerate(diffV):
if(i not in axes): continue
v1 = chr(ord('x') + i)
if(manualEntry and i == self.snapDigits.axisIdx):
v2 = self.snapDigits.getCurrDeltaStr()
else:
v2 = str(round(d, 4))
retStr += axisDeltaFormat.format(axis = v1, axisDelta = v2)
if(transformed):
v3 = str(round(diffVActual[i], 4))
retStr += axisDiffFormat.format(axisDiff = v3)
retStr += ' '
unitT = ''
unitA = ''
if(transformed): unitA = unit
else: unitT = unit
totalDeltaFormat = '({totalDelta}{unit})'
diffVStr = str(round(diffV.length, 4))
retStr += totalDeltaFormat.format(totalDelta = diffVStr, unit = unitT)
totalDiffVFormat = '{{{totalDiffV}{unit}}}'
if(transformed):
diffVStr = str(round(diffVActual.length, 4))
retStr += totalDiffVFormat.format(totalDiffV = diffVStr, unit = unitA)
return retStr
def getAllSnapLocs(self, snapToAxisLine):
snapLocs = self.getSnapLocs()
snapLocs.append(bpy.context.scene.cursor.location)
snapLocs.append(Vector((0, 0, 0)))
if(snapToAxisLine):
snapLocs += self.customAxis.getSnapPts()
vertCnt = 0
aos = [bpy.context.object] if bpy.context.object != None else []
objs = bpy.context.selected_objects + aos
for obj in objs:
snapLocs.append(obj.location)
if(obj.type == 'MESH'):
if(vertCnt + len(obj.data.vertices) < self.MAX_SNAP_VERT_CNT):
snapLocs += [obj.matrix_world @ v.co for v in obj.data.vertices]
vertCnt =+ len(obj.data.vertices)
else:
break
return snapLocs
def getTMInfoAndOrig(self, rmInfo, transType, origType, freezeOrient, \
axisScale, refLineOrig, selCo):
obj = bpy.context.object
if(self.tm != None and self.freezeOrient and transType == 'FACE'):
tm, invTm = self.tm, self.tm.inverted_safe()
else:
tm, invTm = self.getTransMatsForOrient(rmInfo, obj, transType, axisScale)
if(self.orig != None and freezeOrient and origType == 'FACE'):
orig = self.orig
else:
orig = self.getCurrOrig(rmInfo, obj, origType, refLineOrig, selCo)
return tm, invTm, orig
def get3dLocSnap(self, rmInfo, snapParams = None):
if(snapParams == None): snapParams = SnapParams(self)
self.rmInfo = rmInfo
self.snapParams = snapParams
refreshStatus = snapParams.refreshStatus
snapToAxisLine = snapParams.snapToAxisLine
lastCo1Axis = snapParams.lastCo1Axis
vertSnap = snapParams.vertSnap
gridSnap = snapParams.gridSnap
angleSnap = snapParams.angleSnap
refLine = snapParams.refLine
refLineOrig = snapParams.refLineOrig
selCo = snapParams.selCo
inEdit = snapParams.inEdit
hasSel = snapParams.hasSel
transType = snapParams.transType
origType = snapParams.origType
axisScale = snapParams.axisScale
vec = snapParams.vec
freeAxesN = snapParams.freeAxesN
snapToPlane = snapParams.snapToPlane
self.snapCo = None
region = rmInfo.region
rv3d = rmInfo.rv3d
xy = [rmInfo.xy[0] - snapParams.xyDelta[0], \
rmInfo.xy[1] - snapParams.xyDelta[1]]
loc = None
tm, invTm, orig = self.getTMInfoAndOrig(rmInfo, transType, \
origType, self.freezeOrient, axisScale, refLineOrig, selCo)
# Must be done after the call to getTMInfoAndOrig
if(hasSel): self.freezeOrient = True
vec = snapParams.vec if (snapParams.vec != None) else orig
self.lastSnapTypes = set()
unit = unitMap.get(getUnit())
if(unit == None): unit = ''
digitsValid = True
# ~ freeAxesC = self.getFreeAxesCombined()
# ~ freeAxesN = self.getFreeAxesNormalized()
# ~ freeAxesG = self.getFreeAxesGlobal()
if(FTProps.dispSnapInd or vertSnap):
#TODO: Called very frequently (store the tree [without duplicating data])
snapLocs = self.getAllSnapLocs((snapToAxisLine and \
'AXIS' in {transType, origType, axisScale})) + [orig]
kd = kdtree.KDTree(len(snapLocs))
for i, l in enumerate(snapLocs):
kd.insert(getCoordFromLoc(region, rv3d, l).to_3d(), i)
kd.balance()
coFind = Vector(xy).to_3d()
searchResult = kd.find_range(coFind, FTProps.snapDist)
if(len(searchResult) != 0):
co, idx, dist = min(searchResult, key = lambda x: x[2])
self.snapCo = snapLocs[idx]
if(vertSnap):
if(self.snapCo != None):
loc = self.snapCo
else:
selObj, loc, normal, faceIdx = getSelFaceLoc(region, rv3d, xy, \
self.MAX_SNAP_FACE_CNT, checkEdge = True)
if(loc != None):
loc = tm @ loc
self.lastSnapTypes.add('loc')
else:
loc = region_2d_to_location_3d(region, rv3d, xy, vec)
loc = tm @ loc
# TODO: Get gridSnap and angleSnap out of this if
if((transType != 'GLOBAL' and inEdit) or \
snapToPlane or gridSnap or \
self.snapDigits.hasVal() or \
(inEdit and (len(freeAxesN) < 3 or angleSnap))):
# snapToPlane means global constrain axes selection is a plane
# ~ if(snapToPlane or refLineOrig == None): refCo = orig
# ~ else: refCo = refLineOrig
refCo = tm @ orig
if(self.snapDigits.hasVal()):
delta = self.snapDigits.getCurrDelta()
loc = tm @ orig + delta
self.lastSnapTypes.add('keyboard')
else:
# Special condition for lock to single axis
# ~ if(len(freeAxesN) == 1 and refLineOrig != None):
# ~ refCo = tm @ refLineOrig
if(len(freeAxesN) == 2):
constrAxes = freeAxesN
loc = refCo.copy()
# Any other two points on the plane
ppt1 = loc.copy()
ppt1[constrAxes[0]] = loc[constrAxes[0]] + 10
ppt2 = loc.copy()
ppt2[constrAxes[1]] = loc[constrAxes[1]] + 10
ppt3 = ppt2.copy()
ppt2[constrAxes[0]] = loc[constrAxes[0]] + 10
# Raycast from 2d point onto the plane
pt = getPtProjOnPlane(region, rv3d, xy, invTm @ loc, \
invTm @ ppt1, invTm @ ppt2, invTm @ ppt3)
for axis in constrAxes:
# TODO: Better handling of boundary condition
if(pt == None or pt[axis] > 1000):
loc[axis] = refCo[axis]
else: loc[axis] = (tm @ pt)[axis]
self.lastSnapTypes.add('axis2')
if(len(freeAxesN) == 1):
if(lastCo1Axis): refCo = self.lastSelCo #TODO: More testing
axis = freeAxesN[0]
# Any one point on axis
ptOnAxis = refCo.copy()
# Convert everything to 2d
lastCo2d = getCoordFromLoc(region, rv3d, invTm @ refCo)
# Very small distance so that the point is on viewport
# TODO: This is not foolproof :(
ptOnAxis[axis] += .01
ptOnAxis2d = getCoordFromLoc(region, rv3d, invTm @ ptOnAxis)
# Find 2d projection (needed)
pt2d = geometry.intersect_point_line(xy, lastCo2d, ptOnAxis2d)[0]
# Any other 2 points on the plane, on which the axis lies
ppt1 = refCo.copy()
ppt1[axis] += 10
newAxis = [i for i in range(0, 3) if i != axis][0]
ppt2 = refCo.copy()
ppt2[newAxis] += 10
# Raycast from 2d point onto the plane
pt = getPtProjOnPlane(region, rv3d, pt2d, \
invTm @ refCo, invTm @ ppt1, invTm @ ppt2)
loc = refCo.copy()
if(pt == None or pt[axis] > 1000):
loc[axis] = refCo[axis]
else: loc[axis] = (tm @ pt)[axis]
self.lastSnapTypes.add('axis1')
if(not self.snapDigits.hasVal() and gridSnap):
if(axisScale in {'AXIS' or 'REFERENCE'}):
# Independent of view distance
diffV = (loc - refCo)
loc = refCo + round(diffV.length) * (diffV / diffV.length)
else:
rounding = getViewDistRounding(rmInfo.space3d, rv3d)
loc = tm @ roundedVect(rmInfo.space3d, invTm @ loc, \
rounding, freeAxesN)
self.lastSnapTypes.add('grid')
if(not self.snapDigits.hasVal() and angleSnap and len(refLine) > 0):
# ~ freeAxesC = [0, 1, 2] if len(freeAxesC) == 0 else freeAxesC
snapStart = tm @ orig
actualLoc = loc.copy()
#First decide the main movement axis
diff = [abs(v) for v in (actualLoc - snapStart)]
maxDiff = max(diff)
axis = diff.index(maxDiff)
loc = snapStart.copy()
loc[axis] = actualLoc[axis]
snapIncr = 45 / self.angleSnapSteps
snapAngles = [radians(snapIncr * a) \
for a in range(0, self.angleSnapSteps + 1)]
l1 = actualLoc[axis] - snapStart[axis] #Main axis diff value
for i in range(0, 3):
if(i != axis and (i in freeAxesN)):
l2 = (actualLoc[i] - snapStart[i]) #Minor axis value
angle = abs(atan(l2 / l1)) if l1 != 0 else 0
dirn = (l1 * l2) / abs(l1 * l2) if (l1 * l2) != 0 else 1
prevDiff = LARGE_NO
for j in range(0, len(snapAngles) + 1):
if(j == len(snapAngles)):
loc[i] = snapStart[i] + \
dirn * l1 * tan(snapAngles[-1])
break
cmpAngle = snapAngles[j]
if(abs(angle - cmpAngle) > prevDiff):
loc[i] = snapStart[i] + \
dirn * l1 * tan(snapAngles[j-1])
break
prevDiff = abs(angle - cmpAngle)
self.lastSnapTypes.add('angle')
if(refreshStatus and inEdit):
refPt = tm @ orig
newPt = loc.copy()
text = self.getStatusStr(unit, invTm, refPt, newPt)
else:
text = None
self.setStatus(rmInfo.area, text)
loc = invTm @ loc
self.lastSelCo = loc
self.tm = tm
self.orig = orig
return loc
def getEditCoPair(self):
refLineOrig = self.getRefLineOrig()
if(self.lastSelCo == None or refLineOrig == None):
return []
if(self.snapParams == None):
origType = bpy.context.window_manager.bezierToolkitParams.origType
refLineOrig = self.getRefLineOrig()
selCo = self.getSelCo()
else:
origType = self.snapParams.origType
refLineOrig = self.snapParams.refLineOrig
selCo = self.snapParams.selCo
orig = self.getCurrOrig(self.rmInfo, bpy.context.object, origType, \
refLineOrig, selCo)
return (self.tm @ orig, self.tm @ self.lastSelCo)
def setStatus(self, area, text): #TODO Global
area.header_text_set(text)
# Tightly coupled with get3dLocSnap
def updateGuideBatches(self, bglDrawMgr):
# Default values for resetting the bgl lines and points
drawAxes = [0, 1, 2]
axisLineCos = [[], [], []]
axisLineCols = [[], [], []]
snapLineCos = []
snapLineCols = []
custAxisLineCos = []
custAxisLineCols = []
custAxisGradStart = None
custAxisGradEnd = None
custAxisPtCos = []
custAxisPtCols = []
snapIndPtCos = []
snapIndPtCols = []
rmInfo = self.rmInfo
if(rmInfo != None): # self.snapParams is also not None
snapParams = self.snapParams
refLine = snapParams.refLine
refLineOrig = snapParams.refLineOrig
selCo = snapParams.selCo
freeAxesN = snapParams.freeAxesN
transType = snapParams.transType
origType = snapParams.origType
axisScale = snapParams.axisScale
dispAxes = snapParams.dispAxes
tm, invTm, orig = self.getTMInfoAndOrig(rmInfo, transType, \
origType, self.freezeOrient, axisScale, refLineOrig, selCo)
if(dispAxes and FTProps.dispAxes and ((refLineOrig != None \
or transType == 'VIEW' or len(freeAxesN) == 1) or (len(freeAxesN) > 0 \
and origType != 'REFERENCE'))):
colors = [(.6, 0.2, 0.2, 1), (0.2, .6, 0.2, 1), (0.2, 0.4, .6, 1)]
l = 2 * rmInfo.rv3d.view_distance
if (self.lastSelCo != None and len(freeAxesN) == 1): orig = self.lastSelCo
refCo = tm @ orig
for axis in freeAxesN[:2]:
axisLineCols[axis] = [colors[axis]]
pt1 = refCo.copy()
pt2 = refCo.copy()
pt1[axis] = l + refCo[axis]
pt2[axis] = -l + refCo[axis]
axisLineCos[axis] = [invTm @ pt1, invTm @ pt2]
if(refLineOrig != None and self.lastSelCo != None and \
(self.angleSnap or ('keyboard' in self.lastSnapTypes \
and self.snapDigits.polar))):
snapLineCos = [orig, self.lastSelCo]
snapLineCols = [(.4, .4, .4, 1)]
ptCol = (1, 1, 1, 1)
if(self.customAxis.length() != 0 and (self.customAxis.inDrawAxis == True or \
'AXIS' in {transType, origType, axisScale})):
apts = self.customAxis.axisPts
custAxisLineCos = [apts[0], apts[1]]
custAxisLineCols = [(1, 1, 1, 1)]
custAxisGradStart = .9
custAxisGradEnd = .3
custAxisPtCos = self.customAxis.getSnapPts()
custAxisPtCols = [(1, .4, 0, 1)]
if(FTProps.dispSnapInd and self.snapCo != None):
snapIndPtCos = [self.snapCo]
snapIndPtCols = [FTProps.colHltTip] #[(1, .4, 0, 1)]
for i, axis in enumerate(drawAxes):
axisGradStart = .2 if len(axisLineCos[i]) > 0 else None
axisGradEnd = .9 if len(axisLineCos[i]) > 0 else None
bglDrawMgr.addLineInfo('snapAxis'+str(axis), FTProps.axisLineWidth, \
axisLineCols[i], axisLineCos[i], axisGradStart, axisGradEnd)
bglDrawMgr.addLineInfo('SnapLine', FTProps.axisLineWidth, \
snapLineCols, snapLineCos)
bglDrawMgr.addLineInfo('CustAxisLine', FTProps.axisLineWidth, custAxisLineCols, \
custAxisLineCos, custAxisGradStart, custAxisGradEnd, mid = False)
bglDrawMgr.addPtInfo('CustAxisPt', FTProps.snapPtSize, custAxisPtCols, \
custAxisPtCos)
bglDrawMgr.addPtInfo('SnapPt', FTProps.snapPtSize, snapIndPtCols, snapIndPtCos)
################################## Flexi Tool Classes ##################################
#
# Operator
# |
# ModalBaseFlexiOp
# |
# -------------------------------------
# | |
# ModalDrawBezierOp ModalFlexiEditBezierOp
# |
# ---------------------------------
# | |
# ModalFlexiDrawBezierOp ModalFlexiDrawGreaseOp
class ModalBaseFlexiOp(Operator):
running = False
drawHdlrRef = None
drawTxtHdlrRef = None
drawFunc = None
shader = None
bglDrawMgr = None
opObj = None
pointSize = 4 # For Draw (Marker is of diff size)
def drawKeyMap():
regions = [r for area in bpy.context.screen.areas if area.type == 'VIEW_3D' \
for r in area.regions if r.type == 'WINDOW']
maxArea = max(r.width * r.height for r in regions)
currRegion = [r for r in bpy.context.area.regions if r.type == 'WINDOW'][0]
# Only display in window with max area
if(currRegion.width * currRegion.height < maxArea): return
toolRegion = [r for r in bpy.context.area.regions if r.type == 'TOOLS'][0]
xOff1 = (10 + toolRegion.width) if(FTProps.keyMapNextToTool) else FTProps.keyMapLocX
maxWidth = 0
yStart = 10 if(FTProps.keyMapNextToTool) else FTProps.keyMapLocY
yOff = yStart
font_id = 0
if(ModalBaseFlexiOp.opObj != None and FTProps.showKeyMap):
descrCol = [0] + list(FTProps.colKeymapText)
keyCol = [0] + list(FTProps.colKeymapKey)
toolType = ModalBaseFlexiOp.opObj.getToolType()
config, labels, keys = FTHotKeys.getHKDispLines(toolType)
blf.size(font_id, FTProps.keyMapFontSize)
maxLabelWidth = max(blf.dimensions(font_id, l+'XXXXX')[0] for l in labels)
xOff2 = xOff1 + maxLabelWidth
lineHeight = 1.2 * max(blf.dimensions(font_id, l)[1] for l in labels)
blf.position(font_id, xOff1, yOff, 0)
blf.color(*descrCol)
blf.draw(font_id, '*')
dim = blf.dimensions(font_id, '*')
blf.position(font_id, xOff1 + dim[0], yOff, 0)
blf.draw(font_id, ' Indicates Configurable Hot Keys')
yOff += 1.5 * lineHeight
for i, label in enumerate(labels):
blf.color(*descrCol)
blf.position(font_id, xOff1, yOff, 0)
blf.draw(font_id, label)
if(config[i]):
dim = blf.dimensions(font_id, label)
blf.position(font_id, xOff1 + dim[0], yOff, 0)
blf.draw(font_id, '*')
blf.color(*keyCol)
blf.position(font_id, xOff2, yOff, 0)
blf.draw(font_id, keys[i])
yOff += lineHeight
maxWidth = maxLabelWidth + max(blf.dimensions(font_id, l)[0] for l in keys)
header = toolType.title() + ' Keymap'
headerX = xOff1 + (maxWidth - blf.dimensions(font_id, header)[0]) / 2
blf.position(font_id, headerX, yOff + lineHeight * .5, 0)
blf.color(*descrCol)
blf.draw(font_id, header)
mathFnTxts = MathFnDraw.getMathFnTxts()
mathFnCol = [0] + list(FTProps.colMathFnTxt)
blf.size(font_id, FTProps.mathFnTxtFontSize)
blf.color(*mathFnCol)
lineHeight = blf.dimensions(font_id, 'yX')[1]
yOff = lineHeight / 2
if(mathFnTxts != None):
for t in mathFnTxts:
mathFnPos = (currRegion.width - blf.dimensions(font_id, t)[0]) / 2
# ~ mathFnPos = xOff1 + maxWidth + 10
blf.position(font_id, mathFnPos, yOff, 0)
blf.draw(font_id, t)
yOff += 1.5 * lineHeight
def addDrawHandlers(context):
hdlr = ModalBaseFlexiOp.opObj.__class__.drawHandler
ModalBaseFlexiOp.drawHdlrRef = \
bpy.types.SpaceView3D.draw_handler_add(hdlr, (), "WINDOW", "POST_VIEW")
ModalBaseFlexiOp.drawTxtHdlrRef = \
bpy.types.SpaceView3D.draw_handler_add(ModalBaseFlexiOp.drawKeyMap, \
(), "WINDOW", "POST_PIXEL")
def removeDrawHandlers():
if(ModalBaseFlexiOp.drawHdlrRef != None):
bpy.types.SpaceView3D.draw_handler_remove(ModalBaseFlexiOp.drawHdlrRef, \
"WINDOW")
ModalBaseFlexiOp.drawHdlrRef = None
if(ModalBaseFlexiOp.drawTxtHdlrRef != None):
bpy.types.SpaceView3D.draw_handler_remove(ModalBaseFlexiOp.drawTxtHdlrRef, \
"WINDOW")
ModalBaseFlexiOp.drawTxtHdlrRef = None
def drawHandlerBase():
if(ModalBaseFlexiOp.shader != None):
ModalBaseFlexiOp.bglDrawMgr.redraw()
def tagRedraw():
areas = [a for a in bpy.context.screen.areas if a.type == 'VIEW_3D']
for a in areas:
a.tag_redraw()
# Called back after add-on preferences are changed
def propsChanged():
if(ModalBaseFlexiOp.opObj != None and \
ModalBaseFlexiOp.opObj.snapper != None):
ModalBaseFlexiOp.opObj.snapper.updateSnapKeyMap()
ModalBaseFlexiOp.hdlColMap ={'FREE': FTProps.colHdlFree, \
'VECTOR': FTProps.colHdlVector, \
'ALIGNED': FTProps.colHdlAligned, \
'AUTO': FTProps.colHdlAuto}
def resetDisplayBase():
ModalBaseFlexiOp.bglDrawMgr.reset()
ModalBaseFlexiOp.tagRedraw()
def refreshDisplayBase(segDispInfos, bptDispInfos, snapper):
areaRegionInfo = getAllAreaRegions()
updateBezierBatches(ModalDrawBezierOp.bglDrawMgr, segDispInfos, \
bptDispInfos, areaRegionInfo)
if(snapper != None):
snapper.updateGuideBatches(ModalBaseFlexiOp.bglDrawMgr)
ModalBaseFlexiOp.tagRedraw()
@persistent
def loadPostHandler(dummy):
if(ModalBaseFlexiOp.shader != None):
ModalBaseFlexiOp.resetDisplayBase()
ModalBaseFlexiOp.running = False
@persistent
def loadPreHandler(dummy):
ModalBaseFlexiOp.removeDrawHandlers()
if(ModalBaseFlexiOp.drawFunc != None):
bpy.types.VIEW3D_HT_tool_header.draw = ModalBaseFlexiOp.drawFunc
@classmethod
def poll(cls, context):
return not ModalBaseFlexiOp.running
def getToolType(self):
raise NotImplementedError('Call to abstract method.')
def preInvoke(self, context, event):
pass # placeholder
def subInvoke(self, context, event):
return {'RUNNING_MODAL'} # placeholder
def invoke(self, context, event):
ModalBaseFlexiOp.opObj = self
ModalBaseFlexiOp.running = True
self.preInvoke(context, event)
ModalBaseFlexiOp.addDrawHandlers(context)
ModalBaseFlexiOp.drawFunc = bpy.types.VIEW3D_HT_tool_header.draw
bpy.types.VIEW3D_HT_tool_header.draw = drawSettingsFT
context.space_data.show_region_tool_header = True
self.snapper = Snapper(context, self.getSnapLocs, \
self.getRefLine, self.getRefLineOrig, self.getSelCo, \
self.getCurrLine, self.hasSelection, self.isEditing)
self.rmInfo = None
ModalBaseFlexiOp.shader = gpu.shader.from_builtin('FLAT_COLOR')
ModalBaseFlexiOp.bglDrawMgr = BGLDrawMgr(ModalBaseFlexiOp.shader)
# ~ ModalBaseFlexiOp.shader.bind()
context.window_manager.modal_handler_add(self)
ModalBaseFlexiOp.ColGreaseHltSeg = (.3, .3, .3, 1) # Not used
FTProps.updateProps(None, context)
FTHotKeys.updateHotkeys(None, context)
FTHotKeys.initSnapMetaFromPref(context)
self.clickT, self.pressT = None, None
self.click, self.doubleClick = False, False
return self.subInvoke(context, event)
def modal(self, context, event):
snapper = self.snapper
if(event.type == 'WINDOW_DEACTIVATE' and event.value == 'PRESS'):
snapper.initialize()
return {'PASS_THROUGH'}
if(not self.isToolSelected(context)): # Subclass
self.cancelOp(context)
return {"CANCELLED"}
self.click, self.doubleClick = False, False
if(event.type == 'LEFTMOUSE'):
if(event.value == 'PRESS'):
self.pressT = time.time()
elif(event.value == 'RELEASE'):
t = time.time()
if(self.clickT != None and (t - self.clickT) < DBL_CLK_DURN):
self.clickT = None
self.doubleClick = True
elif(self.pressT != None and (t - self.pressT) < SNGL_CLK_DURN):
self.clickT = t
self.click = True
self.pressT = None
snapProc = snapper.procEvent(context, event)
metakeys = snapper.getMetakeys()
if(FTHotKeys.isHotKey(FTHotKeys.hkToggleKeyMap, event.type, metakeys)):
if(event.value == 'RELEASE'):
try:
prefs = context.preferences.addons[__name__].preferences
prefs.showKeyMap = not prefs.showKeyMap
except Exception as e:
print(e)
FTProps.showKeyMap = not FTProps.showKeyMap
return {'RUNNING_MODAL'}
# Special condition for case where user has configured a different snap key
# In such case, pass through mouse clicks, if there's a meta key held down...
# to allow e.g. alt + LMB to rotate view
if(snapProc != EVT_CONS and any(metakeys) and \
not any([snapper.angleSnap, snapper.gridSnap, snapper.vertSnap]) and \
((event.type == 'LEFTMOUSE' and event.value in {'PRESS', 'RELEASE'}) \
or event.type == 'MOUSEMOVE')):
return {'PASS_THROUGH'}
rmInfo = RegionMouseXYInfo.getRegionMouseXYInfo(event, self.exclToolRegion())
ret = FTMenu.procMenu(self, context, event, rmInfo == None)
if(ret):
# Menu displayed on release, so retain metakeys till release
if(event.value == 'RELEASE'):
snapper.resetMetakeys()
snapper.resetSnapKeys()
return {'RUNNING_MODAL'}
if(self.isEditing() and self.rmInfo != rmInfo):
return {'RUNNING_MODAL'}
if(rmInfo == None):
return {'PASS_THROUGH'}
self.rmInfo = rmInfo
ret = snapper.customAxis.procDrawEvent(context, event, snapper, rmInfo)
evtCons = (ret or snapProc == EVT_CONS)
# Ignore all PRESS events if consumed, since action is taken only on RELEASE...
# ...except 1) wheelup / down where there is no release & 2) snap / meta where...
# ...refresh is needed even on press
# TODO: Simplify the condition (Maybe return EVT values from all proc methods)
if(evtCons and event.value == 'PRESS' and \
event.type != 'MOUSEMOVE' and \
not event.type.startswith('WHEEL') and (snapProc != EVT_META_OR_SNAP)):
return {'RUNNING_MODAL'}
if(FTHotKeys.isHotKey(FTHotKeys.hkSwitchOut, event.type, metakeys)):
if(event.value == 'RELEASE'):
self.cancelOp(context)
resetToolbarTool()
return {"CANCELLED"}
return {'RUNNING_MODAL'}
evtCons = (evtCons or (snapProc == EVT_META_OR_SNAP))
return self.subModal(context, event, evtCons)
def cancelOpBase(self):
for a in bpy.context.screen.areas:
if (a.type == 'VIEW_3D'): a.header_text_set(None)
ModalBaseFlexiOp.removeDrawHandlers()
ModalBaseFlexiOp.running = False
bpy.types.VIEW3D_HT_tool_header.draw = ModalBaseFlexiOp.drawFunc
self.snapper = None
ModalBaseFlexiOp.opObj = None
def getSnapLocs(self):
return self.getSnapLocsImpl()
################################## Flexi Draw Bezier Curve ###############################
#
# BaseDraw
# |
# -------------------------------
# | |
# Primitive2DDraw BezierDraw
# |
# -----------------------------
# | |
# ClosedShapeDraw MathFnDraw
# |
# ---------------------------------
# | | |
# RectangleDraw PolygonDraw EllipseDraw
class BaseDraw:
def __init__(self, parent):
self.parent = parent
self.initialize()
def initialize(self):
self.curvePts = []
def newPoint(self, loc, ltype, rtype):
self.curvePts.append([loc, loc, loc, ltype, rtype])
def setCurvePt(self, idx, pt):
self.curvePts[idx] = pt
def popCurvePt(self):
self.curvePts.pop()
def setCurvePts(self, curvePts):
self.curvePts = curvePts
class Primitive2DDraw(BaseDraw):
def __init__(self, parent):
super(Primitive2DDraw, self).__init__(parent)
self.shapeSegCnt = 4
self.curveObjOrigin = None
def initialize(self):
super(Primitive2DDraw, self).initialize()
self.bbStart = None
self.bbEnd = None
self.XYstart = None
self.freeAxesN = None
def set2DAxes(self):
params = bpy.context.window_manager.bezierToolkitParams
self.freeAxesN = self.parent.snapper.getFreeAxesNormalized()
if(len(self.freeAxesN) > 2 and \
params.snapOrient in {'GLOBAL', 'REFERENCE', 'CURR_POS'}):
self.freeAxesN = getClosestPlaneToView(self.parent.rmInfo.rv3d)
def getNumSegsLimits(self):
return 2, 100
def getShapePts(self, mode, numSegs, bbStart, bbEnd, center2d, startAngle, \
theta, axisIdxs, z):
raise NotImplementedError('Call to abstract method.')
# index: ((incr_key, decr_key), metakey, description)
dynamicParams = {
0:(['UP_ARROW', 'DOWN_ARROW'], 'Up (incr) / Down (decr)'),
1:(['RIGHT_ARROW', 'LEFT_ARROW'], 'Left (incr) / Right (decr)'),
2:(['PAGE_UP', 'PAGE_DOWN'], 'Page Up (incr) / Page Down (decr)'),
3:(['W', 'S'], 'W (incr) / S (decr)'),
4:(['A', 'D'], 'A (incr) / D (decr)'),
5:(['LEFT_BRACKET', 'RIGHT_BRACKET'], '[ (incr) / ] (decr)'),
}
def getParamCnt():
return len(Primitive2DDraw.dynamicParams)
def getParamHotKeyDescriptions():
return [Primitive2DDraw.dynamicParams[p][1] \
for p in range(len(Primitive2DDraw.dynamicParams))]
# default definition of all params
# Format (can be overriden in subclass):
# def updateParam0(self, event, rmInfo, isIncr): # 0-5
# pass
for i in range(len(dynamicParams)):
exec('def updateParam' + str(i) + '(self, event, rmInfo, isIncr):\n\tpass')
def afterShapeSegCnt(self): # Call back
pass
def getCurvePts(self, numSegs, axisIdxs, z = None):
params = bpy.context.window_manager.bezierToolkitParams
tm = self.parent.snapper.tm if self.parent.snapper.tm != None else Matrix()
idx0, idx1, idx2 = axisIdxs
startAngle = params.drawStartAngle
sweep = params.drawAngleSweep
bbEnd = tm @ self.bbEnd
mode = params.drawObjMode
if(mode == 'CENTER'):
diffV = (self.bbEnd - self.bbStart)
bbStart = tm @ (self.bbStart - diffV)
else:
bbStart = tm @ self.bbStart
cX = (bbEnd[idx0] - bbStart[idx0]) / 2
cY = (bbEnd[idx1] - bbStart[idx1]) / 2
if(cX == 0 and cY == 0):
return None
if(z == None): z = bbStart[idx2]
center2d = complex(cX, cY)
snapOrigin = bpy.context.window_manager.bezierToolkitParams.snapOrigin
orig = complex(bbStart[idx0], bbStart[idx1])
self.curveObjOrigin = tm.inverted_safe() @ \
get3DVector(orig + center2d, axisIdxs, z)
curvePts = self.getShapePts(mode, numSegs, bbStart, bbEnd, center2d, \
startAngle, sweep, axisIdxs, z)
if(curvePts == None): return None
curvePts = [[tm.inverted_safe() @ p if type(p) != str \
else p for p in pts] for pts in curvePts]
return curvePts
def updateCurvePts(self):
axisIdxs = self.freeAxesN + sorted(list({0, 1, 2} - set(self.freeAxesN)))
curvePts = self.getCurvePts(axisIdxs = axisIdxs, \
numSegs = self.shapeSegCnt)
if(curvePts != None):
self.setCurvePts(curvePts)
else:
self.setCurvePts([[self.bbStart, self.bbStart, self.bbStart], \
[self.bbEnd, self.bbEnd, self.bbEnd]])
def getPtLoc(self):
parent = self.parent
rmInfo = parent.rmInfo
if(self.freeAxesN == None):
self.set2DAxes()
return self.parent.snapper.get3dLocSnap(rmInfo, \
SnapParams(parent.snapper, lastCo1Axis = True, freeAxesN = self.freeAxesN, \
snapToPlane = (len(self.freeAxesN) == 2)))
def procDrawEvent(self, context, event, snapProc):
parent = self.parent
rmInfo = parent.rmInfo
metakeys = parent.snapper.getMetakeys()
if(len(self.curvePts) > 0 and not snapProc):
if(event.type in {'WHEELDOWNMOUSE', 'WHEELUPMOUSE', \
'NUMPAD_PLUS', 'NUMPAD_MINUS','PLUS', 'MINUS'}):
if(event.type in {'NUMPAD_PLUS', 'NUMPAD_MINUS', 'PLUS', 'MINUS'} \
and event.value == 'PRESS'):
return {'RUNNING_MODAL'}
self.updateSegCount(event, rmInfo, \
(event.type =='WHEELUPMOUSE' or event.type.endswith('PLUS')))
return {'RUNNING_MODAL'}
for i in range(len(self.dynamicParams)):
hotkeys = self.dynamicParams[i][0]
if(event.type in hotkeys):
if(event.value == 'RELEASE'):
exec('self.updateParam' + str(i) + \
'(event, rmInfo, (event.type == hotkeys[0]))')
return {'RUNNING_MODAL'}
if(event.type == 'H' or event.type == 'h'):
if(event.value == 'RELEASE'):
ModalDrawBezierOp.h = not ModalDrawBezierOp.h
self.parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)
return {"RUNNING_MODAL"}
if(self.bbStart != None and (event.type == 'RET' or event.type == 'SPACE')):
if(event.value == 'RELEASE'):
self.updateCurvePts()
self.parent.confirm(context, event, self.curveObjOrigin)
self.parent.snapper.resetSnap()
self.parent.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
if(event.type == 'ESC'):
if(event.value == 'RELEASE'):
self.parent.initialize() # should not access parent.initialize
self.parent.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
if(not snapProc and event.type == 'LEFTMOUSE' and event.value == 'RELEASE'):
if(self.bbStart == None):
self.set2DAxes()
loc = self.getPtLoc()
self.XYstart = self.parent.rmInfo.xy
self.bbStart = loc
self.bbEnd = loc
self.setCurvePts([[loc, loc, loc, 'FREE', 'FREE'], \
[loc, loc, loc, 'FREE', 'FREE']])
else:
loc = self.getPtLoc()
self.bbEnd = loc
self.updateCurvePts()
parent.confirm(context, event, self.curveObjOrigin)
return {"RUNNING_MODAL"}
if (snapProc or event.type == 'MOUSEMOVE'):
if(self.bbStart != None):
self.bbEnd = self.getPtLoc()
self.updateCurvePts()
parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)
return {"RUNNING_MODAL"}
return {"PASS_THROUGH"}
#Reference point for restrict angle or lock axis
def getRefLine(self):
if(self.bbStart != None):
if(self.bbEnd != None):
return[self.bbStart, self.bbEnd]
else:
return [self.bbStart]
return []
def getRefLineOrig(self):
refLine = self.getRefLine()
return refLine[0] if len(refLine) > 0 else None
class MathFnDraw(Primitive2DDraw):
# Prefixes for param names generated dynamically for constants
startPrefix = 'mathFnStart_'
incrPrefix = 'mathFnIncr_'
mathFnFileExt = 'mfn'
# Default values
defFnType = 'XY'
defFNRes = 10
defFNXYName = ''#'Sine Function'
defFNXYDescr = ''#'Sine wave with A: Amplitude, B: Frequency, C: Phase shift'
defFnXY = 'sin(x)'#'A * sin(B * x + C)'
defFnParam1 = ''#'B * cos(t + C)'
defFnParam2 = ''#'A * sin(t + D)'
defTMapTo = 'HORIZONTAL'
defTScale = 3
defTStart = 0
defXYMap = 'NORMAL_XY'
defClipVal = 10
defConstStart = 1
defConstIncr = 0.1
# XML tags / attributes
xDocTag = "drawMathFn"
xFnName = 'name'
xFnDescr = 'descr'
xFnCurveRes = 'curveRes'
xFnType = 'type'
xFns = 'functions'
xEquation = 'equation'
xXYFn = 'xyFn'
xClipVal = 'clipValue'
xParamFn1 = 'paramFn1'
xParamFn2 = 'paramFn2'
xTMapTo = 'tMapTo'
xTScaleFact = 'tScaleFact'
xTStart = 'tStartVal'
xConstPrefix = 'constant_'
xValue = 'value'
xIncr = 'incr'
mathFnDirty = True
mathFnItems = None
mathFnNoSelItem = ('NO_SEL', '', 'Function not saved')
def __init__(self, parent, star = False):
super(MathFnDraw, self).__init__(parent)
params = bpy.context.window_manager.bezierToolkitParams
self.shapeSegCnt = params.mathFnResolution
def getNumSegsLimits(self):
return 2, 99999
def updateSegCount(self, event, rmInfo, isIncr):
minSegs, maxSegs = self.getNumSegsLimits()
params = bpy.context.window_manager.bezierToolkitParams
if(isIncr and params.mathFnResolution < maxSegs): params.mathFnResolution += 1
if(not isIncr and params.mathFnResolution > minSegs): params.mathFnResolution -= 1
self.shapeSegCnt = params.mathFnResolution
self.afterShapeSegCnt()
self.updateCurvePts()
self.parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)
return True
for i in range(len(Primitive2DDraw.dynamicParams)):
fnStr = 'def updateParam' + str(i) + '(self, event, rmInfo, isIncr):\n\t' + \
'params = bpy.context.window_manager.bezierToolkitParams\n\t' + \
'incr = params.' + incrPrefix + str(i) + '\n\t' + \
'params.'+ startPrefix + str(i) + ' += incr if(isIncr) else -incr\n\t' + \
'self.updateCurvePts()\n\t' + \
'self.parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)\n\t' + \
'areas = [a for a in bpy.context.screen.areas]\n\t' + \
'for a in areas:\n\t\t' + \
'a.tag_redraw()\n\t' + \
'return True\n\t'
exec(fnStr)
def afterShapeSegCnt(self):
bpy.context.window_manager.bezierToolkitParams.mathFnResolution = self.shapeSegCnt
areas = [a for a in bpy.context.screen.areas]
for a in areas:
a.tag_redraw()
def testFn(expr, var): # var = 'x' or 'y'
exec(var + ' = 1')
# ~ exec(var.upper() + ' = 1') ??
try:
eval(expr)
return True
except Exception as e:
return False
# Returns None in case of invalid function
def isInverted(expr): # var = 'x' or 'y'
if(not MathFnDraw.testFn(expr, 'x')):
if(not MathFnDraw.testFn(expr, 'y')): return None
else: return True
return False
def getEvaluatedExpr(expr):
params = bpy.context.window_manager.bezierToolkitParams
for j in range(Primitive2DDraw.getParamCnt()):
expr = expr.replace(str(chr(ord('A') + j)), \
str(round(eval('params.'+ MathFnDraw.startPrefix + str(j)), 4)))
return expr
def getShapePts(self, mode, numSegs, bbStart, bbEnd, center2d, startAngle, \
theta, axisIdxs, z):
params = bpy.context.window_manager.bezierToolkitParams
curvePts = []
idx0, idx1, idx2 = axisIdxs
self.shapeSegCnt = params.mathFnResolution # Synch with params
if(params.mathFnType == 'PARAMETRIC'):
fn1 = MathFnDraw.getEvaluatedExpr(params.drawMathFnParametric1)
fn2 = MathFnDraw.getEvaluatedExpr(params.drawMathFnParametric2)
if(not MathFnDraw.testFn(fn1, 't')): return curvePts
if(not MathFnDraw.testFn(fn2, 't')): return curvePts
# Let the plot be always centered
if(mode == 'CENTER'):
bbStart[idx0] += center2d.real
bbStart[idx1] += center2d.imag
scaleFact = params.drawMathTScaleFact
if(params.drawMathTMapTo in {'x', 'y', 'xy'}):
xSpan = (bbEnd[idx0] - bbStart[idx0])
ySpan = (bbEnd[idx1] - bbStart[idx1])
else:
xSpan = (self.parent.rmInfo.xy[0] - self.XYstart[0])
ySpan = (self.parent.rmInfo.xy[1] - self.XYstart[1])
scaleFact = scaleFact / 25 # Device dependent!
if(params.drawMathTMapTo in {'X', 'HORIZONTAL'}):
span = scaleFact * xSpan
elif(params.drawMathTMapTo in {'Y', 'VERTICAL'}):
span = scaleFact * ySpan
else:
span = scaleFact * sqrt(xSpan * xSpan + ySpan * ySpan)
intervals = int(self.shapeSegCnt * abs(span))
if(intervals == 0): intervals = self.shapeSegCnt
incr = span / intervals
t = params.drawMathTStart
for step in range(intervals):
try:
x = bbStart[idx0] + eval(fn1)
y = bbStart[idx1] + eval(fn2)
pt2d = complex(x, y)
pt = get3DVector(pt2d, axisIdxs, z)
curvePts.append([pt, pt, pt, 'VECTOR', 'VECTOR'])
except Exception as e:
print(e, fn1, fn2)
pass
t += incr
else:
expr = MathFnDraw.getEvaluatedExpr(params.drawMathFn)
clip = params.mathFnclipVal
inverted = MathFnDraw.isInverted(expr)
if(inverted == None):
return curvePts
span = (bbEnd[idx1] - bbStart[idx1]) if(inverted) \
else (bbEnd[idx0] - bbStart[idx0])
intervals = int(self.shapeSegCnt * abs(span))
if(intervals == 0): intervals = self.shapeSegCnt
incr = span / intervals
indep = bbStart[idx0] if(not inverted) else bbStart[idx1]
for step in range(intervals):
try:
if(inverted): y = indep
else: x = indep
dep = eval(expr)
if(abs(dep) > clip):
dep = clip * (dep / abs(dep))
if(inverted): x = dep + bbStart[idx0] + (center2d.real \
if(mode == 'CENTER') else 0)
else: y = dep + bbStart[idx1] + (center2d.imag \
if(mode == 'CENTER') else 0)
pt2d = complex(x, y)
pt = get3DVector(pt2d, axisIdxs, z)
curvePts.append([pt, pt, pt, 'VECTOR', 'VECTOR'])
except Exception as e:
print(e, expr)
pass
indep += incr
return curvePts
def getMathFnTxts():
INVALID = ''
fnTxts = None
params = bpy.context.window_manager.bezierToolkitParams
# TODO: Should be somewhere else
tool = bpy.context.workspace.tools.from_space_view3d_mode('OBJECT', create = False)
if(tool.idname == FlexiDrawBezierTool.bl_idname and params.drawObjType == 'MATH'):
if(params.mathFnType == 'PARAMETRIC'):
fn1 = MathFnDraw.getEvaluatedExpr(params.drawMathFnParametric1)
fn2 = MathFnDraw.getEvaluatedExpr(params.drawMathFnParametric2)
fnTxts = ['y = ' + (fn2 if MathFnDraw.testFn(fn2, 't') else INVALID), \
'x = '+ (fn1 if MathFnDraw.testFn(fn1, 't') else INVALID)]
else:
expr = MathFnDraw.getEvaluatedExpr(params.drawMathFn)
inverted = MathFnDraw.isInverted(expr)
if(inverted == None):
fnTxts = [INVALID]
elif(inverted):
fnTxts = ['x = ' + expr]
else:
fnTxts = ['y = ' + expr]
return fnTxts
def getMathFnFolder(create = True):
userPath = bpy.utils.resource_path('USER')
configPath = os.path.join(userPath, "config")
mathFnFolder = configPath + '/mathFunctions'
if(create and not os.path.isdir(mathFnFolder)):
os.makedirs(mathFnFolder)
return mathFnFolder
def getMathFnList(dummy1 = None, dummy2 = None):
if(MathFnDraw.mathFnItems == None or MathFnDraw.mathFnDirty):
mathFnFolder = MathFnDraw.getMathFnFolder()
fNames = sorted([fName for fName in os.listdir(mathFnFolder)
if fName.endswith('.' + MathFnDraw.mathFnFileExt)], key=lambda s: s.lower())
MathFnDraw.mathFnItems = [MathFnDraw.mathFnNoSelItem]
for fName in fNames:
with open(mathFnFolder + '/' + fName) as f:
doc = minidom.parse(f)
fnName = doc.documentElement.getAttribute(MathFnDraw.xFnName)
fnDescr = doc.documentElement.getAttribute(MathFnDraw.xFnDescr)
MathFnDraw.mathFnItems.append((fnName, fnName, fnDescr))
MathFnDraw.mathFnDirty = False
return MathFnDraw.mathFnItems
def refreshDefaultParams():
params = bpy.context.window_manager.bezierToolkitParams
# ~ params.mathFnDescr = MathFnDraw.defFNXYDescr
# ~ params.mathFnResolution = MathFnDraw.defFNRes
# ~ params.mathFnType = MathFnDraw.defFnType
# ~ params.drawMathFn = MathFnDraw.defFnXY
# ~ params.mathFnclipVal = MathFnDraw.defClipVal
for i in range(Primitive2DDraw.getParamCnt()):
char = chr(ord('A') + i)
exec('params.' + MathFnDraw.startPrefix + str(i) + ' = ' + \
str(MathFnDraw.defConstStart))
exec('params.' + MathFnDraw.incrPrefix + str(i) + ' = ' + \
str(MathFnDraw.defConstIncr))
def refreshParamsFromFile(dummy1 = None, dummy2 = None):
params = bpy.context.window_manager.bezierToolkitParams
mathFnSel = params.mathFnList
if(mathFnSel == MathFnDraw.mathFnNoSelItem[0]):
refreshDefaultParams()
return
mathFnFolder = MathFnDraw.getMathFnFolder()
filepath = mathFnFolder + '/' + mathFnSel + '.' + MathFnDraw.mathFnFileExt
with open(filepath) as f:
doc = minidom.parse(f)
docElem = doc.documentElement
fnName = docElem.getAttribute(MathFnDraw.xFnName)
fnDescr = docElem.getAttribute(MathFnDraw.xFnDescr)
fnType = docElem.getAttribute(MathFnDraw.xFnType)
fnCurveRes = float(docElem.getAttribute(MathFnDraw.xFnCurveRes))
params.mathFnName = fnName
params.mathFnDescr = fnDescr
params.mathFnType = fnType
params.mathFnResolution = fnCurveRes
fnElem = docElem.getElementsByTagName(MathFnDraw.xFns)[0]
if(fnType == 'PARAMETRIC'):
fnTMapTo = fnElem.getAttribute(MathFnDraw.xTMapTo)
params.drawMathTMapTo = fnTMapTo
fnTScaleFact = float(fnElem.getAttribute(MathFnDraw.xTScaleFact))
params.drawMathTScaleFact = fnTScaleFact
fnTStart = float(fnElem.getAttribute(MathFnDraw.xTStart))
params.drawMathTStart = fnTStart
elem = fnElem.getElementsByTagName(MathFnDraw.xParamFn1)[0]
paramFn1 = elem.getAttribute(MathFnDraw.xEquation)
params.drawMathFnParametric1 = paramFn1
elem = fnElem.getElementsByTagName(MathFnDraw.xParamFn2)[0]
paramFn2 = elem.getAttribute(MathFnDraw.xEquation)
params.drawMathFnParametric2 = paramFn2
else:
fnYClip = float(fnElem.getAttribute(MathFnDraw.xClipVal))
params.mathFnclipVal = fnYClip
elem = fnElem.getElementsByTagName(MathFnDraw.xXYFn)[0]
xyFn = elem.getAttribute(MathFnDraw.xEquation)
params.drawMathFn = xyFn
for i in range(Primitive2DDraw.getParamCnt()):
char = chr(ord('A') + i)
elem = fnElem.getElementsByTagName(MathFnDraw.xConstPrefix + char)[0]
startVal = float(elem.getAttribute(MathFnDraw.xValue))
incrVal = float(elem.getAttribute(MathFnDraw.xIncr))
exec('params.' + MathFnDraw.startPrefix + str(i) + ' = ' + str(startVal))
exec('params.' + MathFnDraw.incrPrefix + str(i) + ' = ' + str(incrVal))
areas = [a for a in bpy.context.screen.areas]
for a in areas:
a.tag_redraw()
class ResetMathFn(Operator):
bl_idname = "object.reset_math_fn"
bl_label = "Reset"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
MathFnDraw.refreshParamsFromFile()
except:
MathFnDraw.refreshDefaultParams()
return {'FINISHED'}
# TODO: Better validation and error handling
class SaveMathFn(Operator):
bl_idname = "object.save_math_fn"
bl_label = "Save"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
params = bpy.context.window_manager.bezierToolkitParams
fnName = params.mathFnName
fnDescr = params.mathFnDescr
fnType = params.mathFnType
fnCurveRes = params.mathFnResolution
if(fnName.strip() == ''): # Error Condition #1
return {'FINISHED'}
doc = minidom.getDOMImplementation().createDocument(None, MathFnDraw.xDocTag, None)
docElem = doc.documentElement
docElem.setAttribute(MathFnDraw.xFnName, fnName)
docElem.setAttribute(MathFnDraw.xFnDescr, fnDescr)
docElem.setAttribute(MathFnDraw.xFnType, fnType)
docElem.setAttribute(MathFnDraw.xFnCurveRes, str(fnCurveRes))
fnElem = doc.createElement(MathFnDraw.xFns)
docElem.appendChild(fnElem)
if(fnType == 'PARAMETRIC'):
paramFn1 = params.drawMathFnParametric1
paramFn2 = params.drawMathFnParametric2
if(paramFn1.strip() == '' or paramFn2.strip() == ''): # Error Condition #2
return {'FINISHED'}
fnTMapTo = params.drawMathTMapTo
fnElem.setAttribute(MathFnDraw.xTMapTo, str(fnTMapTo))
fnTScaleFact = params.drawMathTScaleFact
fnElem.setAttribute(MathFnDraw.xTScaleFact, str(fnTScaleFact))
fnTStart = params.drawMathTStart
fnElem.setAttribute(MathFnDraw.xTStart, str(fnTStart))
elem = doc.createElement(MathFnDraw.xParamFn1)
elem.setAttribute(MathFnDraw.xEquation, paramFn1)
fnElem.appendChild(elem)
elem = doc.createElement(MathFnDraw.xParamFn2)
elem.setAttribute(MathFnDraw.xEquation, paramFn2)
fnElem.appendChild(elem)
else:
xyFn = params.drawMathFn
if(xyFn.strip() == ''): # Error Condition #3
return {'FINISHED'}
elem = doc.createElement(MathFnDraw.xXYFn)
elem.setAttribute(MathFnDraw.xEquation, xyFn)
fnElem.appendChild(elem)
fnYClip = params.mathFnclipVal
fnElem.setAttribute(MathFnDraw.xClipVal, str(fnYClip))
for i in range(Primitive2DDraw.getParamCnt()):
char = chr(ord('A') + i)
startVal = round(eval('params.' + MathFnDraw.startPrefix + str(i)), 4)
incrVal = round(eval('params.' + MathFnDraw.incrPrefix + str(i)), 4)
elem = doc.createElement(MathFnDraw.xConstPrefix + char)
elem.setAttribute(MathFnDraw.xValue, str(startVal))
elem.setAttribute(MathFnDraw.xIncr, str(incrVal))
fnElem.appendChild(elem)
mathFnFolder = MathFnDraw.getMathFnFolder()
fnFile = mathFnFolder + '/' + fnName + '.' + MathFnDraw.mathFnFileExt
try:
with open(fnFile,"w") as f:
doc.writexml(f)
MathFnDraw.mathFnDirty = True
params.mathFnList = fnName
except: # Error Condition #4
self.report({'ERROR'}, 'Error saving math function file')
return {'FINISHED'}
# TODO: Better validation and error handling
class LoadMathFn(Operator):
bl_idname = "object.load_math_fn"
bl_label = "Load"
bl_options = {'REGISTER', 'UNDO'}
filter_glob : StringProperty(default = '*.' + MathFnDraw.mathFnFileExt, options={'HIDDEN'})
filepath : StringProperty(subtype='FILE_PATH')
def execute(self, context):
params = bpy.context.window_manager.bezierToolkitParams
try:
with open(self.filepath) as f:
doc = minidom.parse(f)
fnName = doc.documentElement.getAttribute(MathFnDraw.xFnName)
mathFnFolder = MathFnDraw.getMathFnFolder()
destPath = mathFnFolder + '/' + fnName + '.' + MathFnDraw.mathFnFileExt
copyfile(self.filepath, destPath)
MathFnDraw.mathFnDirty = True
params.mathFnList = fnName
except:
self.report({'ERROR'}, 'Error importing math function file')
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class DeleteMathFn(bpy.types.Operator):
bl_idname = "object.delete_math_fn"
bl_label = "Remove function from list and delete it permanently?"
bl_options = {'REGISTER', 'INTERNAL'}
# ~ @classmethod
# ~ def poll(cls, context):
# ~ return True
def execute(self, context):
params = bpy.context.window_manager.bezierToolkitParams
fnName = params.mathFnList
if(fnName != MathFnDraw.mathFnNoSelItem[0]):
mathFnFolder = MathFnDraw.getMathFnFolder()
fnFile = mathFnFolder + '/' + fnName + '.' + MathFnDraw.mathFnFileExt
try:
os.remove(fnFile)
MathFnDraw.mathFnDirty = True
params.mathFnList = MathFnDraw.mathFnNoSelItem[0]
except:
self.report({'ERROR'}, 'Error deleting math function file')
return {'FINISHED'}
def invoke(self, context, event):
params = bpy.context.window_manager.bezierToolkitParams
fnName = params.mathFnList
if(fnName != MathFnDraw.mathFnNoSelItem[0]):
return context.window_manager.invoke_props_dialog(self)
else:
return {'FINISHED'}
class ClosedShapeDraw(Primitive2DDraw):
def __init__(self, parent, star = False):
super(ClosedShapeDraw, self).__init__(parent)
def updateSegCount(self, event, rmInfo, isIncr):
minSegs, maxSegs = self.getNumSegsLimits()
if(isIncr and self.shapeSegCnt < maxSegs): self.shapeSegCnt += 1
if(not isIncr and self.shapeSegCnt > minSegs): self.shapeSegCnt -= 1
self.afterShapeSegCnt()
self.updateCurvePts()
self.parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)
return True
def updateParam1(self, event, rmInfo, isIncr):
params = bpy.context.window_manager.bezierToolkitParams
theta = params.drawAngleSweep
if(isIncr):
if(theta >= -10 and theta <= 0): params.drawAngleSweep = 10
elif(theta > 350): params.drawAngleSweep = -350
else: params.drawAngleSweep = theta + 10
else:
if(theta <= 10 and theta >= 0): params.drawAngleSweep = -10
elif(theta < -350): params.drawAngleSweep = 350
else: params.drawAngleSweep = theta - 10
self.updateCurvePts()
self.parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)
return True
class RectangleDraw(ClosedShapeDraw):
def __init__(self, parent, star = False):
super(RectangleDraw, self).__init__(parent)
def getShapePts(self, mode, numSegs, bbStart, bbEnd, center2d, startAngle, \
theta, axisIdxs, z):
idx0, idx1, idx2 = axisIdxs
pt0 = complex(bbStart[idx0], bbStart[idx1])
pt1 = complex(bbEnd[idx0], bbStart[idx1])
pt2 = complex(bbEnd[idx0], bbEnd[idx1])
pt3 = complex(bbStart[idx0], bbEnd[idx1])
curvePts = []
for pt2d in [pt0, pt1, pt2, pt3, pt0]:
pt = get3DVector(pt2d, axisIdxs, z)
curvePts.append([pt, pt, pt, 'VECTOR', 'VECTOR'])
return curvePts
class PolygonDraw(ClosedShapeDraw):
def updateParam0(self, event, rmInfo, isIncr):
params = bpy.context.window_manager.bezierToolkitParams
offset = params.drawStarOffset
params.drawStarOffset += 0.1 if(isIncr) else -0.1
self.updateCurvePts()
self.parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)
return True
def getNumSegsLimits(self):
return 3, 100
def __init__(self, parent, star = False):
super(PolygonDraw, self).__init__(parent)
self.star = star
def getShapePts(self, mode, numSegs, bbStart, bbEnd, center2d, startAngle, \
theta, axisIdxs, z):
params = bpy.context.window_manager.bezierToolkitParams
params.drawSides = numSegs
idx0, idx1, idx2 = axisIdxs
cX, cY = center2d.real, center2d.imag
radius = sqrt(cX * cX + cY * cY)
offset = params.drawStarOffset
orig = complex(bbStart[idx0], bbStart[idx1])
if(cX == 0): startAngle = 90 * ((cY / abs(cY)) if cY != 0 else 1)
else: startAngle = degrees(atan(cY / cX))
if(cX < 0): startAngle += 180
thetaIncr = theta / numSegs
curvePts = []
for segCnt in range(numSegs + 1):
if(self.star):
angle = startAngle + thetaIncr * segCnt - thetaIncr / 2
shift = complex(offset * radius * cos(radians(angle)), \
offset * radius * sin(radians(angle)))
pt = get3DVector(orig + center2d + shift, axisIdxs, z)
curvePts.append([pt, pt, pt, 'VECTOR', 'VECTOR'])
if(not self.star or segCnt < numSegs):
angle = startAngle + thetaIncr * segCnt
shift = complex(radius * cos(radians(angle)), radius * sin(radians(angle)))
pt = get3DVector(orig + center2d + shift, axisIdxs, z)
curvePts.append([pt, pt, pt, 'VECTOR', 'VECTOR'])
return curvePts
class EllipseDraw(ClosedShapeDraw):
# https://math.stackexchange.com/questions/22064/calculating-a-point-that-lies-on-an-ellipse-given-an-angle
def getPtAtAngle(a, b, theta):
if (theta < 0): theta += 2 * pi
denom = sqrt(b * b + a * a * tan(theta) * tan(theta))
num = (a * b)
x = num / denom
if(pi / 2 < theta <= 3 * pi / 2): x = -x
y = x * tan(theta)
return complex(x, y)
def __init__(self, parent):
super(EllipseDraw, self).__init__(parent)
def updateParam0(self, event, rmInfo, isIncr):
params = bpy.context.window_manager.bezierToolkitParams
theta = params.drawStartAngle
if(isIncr):
if(theta > 350): params.drawStartAngle = 0
else: params.drawStartAngle = theta + 10
else:
if(theta < -350): params.drawStartAngle = 0
else: params.drawStartAngle = theta - 10
self.updateCurvePts()
self.parent.redrawBezier(rmInfo, hdlPtIdxs = {}, hltEndSeg = False)
return True
def getShapePts(self, mode, numSegs, bbStart, bbEnd, center2d, startAngle, \
theta, axisIdxs, z):
idx0, idx1, idx2 = axisIdxs
cX, cY = center2d.real, center2d.imag
if(cX == 0 or cY == 0):
return None
radius = complex(cX, cY) # Actually same as center2d
orig = complex(bbStart[idx0], bbStart[idx1])
large_arc = 0
rotation = 0
sweep = 1
startIdx = 0
endIdx = 1
rvs = False
if(theta < 0):
sweep = 0
startIdx = 1
endIdx = 0
rvs = True
pt1 = EllipseDraw.getPtAtAngle(abs(cX), abs(cY), radians(startAngle))
a1 = startAngle + theta / 2
if(a1 > 360): a1 = a1 - 360
if(a1 < -360): a1 = a1 + 360
a2 = startAngle + theta
if(a2 > 360): a2 = a2 - 360
if(a2 < -360): a2 = a2 + 360
pt2 = EllipseDraw.getPtAtAngle(abs(cX), abs(cY), radians(a1))
pt3 = EllipseDraw.getPtAtAngle(abs(cX), abs(cY), radians(a2))
pt1 = orig + pt1 + center2d
pt2 = orig + pt2 + center2d
pt3 = orig + pt3 + center2d
endPts = [pt1, pt2]
segs1 = getSegsForArc(endPts[startIdx], radius, 1, endPts[endIdx], \
10, axisIdxs, z)
endPts = [pt2, pt3]
segs2 = getSegsForArc(endPts[startIdx], radius, 1, endPts[endIdx], \
10, axisIdxs, z)
segElems = [segs1, segs2]
segs = segElems[startIdx] + segElems[endIdx]
curvePts = getWSDataForSegs(segs)
if(len(curvePts) < 2): return None
pts = getInterpBezierPts(curvePts, subdivPerUnit = 100, segLens = None)
if(len(pts) < 2): return None
vertCos = getInterpolatedVertsCo(pts, numSegs)
# TODO: A more efficient approach for dividing ellipse uniformly
newSegs = []
for i in range(1, len(vertCos)):
segStart = complex(vertCos[i-1][idx0], vertCos[i-1][idx1])
segEnd = complex(vertCos[i][idx0], vertCos[i][idx1])
endPts = [segStart, segEnd]
segs1 = getSegsForArc(endPts[startIdx], radius, sweep, \
endPts[endIdx], 1, axisIdxs, z)
newSegs += segs1
if(rvs): newSegs = reversed(newSegs)
curvePts = getWSDataForSegs(newSegs)
if(len(curvePts) < 2): return None
if(vectCmpWithMargin(curvePts[0][1], curvePts[-1][1])):
ldiffV = curvePts[0][1] - curvePts[0][0]
rdiffV = curvePts[-1][2] - curvePts[-1][1]
if(vectCmpWithMargin(ldiffV, rdiffV)):
curvePts[0][3] = 'ALIGNED'
curvePts[0][4] = 'ALIGNED'
curvePts[-1][3] = 'ALIGNED'
curvePts[-1][4] = 'ALIGNED'
return curvePts
class BezierDraw(BaseDraw):
def __init__(self, parent):
super(BezierDraw, self).__init__(parent)
self.reset()
def reset(self):
self.capture = False
self.grabRepos = False
self.dissociateHdl = False
def moveBezierPt(self, loc):
if(len(self.curvePts) > 0):
pt = self.curvePts[-1][:]
self.setCurvePt(-1, [loc, loc, loc, pt[3], pt[4]])
def movePointByDelta(self, delta):
if(len(self.curvePts) > 0):
pt = self.curvePts[-1][:]
pt[1] += delta
self.setCurvePt(-1, pt)
def moveBptElem(self, handle, loc):
idx = {'left':0, 'pt':1, 'right':2}[handle]
if(len(self.curvePts) > 0):
pt = self.curvePts[-1][:]
pt[idx] = loc
self.setCurvePt(-1, pt)
def moveBptElemByDelta(self, handle, delta):
idx = {'left':0, 'pt':1, 'right':2}[handle]
if(len(self.curvePts) > 0):
pt = self.curvePts[-1][:]
pt[idx] += delta
self.setCurvePt(-1, pt)
def resetHandle(self, handle):
if(len(self.curvePts) > 0):
self.moveBptElem(handle, self.curvePts[-1][1])
def isHandleSet(self):
if(len(self.curvePts) == 0): return False
co = self.curvePts[-1][1]
lh = self.curvePts[-1][0]
rh = self.curvePts[-1][2]
if(not vectCmpWithMargin(co, lh) or not vectCmpWithMargin(co, rh)): return True
return False
def procDrawEvent(self, context, event, snapProc):
rmInfo = self.parent.rmInfo
snapper = self.parent.snapper
metakeys = snapper.getMetakeys()
if(self.capture and FTHotKeys.isHotKey(FTHotKeys.hkGrabRepos, \
event.type, metakeys)):
if(event.value == 'RELEASE'):
self.dissociateHdl = False
self.grabRepos = not self.grabRepos
return {"RUNNING_MODAL"}
if(self.capture and FTHotKeys.isHotKey(FTHotKeys.hkDissociateHdl, \
event.type, metakeys)):
if(event.value == 'RELEASE'):
self.grabRepos = False
self.dissociateHdl = not self.dissociateHdl
return {"RUNNING_MODAL"}
# This can happen only when space was entered and something was there
# for Snapper to process
if (snapProc and snapper.digitsConfirmed):
snapper.resetSnap()
# Because resetSnap sets this to False (TODO: Refactor resetSnap)
snapper.digitsConfirmed = True
# First space / enter is equivalent to mouse press without release
if(not self.capture):
self.capture = True
snapper.setStatus(rmInfo.area, None)
return {'RUNNING_MODAL'}
else:
# Second space / enter means it should be processed here,
# set snapProc to False so this modal will process it
snapProc = False
if(not snapProc and event.type == 'ESC'):
if(event.value == 'RELEASE'):
if(self.grabRepos):
self.grabRepos = False
elif(self.dissociateHdl):
self.dissociateHdl = False
elif(self.capture and self.isHandleSet()):
self.resetHandle('left')
self.resetHandle('right')
# Needed to indicate next space / entered to be processed here
snapper.digitsConfirmed = True
snapper.setStatus(rmInfo.area, None)
else:
self.parent.initialize() # TODO: should not access parent.initialize
self.parent.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
if(not snapProc and \
FTHotKeys.isHotKey(FTHotKeys.hkResetLastHdl, event.type, metakeys)):
if(event.value == 'RELEASE'):
if(len(self.curvePts) > 1):
if(len(self.curvePts) > 2):
idx = len(self.curvePts) - 2
pt = self.curvePts[idx][:]
pt[2] = pt[1].copy()
self.setCurvePt(idx, pt)
self.parent.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
if(not snapProc and \
FTHotKeys.isHotKey(FTHotKeys.hkUndoLastSeg, event.type, metakeys)):
if(event.value == 'RELEASE'):
snapper.resetSnap()
if(not self.capture):
if(len(self.curvePts) > 0):
self.popCurvePt()
#Because there is an extra point (the current one)
if(len(self.curvePts) <= 1):
self.initialize()
self.reset()
else:
loc = snapper.get3dLocSnap(rmInfo)
self.moveBezierPt(loc)
self.capture = False
self.parent.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
if(not snapProc and (event.type == 'RET' or event.type == 'SPACE')):
if(event.value == 'RELEASE'):
if(snapper.digitsConfirmed):
self.reset()
snapper.digitsConfirmed = False
loc = snapper.get3dLocSnap(rmInfo)
self.newPoint(loc, 'ALIGNED', 'ALIGNED')
self.parent.redrawBezier(rmInfo)
else:
if(len(self.curvePts) > 0): self.popCurvePt()
self.parent.confirm(context, event)
snapper.resetSnap()
self.parent.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
if(not snapProc and event.type == 'LEFTMOUSE' and event.value == 'PRESS'):
if(len(self.curvePts) == 0):
loc = snapper.get3dLocSnap(rmInfo)
self.newPoint(loc, 'ALIGNED', 'ALIGNED')
# Special condition for hot-key single axis lock (useful)
if(len(snapper.freeAxes) == 1 and len(self.curvePts) > 1):
snapper.resetSnap()
if(self.capture): self.parent.pressT = None # Lock capture
else: self.capture = True
return {'RUNNING_MODAL'}
if (not snapProc and event.type == 'LEFTMOUSE' and event.value == 'RELEASE'):
# ~ if(snapper.isLocked()):
# ~ if(len(self.curvePts) == 1):
# ~ self.moveBptElem('right', \
# ~ snapper.get3dLocSnap(rmInfo))# changes only rt handle
# ~ return {'RUNNING_MODAL'}
# See the special condition above in event.value == 'PRESS'
# ~ if(len(snapper.freeAxes) > 1):
snapper.resetSnap()
self.reset()
# Rare condition: This happens e. g. when user clicks on header menu
# like Object->Transform->Move. These ops consume press event but not release
# So update the snap locations anyways if there was some transformation
if(len(self.curvePts) == 0):
self.parent.updateSnapLocs() # Subclass (TODO: have a relook)
elif(self.parent.doubleClick):
if(len(self.curvePts) > 0): self.popCurvePt()
self.parent.confirm(context, event)
self.parent.redrawBezier(rmInfo)
else:
if(self.parent.click):
loc = self.curvePts[-1][1]
self.moveBptElem('left', loc)
self.moveBptElem('right', loc)
else:
loc = snapper.get3dLocSnap(rmInfo)
# ~ if(len(self.curvePts) == 1):
# ~ self.moveBptElem('right', loc)# changes only rt handle
self.newPoint(loc, 'ALIGNED', 'ALIGNED')
self.parent.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
# Refresh also in case of snapper events
# except when digitsConfirmed (to give user opportunity to draw a straight line)
# ~ if ((snapProc and not snapper.digitsConfirmed) \
if (snapProc or event.type == 'MOUSEMOVE'):
bpy.context.window.cursor_set("DEFAULT")
hdlPtIdxs = None
if(len(self.curvePts) > 0):
if(self.capture):
lastPt = self.curvePts[-1][:]
if(self.grabRepos):
rtHandle = lastPt[2].copy()
xy2 = getCoordFromLoc(rmInfo.region, rmInfo.rv3d, lastPt[1])
xy1 = getCoordFromLoc(rmInfo.region, rmInfo.rv3d, rtHandle)
loc = snapper.get3dLocSnap(rmInfo, SnapParams(snapper, \
xyDelta = [xy1[0] - xy2[0], xy1[1] - xy2[1]]))
delta = loc - lastPt[1]
self.moveBptElemByDelta('pt', delta)
self.moveBptElemByDelta('left', delta)
self.moveBptElemByDelta('right', delta)
else:
loc = snapper.get3dLocSnap(rmInfo)
delta = (loc - lastPt[1])
if(self.dissociateHdl):
lastPt[3] = 'FREE'
lastPt[4] = 'FREE'
else:
lastPt[0] = lastPt[1] - delta
lastPt[3] = 'ALIGNED'
lastPt[4] = 'ALIGNED'
lastPt[2] = lastPt[1] + delta
self.setCurvePt(-1, lastPt)
hdlPtIdxs = {len(self.curvePts) - 1}
else:
loc = snapper.get3dLocSnap(rmInfo)
self.moveBezierPt(loc)
hdlPtIdxs = {len(self.curvePts) - 2}
self.parent.redrawBezier(rmInfo, hdlPtIdxs = hdlPtIdxs)
return {'RUNNING_MODAL'}
return {'PASS_THROUGH'} if not snapProc else {'RUNNING_MODAL'}
def getRefLine(self):
if(len(self.curvePts) > 0):
idx = 0
if(self.capture):
if(self.grabRepos and len(self.curvePts) > 1): idx = -2
else: idx = -1
# There should always be min 2 pts if not capture, check anyway
elif(len(self.curvePts) > 1):
idx = -2
if((len(self.curvePts) + (idx - 1)) >= 0):
return[self.curvePts[idx-1][1], self.curvePts[idx][1]]
else:
return[self.curvePts[idx][1]]
return []
def getRefLineOrig(self):
refLine = self.getRefLine()
return refLine[-1] if len(refLine) > 0 else None
class ModalDrawBezierOp(ModalBaseFlexiOp):
# Static members shared by flexi draw and flexi grease
markerSize = 8
h = False
drawObjMap = {}
#static method
def drawHandler():
ModalBaseFlexiOp.drawHandlerBase()
def updateDrawType(dummy, context):
opObj = ModalDrawBezierOp.opObj
if(opObj != None):
opObj.setDrawObj()
opObj.initialize()
opObj.resetDisplay()
ModalDrawBezierOp.updateDrawSides(dummy, context)
def updateDrawSides(dummy, context):
params = bpy.context.window_manager.bezierToolkitParams
opObj = ModalDrawBezierOp.opObj
if(opObj != None and opObj.drawType != 'BEZIER'):
opObj.drawObj.shapeSegCnt = params.drawSides
def getToolType(self):
return TOOL_TYPE_FLEXI_DRAW
def resetDisplay(self):
ModalBaseFlexiOp.resetDisplayBase()
def setDrawObj(self):
params = bpy.context.window_manager.bezierToolkitParams
self.drawObj = ModalDrawBezierOp.drawObjMap[params.drawObjType]
self.drawType = params.drawObjType
def preInvoke(self, context, event):
self.bezierDrawObj = BezierDraw(self)
self.rectangleDrawObj = RectangleDraw(self)
self.ellipseDrawObj = EllipseDraw(self)
self.polygonDrawObj = PolygonDraw(self)
self.starDrawObj = PolygonDraw(self, star = True)
self.mathDrawObj = MathFnDraw(self)
ModalDrawBezierOp.drawObjMap = \
{'BEZIER': self.bezierDrawObj, \
'RECTANGLE': self.rectangleDrawObj, \
'ELLIPSE': self.ellipseDrawObj, \
'POLYGON': self.polygonDrawObj, \
'STAR': self.starDrawObj, \
'MATH': self.mathDrawObj, \
}
self.setDrawObj()
#This will be called multiple times not just at the beginning
def initialize(self):
self.drawObj.initialize()
self.snapper.initialize()
def subInvoke(self, context, event):
bpy.app.handlers.undo_post.append(self.postUndoRedo)
bpy.app.handlers.redo_post.append(self.postUndoRedo)
try:
ModalDrawBezierOp.markerSize = \
context.preferences.addons[__name__].preferences.markerSize
except Exception as e:
# ~ print("BezierUtils: Error fetching default sizes in Draw Bezier", e)
ModalDrawBezierOp.markerSize = 8
self.updateDrawType(context)
self.updateDrawSides(context)
return {"RUNNING_MODAL"}
def cancelOp(self, context):
self.resetDisplay()
bpy.app.handlers.undo_post.remove(self.postUndoRedo)
bpy.app.handlers.redo_post.remove(self.postUndoRedo)
return self.cancelOpBase()
def postUndoRedo(self, scene, dummy = None): # signature different in 2.8 and 2.81?
self.updateSnapLocs() # subclass method
def confirm(self, context, event, location = None):
metakeys = self.snapper.getMetakeys()
shift = self.snapper.angleSnap # Overloaded key op
autoclose = (self.drawType == 'BEZIER' and shift and \
(event.type == 'SPACE' or event.type == 'RET'))
self.save(context, event, autoclose, location)
self.resetDisplay()
self.initialize()
def exclToolRegion(self):
return True
def isEditing(self):
return len(self.drawObj.curvePts) > 0
def hasSelection(self):
return self.isEditing()
# Common subModal for Flexi Draw and Flexi Grease
def baseSubModal(self, context, event, snapProc):
return self.drawObj.procDrawEvent(context, event, snapProc)
def refreshMarkerPos(self, rmInfo):
colMap = self.getColorMap()
colMarker = colMap['MARKER_COLOR']
markerLoc = self.snapper.get3dLocSnap(rmInfo)
self.resetDisplay()
self.bglDrawMgr.addPtInfo('drawMarker', ModalDrawBezierOp.markerSize, \
[colMarker], [markerLoc])
ModalBaseFlexiOp.refreshDisplayBase(segDispInfos = [], bptDispInfos = [], \
snapper = self.snapper)
def redrawBezier(self, rmInfo, lastSegOnly = False, hdlPtIdxs = None, \
hltEndSeg = True):
curvePts = self.drawObj.curvePts
ptCnt = len(curvePts)
if(ptCnt == 0):
self.refreshMarkerPos(rmInfo)
return
self.bglDrawMgr.resetPtInfo('drawMarker')
colMap = self.getColorMap()
colSelSeg = colMap['SEL_SEG_COLOR']
colNonAdjSeg = colMap['NONADJ_SEG_COLOR']
colTip = colMap['TIP_COLOR']
colEndTip = colMap['ENDPT_TIP_COLOR']
segColor = colSelSeg
tipColors = [colTip, colEndTip, colTip] if (not ModalDrawBezierOp.h) \
else [None, colEndTip, None]
segDispInfos = []
bptDispInfos = []
if(hdlPtIdxs == None): hdlPtIdxs = {ptCnt - 2} # Default last but one
elif(len(hdlPtIdxs) == 0): hdlPtIdxs = range(ptCnt)
for hdlPtIdx in hdlPtIdxs:
bptDispInfos.append(BptDisplayInfo(curvePts[hdlPtIdx], tipColors, \
handleNos = [0, 1] if (not ModalDrawBezierOp.h) else []))
startIdx = 0
if(lastSegOnly and ptCnt > 1):
startIdx = ptCnt - 2
for i in range(startIdx, ptCnt - 1):
if(not hltEndSeg or i == ptCnt - 2): segColor = colSelSeg
else: segColor = colNonAdjSeg
segDispInfos.append(SegDisplayInfo([curvePts[i], curvePts[i+1]], segColor))
ModalBaseFlexiOp.refreshDisplayBase(segDispInfos, bptDispInfos, self.snapper)
def getRefLine(self):
return self.drawObj.getRefLine()
def getRefLineOrig(self):
return self.drawObj.getRefLineOrig()
def getSelCo(self):
return self.getRefLineOrig()
def getCurrLine(self):
return self.getRefLine()
class ModalFlexiDrawBezierOp(ModalDrawBezierOp):
bl_description = "Flexible drawing of Bezier curves in object mode"
bl_idname = "wm.flexi_draw_bezier_curves"
bl_label = "Flexi Draw Bezier Curves"
bl_options = {'REGISTER', 'UNDO'}
def __init__(self):
pass
# For some curve-changing ops (like reset rotation); possible in draw
def updateAfterGeomChange(self, scene = None, dummy = None): # 3 params in 2.81
self.updateSnapLocs()
def isToolSelected(self, context):
if(context.mode != 'OBJECT'):
return False
tool = context.workspace.tools.from_space_view3d_mode('OBJECT', create = False)
if(tool == None or tool.idname != FlexiDrawBezierTool.bl_idname):
# if(tool == None or tool.idname != 'flexi_bezier.draw_tool'):
return False
return True
def getColorMap(self):
return {'SEL_SEG_COLOR': FTProps.colDrawSelSeg,
'NONADJ_SEG_COLOR': FTProps.colDrawNonHltSeg,
'TIP_COLOR': FTProps.colHdlPtTip,
'ENDPT_TIP_COLOR': FTProps.colBezPt,
'MARKER_COLOR': FTProps.colDrawMarker}
def cancelOp(self, context):
bpy.app.handlers.depsgraph_update_post.remove(self.updateAfterGeomChange)
super(ModalFlexiDrawBezierOp, self).cancelOp(context)
def preInvoke(self, context, event):
super(ModalFlexiDrawBezierOp, self).preInvoke(context, event)
# If the operator is invoked from context menu, enable the tool on toolbar
if(not self.isToolSelected(context) and context.mode == 'OBJECT'):
bpy.ops.wm.tool_set_by_id(name = FlexiDrawBezierTool.bl_idname)
# bpy.ops.wm.tool_set_by_id(name = 'flexi_bezier.draw_tool')
# Object name -> [spline index, [pts]]
# Not used right now (maybe in case of large no of curves)
self.snapInfos = {}
self.updateSnapLocs()
bpy.app.handlers.depsgraph_update_post.append(self.updateAfterGeomChange)
def subModal(self, context, event, snapProc):
rmInfo = self.rmInfo
metakeys = self.snapper.getMetakeys()
if(FTHotKeys.isHotKey(FTHotKeys.hkToggleDrwEd, event.type, metakeys)):
if(event.value == 'RELEASE'):
bpy.ops.wm.tool_set_by_id(name = FlexiEditBezierTool.bl_idname)
# bpy.ops.wm.tool_set_by_id(name = 'flexi_bezier.edit_tool')
return {"RUNNING_MODAL"}
return self.baseSubModal(context, event, snapProc)
def getSnapLocsImpl(self):
locs = []
infos = [info for values in self.snapInfos.values() for info in values]
for info in infos:
locs += info[1]
if(len(self.drawObj.curvePts) > 0):
locs += [pt[1] for pt in self.drawObj.curvePts[:-1]]
# ~ locs += [self.curvePts[-1][0], self.curvePts[-1][1], self.curvePts[-1][2]]
return locs
def updateSnapLocs(self, objNames = None):
updateCurveEndPtMap(self.snapInfos, addObjNames = objNames)
def createCurveObj(self, context, startObj = None, \
startSplineIdx = None, endObj = None, endSplineIdx = None, autoclose = False):
# First create the new curve
collection = context.collection
if(collection == None):
collection = context.scene.collection
obj = createObjFromPts(self.drawObj.curvePts, '3D', collection, autoclose)
# Undo stack in case the user does not want to join
if(endObj != None or startObj != None):
obj.select_set(True)
# ~ bpy.context.view_layer.objects.active = obj
bpy.ops.ed.undo_push()
else:
return obj
endObjs = []
# Connect the end curve (if exists) first
splineIdx = endSplineIdx
if(endObj != None and startObj != endObj):
# first separate splines
endObjs, changeCnt = splitCurve([endObj], split = 'spline', newColl = False)
# then join the selected spline from end curve with new obj
obj = joinSegs([endObjs[splineIdx], obj], optimized = True, \
straight = False, srcCurve = endObjs[splineIdx])
endObjs[splineIdx] = obj
#Use this if there is no start curve
objComps = endObjs
if(startObj != None):
# Repeat the above process for start curve
startObjs, changeCnt = splitCurve([startObj], split = 'spline', \
newColl = False)
obj = joinSegs([startObjs[startSplineIdx], obj], \
optimized = True, straight = False, srcCurve = startObjs[startSplineIdx])
if(startObj == endObj and startSplineIdx != endSplineIdx):
# If startSplineIdx == endSplineIdx the join call above would take care
# but if they are different they need to be joined with a separate call
obj = joinSegs([startObjs[endSplineIdx], obj], \
optimized = True, straight = False, \
srcCurve = startObjs[endSplineIdx])
# can't replace the elem with new obj as in case of end curve
# (see the seq below)
startObjs.pop(endSplineIdx)
if(endSplineIdx < startSplineIdx):
startSplineIdx -= 1
# Won't break even if there were no endObjs
objComps = startObjs[:startSplineIdx] + endObjs[:splineIdx] + \
[obj] + endObjs[(splineIdx + 1):] + startObjs[(startSplineIdx + 1):]
obj = joinCurves(objComps)
if(any(p.co.z != 0 for s in obj.data.splines for p in s.bezier_points)):
obj.data.dimensions = '3D'
return obj
#TODO: At least store in map instead of linear search
def getSnapObjs(self, context, locs):
retVals = [[None, 0, 0]] * len(locs)
foundVals = 0
for obj in bpy.data.objects:
if(isBezier(obj)):
mw = obj.matrix_world
for i, s in enumerate(obj.data.splines):
if(s.use_cyclic_u or len(s.bezier_points) == 0): continue
for j, loc in enumerate(locs):
p = s.bezier_points[0]
if(vectCmpWithMargin(loc, mw @ p.co)):
retVals[j] = [obj, i, 0]
foundVals += 1
p = s.bezier_points[-1]
if(vectCmpWithMargin(loc, mw @ p.co)):
retVals[j] = [obj, i, -1]
foundVals += 1
if(foundVals == len(locs)):
return retVals
return retVals
def save(self, context, event, autoclose, location, align = True):
curvePts = self.drawObj.curvePts
if(len(curvePts) > 1):
startObj, startSplineIdx, ptIdx2, endObj, endSplineIdx, ptIdx1 = \
[x for y in self.getSnapObjs(context, [curvePts[0][1],
curvePts[-1][1]]) for x in y]
ctrl = self.snapper.gridSnap # Overloaded key op
# ctrl pressed and there IS a snapped end obj,
# so user does not want connection
# (no option to only connect to starting curve when end object exists)
if(ctrl and endObj != None):
obj = self.createCurveObj(context, autoclose = False)
else:
startObjName = startObj.name if(startObj != None) else ''
endObjName = endObj.name if(endObj != None) else ''
obj = self.createCurveObj(context, startObj, startSplineIdx, endObj, \
endSplineIdx, autoclose)
if(align and startObj == None and endObj == None):
alignToNormal(obj)
bpy.context.evaluated_depsgraph_get().update()
if(location == None): location = getObjBBoxCenter(obj)
if(location != None):
shiftOrigin(obj, location)
obj.location = location
bpy.context.evaluated_depsgraph_get().update()
params = bpy.context.window_manager.bezierToolkitParams
copyProperties(params.copyPropsObj, obj)
#TODO: Why try?
try:
obj.select_set(True)
# ~ bpy.context.view_layer.objects.active = obj
self.updateSnapLocs([obj.name, startObjName, endObjName])
except Exception as e:
pass
bpy.ops.ed.undo_push()
################### Flexi Draw Grease Bezier ###################
class ModalFlexiDrawGreaseOp(ModalDrawBezierOp):
bl_description = "Flexible drawing of Bezier curves as grease pencil strokes"
bl_idname = "wm.flexi_draw_grease_bezier_curves"
bl_label = "Flexi Draw Grease Bezier Curves"
bl_options = {'REGISTER', 'UNDO'}
h = False
def getToolType(self):
return TOOL_TYPE_FLEXI_GREASE
def __init__(self):
# ~ curveDispRes = 200
# ~ super(ModalFlexiDrawGreaseOp, self).__init__(curveDispRes)
pass
def isToolSelected(self, context):
if(context.mode != 'PAINT_GPENCIL'):
return False
tool = context.workspace.tools.from_space_view3d_mode('PAINT_GPENCIL', \
create = False)
if(tool == None or tool.idname != FlexiGreaseBezierTool.bl_idname):
# if(tool == None or tool.idname != 'flexi_bezier.grease_draw_tool'):
return False
return True
def getColorMap(self):
return {'SEL_SEG_COLOR': FTProps.colGreaseSelSeg,
'NONADJ_SEG_COLOR': ModalBaseFlexiOp.ColGreaseHltSeg, #Not used
'TIP_COLOR': FTProps.colHdlPtTip,
'ENDPT_TIP_COLOR': FTProps.colGreaseBezPt,
'MARKER_COLOR': FTProps.colGreaseMarker, }
def preInvoke(self, context, event):
super(ModalFlexiDrawGreaseOp, self).preInvoke(context, event)
# If the operator is invoked from context menu, enable the tool on toolbar
if(not self.isToolSelected(context) and context.mode == 'PAINT_GPENCIL'):
bpy.ops.wm.tool_set_by_id(name = FlexiGreaseBezierTool.bl_idname)
# bpy.ops.wm.tool_set_by_id(name = 'flexi_bezier.grease_draw_tool')
o = context.object
if(o == None or o.type != 'GPENCIL'):
d = bpy.data.grease_pencils.new('Grease Pencil Data')
o = bpy.data.objects.new('Grease Pencil', d)
context.scene.collection.objects.link(o)
self.gpencil = o
self.subdivCos = []
self.interpPts = []
self.subdivPerUnit = None
# overridden
def redrawBezier(self, rmInfo, hdlPtIdxs = None, hltEndSeg = True):
curvePts = self.drawObj.curvePts
ptCnt = len(curvePts)
subdivCos = self.subdivCos if ptCnt > 1 else []
self.bglDrawMgr.addLineInfo('gpSubdivLines', FTProps.lineWidth, \
[FTProps.colGreaseNonHltSeg], getLinesFromPts(subdivCos))
if(ModalFlexiDrawGreaseOp.h):
self.bglDrawMgr.resetPtInfo('gpSubdivPts')
else:
self.bglDrawMgr.addPtInfo('gpSubdivPts', \
FTProps.greaseSubdivPtSize, [FTProps.colGreaseSubdiv], subdivCos)
if(self.drawType != 'BEZIER' and len(curvePts) > 0):
ModalBaseFlexiOp.refreshDisplayBase(segDispInfos = [], bptDispInfos = [], \
snapper = self.snapper)
else:
super(ModalFlexiDrawGreaseOp, self).redrawBezier(rmInfo, lastSegOnly = True, \
hdlPtIdxs = {ptCnt-1}, hltEndSeg = hltEndSeg)
def initialize(self):
super(ModalFlexiDrawGreaseOp, self).initialize()
self.subdivCos = []
self.interpPts = []
self.updateSnapLocs()
def subModal(self, context, event, snapProc):
curvePts = self.drawObj.curvePts
rmInfo = self.rmInfo
metakeys = self.snapper.getMetakeys()
if(not metakeys[2]):
cntIncr = 5 #if(self.isDrawShape) else 5
if(self.drawType in {'BEZIER', 'ELLIPSE'} and \
event.type in {'WHEELDOWNMOUSE', 'WHEELUPMOUSE', 'NUMPAD_PLUS', \
'NUMPAD_MINUS','PLUS', 'MINUS'} and len(curvePts) > 1):
if(event.type in {'NUMPAD_PLUS', 'NUMPAD_MINUS', 'PLUS', 'MINUS'} \
and event.value == 'PRESS'):
return {'RUNNING_MODAL'}
elif(event.type =='WHEELUPMOUSE' or event.type.endswith('PLUS')):
self.subdivAdd(cntIncr)
elif(event.type =='WHEELDOWNMOUSE' or event.type.endswith('MINUS')):
self.subdivAdd(-cntIncr)
self.redrawBezier(rmInfo)
return {'RUNNING_MODAL'}
if(event.type == 'H' or event.type == 'h'):
if(event.value == 'RELEASE'):
ModalFlexiDrawGreaseOp.h = not ModalFlexiDrawGreaseOp.h
self.redrawBezier(self.rmInfo)
return {"RUNNING_MODAL"}
ptCnt = len(curvePts)
retVal = self.baseSubModal(context, event, snapProc)
newPtCnt = len(curvePts)
# ~ if(newPtCnt - ptCnt != 0):
if(len(curvePts) > 0):
if(self.subdivPerUnit == None):
viewDist = context.space_data.region_3d.view_distance
self.initSubdivPerUnit = 5000.0 / viewDist # TODO: default configurable?
self.subdivPerUnit = 0.02 * self.initSubdivPerUnit
self.snapLocs.append(curvePts[0][1])
if(len(curvePts) > 1):
slens = self.getCurveSegLens()
self.updateInterpPts(slens)
self.updateSubdivCos(sum(slens))
self.redrawBezier(rmInfo)
return retVal
def getCurveSegLens(self):
clen = []
curvePts = self.drawObj.curvePts
for i in range(1, len(curvePts) - 1):
clen.append(getSegLen([curvePts[i-1][1], curvePts[i-1][2], \
curvePts[i][0], curvePts[i][1]]))
return clen
def updateSubdivCos(self, clen = None):
if(self.drawType in {'POLYGON', 'STAR', 'RECTANGLE'}):
self.subdivCos = [p[0] for p in self.drawObj.curvePts]
elif(self.interpPts != []):
if(clen == None): clen = sum(self.getCurveSegLens())
cnt = round(self.subdivPerUnit * clen)
if(cnt > 0):
self.subdivCos = getInterpolatedVertsCo(self.interpPts, cnt)#[1:-1]
return
self.subdivCos = []
def updateInterpPts(self, slens):
curvePts = self.drawObj.curvePts[:] if(self.drawType != 'BEZIER') else \
self.drawObj.curvePts[:-1]
self.interpPts = getInterpBezierPts(curvePts, self.initSubdivPerUnit, slens)
return self.interpPts
def subdivAdd(self, addCnt):
slens = self.getCurveSegLens()
clen = sum(slens)
if(clen == 0): return
cnt = self.subdivPerUnit * clen + addCnt
if(cnt < 1): cnt = 1
self.subdivPerUnit = (cnt / clen)
self.updateSubdivCos(clen)
def getSnapLocsImpl(self):
return self.snapLocs
def updateSnapLocs(self):
self.snapLocs = []
gpencils = [o for o in bpy.data.objects if o.type == 'GPENCIL']
for gpencil in gpencils:
mw = gpencil.matrix_world
for layer in gpencil.data.layers:
for f in layer.frames:
for s in f.strokes:
if(len(s.points) > 0): # Shouldn't be needed, but anyway...
self.snapLocs += [mw @ s.points[0].co, mw @ s.points[-1].co]
def save(self, context, event, autoclose, location):
layer = self.gpencil.data.layers.active
if(layer == None):
layer = self.gpencil.data.layers.new('GP_Layer', set_active = True)
if(len(layer.frames) == 0):
layer.frames.new(0)
frame = layer.frames[-1]
invMw = self.gpencil.matrix_world.inverted_safe()
if(len(self.subdivCos) > 0):
brush = context.scene.tool_settings.gpencil_paint.brush
lineWidth = brush.size
strength = brush.gpencil_settings.pen_strength
stroke = frame.strokes.new()
stroke.display_mode = '3DSPACE'
stroke.points.add(count = len(self.subdivCos))
for i in range(0, len(self.subdivCos)):
pt = self.subdivCos[i]
stroke.points[i].co = self.gpencil.matrix_world.inverted_safe() @ pt
stroke.points[i].strength = strength
if(autoclose):
stroke.points.add(count = 1)
stroke.points[-1].co = stroke.points[0].co.copy()
stroke.points[-1].strength = strength
stroke.line_width = lineWidth
self.snapLocs += [self.subdivCos[0][1], self.subdivCos[-1][1]]
bpy.ops.ed.undo_push()
################### Flexi Edit Bezier Curve ###################
class EditSegDisplayInfo(SegDisplayInfo):
def __init__(self, segPts, segColor, subdivCos):
super(EditSegDisplayInfo, self).__init__(segPts, segColor)
self.subdivCos = subdivCos
# fromMix True: points after shape key value / eval_time applied
def getBptData(obj, withShapeKey = True, shapeKeyIdx = None, fromMix = True, \
updateDeps = False, local = False):
# Less readable but more convenient than class
# Format: [handle_left, co, handle_right, handle_left_type, handle_right_type]
worldSpaceData = []
mw = Matrix() if local else obj.matrix_world
keydata = None
dataIdx = 0
shapeKey = obj.active_shape_key
tmpsk = None
if(withShapeKey and shapeKey != None):
if(shapeKeyIdx == None):
shapeKeyIdx = obj.active_shape_key_index
if(fromMix):
if(not obj.data.shape_keys.use_relative):
val = obj.data.shape_keys.eval_time
else:
val = obj.data.shape_keys.key_blocks[obj.active_shape_key_index].value
if(floatCmpWithMargin(val, 0)):
keyBlock = obj.data.shape_keys.key_blocks[0]
else:
tmpsk = obj.shape_key_add(name = 'tmp', from_mix = True)
keyBlock = obj.data.shape_keys.key_blocks[tmpsk.name]
else:
keyBlock = obj.data.shape_keys.key_blocks[shapeKeyIdx]
keydata = keyBlock.data
for spline in obj.data.splines:
pts = []
for pt in spline.bezier_points:
lt, rt = pt.handle_left_type, pt.handle_right_type
if(keydata != None):
pt = keydata[dataIdx]
dataIdx += 1
pts.append([mw @ pt.handle_left, mw @ pt.co, mw @ pt.handle_right, lt, rt])
worldSpaceData.append(pts)
if(tmpsk != None):
obj.shape_key_remove(tmpsk)
obj.active_shape_key_index = shapeKeyIdx
if(updateDeps): bpy.context.evaluated_depsgraph_get().update()
return worldSpaceData
#TODO: splineIdx not needed if ptCnt given
def getAdjIdx(obj, splineIdx, startIdx, offset = 1, ptCnt = None):
spline = obj.data.splines[splineIdx]
if(ptCnt == None):
ptCnt = len(spline.bezier_points)
if(not spline.use_cyclic_u and
((startIdx + offset) >= ptCnt or (startIdx + offset) < 0)):
return None
return (ptCnt + startIdx + offset) % ptCnt # add ptCnt for negative offset
def getBezierDataForSeg(obj, splineIdx, segIdx, withShapeKey = True, shapeKeyIdx = None, \
fromMix = True, updateDeps = False):
wsData = getBptData(obj, withShapeKey, shapeKeyIdx, fromMix, updateDeps)
pt0 = wsData[splineIdx][segIdx]
segEndIdx = getAdjIdx(obj, splineIdx, segIdx)
if(segEndIdx == None):
return []
pt1 = wsData[splineIdx][segEndIdx]
return [pt0, pt1]
def getSegPtsInSpline(wsData, splineIdx, ptIdx, cyclic):
splinePts = wsData[splineIdx]
if(ptIdx < (len(splinePts) - 1) ): ptRange = [ptIdx, ptIdx + 1]
elif(ptIdx == (len(splinePts) - 1) and cyclic):
ptRange = [-1, 0]
if(splinePts[-1][4] == 'VECTOR'):
splinePts[-1][2] = (splinePts[-1][1] + \
1/3 * (splinePts[-1][1] - splinePts[0][1]))
if(splinePts[0][3] == 'VECTOR'):
splinePts[0][0] = (splinePts[0][1] + \
1/3 * (splinePts[0][1] - splinePts[-1][1]))
else: return []
return [[splinePts[x][i] for i in range(5)] for x in ptRange]
def getInterpSegPts(wsData, splineIdx, ptIdx, cyclic, res, maxRes):
segPts = getSegPtsInSpline(wsData, splineIdx, ptIdx, cyclic)
areaRegionInfo = getAllAreaRegions() # TODO: To be passed from caller
return getPtsAlongBezier2D(segPts, areaRegionInfo, res, maxRes)
# Wrapper for spatial search within segment
def getClosestPt2dWithinSeg(region, rv3d, coFind, selObj, selSplineIdx, selSegIdx, \
withHandles, withBezPts):
infos = {selObj: {selSplineIdx:[[selSegIdx],[]]}}
# set selObj in objs for CurveBezPts
return getClosestPt2d(region, rv3d, coFind, [selObj], infos, withHandles, \
withBezPts, withObjs = False, maxSelObjRes = MAX_SEL_CURVE_RES)
def getClosestPt2d(region, rv3d, coFind, objs, selObjInfos, withHandles = True, \
withBezPts = True, withObjs = True, maxSelObjRes = MAX_NONSEL_CURVE_RES, \
withShapeKey = True):
objLocMap = {}
objLocList = [] # For mapping after search returns
objInterpLocs = []
objInterpCounts = []
objBezPtCounts = []
objSplineEndPts = []
for obj in objs:
#TODO: Check of shape key bounding box
if(obj.active_shape_key == None and \
not isPtIn2dBBox(obj, region, rv3d, coFind, FTProps.snapDist)):
continue
wsDataSK = None
# Curve data with shape key value applied (if shape key exists)
wsData = getBptData(obj, fromMix = True, updateDeps = True)
if(withShapeKey and obj.active_shape_key != None):
# active shape key data with value = 1
wsDataSK = getBptData(obj, fromMix = False)
for i, spline in enumerate(obj.data.splines):
for j, pt in enumerate(spline.bezier_points):
objLocList.append([obj, i, j])
if(withObjs):
interpLocs = \
getInterpSegPts(wsData, i, j, spline.use_cyclic_u, \
res = SEARCH_CURVE_RES, maxRes = MAX_NONSEL_CURVE_RES)[1:-1]
if(wsDataSK != None):
interpLocs += \
getInterpSegPts(wsDataSK, i, j, spline.use_cyclic_u, \
res = SEARCH_CURVE_RES, \
maxRes = MAX_NONSEL_CURVE_RES)[1:-1]
objInterpLocs += interpLocs
objInterpCounts.append(len(interpLocs))
if(withBezPts):
cnt = 1
objSplineEndPts.append(wsData[i][j][1])#mw @ pt.co)
if(wsDataSK != None):
objSplineEndPts.append(wsDataSK[i][j][1])#mw @ pt.co)
cnt += 1
objBezPtCounts.append(cnt)
selObjLocList = [] # For mapping after search returns
selObjHdlList = [] # Better to create a new one, even if some redundancy
segInterpLocs = []
selObjInterpCounts = []
selObjHdlCounts = []
hdls = []
for selObj in selObjInfos.keys():
wsDataSK = None
# Curve data with shape key value applied (if shape key exists)
wsData = getBptData(selObj, fromMix = True, updateDeps = True)
if(withShapeKey and selObj.active_shape_key != None):
# active shape key data with value = 1
wsDataSK = getBptData(selObj, fromMix = False)
info = selObjInfos[selObj]
for splineIdx in info.keys():
cyclic = selObj.data.splines[splineIdx].use_cyclic_u
segIdxs = info[splineIdx][0]
for segIdx in segIdxs:
selObjLocList.append([selObj, splineIdx, segIdx])
interpLocs = getInterpSegPts(wsData, splineIdx, segIdx, cyclic, \
res = SEARCH_CURVE_RES * 5, maxRes = maxSelObjRes)[1:-1]
if(wsDataSK != None):
interpLocs += \
getInterpSegPts(wsDataSK, splineIdx, segIdx, cyclic, \
res = SEARCH_CURVE_RES * 5, maxRes = maxSelObjRes)[1:-1]
segInterpLocs += interpLocs
selObjInterpCounts.append(len(interpLocs))
if(withHandles):
ptIdxs = info[splineIdx][1]
for ptIdx in ptIdxs:
selObjHdlList.append([selObj, splineIdx, ptIdx])
hdlCnt = 2
if(wsDataSK != None):
pt = wsDataSK[splineIdx][ptIdx]
hdls += [pt[0], pt[2]]
else:
pt = wsData[splineIdx][ptIdx]
hdls += [pt[0], pt[2]]
selObjHdlCounts.append(hdlCnt)
searchPtsList = [[], [], [], [], [], []]
retStr = [[], [], [], [], [], []]
#'SelHandles', 'SegLoc', 'CurveBezPt', 'CurveLoc'
searchPtsList[0], retStr[0] = hdls, 'SelHandles'
searchPtsList[1], retStr[1] = objSplineEndPts, 'CurveBezPt'
searchPtsList[2], retStr[2] = segInterpLocs, 'SegLoc'
searchPtsList[3], retStr[3] = objInterpLocs, 'CurveLoc'
# TODO: Remove duplicates before sending for search?
searchPtsList = [[getCoordFromLoc(region, rv3d, pt).to_3d() \
for pt in pts] for pts in searchPtsList]
srs = NestedListSearch(searchPtsList).findInLists(coFind, \
searchRange = FTProps.snapDist)
if(len(srs) == 0):
return None
sr = min(srs, key = lambda x: x[3])
if(sr[0] > 1):
# If seg loc then first priority to the nearby handle, end pt (even if farther)
sr = min(srs, key = lambda x: (x[0], x[3]))
idx = sr[1]
retId = retStr[sr[0]]
if(sr[0] == 0): # SelHandles
listIdx = NestedListSearch.findListIdx(selObjHdlCounts, idx)
obj, splineIdx, ptIdx = selObjHdlList[listIdx]
# ~ obj, splineIdx, ptIdx = selObjHdlList[int(idx / 2)]
return retId, obj, splineIdx, ptIdx, 2 * (idx % 2)
elif(sr[0] == 1): # CurveBezPt
listIdx = NestedListSearch.findListIdx(objBezPtCounts, idx)
obj, splineIdx, ptIdx = objLocList[listIdx]
# ~ obj, splineIdx, ptIdx = objLocList[int(idx / ptIdxCnt)]
return retId, obj, splineIdx, ptIdx, 1 # otherInfo = segIdx
elif(sr[0] == 2): # SegLoc
listIdx = NestedListSearch.findListIdx(selObjInterpCounts, idx)
obj, splineIdx, segIdx = selObjLocList[listIdx]
return retId, obj, splineIdx, segIdx, segInterpLocs[idx]
else: # CurveLoc
listIdx = NestedListSearch.findListIdx(objInterpCounts, idx)
obj, splineIdx, segIdx = objLocList[listIdx]
return retId, obj, splineIdx, segIdx, objInterpLocs[idx]
class NestedListSearch:
# Find the list element containing the given idx from flattened list
# return the index of the list element containing the idx
def findListIdx(counts, idx):
cumulCnt = 0
cntIdx= 0
while(idx >= cumulCnt):
cumulCnt += counts[cntIdx] # cntIdx can never be >= len(counts)
cntIdx += 1
return cntIdx - 1
def __init__(self, ptsList):
self.ptsList = ptsList
self.kd = kdtree.KDTree(sum(len(pts) for pts in ptsList))
idx = 0
self.counts = []
for i, pts in enumerate(ptsList):
self.counts.append(len(pts))
for j, pt in enumerate(pts):
self.kd.insert(pt, idx)
idx += 1
self.kd.balance()
def findInLists(self, coFind, searchRange):
if(searchRange == None):
foundVals = [self.kd.find(coFind)]
else:
foundVals = self.kd.find_range(coFind, searchRange)
foundVals = sorted(foundVals, key = lambda x: x[2])
searchResults = []
for co, idx, dist in foundVals:
listIdx = NestedListSearch.findListIdx(self.counts, idx)
ptIdxInList = idx - sum(len(self.ptsList[i]) for i in range(0, listIdx))
searchResults.append([listIdx, ptIdxInList, co, dist])
return searchResults
class SelectCurveInfo:
def __init__(self, obj, splineIdx):
self.obj = obj
self.splineIdx = splineIdx
self.updateWSData()
# User Selection (mouse click); format ptIdx: set(sel)...
# where sel: -1->seg, 0->left hdl, 1->bezier pt, 2->right hdl
self.ptSels = {}
# Highlighted point (mouse move)
# 'ptIdx': ptIdx, 'hltIdx':hltIdx {-1, 0, 1, 2} (just as in sel above)
self.hltInfo = {}
# obj.name gives exception if obj is not in bpy.data.objects collection,
# so keep a copy
self.objName = obj.name
self.interpPts = {}
# Format 'ptIdx': segIdx, 'hdlIdx': hdlIdx, 'loc':loc, 't':t
# hdlIdx - {-1, 0, 1, 2} similar to sel in ptSels
self.clickInfo = {}
def __hash__(self):
return hash((self.objName, self.splineIdx))
def updateWSData(self):
self.hasShapeKey = (self.obj.active_shape_key != None)
self.shapeKeyIdx = self.obj.active_shape_key_index if self.hasShapeKey else -1
# for shape keys
self.keyStartIdx = sum(len(self.obj.data.splines[i].bezier_points) \
for i in range(self.splineIdx))
# WS Data of the shape key (if exists)
self.wsData = getBptData(self.obj, fromMix = False)[self.splineIdx]
# For convenience
def getAdjIdx(self, ptIdx, offset = 1):
return getAdjIdx(self.obj, self.splineIdx, ptIdx, offset)
def getBezierPt(self, ptIdx):
return self.obj.data.splines[self.splineIdx].bezier_points[ptIdx]
def getShapeKeyData(self, ptIdx, keyIdx = None):
if(not self.hasShapeKey): return None
if(keyIdx == None): keyIdx = self.obj.active_shape_key_index
keydata = self.obj.data.shape_keys.key_blocks[keyIdx].data
keyIdx = self.keyStartIdx + ptIdx
return keydata[keyIdx] if(keyIdx < len(keydata)) else None
def getAllShapeKeysData(self, ptIdx):
if(not self.hasShapeKey): return None
pts = []
for keyIdx in range(len(self.obj.data.shape_keys.key_blocks)):
pts.append(self.getShapeKeyData(ptIdx, keyIdx))
return pts
def getSegPtsInfo(self, ptIdx):
nextIdx = self.getAdjIdx(ptIdx)
pt0 = self.wsData[ptIdx]
pt1 = self.wsData[nextIdx]
return nextIdx, pt0, pt1
def getSegPts(self, ptIdx):
nextIdx, pt0, pt1 = self.getSegPtsInfo(ptIdx)
return (pt0, pt1)
# All selected points which have handles displayed (for example for snapping)
def getAllPtsWithHdls(self):
ptIdxs = set(self.ptSels.keys())
nextIdxs = set(self.getSegPtsInfo(p)[0] for p in ptIdxs if -1 in self.ptSels[p])
ptIdxs = ptIdxs.union(nextIdxs)
return sorted(self.wsData[p] for p in ptIdxs)
def setClickInfo(self, ptIdx, hdlIdx, clickLoc, lowerT = 0.001, higherT = .999):
self.clickInfo = None
if(clickLoc != None):
nextIdx, pt0, pt1 = self.getSegPtsInfo(ptIdx)
t = getTForPt([pt0[1], pt0[2], pt1[0], pt1[1]], clickLoc)
if(t != None and (t < lowerT or t > higherT)):
hdlIdx = 1
if(t > higherT): ptIdx = nextIdx
else:
self.clickInfo = {'ptIdx': ptIdx, 'hdlIdx': hdlIdx, \
'loc':clickLoc, 't':t}
if(self.clickInfo == None):
self.clickInfo = {'ptIdx': ptIdx, 'hdlIdx': hdlIdx}
def addSel(self, ptIdx, sel, toggle = False):
self.addSels(ptIdx, set([sel]), toggle)
def addSels(self, ptIdx, sels, toggle = False):
# TODO: Check this condition at other places
if( -1 in sels and self.getAdjIdx(ptIdx) == None): sels.remove(-1)
if(len(sels) == 0): return
currSels = self.ptSels.get(ptIdx)
if(currSels == None): currSels = set()
modSels = currSels.union(sels)
if(toggle): modSels -= currSels.intersection(sels)
self.ptSels[ptIdx] = modSels
if(len(self.ptSels[ptIdx]) == 0 ): self.ptSels.pop(ptIdx)
def removeSel(self, ptIdx, sel):
self.removeSels(ptIdx, {sel})
def removeSels(self, ptIdx, sels):
for sel in sels:
currSels = self.ptSels.get(ptIdx)
if(currSels != None and sel in currSels):
currSels.remove(sel)
if(len(currSels) == 0): self.ptSels.pop(ptIdx)
def resetClickInfo(self):
self.clickInfo = {}
def resetPtSel(self):
self.ptSels = {}
def resetHltInfo(self):
self.hltInfo = {}
def getHltInfo(self):
return self.hltInfo
def setHltInfo(self, ptIdx, hltIdx):
self.hltInfo = {'ptIdx': ptIdx, 'hltIdx':hltIdx}
def getClickLoc(self):
return self.clickInfo.get('loc')
def getSelCo(self):
if(len(self.clickInfo) > 0):
hdlIdx = self.clickInfo['hdlIdx']
if(hdlIdx == -1):
return self.clickInfo['loc']
else:
ptIdx = self.clickInfo['ptIdx']
pt0 = self.wsData[ptIdx]
return pt0[hdlIdx]
return None
def subdivSeg(self, subdivCnt):
if(self.hasShapeKey): return False
if(subdivCnt > 1):
invMw = self.obj.matrix_world.inverted_safe()
ts = []
addCnt = 0
for ptIdx in sorted(self.ptSels.keys()):
if(-1 in self.ptSels[ptIdx]):
vertCos = getInterpolatedVertsCo(self.interpPts[ptIdx], \
subdivCnt)[1:-1]
changedIdx = ptIdx + addCnt
insertBezierPts(self.obj, self.splineIdx, changedIdx, \
[invMw @ v for v in vertCos], 'FREE')
addCnt += len(vertCos)
return addCnt > 0
def bevelPts(self, bevelCnt, deltaPos):
if(self.hasShapeKey): return False
pts, ptSels = self.getBevelPts(bevelCnt, self.wsData, deltaPos)
spline = self.obj.data.splines[self.splineIdx]
newPtCnt = len(pts) - len(self.wsData)
spline.bezier_points.add(newPtCnt)
for pt in spline.bezier_points:
pt.handle_left_type = 'FREE'
pt.handle_right_type = 'FREE'
invMw = self.obj.matrix_world.inverted_safe()
for i, pt in enumerate(spline.bezier_points):
pt.handle_left = invMw @ pts[i][0]
pt.co = invMw @ pts[i][1]
pt.handle_right = invMw @ pts[i][2]
pt.handle_left_type = pts[i][3]
pt.handle_right_type = pts[i][4]
self.updateWSData()
self.ptSels = ptSels
return True
def initSubdivMode(self, rv3d):
if(self.hasShapeKey): return False
changed = False
for ptIdx in self.ptSels.keys():
if(-1 in self.ptSels[ptIdx]):
self.interpPts[ptIdx] = getPtsAlongBezier3D(self.getSegPts(ptIdx), rv3d,
curveRes = 1000, minRes = 1000)
changed = True
return changed
def isBevelabel(self, rv3d):
if(self.hasShapeKey): return False
changed = False
for ptIdx in self.ptSels.keys():
ptIdxs = [ptIdx]
if(-1 in self.ptSels[ptIdx]): ptIdxs.append(self.getAdjIdx(ptIdx))
elif(1 not in self.ptSels[ptIdx]): continue # only pt and seg selection
for idx in ptIdxs:
prevIdx = self.getAdjIdx(idx, -1)
nextIdx = self.getAdjIdx(idx)
if(nextIdx != None and prevIdx != None and \
not hasAlignedHandles(self.wsData[idx])):
changed = True
break
return changed
def getLastSegIdx(self):
return getLastSegIdx(self.obj, self.splineIdx)
# Remove all selected segments
# Returns map with spline index and seg index change after every seg removal
def removeSegs(self):
changedSelMap = {}
if(self.hasShapeKey): return changedSelMap
segSels = [p for p in self.ptSels if -1 in self.ptSels[p]]
cumulSegIdxIncr = 0
changedSplineIdx = self.splineIdx
segIdxIncr = 0
for segIdx in sorted(segSels):
changedSegIdx = segIdx + cumulSegIdxIncr
splineIdxIncr, segIdxIncr = removeBezierSeg(self.obj, \
changedSplineIdx, changedSegIdx)
changedSplineIdx += splineIdxIncr
cumulSegIdxIncr += segIdxIncr
changedSelMap[segIdx] = [splineIdxIncr, segIdxIncr]
return changedSelMap
def straightenHandle(self, ptIdx, hdlIdx, allShapekeys = False):
bpt = self.getBezierPt(ptIdx)
prevIdx = self.getAdjIdx(ptIdx, -1)
nextIdx = self.getAdjIdx(ptIdx)
prevPts = None
nextPts = None
if(self.hasShapeKey):
if(allShapekeys):
pts = self.getAllShapeKeysData(ptIdx)
if(prevIdx != None): prevPts = self.getAllShapeKeysData(prevIdx)
if(nextIdx != None): nextPts = self.getAllShapeKeysData(nextIdx)
else:
pts = [self.getShapeKeyData(ptIdx)]
if(prevIdx != None): prevPts = [self.getShapeKeyData(prevIdx)]
if(nextIdx != None): nextPts = [self.getShapeKeyData(nextIdx)]
else:
pts = [bpt]
if(prevIdx != None): prevPts = [self.getBezierPt(prevIdx)]
if(nextIdx != None): nextPts = [self.getBezierPt(nextIdx)]
if(hdlIdx == 0):
if(bpt.handle_left_type != 'VECTOR'): bpt.handle_left_type = 'FREE'
for i in range(len(pts)):
pt = pts[i]
if(prevPts != None): diffV = (pt.co - prevPts[i].co)
else: diffV = (nextPts[i].co - pt.co)
pt.handle_left = pt.co - diffV / 3
elif(hdlIdx == 2):
if(bpt.handle_right_type != 'VECTOR'): bpt.handle_right_type = 'FREE'
for i in range(len(pts)):
pt = pts[i]
if(nextPts != None): diffV = (nextPts[i].co - pt.co)
else: diffV = (pt.co - prevPts[i].co)
pt.handle_right = pt.co + diffV / 3
def straightenSelHandles(self):
changed = False
for ptIdx in self.ptSels:
for hdlIdx in self.ptSels[ptIdx]:
self.straightenHandle(ptIdx, hdlIdx)
changed = True
return changed
def alignHandle(self, ptIdx, hdlIdx, allShapekeys = False):
if (hdlIdx == -1): return False
oppIdx = 2 - hdlIdx
if(self.hasShapeKey):
if(allShapekeys): pts = self.getAllShapeKeysData(ptIdx)
else: pts = [self.getShapeKeyData(ptIdx)]
else: pts = [self.getBezierPt(ptIdx)]
bpt = self.getBezierPt(ptIdx)
if(hdlIdx == 0 and bpt.handle_left_type != 'ALIGNED'):
bpt.handle_left_type = 'FREE'
if(hdlIdx == 2 and bpt.handle_right_type != 'ALIGNED'):
bpt.handle_right_type = 'FREE'
for pt in pts:
if(hdlIdx == 0): pt.handle_left = pt.co - \
(pt.co - pt.handle_left).length * (pt.handle_right - pt.co).normalized()
else: pt.handle_right = pt.co + \
(pt.co - pt.handle_right).length * (pt.co - pt.handle_left).normalized()
return True
def alignSelHandles(self):
changed = False
invMw = self.obj.matrix_world.inverted_safe()
for ptIdx in self.ptSels:
sels = self.ptSels[ptIdx]
for hdlIdx in sels:
changed = self.alignHandle(ptIdx, hdlIdx) or changed
return changed
def insertNode(self, handleType, select = True):
if(self.hasShapeKey): return False
invMw = self.obj.matrix_world.inverted_safe()
insertBezierPts(self.obj, self.splineIdx, \
self.clickInfo['ptIdx'], [invMw @ self.clickInfo['loc']], handleType)
return True
def removeNode(self):
if(self.hasShapeKey): return False
changed = False
toRemove = set() # Bezier points to remove from object
toRemoveSel = set() # Selection entry to remove from ptSels
nodeSels = [p for p in self.ptSels if 1 in self.ptSels[p]]
for ptIdx in nodeSels:
self.ptSels.pop(ptIdx)
if(len(nodeSels) > 0):
removeBezierPts(self.obj, self.splineIdx, nodeSels)
changed = True
if(changed):
selIdxs = sorted(self.ptSels.keys())
cnt = 0
for ptIdx in nodeSels:
cIdxs = [i for i in selIdxs if i >= (ptIdx - cnt)]
for idx in cIdxs:
sels = self.ptSels.pop(idx - cnt)
self.ptSels[idx - cnt - 1] = sels
cnt += 1
return changed
def getBevelPts(self, bevelCnt, pts, deltaPos):
deltaLen = deltaPos.length
if(floatCmpWithMargin(deltaLen, DEF_ERR_MARGIN)):
return pts, self.ptSels
# http://launchpadlibrarian.net/12692602/rcp.svg
kFact = (bevelCnt/3) * (sqrt(2) - 1)
maxT = .5
# Deep copy
pts = [[p if isinstance(p, str) else p.copy() for p in pt] for pt in pts]
newPts = []
newSelPtIdxs = []
# Add both points of the selected segments in selection
bevelPtIdxs = set()
for ptIdx in self.ptSels.keys():
if(1 in self.ptSels[ptIdx]): bevelPtIdxs.add(ptIdx)
if(-1 in self.ptSels[ptIdx]):
adjIdx = self.getAdjIdx(ptIdx)
bevelPtIdxs.add(ptIdx)
bevelPtIdxs.add(adjIdx)
ptSels = {k:self.ptSels[k].copy() for k in self.ptSels.keys()}
# Extra loop because next points need to be determined beforehand
for ptIdx in bevelPtIdxs:
if(ptSels.get(ptIdx) == None): ptSels[ptIdx] = {1}
else: ptSels[ptIdx].add(1)
nextIdx = self.getAdjIdx(ptIdx)
prevIdx = self.getAdjIdx(ptIdx, -1)
if(prevIdx != None and nextIdx != None and \
not hasAlignedHandles(pts[ptIdx])):
newSelPtIdxs.append(ptIdx)
for ptIdx, pt in enumerate(pts):
if(ptIdx in newSelPtIdxs):
nextIdx = self.getAdjIdx(ptIdx)
prevIdx = self.getAdjIdx(ptIdx, -1)
prevPt = pts[prevIdx][:]
diffV = (pt[1] - prevPt[1])
segLen = diffV.length
if(segLen < .001):
newPts.append(pt)
else:
t = deltaLen / segLen
if(t > maxT and (prevIdx in newSelPtIdxs)):
t = maxT
k = kFact * (segLen / 2)
elif(t > 1):
t = 1
k = kFact * segLen
else:
k = kFact * deltaLen
seg = [pts[prevIdx][1], pts[prevIdx][2], pt[0], pt[1]]
partialSeg = getPartialSeg(seg, t0 = 0, t1 = 1 - t)
newPt = partialSeg[3]
tangent0 = getTangentAtT(pts[prevIdx][1], pts[prevIdx][2], \
pt[0], pt[1], 1 - t)
pt0_2 = newPt + k * (tangent0.normalized())
pt0 = [partialSeg[2], newPt, pt0_2, 'FREE', 'FREE']
newPts.append(pt0)
prevPt[2] = partialSeg[1]
nextPt = pts[nextIdx][:]
diffV = (nextPt[1] - pt[1])
segLen = diffV.length
if(floatCmpWithMargin(segLen, 0)):
newPts.append(pt)
else:
t = deltaLen / segLen
if(t > maxT and (nextIdx in newSelPtIdxs)):
t = maxT
k = kFact * (segLen / 2)
elif(t > 1):
t = 1
k = kFact * segLen
else:
k = kFact * deltaLen
seg = [pt[1], pt[2], pts[nextIdx][0], pts[nextIdx][1]]
partialSeg = getPartialSeg(seg, t0 = t, t1 = 1)
newPt = partialSeg[0]
tangent1 = getTangentAtT(pt[1], pt[2], \
pts[nextIdx][0], pts[nextIdx][1], t)
pt1_0 = newPt - k * (tangent1.normalized())
pt1 = [pt1_0, newPt, partialSeg[1], 'FREE', 'FREE']
newPts.append(pt1)
nextPt[0] = partialSeg[2]
else:
newPts.append(pt)
newPtSels = {}
cnt = 0
for ptIdx in sorted(ptSels.keys()):
newPtSels[ptIdx + cnt] = ptSels[ptIdx].copy()
if(ptIdx in newSelPtIdxs):
newPtSels[ptIdx + cnt].add(-1)
newPtSels[ptIdx + cnt + 1] = {1}
cnt += 1
return newPts, newPtSels
def getDisplayInfos(self, hideHdls = False, subdivCnt = 0, \
bevelCnt = 0, newPos = None, deltaPos = None):
# Making long short
cHltTip = FTProps.colHltTip
cBezPt = FTProps.colBezPt
cHdlPt = FTProps.colHdlPtTip
cAdjBezTip = FTProps.colAdjBezTip
cNonHltSeg = FTProps.colDrawNonHltSeg
segDispInfos = []
bptDispInfos = []
pts = self.wsData[:]
ptSels = self.ptSels
if(newPos != None):
# TODO: This method is in EditCurveInfo
nPtIdxs, nPts = self.getOffsetSegPts(newPos)
# Update list with new position (editing)
for i, ptIdx in enumerate(nPtIdxs): pts[ptIdx] = nPts[i]
elif(deltaPos != None):
pts, ptSels = self.getBevelPts(bevelCnt, pts, deltaPos)
# Default display of spline
for i, pt in enumerate(pts):
bptDispInfos.append(BptDisplayInfo(pt, [cAdjBezTip]))
if(i > 0):
segDispInfos.append(SegDisplayInfo([pts[i-1], pt], cNonHltSeg))
lastIdx = self.getAdjIdx(len(self.wsData) - 1) # In case cyclic...
if(lastIdx != None):
segDispInfos.append(SegDisplayInfo([pts[-1], pts[0]], cNonHltSeg))
hltInfo = self.getHltInfo()
hltPtIdx = hltInfo.get('ptIdx')
hltIdx = hltInfo.get('hltIdx')
# Process highlighted segments before selected ones because...
# selected segments take priority over highlighted
if(hltIdx == -1):
segDispInfos[hltPtIdx].segColor = FTProps.colDrawHltSeg
bptDispInfos[hltPtIdx].tipColors[1] = cBezPt
nextIdx = self.getAdjIdx(hltPtIdx)
bptDispInfos[nextIdx].tipColors[1] = cBezPt
# Process selections
for ptIdx in sorted(ptSels.keys()):
sels = ptSels[ptIdx]
if(hideHdls):
tipColors = [None, cBezPt, None]
handleNos = []
else:
tipColors = [cHdlPt, cBezPt, cHdlPt]
handleNos = [0, 1]
bptDispInfos[ptIdx].tipColors = tipColors[:]
bptDispInfos[ptIdx].handleNos = handleNos
for hdlIdx in sorted(sels): # Start with seg selection i. e. -1
if(hdlIdx == -1):
nextIdx = getAdjIdx(self.obj, self.splineIdx, ptIdx, ptCnt = len(pts))
segPts = [pts[ptIdx], pts[nextIdx]]
# process next only if there are no selection pts with that idx
if(nextIdx not in ptSels.keys()):
bptDispInfos[nextIdx].tipColors = tipColors[:]
bptDispInfos[nextIdx].handleNos = handleNos
vertCos = []
if(subdivCnt > 1):
vertCos = getInterpolatedVertsCo(self.interpPts[ptIdx], \
subdivCnt)[1:-1]
selSegDispInfo = EditSegDisplayInfo(segPts, \
FTProps.colDrawSelSeg, vertCos)
segDispInfos[ptIdx] = selSegDispInfo
elif(hdlIdx == 1 or not hideHdls):
bptDispInfos[ptIdx].tipColors[hdlIdx] = FTProps.colSelTip
# Process highlighted points after selected ones because...
# highlighted points take priority over selected
if(hltIdx in {0, 1, 2}):
bptDispInfos[hltPtIdx].tipColors[hltIdx] = cHltTip
return [segDispInfos, bptDispInfos]
class EditCurveInfo(SelectCurveInfo):
def __init__(self, obj, splineIdx, ptSels = None):
super(EditCurveInfo, self).__init__(obj, splineIdx)
if(ptSels != None):
self.ptSels = ptSels
def syncAlignedHdl(self, pt, ctrlPLoc, hdlIdx):
typeIdx = 3 if hdlIdx == 0 else 4
if(pt[typeIdx] == 'ALIGNED'):
oppTypeIdx = 4 if hdlIdx == 0 else 3
if(pt[oppTypeIdx] in {'VECTOR', 'ALIGNED'}):
oppHdlIdx = 2 if hdlIdx == 0 else 0
oppHdlV = ctrlPLoc - pt[oppHdlIdx]
if(oppHdlV.length != 0):
currL = (ctrlPLoc - pt[hdlIdx]).length
pt[hdlIdx] = ctrlPLoc + currL * oppHdlV / oppHdlV.length
def setAlignedHdlsCo(self, pt, hdlIdx, ctrlPLoc):
typeIdx = 3 if hdlIdx == 0 else 4
if(pt[typeIdx] == 'ALIGNED'):
oppTypeIdx = 4 if hdlIdx == 0 else 3
if(pt[oppTypeIdx] != 'VECTOR'):
pt[hdlIdx] += (ctrlPLoc - pt[1])
else:
self.syncAlignedHdl(pt, ctrlPLoc, hdlIdx)
def setFreeHdlsCo(self, pt, hdlIdx, newLoc):
typeIdx = 3 if hdlIdx == 0 else 4
if(pt[typeIdx] == 'FREE'):
pt[hdlIdx] += (newLoc - pt[1])
def setVectHdlsCo(self, pt, newLoc, hdlIdx, prevPt, nextPt):
typeIdx = 3 if hdlIdx == 0 else 4
if(pt[typeIdx] == 'VECTOR'):
typeIdx = 3 if hdlIdx == 0 else 4
pts = [prevPt, nextPt] if hdlIdx == 0 else [nextPt, prevPt]
diffV = None
if(pts[0] != None): diffV = pts[0][1] - newLoc
if(diffV == None and pts[1] != None):
diffV = newLoc - pts[1][1]
if(diffV == None): pt[hdlIdx] = newLoc
else: pt[hdlIdx] = newLoc + diffV * 1 / 3
# Calculate both handle and adjacent pt handles in case of Vector type
# TODO: AUTO has a separate logic set to ALIGNED for now
def syncCtrlPtHdls(self, ptIdx, newLoc):
wsData = getBptData(self.obj, fromMix = False)
pt = wsData[self.splineIdx][ptIdx]
prevIdx = self.getAdjIdx(ptIdx, -1)
prevPt = None if prevIdx == None else wsData[self.splineIdx][prevIdx]
nextIdx = self.getAdjIdx(ptIdx)
nextPt = None if nextIdx == None else wsData[self.splineIdx][nextIdx]
ptIdxs = [ptIdx]
pts = [pt]
for typeIdx in [3, 4]:
if(pt[typeIdx] == 'AUTO'): pt[typeIdx] = 'ALIGNED'
for hdlIdx in [0, 2]:
self.setVectHdlsCo(pt, newLoc, hdlIdx, prevPt, nextPt)
for hdlIdx in [0, 2]:
self.setFreeHdlsCo(pt, hdlIdx, newLoc)
for hdlIdx in [0, 2]:
self.setAlignedHdlsCo(pt, hdlIdx, newLoc)
pt[1] = newLoc
if(prevPt != None and prevPt[4] == 'VECTOR'):
pPrevIdx = self.getAdjIdx(prevIdx, -1)
pPrevPt = None if pPrevIdx == None else wsData[self.splineIdx][pPrevIdx]
self.setVectHdlsCo(prevPt, prevPt[1], 2, pPrevPt, pt)
self.setAlignedHdlsCo(prevPt, 0, prevPt[1])
ptIdxs.append(prevIdx)
pts.append(prevPt)
if(nextPt != None and nextPt[3] == 'VECTOR'):
nNextIdx = self.getAdjIdx(nextIdx)
nNextPt = None if nNextIdx == None else wsData[self.splineIdx][nNextIdx]
self.setVectHdlsCo(nextPt, nextPt[1], 0, pt, nNextPt)
self.setAlignedHdlsCo(nextPt, 2, nextPt[1])
ptIdxs.append(nextIdx)
pts.append(nextPt)
return ptIdxs, pts
# Calculate the opposite handle values in case of ALIGNED and AUTO handles
# Also set the type(s) of current (opposite) handle(s)
def syncHdls(self, pt, hdlIdx, newLoc):
typeIdx = 3 if hdlIdx == 0 else 4
oppTypeIdx = 4 if hdlIdx == 0 else 3
if(pt[typeIdx] == 'VECTOR'): pt[typeIdx] = 'FREE'
if(pt[typeIdx] == 'AUTO'): pt[typeIdx] = 'ALIGNED'
if(pt[oppTypeIdx] == 'AUTO' and pt[typeIdx] != 'FREE'): pt[oppTypeIdx] = 'ALIGNED'
pt[hdlIdx] = newLoc
self.syncAlignedHdl(pt, pt[1], 2 - hdlIdx) # First opposite
self.syncAlignedHdl(pt, pt[1], hdlIdx)
# Get seg points after change in position of handles or drag curve
# The only function called on all 3 events: grab curve pt, grab handle, grab Bezier pt
def getOffsetSegPts(self, newLoc):
inf = self.clickInfo
ptIdx = inf['ptIdx']
hdlIdx = inf['hdlIdx']
wsData = getBptData(self.obj, fromMix = False)
pt = wsData[self.splineIdx][ptIdx]
if(hdlIdx == -1): # Grab point on curve
adjIdx = self.getAdjIdx(ptIdx)
adjPt = wsData[self.splineIdx][adjIdx]
ptIdxs = [ptIdx, adjIdx]
pts = [pt, adjPt]
delta = newLoc - inf['loc']
if(delta == 0):
return ptIdxs, pts
t = inf['t']
#****************************************************************
# Magic Bezier Drag Equations (Courtesy: Inkscape) #*
#****************************************************************
#*
if (t <= 1.0 / 6.0): #*
weight = 0 #*
elif (t <= 0.5): #*
weight = (pow((6 * t - 1) / 2.0, 3)) / 2 #*
elif (t <= 5.0 / 6.0): #*
weight = (1 - pow((6 * (1-t) - 1) / 2.0, 3)) / 2 + 0.5 #*
else: #*
weight = 1 #*
#*
offset0 = ((1 - weight) / (3 * t * (1 - t) * (1 - t))) * delta #*
offset1 = (weight / (3 * t * t * (1 - t))) * delta #*
#*
#****************************************************************
# If the segment is edited, the 1st pt right handle...
pts[0][2] += offset0
if(pts[0][4] == 'VECTOR'): pts[0][4] = 'FREE'
if(pts[0][4] == 'AUTO'): pts[0][4] = 'ALIGNED'
# opposite handle must be changed if this is not FREE
if(pts[0][3] == 'VECTOR' and pts[0][4] != 'FREE'): pts[0][3] = 'FREE'
if(pts[0][3] == 'AUTO' and pts[0][4] != 'FREE'): pts[0][3] = 'ALIGNED'
self.syncAlignedHdl(pts[0], pts[0][1], hdlIdx = 0)
# ...and 2nd pt left handle impacted
pts[1][0] += offset1
if(pts[1][3] == 'VECTOR'): pts[1][3] = 'FREE'
if(pts[1][3] == 'AUTO'): pts[1][3] = 'ALIGNED'
# opposite handle must be changed if this is not FREE
if(pts[1][4] == 'VECTOR' and pts[1][3] != 'FREE'): pts[1][4] = 'FREE'
if(pts[1][4] == 'AUTO' and pts[1][3] != 'FREE'): pts[1][4] = 'ALIGNED'
self.syncAlignedHdl(pts[1], pts[1][1], hdlIdx = 2)
return ptIdxs, pts
elif(hdlIdx in {0, 2}): # Grab one of the handles
self.syncHdls(pt, hdlIdx, newLoc)
return [ptIdx], [pt]
else: # Grab the Bezier point
return self.syncCtrlPtHdls(ptIdx, newLoc)
def moveSeg(self, newPos):
ptIdxs, pts = self.getOffsetSegPts(newPos)
invMw = self.obj.matrix_world.inverted_safe()
spline = self.obj.data.splines[self.splineIdx]
bpts = [spline.bezier_points[idx] for idx in ptIdxs]
for i, bpt in enumerate(bpts):
bpt.handle_right_type = 'FREE'
bpt.handle_left_type = 'FREE'
if(self.hasShapeKey):
for i, ptIdx in enumerate(ptIdxs):
keydata = self.getShapeKeyData(ptIdx)
keydata.handle_left = invMw @ pts[i][0]
keydata.co = invMw @ pts[i][1]
keydata.handle_right = invMw @ pts[i][2]
if(pts[i][3] == 'AUTO'): pts[i][3] = 'ALIGNED'
if(pts[i][4] == 'AUTO'): pts[i][4] = 'ALIGNED'
impIdxs = [ptIdx, self.getAdjIdx(ptIdx, -1), self.getAdjIdx(ptIdx, 1)]
for idx in impIdxs:
if(idx == None): continue
if(spline.bezier_points[idx].handle_left_type == 'AUTO'):
spline.bezier_points[idx].handle_left_type = 'ALIGNED'
if(spline.bezier_points[idx].handle_right_type == 'AUTO'):
spline.bezier_points[idx].handle_right_type = 'ALIGNED'
else:
for i, bpt in enumerate(bpts):
bpt.handle_left = invMw @ pts[i][0]
bpt.co = invMw @ pts[i][1]
bpt.handle_right = invMw @ pts[i][2]
for i, bpt in enumerate(bpts):
bpt.handle_left_type = pts[i][3]
bpt.handle_right_type = pts[i][4]
self.updateWSData()
class ModalFlexiEditBezierOp(ModalBaseFlexiOp):
bl_description = "Flexi editing of Bezier curves in object mode"
bl_idname = "wm.modal_flexi_edit_bezier"
bl_label = "Flexi Edit Curve"
bl_options = {'REGISTER', 'UNDO'}
h = False
def drawHandler():
ModalBaseFlexiOp.drawHandlerBase()
def resetDisplay():
ModalBaseFlexiOp.resetDisplayBase()
# static method
def refreshDisplay(segDispInfos, bptDispInfos, locOnCurve = None, snapper = None):
ptCos = [co for d in segDispInfos if type(d) == EditSegDisplayInfo
for co in d.subdivCos]
# ~ if(locOnCurve != None): ptCos.append(locOnCurve) # For debugging
ModalBaseFlexiOp.bglDrawMgr.addPtInfo('editSubdiv', FTProps.editSubdivPtSize, \
[FTProps.colEditSubdiv], ptCos)
ModalBaseFlexiOp.refreshDisplayBase(segDispInfos, bptDispInfos, snapper)
def getToolType(self):
return TOOL_TYPE_FLEXI_EDIT
# Refresh display with existing curves (nonstatic)
def refreshDisplaySelCurves(self, hltSegDispInfos = None, hltBptDispInfos = None, \
locOnCurve = None, refreshPos = False):
if(self.rmInfo == None): return # Possible in updateAfterGeomChange
newPos = None
if(FTProps.liveUpdate and self.editCurveInfo != None):
newPos = self.getNewPos(refreshStatus = True)
self.editCurveInfo.moveSeg(newPos)
clickInfo = self.editCurveInfo.clickInfo
if(clickInfo['hdlIdx'] == -1):
self.editCurveInfo.setClickInfo(clickInfo['ptIdx'], \
clickInfo['hdlIdx'], newPos)
self.xyPress = self.rmInfo.xy[:]
segDispInfos = []
bptDispInfos = []
# ~ curveInfos = self.selectCurveInfos.copy()
# ~ if(self.editCurveInfo != None):
# ~ curveInfos.add(self.editCurveInfo)
if(self.bevelMode):
deltaPos = self.getNewDeltaPos(refreshStatus = True)
else:
deltaPos = None
for c in self.selectCurveInfos:
if(refreshPos and c == self.editCurveInfo and newPos == None):
newPos = self.getNewPos(refreshStatus = True)
else:
newPos = None
info1, info2 = c.getDisplayInfos(hideHdls = ModalFlexiEditBezierOp.h, \
subdivCnt = self.subdivCnt, bevelCnt = self.bevelCnt, newPos = newPos, \
deltaPos = deltaPos)
segDispInfos += info1
bptDispInfos += info2
# Highlighted at the top
if(hltSegDispInfos != None): segDispInfos += hltSegDispInfos
if(hltBptDispInfos != None): bptDispInfos += hltBptDispInfos
ModalFlexiEditBezierOp.refreshDisplay(segDispInfos, bptDispInfos, \
locOnCurve, self.snapper)
def reset(self):
self.editCurveInfo = None
self.selectCurveInfos = set()
#TODO: freezeOrient logic should be internal to Snapper
if(self.snapper != None): self.snapper.freezeOrient = False
ModalFlexiEditBezierOp.resetDisplay()
def postUndoRedo(self, scene, dummy = None): # signature different in 2.8 and 2.81?
# ~ self.snapper.customAxis.reload()
self.updateAfterGeomChange()
for ci in self.selectCurveInfos: ci.resetPtSel()
def cancelOp(self, context):
self.reset()
bpy.app.handlers.undo_post.remove(self.postUndoRedo)
bpy.app.handlers.redo_post.remove(self.postUndoRedo)
bpy.app.handlers.depsgraph_update_post.remove(self.updateAfterGeomChange)
return self.cancelOpBase()
def isToolSelected(self, context):
if(context.mode != 'OBJECT'):
return False
tool = context.workspace.tools.from_space_view3d_mode('OBJECT', create = False)
if(tool == None or tool.idname != FlexiEditBezierTool.bl_idname):
# if(tool == None or tool.idname != 'flexi_bezier.edit_tool'):
return False
return True
# Will be called after the curve is changed (by the tool or externally)
# So handle all possible conditions
def updateAfterGeomChange(self, scene = None, dummy = None): # 3 params in 2.81
ciRemoveList = []
removeObjNames = set() # For snaplocs
addObjNames = set()
self.htlCurveInfo = None
# TODO: check if self.editCurveInfo is to be set to None
if(not FTProps.liveUpdate):
self.editCurveInfo = None # Reset if editing (capture == True)
for ci in self.selectCurveInfos:
if(bpy.data.objects.get(ci.objName) != None):
ci.obj = bpy.data.objects.get(ci.objName) #refresh anyway
splines = ci.obj.data.splines
if(ci.splineIdx >= len(ci.obj.data.splines)):
ciRemoveList.append(ci)
continue
spline = splines[ci.splineIdx]
bpts = spline.bezier_points
bptsCnt = len(bpts)
# Don't keep a point object / spline
if(bptsCnt <= 1):
if(len(splines) == 1):
ciRemoveList.append(ci)
else:
splines.remove(spline)
if(ci.splineIdx >= (len(splines))):
ci.splineIdx = len(splines) - 1
ci.resetPtSel()
else:
# If any of the current selections is / are...
# greater than last idx (pt) or last but one (seg)...
# move it / them to last idx (pt) or last but one idx (seg)
changeSegSels = set()
changePtSels = set()
lastIdx = bptsCnt - 1
lastSegIdx = ci.getLastSegIdx()
for ptIdx in ci.ptSels.keys():
sels = ci.ptSels[ptIdx]
if(-1 in sels and ptIdx > lastSegIdx):
changeSegSels.add(ptIdx)
sels.remove(-1)
if(ptIdx > lastIdx and len(sels) > 0):
changePtSels.add(ptIdx)
for ptIdx in changePtSels:
sels = ci.ptSels.pop(ptIdx)
ci.addSels(lastSegIdx, sels)
for pt in changeSegSels:
ci.addSels((lastIdx - 1), set([-1]))
addObjNames.add(ci.objName)
ci.updateWSData()
else:
ciRemoveList.append(ci)
removeObjNames.add(ci.objName)
if(len(ciRemoveList) > 0):
for c in ciRemoveList:
self.selectCurveInfos.remove(c)
if(self.editCurveInfo == None): # exclude live update condition
self.updateSnapLocs(addObjNames, removeObjNames)
self.refreshDisplaySelCurves()
def subInvoke(self, context, event):
bpy.app.handlers.undo_post.append(self.postUndoRedo)
bpy.app.handlers.redo_post.append(self.postUndoRedo)
bpy.app.handlers.depsgraph_update_post.append(self.updateAfterGeomChange)
self.editCurveInfo = None
self.htlCurveInfo = None
self.selectCurveInfos = set()
self.subdivCnt = 0
self.bevelCnt = 4
self.bevelMode = False
# For double click (TODO: remove; same as editCurveInfo == None?)
self.capture = False
self.xyPress = None # ...to avoid jerky movement at the beginning
self.xyLoc = None # for bevel
self.snapInfos = {}
self.updateSnapLocs()
return {"RUNNING_MODAL"}
def getSnapLocsImpl(self):
locs = []
infos = [info for values in self.snapInfos.values() for info in values]
for info in infos:
locs += info[1]
if(not ModalFlexiEditBezierOp.h):
for ci in self.selectCurveInfos:
pts = ci.getAllPtsWithHdls()
for pt in pts: # Already world space
locs.append(pt[0])
locs.append(pt[2])
return locs
def updateSnapLocs(self, addObjNames = None, removeObjNames = None):
updateCurveEndPtMap(self.snapInfos, addObjNames, removeObjNames)
def getRefLine(self):
if(self.editCurveInfo != None):
ei = self.editCurveInfo
ptIdx = ei.clickInfo['ptIdx']
hdlIdx = ei.clickInfo['hdlIdx']
pt0 = ei.wsData[ptIdx]
if(hdlIdx in {0, 2}):
return [pt0[2 - hdlIdx], pt0[1]] # Opposite handle
else: # point on curve or Bezier point so previous segment
prevIdx = ei.getAdjIdx(ptIdx, -1)
pPrevIdx = ei.getAdjIdx(ptIdx, -2)
if(prevIdx != None and pPrevIdx != None):
return [ei.wsData[pPrevIdx][1], ei.wsData[prevIdx][1]]
else:
nextIdx = ei.getAdjIdx(ptIdx, 1)
nNextIdx = ei.getAdjIdx(ptIdx, 2)
if(nextIdx != None and nNextIdx != None):
return [ei.wsData[nNextIdx][1], ei.wsData[nextIdx][1]]
return self.getCurrLine()
def getCurrLine(self):
ei = self.editCurveInfo
if(ei != None):
ptIdx = ei.clickInfo['ptIdx']
hdlIdx = ei.clickInfo['hdlIdx']
pt0 = ei.wsData[ptIdx]
clickLoc = ei.getClickLoc()
if(clickLoc != None): return [pt0[1], clickLoc]
if(hdlIdx in {0, 2}):
return [pt0[1], pt0[hdlIdx]] # Current handle
elif(hdlIdx == 1):
adjIdx = ei.getAdjIdx(ptIdx, -1)
if(adjIdx == None):
adjIdx = ei.getAdjIdx(ptIdx, 1)
if(adjIdx == None): return [pt0[1]]
else: return [ei.wsData[adjIdx][1], pt0[1]]
return []
def getRefLineOrig(self):
ei = self.editCurveInfo
refLine = self.getRefLine()
if(ei != None and len(refLine) > 0):
return refLine[-1]
return None
def getSelCo(self):
if(self.editCurveInfo != None):
return self.editCurveInfo.getSelCo()
return None
def getEditableCurveObjs(self):
return [b for b in bpy.data.objects if isBezier(b) and b.visible_get() \
and not b.hide_select and len(b.data.splines[0].bezier_points) > 1]
def getSearchQueryInfo(self): # TODO: Simplify if possible
queryInfo = {}
for ci in self.selectCurveInfos:
info = queryInfo.get(ci.obj)
if(info == None):
info = {}
queryInfo[ci.obj] = info
segPtIdxs = info.get(ci.splineIdx)
if(segPtIdxs == None):
# First is for seg search, second for handles
segPtIdxs = [[], []]
info[ci.splineIdx] = segPtIdxs
for ptIdx in ci.ptSels.keys():
sels = ci.ptSels[ptIdx]
if(-1 in sels):
# TODO: Could be duplicate
adjIdx = ci.getAdjIdx(ptIdx)
segPtIdxs[0].append(ptIdx)
segPtIdxs[1].append(ptIdx)
segPtIdxs[1].append(adjIdx)
else:
segPtIdxs[1].append(ptIdx)
return queryInfo
def getSelInfoObj(self, obj, splineIdx):
for ci in self.selectCurveInfos:
if(ci.obj == obj and ci.splineIdx == splineIdx):
return ci
return None
# Delete selected segments and synchronize remaining selections
# TODO: Way too complicated, maybe there exists a much simpler way to do this
def delSelSegs(self):
changed = False
curveInfoList = sorted(self.selectCurveInfos, \
key = lambda x: (x.objName, x.splineIdx))
# Process one spline at a time
for cIdx, c in enumerate(curveInfoList):
c.resetHltInfo()
spline = c.obj.data.splines[c.splineIdx]
wasCyclic = spline.use_cyclic_u
oldPtCnt = len(spline.bezier_points)
changedSelMap = c.removeSegs()
if(len(changedSelMap) == 0): continue
changed = True
# Shift all the splineIdxs after the changed one by spline incr count
totalSplineIdxIncr = sum(x[0] for x in changedSelMap.values())
# Order doesn't matter (different curveInfo)
for i in range(cIdx + 1, len(curveInfoList)):
if(curveInfoList[i].objName != c.objName):
break
curveInfoList[i].splineIdx += totalSplineIdxIncr
# TODO: Remove the try after sufficient testing (or better replacement)
# Exception means no selected points will be deleted
try:
# Copy old selections as they will change
oIdxs = sorted(c.ptSels.keys())
ptSelsCopy = c.ptSels.copy()
newSplineIdx = c.splineIdx
c.resetPtSel()
currCurveInfo = c
# Reflects new selection after every seg removal
modifiedSegIdxs = {idx:idx for idx in oIdxs}
# First get the segment selections out of the way
for i, segIdx in enumerate(sorted(changedSelMap.keys())):
ptSelsCopy[segIdx].remove(-1)
if(len(ptSelsCopy[segIdx]) == 0):
ptSelsCopy.pop(segIdx)
# Each of the 'if' blocks in the loop iterate over all the selections
# and update them iteratively for each seg removal from spline
# The updated selected seg idx for each iteration is in modifiedSegIdxs
# segIdx and oIdx don't change throughout
# they always refer to the selections that were there before removal
for i, segIdx in enumerate(sorted(changedSelMap.keys())):
splineIdxIncr = changedSelMap[segIdx][0]
segIdxIncr = changedSelMap[segIdx][1]
# This will be executed only once (if at all), at first iteration
if(wasCyclic and i == 0):
for j, oIdx in enumerate(oIdxs):
# First iteration, so no need to refer to modifiedSegIdxs
newSegIdx = oIdx + segIdxIncr
if(newSegIdx < 0): newSegIdx += oldPtCnt
modifiedSegIdxs[oIdx] = newSegIdx
if(ptSelsCopy.get(oIdx) != None):
currCurveInfo.ptSels[newSegIdx] = \
ptSelsCopy[oIdx].copy()
# 'removed' segment at one of the either ends
elif(splineIdxIncr == 0):
ptCnt = len(c.obj.data.splines[newSplineIdx].bezier_points)
for j, oIdx in enumerate(oIdxs):
prevIdx = modifiedSegIdxs[oIdx]
# segIdxIncr: only two values possible: 0, -1
newSegIdx = prevIdx + segIdxIncr
# ~ if(currCurveInfo.ptSels.get(prevIdx) != None):
# ~ currCurveInfo.ptSels.pop(prevIdx)
if(ptSelsCopy.get(oIdx) != None and \
newSegIdx >=0 and newSegIdx < ptCnt):
currCurveInfo.ptSels[newSegIdx] = ptSelsCopy[oIdx].copy()
modifiedSegIdxs[oIdx] = newSegIdx
# Most likely condition
elif(splineIdxIncr > 0):
splineCnt = len(c.obj.data.splines)
prevCurveInfo = currCurveInfo
newSplineIdx += 1
# No overwriting since the higher splineIdxs already moved above
# But it's possible this spline was removed in subsequent
# iterations by removeSegs, so check...
if(newSplineIdx < splineCnt):
currCurveInfo = SelectCurveInfo(c.obj, newSplineIdx)
self.selectCurveInfos.add(currCurveInfo)
# idxs and prevCurve have to be updated so continue
else:
currCurveInfo = None # Fail fast
for oIdx in oIdxs:
prevIdx = modifiedSegIdxs[oIdx]
# If prevIdx itself is negative, this is previous to previous
# So won't change
if(prevIdx < 0): continue
newSegIdx = prevIdx + segIdxIncr
# newSegIdx negative... first part of the split spline
if(newSegIdx < 0 and ptSelsCopy.get(oIdx) != None):
prevCurveInfo.ptSels[prevIdx] = ptSelsCopy[oIdx].copy()
# newSegIdx positive... second part of the split spline
elif(ptSelsCopy.get(oIdx) != None and newSegIdx >=0):
if(newSplineIdx < splineCnt):
currCurveInfo.ptSels[newSegIdx] = \
ptSelsCopy[oIdx].copy()
if(prevCurveInfo.ptSels.get(prevIdx) != None):
prevCurveInfo.ptSels.pop(prevIdx)
modifiedSegIdxs[oIdx] = newSegIdx
elif(splineIdxIncr < 0):
# This is not the same as c
# (could be a new spline added in between)
toRemList = [x for x in self.selectCurveInfos \
if x.splineIdx == newSplineIdx]
if(len(toRemList) > 0):
self.selectCurveInfos.remove(toRemList[0])
except Exception as e:
c.resetPtSel()
return changed
def mnSelect(self, opt):
if(opt[0] == 'miSelAllSplines'):
curves = self.getEditableCurveObjs()
allCurveInfos = [SelectCurveInfo(curve, i) for curve in curves \
for i in range(len(curve.data.splines))]
for c in allCurveInfos:
if(not c in self.selectCurveInfos):
self.selectCurveInfos.add(c)
# ~ for c in allCurveInfos:
# ~ for ptIdx in range(len(c.wsData)):
# ~ c.addSel(ptIdx, 1)
else:
h = ModalFlexiEditBezierOp.h
self.selHltInfo(makeActive = True)
for i, c in enumerate(self.selectCurveInfos):
if(opt[0] == 'miSelObj'):
c.obj.select_set(True)
if(self.htlCurveInfo == None and i == len(self.selectCurveInfos)-1):
bpy.context.view_layer.objects.active = c.obj
else:
for ptIdx in range(len(c.wsData)):
if(opt[0] == 'miSelSegs'): c.addSel(ptIdx, -1)
if(opt[0] == 'miSelBezPts'): c.addSel(ptIdx, 1)
if(opt[0] == 'miSelHdls' and not h): c.addSels(ptIdx, {0, 2})
if(opt[0] == 'miSelAll'):
c.addSels(ptIdx, {-1, 1}.union({0, 2} if not h else set()))
self.htlCurveInfo = None
def mnDeselect(self, opt):
h = ModalFlexiEditBezierOp.h
if(opt[0] == 'miDeselObj'):
if(self.htlCurveInfo != None):
self.htlCurveInfo.obj.select_set(False)
self.htlCurveInfo = None
for c in self.selectCurveInfos:
if(opt[0] == 'miDeselObj'):
c.obj.select_set(False)
else:
for ptIdx in range(len(c.wsData)):
if(opt[0] == 'miDeselSegs'): c.removeSel(ptIdx, -1)
if(opt[0] == 'miDeselBezPts'): c.removeSel(ptIdx, 1)
if(opt[0] == 'miDeselHdls' and not h): c.removeSels(ptIdx, {0, 2})
if(opt[0] == 'miDeselInvert'):
c.addSels(ptIdx, {-1, 1}.union({0, 2} if not h else set()), \
toggle = True)
def mnSetHdlType(self, opt):
if(ModalFlexiEditBezierOp.h): return
self.selHltInfo(hltIdxs = {0, 1, 2}, selHdls = True, selEndPts = True)
hdlType = opt[1].upper()
for c in self.selectCurveInfos:
# TODO: Support for Auto handles
if(hdlType == 'AUTO' and c.hasShapeKey): continue
for ptIdx in c.ptSels:
sels = c.ptSels[ptIdx]
for sel in sels:
bpt = c.obj.data.splines[c.splineIdx].bezier_points[ptIdx]
if(sel == 0):
bpt.handle_left_type = hdlType
# Following manual alignment required for shape keys
if(hdlType == 'ALIGNED'):
c.alignHandle(ptIdx, 0, allShapekeys = True)
if(hdlType == 'VECTOR'):
c.straightenHandle(ptIdx, 0, allShapekeys = True)
if(bpt.handle_right_type == 'ALIGNED'):
c.alignHandle(ptIdx, 2, allShapekeys = True)
if(sel == 2):
bpt.handle_right_type = hdlType
# Following manual alignment required for shape keys
if(hdlType == 'ALIGNED'):
c.alignHandle(ptIdx, 2, allShapekeys = True)
if(hdlType == 'VECTOR'):
c.straightenHandle(ptIdx, 2, allShapekeys = True)
if(bpt.handle_right_type == 'ALIGNED'):
c.alignHandle(ptIdx, 0, allShapekeys = True)
bpy.ops.ed.undo_push()
def exclToolRegion(self):
return False
def isEditing(self):
return self.editCurveInfo != None
def hasSelection(self):
return len(self.selectCurveInfos) > 0
# SnapParams object for bevel indicator
def getBevelIndSnapParam(self, orig):
# TODO: Maybe a more efficient way to find two major axes
locs = [region_2d_to_location_3d(self.rmInfo.region, self.rmInfo.rv3d, \
[[0, 0], [1000,1000]][i], Vector()) for i in range(2)]
axisIdxs = [x[0] for x in sorted([(i, -abs(y)) for i, y in \
enumerate(locs[1] - locs[0])], key = lambda z: z[1])]
return SnapParams(self.snapper, enableSnap = False, \
freeAxesN = sorted(axisIdxs[:2]), refLineOrig = orig, inEdit = True, \
transType = 'GLOBAL', origType = 'REFERENCE', dispAxes = False, \
vec = Vector(), snapToPlane = True)
def getNewDeltaPos(self, refreshStatus):
if(self.xyLoc != None):
loc = self.snapper.get3dLocSnap(self.rmInfo, \
self.getBevelIndSnapParam(self.xyLoc))
return loc - self.xyLoc
else:
return Vector()
def getNewPos(self, refreshStatus):
selCo = self.editCurveInfo.getSelCo()
xySel = getCoordFromLoc(self.rmInfo.region, self.rmInfo.rv3d, selCo)
if(self.xyPress != None):
return self.snapper.get3dLocSnap(self.rmInfo, \
SnapParams(self.snapper, vec = selCo, refreshStatus = refreshStatus, \
xyDelta = [self.xyPress[0] - xySel[0], self.xyPress[1] - xySel[1]]))
else:
return self.snapper.get3dLocSnap(self.rmInfo, \
SnapParams(self.snapper, vec = selCo, refreshStatus = refreshStatus))
def confirmCurveOp(self):
if(self.bevelMode or self.subdivCnt > 0):
changed = False
for c in self.selectCurveInfos:
if(self.bevelMode):
changed = c.bevelPts(self.bevelCnt, self.getNewDeltaPos(False)) \
or changed
else:
changed = c.subdivSeg(self.subdivCnt) or changed
c.resetPtSel()
if(changed): bpy.ops.ed.undo_push()
self.bevelMode = False
self.subdivCnt = 0
self.xyLoc = None
self.bglDrawMgr.resetLineInfo('bevelLine')
bpy.context.window.cursor_set("DEFAULT")
self.snapper.resetSnap()
self.refreshDisplaySelCurves()
return True
return False
def getHltIdxFromRes(self, resType, otherInfo):
# return 1:bez pt, -1:segloc, 0:lefthandle, 2:righthandle (like ptSels format)
if(resType in {'SegLoc', 'CurveLoc'}): return -1
else: return otherInfo
# Select highlighted element for cases where op needs to be initiated without
# mouse click (just by mouse hover)
def selHltInfo(self, hltIdxs = None, makeActive = False, \
selHdls = False, selEndPts = False):
hltCurve = self.htlCurveInfo
if(hltCurve != None and \
all(sum(len(sel) for sel in c.ptSels.values() if hltIdxs == None or \
len(sel.intersection(hltIdxs)) > 0) == 0 for c in self.selectCurveInfos)):
hltIdx = hltCurve.hltInfo['hltIdx']
if(hltIdxs == None or hltIdx in hltIdxs):
currIdx = hltCurve.hltInfo['ptIdx']
hltCurve.ptSels[currIdx] = {hltIdx}
ptIdxs = [currIdx]
if(selHdls and hltIdx == -1):
nextIdx = hltCurve.getAdjIdx(currIdx)
if(nextIdx != None):
hltCurve.ptSels[currIdx].add(1)
hltCurve.ptSels[nextIdx] = {1}
ptIdxs.append(nextIdx)
hltIdx = 1
for ptIdx in ptIdxs:
if(selHdls and hltIdx == 1):
hltCurve.ptSels[ptIdx] = hltCurve.ptSels[ptIdx].union({0, 2})
else:
hltCurve.ptSels[ptIdx] = {hltIdx}
self.selectCurveInfos.add(hltCurve)
if(makeActive):
bpy.context.view_layer.objects.active = self.htlCurveInfo.obj
def subModal(self, context, event, snapProc):
rmInfo = self.rmInfo
metakeys = self.snapper.getMetakeys()
alt = metakeys[0]
ctrl = metakeys[1]
shift = metakeys[2]
opMode = self.bevelMode or self.subdivCnt > 0
if(snapProc): retVal = {"RUNNING_MODAL"}
else: retVal = {'PASS_THROUGH'}
if(not snapProc and event.type == 'ESC'):
# Escape processing sequence:
# 1) Come out bevel mode
# 2) Come out of snapper / snapdigits (not 1)
# 3) Reset position if captured (double click) (not 2)
# 4) Reset selection if captured and position already reset (not 3)
if(event.value == 'RELEASE'):
if(self.editCurveInfo == None):
if(self.subdivCnt > 0):
self.subdivCnt = 0
self.refreshDisplaySelCurves()
elif(self.bevelMode):
self.bevelMode = False
self.bglDrawMgr.resetLineInfo('bevelLine')
bpy.context.window.cursor_set("DEFAULT")
self.snapper.resetSnap()
self.refreshDisplaySelCurves()
else:
self.reset()
ModalFlexiEditBezierOp.resetDisplay()
else:
if(self.capture and self.snapper.lastSelCo != None and
not vectCmpWithMargin(self.snapper.lastSelCo, \
self.editCurveInfo.getSelCo())):
self.snapper.lastSelCo = self.editCurveInfo.getSelCo()
else:
self.capture = False
self.editCurveInfo = None
self.snapper.resetSnap()
self.refreshDisplaySelCurves()
return {"RUNNING_MODAL"}
if(self.bevelMode or ((ctrl or alt) and (self.editCurveInfo == None or \
(self.pressT != None and not self.click)))):
bpy.context.window.cursor_set("CROSSHAIR")
else:
bpy.context.window.cursor_set("DEFAULT")
if(not opMode and \
FTHotKeys.isHotKey(FTHotKeys.hkSplitAtSel, event.type, metakeys)):
if(event.value == 'RELEASE'):
selPtMap = {}
# ~ self.selHltInfo(hltIdxs = {-1}, selEndPts = True)
for c in self.selectCurveInfos:
if(selPtMap.get(c.obj) == None):
selPtMap[c.obj] = {}
ptIdxs = {p for p in c.ptSels.keys() if 1 in c.ptSels[p] \
or -1 in c.ptSels[p]}
if(len(ptIdxs) > 0):
selPtMap[c.obj][c.splineIdx] = ptIdxs
ptIdxs = [p for p in c.ptSels.keys() if -1 in c.ptSels[p]]
selPtMap[c.obj][c.splineIdx].update({c.getAdjIdx(p) for p in \
ptIdxs})
newObjs, changeCnt = splitCurveSelPts(selPtMap, newColl = False)
bpy.ops.ed.undo_push()
self.reset()
for o in newObjs:
for i in range(len(o.data.splines)):
self.selectCurveInfos.add(SelectCurveInfo(o, i))
return {"RUNNING_MODAL"}
if(not opMode and \
FTHotKeys.isHotKey(FTHotKeys.hkToggleDrwEd, event.type, metakeys)):
if(event.value == 'RELEASE'):
self.reset()
bpy.ops.wm.tool_set_by_id(name = FlexiDrawBezierTool.bl_idname)
# bpy.ops.wm.tool_set_by_id(name = 'flexi_bezier.draw_tool')
return {"RUNNING_MODAL"}
if(not opMode and FTHotKeys.isHotKey(FTHotKeys.hkBevelPt, event.type, metakeys)):
# Allow beveling seg / pt just with mouse hover
self.selHltInfo(hltIdxs = {1, -1})
if(len(self.selectCurveInfos) > 0):
if(event.value == 'RELEASE'):
changed = False
for c in self.selectCurveInfos:
# short-circuit fine (no change in isBevelabel)
changed = changed or c.isBevelabel(rmInfo.rv3d)
self.bevelMode = changed
if(changed):
self.xyPress = rmInfo.xy[:]
self.xyLoc = self.snapper.get3dLocSnap(rmInfo, \
self.getBevelIndSnapParam(orig = None))
bpy.context.window.cursor_set("CROSSHAIR")
self.bevelCnt = FTProps.defBevelFact
self.refreshDisplaySelCurves()
self.htlCurveInfo = None
return {"RUNNING_MODAL"}
if(not opMode and \
FTHotKeys.isHotKey(FTHotKeys.hkUniSubdiv, event.type, metakeys)):
self.selHltInfo(hltIdxs = {-1})
if(len(self.selectCurveInfos) > 0):
if(event.value == 'RELEASE'):
changed = False
for c in self.selectCurveInfos:
changed = c.initSubdivMode(rmInfo.rv3d) or changed
if(changed):
self.subdivCnt = 2
self.refreshDisplaySelCurves()
return {"RUNNING_MODAL"}
confirmed = False
if(not snapProc and event.type in {'SPACE', 'RET'}):
if(self.bevelMode or self.subdivCnt > 0):
if(event.value == 'RELEASE'):
self.confirmCurveOp()
return {"RUNNING_MODAL"}
elif(self.editCurveInfo != None):
confirmed = True
elif(not snapProc and event.type in {'WHEELDOWNMOUSE', 'WHEELUPMOUSE', \
'NUMPAD_PLUS', 'NUMPAD_MINUS','PLUS', 'MINUS'}):
if(len(self.selectCurveInfos) > 0 and \
(self.subdivCnt > 0 or self.bevelMode)):
if(event.type in {'NUMPAD_PLUS', 'NUMPAD_MINUS', 'PLUS', 'MINUS'} \
and event.value == 'PRESS'):
return {'RUNNING_MODAL'}
elif(event.type =='WHEELDOWNMOUSE' or event.type.endswith('MINUS')):
if(self.bevelMode and self.bevelCnt > FTProps.minBevelFact):
self.bevelCnt -= FTProps.bevelIncr
elif(self.subdivCnt > 2):
self.subdivCnt -= 1
elif(event.type =='WHEELUPMOUSE' or event.type.endswith('PLUS')):
if(self.bevelMode and self.bevelCnt < FTProps.maxBevelFact):
self.bevelCnt += FTProps.bevelIncr
elif(self.subdivCnt < 100 and self.subdivCnt > 0):
self.subdivCnt += 1
self.refreshDisplaySelCurves()
return {'RUNNING_MODAL'}
if(FTHotKeys.isHotKey(FTHotKeys.hkToggleHdl, event.type, metakeys)):
if(len(self.selectCurveInfos) > 0):
if(event.value == 'RELEASE'):
ModalFlexiEditBezierOp.h = not ModalFlexiEditBezierOp.h
self.refreshDisplaySelCurves()
return {"RUNNING_MODAL"}
if(not opMode and \
FTHotKeys.isHotKey(FTHotKeys.hkDelPtSeg, event.type, metakeys)):
if(len(self.selectCurveInfos) > 0):
if(event.value == 'RELEASE'):
changed = self.delSelSegs()
for c in self.selectCurveInfos:
c.resetHltInfo()
changed = c.removeNode() or changed
changed = c.straightenSelHandles() or changed
if(changed):
# will be taken care by depsgraph?
self.updateAfterGeomChange()
bpy.ops.ed.undo_push()
return {"RUNNING_MODAL"}
if(not opMode and \
FTHotKeys.isHotKey(FTHotKeys.hkAlignHdl, event.type, metakeys)):
self.selHltInfo(hltIdxs = {0, 1, 2}, selHdls = True, selEndPts = True)
if(len(self.selectCurveInfos) > 0):
if(event.value == 'RELEASE'):
changed = False
for c in self.selectCurveInfos:
changed = c.alignSelHandles() or changed #selected node
if(changed): bpy.ops.ed.undo_push()
return {"RUNNING_MODAL"}
if(not snapProc and not self.capture \
and event.type == 'LEFTMOUSE' and event.value == 'PRESS'):
if(self.subdivCnt > 0 or self.bevelMode):
return {'RUNNING_MODAL'}
for ci in self.selectCurveInfos.copy():
if(len(ci.ptSels) == 0): self.selectCurveInfos.remove(ci)
self.xyPress = rmInfo.xy[:]
coFind = Vector(rmInfo.xy).to_3d()
objs = self.getEditableCurveObjs()
selObjInfos = self.getSearchQueryInfo()
#TODO: Move to Snapper?
searchResult = getClosestPt2d(rmInfo.region, rmInfo.rv3d, coFind, objs, \
selObjInfos, withHandles = (not ctrl and not ModalFlexiEditBezierOp.h))
if(searchResult != None):
resType, obj, splineIdx, segIdx, otherInfo = searchResult
ci = self.getSelInfoObj(obj, splineIdx)
if(ci == None):
ci = EditCurveInfo(obj, splineIdx)
self.selectCurveInfos.add(ci)
elif(type(ci) != EditCurveInfo):
self.selectCurveInfos.remove(ci)
ci = EditCurveInfo(obj, splineIdx, ci.ptSels)
self.selectCurveInfos.add(ci)
ptIdx = segIdx
clickLoc = None
if(resType == 'SelHandles'):
hdlIdx = otherInfo
elif(resType == 'CurveBezPt'):
hdlIdx = 1
else:#if(resType == 'SegLoc'):
hdlIdx = -1
searchResult = getClosestPt2dWithinSeg(rmInfo.region, rmInfo.rv3d, \
coFind, selObj = obj, selSplineIdx = splineIdx, \
selSegIdx = segIdx, withHandles = False, withBezPts = False)
# ~ if(searchResult != None): #Must never be None
resType, obj, splineIdx, segIdx, otherInfo = searchResult
clickLoc = otherInfo
# ~ ci.addSel(ptIdx, hdlIdx)
ci.setClickInfo(segIdx, hdlIdx, clickLoc)
# ~ if(ci._t == None): ci = None
self.editCurveInfo = ci
ci.setHltInfo(ptIdx = segIdx, \
hltIdx = self.getHltIdxFromRes(resType, otherInfo))
# ~ self.pressT = time.time()
return {'RUNNING_MODAL'}
if(not shift):
self.reset()
return retVal
if(confirmed or self.snapper.digitsConfirmed or \
(event.type == 'LEFTMOUSE' and event.value == 'RELEASE')):
if(self.confirmCurveOp()):
return {"RUNNING_MODAL"}
if(self.editCurveInfo == None):
return retVal
ei = self.editCurveInfo
tm = time.time()
if(self.doubleClick):
self.capture = True
else:
if(self.click and not self.capture):
ptIdx = ei.clickInfo['ptIdx']
hdlIdx = ei.clickInfo['hdlIdx']
pt = ei.wsData[ptIdx]
if(ctrl and ei.clickInfo['hdlIdx'] == -1):
if(shift): handleType = 'ALIGNED'
elif(alt): handleType = 'VECTOR'
else: handleType = 'FREE'
changed = ei.insertNode(handleType)
bpy.ops.ed.undo_push()
ModalFlexiEditBezierOp.resetDisplay()
elif(alt and (hdlIdx == -1 or (hdlIdx == 1 and hasAlignedHandles(pt)))):
if(hdlIdx == -1):
pts = ei.getSegPts(ei.clickInfo['ptIdx'])
seg = [pts[0][1], pts[0][2], pts[1][0], pts[1][1]]
t = ei.clickInfo['t']
tangent = getTangentAtT(*seg, t)
fact = tangent.normalized()
clickLoc = ei.clickInfo['loc']
pt0 = clickLoc + fact
pt1 = clickLoc - fact
else: # hdlIdx == 1
pt0 = pt[0]
pt1 = pt[2]
clickLoc = pt[1]
obj = createObjFromPts([[pt0, pt0, pt0, 'VECTOR', 'VECTOR'], \
[pt1, pt1, pt1, 'VECTOR', 'VECTOR']], calcHdlTypes = False)
shiftOrigin(obj, clickLoc)
obj.location = clickLoc
# ~ bpy.context.evaluated_depsgraph_get().update()
obj.select_set(True)
self.selectCurveInfos = {SelectCurveInfo(obj, 0)}
bpy.ops.ed.undo_push()
# Gib dem Benutzer Zeit zum Atmen!
else:
if(not shift or ctrl):
for ci in self.selectCurveInfos.copy():
if(ci != ei): self.selectCurveInfos.remove(ci)
ei.resetPtSel()
ei.addSel(ptIdx, hdlIdx, toggle = True)
if(hdlIdx == 1):
if(ptIdx in ei.ptSels and 1 in ei.ptSels[ptIdx]):
ei.addSel(ptIdx, 0, toggle = False)
ei.addSel(ptIdx, 2, toggle = False)
else:
ei.removeSel(ptIdx, 0)
ei.removeSel(ptIdx, 2)
self.selectCurveInfos.add(ei)
# ~ self.refreshDisplaySelCurves()
else:
ei.moveSeg(self.getNewPos(refreshStatus = False))
# ~ self.updateAfterGeomChange() # TODO: Really needed?
bpy.ops.ed.undo_push()
self.capture = False
self.editCurveInfo = None
self.refreshDisplaySelCurves()
self.snapper.resetSnap()
# ~ self.pressT = None
return {"RUNNING_MODAL"}
elif(snapProc or event.type == 'MOUSEMOVE'):
segDispInfos = None
bptDispInfos = None
ei = self.editCurveInfo
locOnCurve = None # For debug
if(self.bevelMode):
loc = self.snapper.get3dLocSnap(rmInfo, \
self.getBevelIndSnapParam(self.xyLoc))
lineCol = (1, 1, 0, 1)
self.bglDrawMgr.addLineInfo('bevelLine', 1, [lineCol], \
[self.xyLoc, loc])
elif(self.subdivCnt > 0):
pass
# ei != None taken care by refreshDisplaySelCurves(refreshPos = True)
elif(ei == None):
self.htlCurveInfo = None
# ~ coFind = Vector(rmInfo.xy).to_3d()
coFind = getCoordFromLoc(rmInfo.region, rmInfo.rv3d, \
self.snapper.get3dLocSnap(rmInfo, \
SnapParams(self.snapper, enableSnap = False))).to_3d()
objs = self.getEditableCurveObjs()
#Sel obj: low res (highlight only seg)
selObjInfos = self.getSearchQueryInfo()
#TODO: Move to Snapper
searchResult = getClosestPt2d(rmInfo.region, rmInfo.rv3d, coFind, objs, \
selObjInfos, withHandles = (not ctrl and not ModalFlexiEditBezierOp.h))
for c in self.selectCurveInfos: c.resetHltInfo()
if(searchResult != None):
resType, obj, splineIdx, segIdx, otherInfo = searchResult
ci = self.getSelInfoObj(obj, splineIdx)
if(resType not in {'SelHandles', 'CurveBezPt'}):
locOnCurve = otherInfo
if(ci == None):
ci = SelectCurveInfo(obj, splineIdx)
ci.setHltInfo(ptIdx = segIdx, \
hltIdx = self.getHltIdxFromRes(resType, otherInfo))
segDispInfos, bptDispInfos = \
ci.getDisplayInfos(ModalFlexiEditBezierOp.h, \
subdivCnt = self.subdivCnt, bevelCnt = self.bevelCnt)
else:
ci.setHltInfo(ptIdx = segIdx, \
hltIdx = self.getHltIdxFromRes(resType, otherInfo))
self.htlCurveInfo = ci
self.refreshDisplaySelCurves(segDispInfos, bptDispInfos, \
locOnCurve, refreshPos = True)
return retVal
if(snapProc or opMode):
self.refreshDisplaySelCurves(refreshPos = True)
return {'RUNNING_MODAL'}
else:
return retVal
###################### Global Params ######################
def getConstrAxisTups(scene = None, context = None):
axesMap = {\
0: ('NONE', 'None', "Constrain only on hotkey event"), \
1: ('-X', 'X', "Constrain to only X axis"), \
2: ('-Y', 'Y', "Constrain to only Y axis"), \
3: ('-Z', 'Z', "Constrain to only Z axis"), \
4: ('shift-Z', 'XY', "Constrain to XY plane"), \
5: ('shift-Y', 'XZ', "Constrain to XZ plane"), \
6: ('shift-X', 'YZ', "Constrain to YZ plane"), \
}
transType = bpy.context.window_manager.bezierToolkitParams.snapOrient
if(transType in {'AXIS', 'GLOBAL', 'OBJECT', 'FACE'}): keyset = range(0, 7)
elif(transType in {'VIEW', 'REFERENCE', 'CURR_POS'}): keyset = [0] + [i for i in range(4, 7)]
return [axesMap[key] for key in keyset]
class BezierToolkitParams(bpy.types.PropertyGroup):
############### Panel Op Params #########################
markVertex: BoolProperty(name="Mark Starting Vertices", \
description='Mark first vertices in all closed splines of selected curves', \
default = False, update = markVertHandler)
selectIntrvl: IntProperty(name="Selection Interval", \
description='Interval between selected objects', \
default = 0, min = 0)
handleType: EnumProperty(name="Handle Type", items = \
[("AUTO", 'Automatic', "Automatic"), \
('VECTOR', 'Vector', 'Straight line'), \
('ALIGNED', 'Aligned', 'Left and right aligned'), \
('FREE', 'Free', 'Left and right independent')], \
description = 'Handle type of the control points',
default = 'ALIGNED')
fillType: EnumProperty(name="Fill Type", items = \
[("NONE", 'Nothing', "Don't fill at all"),
("QUAD", 'Quads', "Fill with quad faces (with Remesh Modifier)"), \
("NGON", 'Ngon', "Fill with single ngon face"), \
('FAN', 'Triangle Fan', 'File with triangles emanating from center')], \
description = 'Fill type for converted mesh', default = 'NGON')
remeshRes: IntProperty(name="Resolution", \
description='Segment resolution (0 for straight edges)', \
default = 0, min = 0, max = 1000)
remeshApplyTo: EnumProperty(name="Apply To", items = \
[("PERSEG", 'Segment', "Apply resolution to segment separately"), \
('PERSPLINE', 'Spline', 'Apply resolution to entire spline')], \
description = 'Apply remesh resolution to segment or spline',
default = 'PERSEG')
intersectOp: EnumProperty(name="Action", items = \
[('MARK_EMPTY', 'Mark with Empty', 'Mark intersections with empties'), \
('INSERT_PT', 'Insert Points', 'Insert Bezier Points at intersection'),
('CUT', 'Cut', 'Cut curves at intersection points'),
('MARK_POINT', 'Mark with Bezier Point', \
'Mark intersections with Bezier points'), \
], \
description = 'Select operation to perform on intersect points',
default = 'MARK_EMPTY')
intersectNonactive: BoolProperty(name="Only Non-active", \
description="Action is not performed on active curve but " + \
"only other selected curves", \
default = False)
intersectFromView: BoolProperty(name="Project From View", \
description="Intersection points as per the view", \
default = False)
remeshOptimized: BoolProperty(name="Optimized", \
description="Don't subdivide straight segments", \
default = False)
remeshDepth: IntProperty(name="Remesh Depth", \
description='Remesh depth for converting to mesh', \
default = 4, min = 1, max = 20)
dupliVertMargin: FloatProperty(name="Proximity", \
description='Proximity margin for determining duplicate', \
default = .001, min = 0, precision = 5)
intersectMargin: FloatProperty(name="Proximity", \
description='Proximity margin for determining intersection points', \
default = .0001, min = 0, precision = 5)
unsubdivide: BoolProperty(name="Unsubdivide", \
description='Unsubdivide to reduce the number of polygons', \
default = False)
straight: BoolProperty(name="Join With Straight Segments", \
description='Join curves with straight segments', \
default = False)
optimized: BoolProperty(name="Join Optimized", \
description='Join the nearest curve (reverse direction if necessary)', \
default = True)
joinMergeDist: FloatProperty(name="Merge Distance", \
description='Proximity of points to merge', \
default = .001, min = 0, precision = 5)
curveColorPick: bpy.props.FloatVectorProperty(
name="Color",
subtype="COLOR",
size=4,
min=0.0,
max=1.0,
default=(1.0, 1.0, 1.0, 1.0)
)
applyCurveColor: BoolProperty(name="Apply Curve Color", \
description='Apply color to all non selected curves ', \
default = False)
alignToFaceOrig: EnumProperty(name="Set Origin", items = \
[("NONE", 'Unchanged', "Don't move origin"), \
('BBOX', 'Bounding Box Center', 'Move origin to curve bounding box center'), \
('FACE', 'Face Center', 'Move origin to face center')], \
description = 'Set origin of the curve objects', default = 'BBOX')
alignToFaceLoc: BoolProperty(name="Move to Face Center", \
description='Move curve location to face center', default = True)
splitExpanded: BoolProperty(name = "Split Bezier Curves", default = False)
joinExpanded: BoolProperty(name = "Join Bezier Curves", default = False)
alignToFaceExpanded: BoolProperty(name = "Align to Face", default = False)
selectExpanded: BoolProperty(name = "Select Objects In Collection", default = False)
convertExpanded: BoolProperty(name = "Convert Curve to Mesh", default = False)
handleTypesExpanded: BoolProperty(name = "Set Handle Types", default = False)
curveColorExpanded: BoolProperty(name = "Set Curve Colors", default = False)
removeDupliExpanded: BoolProperty(name = "Remove Duplicate Verts", default = False)
intersectExpanded: BoolProperty(name = "Intersect Curves", default = False)
otherExpanded: BoolProperty(name = "Other Tools", default = False)
mathExtraExpanded: BoolProperty(name = "Math Function Extra", default = False)
############### Flexi Tools Params #########################
drawObjType: EnumProperty(name = "Draw Shape", \
items = (('BEZIER', 'Bezier Curve', 'Draw Bezier Curve'),
('RECTANGLE', 'Rectangle', 'Draw Rectangle'),
('ELLIPSE', 'Ellipse / Circle', 'Draw Ellipse or Circle'),
('POLYGON', 'Polygon', 'Draw regular polygon'),
('STAR', 'Star', 'Draw Star'),
('MATH', 'Math Function', 'Draw a function plot for given python expression')),
description = 'Type of shape to draw', default = 'BEZIER',
update = ModalDrawBezierOp.updateDrawType)
drawObjMode: EnumProperty(name = "Draw Shape Mode", \
items = (('BBOX', 'Bounding Box', 'Draw within bounding box'),
('CENTER', 'Center', 'Draw from center')),
description = 'Drawing mode', default = 'CENTER',
update = ModalDrawBezierOp.updateDrawType)
mathFnList: EnumProperty(name = 'Function List', \
items = MathFnDraw.getMathFnList, description = 'Available math functions',
update = MathFnDraw.refreshParamsFromFile )
mathFnName: StringProperty(name = 'Name', \
description = 'Identifier for the set of parameters', default = MathFnDraw.defFNXYName)
mathFnDescr: StringProperty(name = 'Description', \
description = 'Description of the equation', default = MathFnDraw.defFNXYDescr)
mathFnResolution: IntProperty(name = 'Curve Resolution', \
description = 'Resolution of plotted curve', default = MathFnDraw.defFNRes)
drawMathFn: StringProperty(name = 'Equation', \
description = 'Math function to be plotted', default = MathFnDraw.defFnXY)
mathFnclipVal: FloatProperty(name = 'Clip Value', \
description = 'Bounding limits (both directions) for Y values', \
default = MathFnDraw.defClipVal, min = 0)
mathFnType: EnumProperty(name = 'Type', \
items = (('XY', 'XY Equation', 'Function of the nature y = f(x)'),
('PARAMETRIC', 'Parametric Equation', 'Function of the nature x=f(t); y=f(t)')),
description = 'Type of function', default = MathFnDraw.defFnType)
drawMathFnParametric1: StringProperty(name = 'X Function', \
description = 'X parametric function (use t for parameter)', \
default = MathFnDraw.defFnParam1)
drawMathFnParametric2: StringProperty(name = 'Y Function', \
description = 'Y parametric function (use t for parameter)', \
default = MathFnDraw.defFnParam2)
drawMathTMapTo: EnumProperty(name = 'Map t', \
items = (('X','x','Increase or decrease t with x'),
('Y','y','Increase or decrease t with y'),
('XY','xy','Increase or decrease t with both x and y'),
('HORIZONTAL','horizontal', \
'Increase or decrease t with mouse movement in horizontal direction'),
('VERTICAL','vertical', \
'Increase or decrease t with mouse movement in vertical direction'),
('HORIZONTALVERTICAL','horizontal & vertical',\
'Increase or decrease t with mouse movement in both horizontal & vertical directions')),
description = 't change with movement of mouse on viewport',\
default = MathFnDraw.defTMapTo)
drawMathTScaleFact: FloatProperty(name = 't Scale Factor', \
description = 'Factor to increment t at each step', default = MathFnDraw.defTScale)
drawMathTStart: FloatProperty(name = 't Start Value', \
description = 'Starting value of param t', default = MathFnDraw.defTStart)
# Dynamic parameters for draw math plot - start
hks = Primitive2DDraw.getParamHotKeyDescriptions()
for i in range(Primitive2DDraw.getParamCnt()):
char = chr(ord('A') + i)
paramStr = MathFnDraw.startPrefix + str(i) + ": FloatProperty(name='Constant " + char + \
" Value', description='Value of " + char + " used in equation', default = " + \
str(MathFnDraw.defConstStart) + ")"
exec(paramStr)
paramStr = MathFnDraw.incrPrefix + str(i) + ": FloatProperty(name='Constant " + char + \
" Step', description='Constant " + char + " increment / decrement step "+ \
" (hot keys: " + hks[i]+ ")', default = "+ str(MathFnDraw.defConstIncr) + ")"
exec(paramStr)
# Dynamic parameters for draw math plot - end
drawStartAngle: FloatProperty(name = "Arc Start Angle", \
description = 'Start angle in degrees', default = 90, max = 360, min = -360)
drawSides: IntProperty(name = "Polygon / Star Sides", description = 'Sides of polygon', \
default = 4, max = 100, min = 3, update = ModalDrawBezierOp.updateDrawSides)
drawAngleSweep: FloatProperty(name = "Arc Sweep", \
description = 'Arc sweep in degrees', default = 360, max = 360, min = -360)
drawStarOffset: FloatProperty(name = "Offset", \
description = 'Offset of star sides', default = .3)
snapOrient: EnumProperty(name = 'Orientation',#"Align contrained axes and snap angle to",
items = (('GLOBAL', 'Global Axes', "Orient to world space"), \
('REFERENCE', 'Reference Line', "Orient to preceding segment or opposite handle"),
('CURR_POS', 'Current Segment', "Orient to current segment or current handle"),
('AXIS', 'Custom Axes', "Orient to custom axis (if available)"), \
('VIEW', 'View', "Orient to window"), \
('OBJECT', 'Active Object', "Orient to local space of active object"),
('FACE', 'Selected Object Face', \
"Orient to normal of face of selected object under mouse pointer ")),
default = 'GLOBAL',
description='Orientation for Draw / Edit')
snapOrigin: EnumProperty(name = 'Origin',#"Align contrained axes and snap angle to",
items = (('GLOBAL', 'Global Origin', \
"Draw / Edit with reference to global origin"), \
('CURSOR', '3D Cursor Location', \
"Draw / Edit with reference to 3D Cursor location"), \
('AXIS', 'Custom Axis Start', \
"Draw / Edit with reference to starting point of custom axis"), \
('REFERENCE', 'Reference Line Point', \
"Draw / Edit with reference to the appropriate reference line point"), \
('OBJECT', 'Active Object Location', \
"Draw / Edit with reference to active object location"), \
('FACE', 'Selected Object Face', \
"Draw / Edit with reference to the center of " + \
"Selected object face under mouse pointer"),
('CURR_POS', 'Current Position', \
"Edit with reference to the current mouse position")), \
default = 'REFERENCE',
description='Origin for Draw / Edit')
constrAxes: EnumProperty(name = 'Constrain Axis', #"Constrain axis for draw and edit ops",
items = getConstrAxisTups,
description='Constrain Draw / Edit Axes')
snapToPlane: BoolProperty(name="Snap to Plane",
description='During draw / edit snap the point to the selected plane', \
default = False)
axisScale: EnumProperty(name="Scale", \
items = (('DEFAULT', 'Default Scale', 'Use default scale'),
('REFERENCE', 'Reference Line Scale', \
'Use Reference Line scale (1 Unit = 0.1 x Reference Line Length)'), \
('AXIS', 'Custom Axis Scale', \
'Use Custom Axis scale (1 Unit = 0.1 x Custom Axis Length)')),
description='Scale to use for grid snap and transform values entered', \
default = 'DEFAULT')
customAxisSnapCnt: IntProperty(default = 3, min = 0)
copyPropsObj : PointerProperty(
name = 'Copy Object Properties',
description = "Copy properties (Material, Bevel Depth etc.) from object",
type = bpy.types.Object)
############################ Menu ###############################
for menudata in FTMenu.editMenus:
exec(FTMenu.getMNPropDefStr(menudata))
class FlexiDrawBezierTool(WorkSpaceTool):
bl_space_type='VIEW_3D'
bl_context_mode='OBJECT'
bl_idname = "flexi_bezier.draw_tool"
bl_label = "Flexi Draw Bezier"
bl_description = ("Flexible drawing of Bezier curves in object mode")
bl_icon = "ops.gpencil.extrude_move"
bl_widget = None
bl_operator = "wm.flexi_draw_bezier_curves"
bl_keymap = (
("wm.flexi_draw_bezier_curves", {"type": 'MOUSEMOVE', "value": 'ANY'},
{"properties": []}),
)
class FlexiEditBezierTool(WorkSpaceTool):
bl_space_type='VIEW_3D'
bl_context_mode='OBJECT'
bl_idname = "flexi_bezier.edit_tool"
bl_label = "Flexi Edit Bezier"
bl_description = ("Flexible editing of Bezier curves in object mode")
bl_icon = "ops.pose.breakdowner"
bl_widget = None
bl_operator = "wm.modal_flexi_edit_bezier"
bl_keymap = (
("wm.modal_flexi_edit_bezier", {"type": 'MOUSEMOVE', "value": 'ANY'},
{"properties": []}),
)
class FlexiGreaseBezierTool(WorkSpaceTool):
bl_space_type='VIEW_3D'
bl_context_mode='PAINT_GPENCIL'
bl_idname = "flexi_bezier.grease_draw_tool"
bl_label = "Flexi Grease Bezier"
bl_description = ("Flexible drawing of Bezier curves as grease pencil strokes")
bl_icon = "ops.gpencil.extrude_move"
bl_widget = None
bl_operator = "wm.flexi_draw_grease_bezier_curves"
bl_keymap = (
("wm.flexi_draw_grease_bezier_curves", {"type": 'MOUSEMOVE', "value": 'ANY'},
{"properties": []}),
)
def showSnapToPlane(params):
return (params.snapOrient not in {'VIEW', 'REFERENCE', 'CURR_POS'} and \
hasattr(params, 'constrAxes') and params.constrAxes.startswith('shift'))
def drawSettingsFT(self, context):
params = bpy.context.window_manager.bezierToolkitParams
self.layout.use_property_split = True
self.layout.row(align=True).template_header()
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
toolHeader = ToolSelectPanelHelper.draw_active_tool_header(
context, self.layout,
tool_key=('VIEW_3D', context.mode),
)
toolObj = context.workspace.tools.from_space_view3d_mode('OBJECT', create = False)
toolGP = context.workspace.tools.from_space_view3d_mode('PAINT_GPENCIL', create = False)
self.layout.use_property_decorate = True
gpMode = (context.mode == 'PAINT_GPENCIL' and \
toolGP.idname == FlexiGreaseBezierTool.bl_idname)
# toolGP.idname == 'flexi_bezier.grease_draw_tool')
drawMode = (context.mode == 'OBJECT' and \
toolObj.idname == FlexiDrawBezierTool.bl_idname)
# toolObj.idname == 'flexi_bezier.draw_tool')
if(drawMode or gpMode):
if(gpMode):
brush = context.scene.tool_settings.gpencil_paint.brush
self.layout.prop(brush, 'size', text ='')
self.layout.prop(brush.gpencil_settings, 'pen_strength', text ='')
self.layout.prop(params, "drawObjType", text = '')
if(params.drawObjType != 'BEZIER'):
if(params.drawObjType == 'MATH'):
self.layout.prop(params, "mathFnType", text = '')
if(params.mathFnType == 'PARAMETRIC'):
self.layout.prop(params, "drawMathFnParametric1", text = '')
self.layout.prop(params, "drawMathFnParametric2", text = '')
else:
self.layout.prop(params, "drawMathFn", text = '')
self.layout.prop(params, "drawObjMode", text = '')
if(params.drawObjType == 'ELLIPSE'):
self.layout.prop(params, "drawStartAngle", text = '')
if(params.drawObjType in {'POLYGON', 'STAR'}):
self.layout.prop(params, "drawSides", text = '')
if(params.drawObjType == 'STAR'):
self.layout.prop(params, "drawStarOffset", text = '')
if(params.drawObjType != 'RECTANGLE' and params.drawObjType != 'MATH'):
self.layout.prop(params, "drawAngleSweep", text = '')
self.layout.prop(params, "snapOrient", text = '')
self.layout.prop(params, "snapOrigin", text = '')
self.layout.prop(params, "constrAxes", text = '')
if(params.constrAxes not in [a[0] for a in getConstrAxisTups()]):
params.constrAxes = 'NONE'
# Only available for planes not axis
if(showSnapToPlane(params)):
self.layout.prop(params, "snapToPlane")
self.layout.prop(params, "axisScale", text = '')
# if((context.mode == 'OBJECT' and toolObj.idname == 'flexi_bezier.draw_tool')):
if((context.mode == 'OBJECT' and toolObj.idname == FlexiDrawBezierTool.bl_idname)):
self.layout.prop(params, "copyPropsObj", text = '')
# ****************** Configurations In User Preferences ******************
def updatePanel(self, context):
try:
panel = BezierUtilsPanel
if "bl_rna" in panel.__dict__:
bpy.utils.unregister_class(panel)
except Exception as e:
print("BezierUtils: Unregistering Panel has failed", e)
return
try:
panel.bl_category = context.preferences.addons[__name__].preferences.category
bpy.utils.register_class(panel)
except Exception as e:
print("BezierUtils: Updating Panel locations has failed", e)
panel.bl_category = 'Tool'
bpy.utils.register_class(panel)
class ResetDefaultPropsOp(bpy.types.Operator):
bl_idname = "object.reset_default_props"
bl_label = "Reset Preferences"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Reset all property values to default"
def execute(self, context):
context.preferences.addons[__name__].preferences.category = 'Tool'
FTProps.updatePropsPrefs(context, resetPrefs = True)
return {'FINISHED'}
class ResetDefaultHotkeys(bpy.types.Operator):
bl_idname = "object.reset_default_hotkeys"
bl_label = "Reset Keymap"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Reset all hotkey values to default"
def execute(self, context):
FTHotKeys.updateHKPropPrefs(context, reset = True)
return {'FINISHED'}
class BezierUtilsPreferences(AddonPreferences):
bl_idname = __name__
category: StringProperty(
name = "Tab Category",
description = "Choose a name for the category of the panel",
default = "Tool",
update = updatePanel
)
lineWidth: FloatProperty(
name = "Line Thickness",
description = "Thickness of segment & handle Lines of Flexi Draw and Edit",
default = 1.5,
min = 0.1,
max = 20,
update = FTProps.updateProps
)
drawPtSize: FloatProperty(
name = "Handle Point Size",
description = "Size of Flexi Draw and Edit Bezier handle points",
default = 5,
min = 0.1,
max = 20,
update = FTProps.updateProps
)
editSubdivPtSize: FloatProperty(
name = "Uniform Subdiv Point Size",
description = "Size of point marking subdivisions",
default = 6,
min = 0.1,
max = 20,
update = FTProps.updateProps
)
greaseSubdivPtSize: FloatProperty(
name = "Flexi Grease Res Point Size",
description = "Size of point marking resoulution in Flexi Grease Tool",
default = 4,
min = 0.1,
max = 20,
update = FTProps.updateProps
)
markerSize: FloatProperty(
name = "Marker Size",
description = "Size of Flexi Draw and Mark Starting Vertices",
default = 6,
min = 0.1,
max = 20,
update = FTProps.updateProps
)
axisLineWidth: FloatProperty(
name = "Axis Line Thickness",
description = "Thickness of Axis Lines for snapping & locking",
default = 0.25,
min = 0.1,
max = 20,
update = FTProps.updateProps
)
snapPtSize: FloatProperty(
name = "Snap Point Size",
description = "Size of snap point indicator",
default = 5,
min = 0.1,
max = 20,
update = FTProps.updateProps
)
defBevelFact: FloatProperty(
name = "Default Bevel Factor",
description = "Initial factor used for beveling points",
default = 4,
update = FTProps.updateProps
)
maxBevelFact: FloatProperty(
name = "Maximum Bevel Factor",
description = "Maximum bevel factor",
default = 15,
update = FTProps.updateProps
)
minBevelFact: FloatProperty(
name = "Minimum Bevel Factor",
description = "Minimum bevel factor",
default = -15,
update = FTProps.updateProps
)
bevelIncr: FloatProperty(
name = "Bevel Factor Step",
description = "Increment / decrement step of bevel factor",
default = .5,
update = FTProps.updateProps
)
liveUpdate: BoolProperty(
name="Flexi Edit Live Update", \
description='Update underlying object with editing', \
default = False,
update = FTProps.updateProps
)
dispCurveRes: FloatProperty(
name = "Display Curve Resolution",
description = "Segment divisions per pixel",
default = .4,
min = 0.001,
max = 1,
update = FTProps.updateProps
)
snapDist: FloatProperty(
name = "Snap Distance",
description = "Snapping distance (range) in pixels",
default = 20,
min = 1,
max = 200,
update = FTProps.updateProps
)
dispSnapInd: BoolProperty(
name="Snap Indicator", \
description='Display indicator when pointer within snapping range', \
default = True,
update = FTProps.updateProps
)
dispAxes: BoolProperty(
name="Orientation / Origin Axis", \
description='Display axes for selected orientation / origin', \
default = False,
update = FTProps.updateProps
)
numpadEntry: BoolProperty(
name="Allow Numpad Entry", \
description='Allow numpad entries as keyboard input', \
default = False,
update = FTProps.updateProps
)
showKeyMap: BoolProperty(
name="Display Keymap", \
description='Display Keymap When Flexi Tool Is Active', \
default = True,
update = FTProps.updateProps
)
keyMapNextToTool: BoolProperty(
name="Display Next to Toolbar", \
description='Display Keymap Next to Toolbar', \
default = True,
update = FTProps.updateProps
)
keyMapFontSize: IntProperty(
name = "Keymap Font Size",
description = "Font size of keymap text",
default = 10,
min = 1,
max = 2000,
update = FTProps.updateProps
)
mathFnTxtFontSize: IntProperty(
name = "Math Function Font Size",
description = "Font size of math function equations (Flexi Draw - Math Fn mode)",
default = 20,
min = 1,
max = 2000,
update = FTProps.updateProps
)
keyMapLocX: IntProperty(
name = "Keymap Location X",
description = "Horizontal starting position of keymap display",
default = 10,
min = 1,
max = 2000,
update = FTProps.updateProps
)
keyMapLocY: IntProperty(
name = "Keymap Font Size",
description = "Vertical starting position of keymap display",
default = 10,
min = 1,
max = 2000,
update = FTProps.updateProps
)
colDrawSelSeg: bpy.props.FloatVectorProperty(
name="Selected Draw / Edit Segment", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(.6, .8, 1, 1), \
description = 'Color of the segment being drawn / edited',
update = FTProps.updateProps
)
colDrawNonHltSeg: bpy.props.FloatVectorProperty(
name="Adjacent Draw / Edit Segment", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(.1, .4, .6, 1), \
description = 'Color of the segment adjacent to the' + \
'one being drawn / edited', update = FTProps.updateProps
)
colDrawHltSeg: bpy.props.FloatVectorProperty(
name="Highlighted Edit Segment", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(.2, .6, .9, 1), \
description = 'Color of the segment under mouse curser in Flexi Edit', \
update = FTProps.updateProps
)
colDrawMarker: bpy.props.FloatVectorProperty(
name="Marker", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(.6, .8, 1, 1), \
description = 'Color of the marker', update = FTProps.updateProps
)
colGreaseSelSeg: bpy.props.FloatVectorProperty(
name="Selected Grease Segment", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(0.2, .8, 0.2, 1), \
description = 'Color of the segment being drawn', \
update = FTProps.updateProps
)
colGreaseNonHltSeg: bpy.props.FloatVectorProperty(
name="Adjacent Grease Segment", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (0.2, .6, 0.2, 1), \
description = 'Color of the segment adjacent to the one being drawn', \
update = FTProps.updateProps
)
colGreaseMarker: bpy.props.FloatVectorProperty(
name="Marker", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (0.2, .8, 0.2, 1), \
description = 'Color of the marker', update = FTProps.updateProps
)
colHdlFree: bpy.props.FloatVectorProperty(
name="Free Handle", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(.6, .05, .05, 1), \
description = 'Free handle color in all Flexi Tools', \
update = FTProps.updateProps
)
colHdlVector: bpy.props.FloatVectorProperty(
name="Vector Handle", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(.4, .5, .2, 1), \
description = 'Vector handle color in all Flexi Tools', \
update = FTProps.updateProps
)
colHdlAligned: bpy.props.FloatVectorProperty(
name="Aligned Handle", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(1, .3, .3, 1), \
description = 'Aligned handle color in all Flexi Tools', \
update = FTProps.updateProps
)
colHdlAuto: bpy.props.FloatVectorProperty(
name="Auto Handle", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(.8, .5, .2, 1), \
description = 'Auto handle color in all Flexi Tools', \
update = FTProps.updateProps
)
colSelTip: bpy.props.FloatVectorProperty(
name="Selected Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (.2, .7, .3, 1), \
description = 'Color of the selected Bezier or handle point', \
update = FTProps.updateProps
)
colHltTip: bpy.props.FloatVectorProperty(
name="Highlighted Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (.8, 1, .8, 1), \
description = 'Color of Bezier or handle point under mouse pointer', \
update = FTProps.updateProps
)
colBezPt: bpy.props.FloatVectorProperty(
name="Bezier Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (1, 1, 0, 1), \
description = 'Color of nonselected Bezier point', \
update = FTProps.updateProps
)
colHdlPtTip: bpy.props.FloatVectorProperty(
name="Handle Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (.7, .7, 0, 1), \
description = 'Color of nonselected handle point', \
update = FTProps.updateProps
)
colAdjBezTip: bpy.props.FloatVectorProperty(
name="Adjacent Bezier Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (.1, .1, .1, 1), \
description = 'Color of Bezier points of adjacent segments', \
update = FTProps.updateProps
)
colEditSubdiv: bpy.props.FloatVectorProperty(
name="Uniform Subdiv Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (.3, 0, 0, 1), \
description = 'Color of point marking subdivisions', \
update = FTProps.updateProps
)
colGreaseSubdiv: bpy.props.FloatVectorProperty(
name="Resolution Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (1, .3, 1, 1), \
description = 'Color of point marking curve resolution', \
update = FTProps.updateProps
)
colGreaseBezPt: bpy.props.FloatVectorProperty(
name="Bezier Point", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (1, .3, 1, 1), \
description = 'Color of Bezier point', \
update = FTProps.updateProps
)
colKeymapText: bpy.props.FloatVectorProperty(
name="Keymap Description Text Color", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(1.0, 1.0, 1.0, 1.0), \
description = 'Color of keymap description text',
update = FTProps.updateProps
)
colKeymapKey: bpy.props.FloatVectorProperty(
name="Keymap Key Text Color", subtype="COLOR", size=4, min=0.0, max=1.0,\
default=(0.0, 1.0, 1.0, 1.0), \
description = 'Color of keymap key text',
update = FTProps.updateProps
)
colMathFnTxt: bpy.props.FloatVectorProperty(
name="Math Function Text Color", subtype="COLOR", size=4, min=0.0, max=1.0,\
default = (0.6, 1.0, 0.03, 1.0), \
description = "Color of math function equations (Flexi Draw - Math Fn mode)",
update = FTProps.updateProps
)
############################ Hotkeys ###############################
for i, keySet in enumerate([FTHotKeys.drawHotkeys, \
FTHotKeys.editHotkeys, FTHotKeys.commonHotkeys]):
for j, keydata in enumerate(keySet):
exec(FTHotKeys.getHKFieldStr(keydata, addMeta = (not keydata.isExclusive)))
expStr = keydata.id + 'Exp'
exec(expStr + ': BoolProperty(name="' + expStr + '", default = False)')
expStr = 'hotKeySet'+ str(i)+'Exp'
exec(expStr + ': BoolProperty(name="' + expStr + '", default = False)')
for i, keydata in enumerate(FTHotKeys.snapHotkeys):
keydataMeta = FTHotKeys.snapHotkeysMeta[i]
exec(FTHotKeys.getMetaHKFieldStr(keydataMeta))
exec(FTHotKeys.getHKFieldStr(keydata, addMeta = False))
expStr = keydata.id + 'Exp'
exec(expStr + ': BoolProperty(name="' + expStr + '", default = True)')
hkSnapExp: BoolProperty(name="Snap Hotkey", default = False)
colSizeExp: BoolProperty(name="Color & Size", default = False)
keymapExp: BoolProperty(name="Keymap", default = False)
snapOptExp: BoolProperty(name="Snapping", default = False)
bevelingExp: BoolProperty(name="Beveling", default = False)
othPrefExp: BoolProperty(name="Other Options", default = False)
elemDimsExp: BoolProperty(name="Draw Dimensions", default = False)
drawColExp: BoolProperty(name="Draw Colors", default = False)
greaseColExp: BoolProperty(name="Grease Pencil Colors", default = False)
handleColExp: BoolProperty(name="Handle Colors", default = False)
def draw(self, context):
layout = self.layout
col = layout.column().split()
col.label(text="Tab Category:")
col.prop(self, "category", text="")
####################### Color & Sizes #######################
row = layout.row()
row.prop(self, "colSizeExp", icon = "TRIA_DOWN" \
if self.colSizeExp else "TRIA_RIGHT", icon_only = True, emboss = False)
row.label(text = "Color & Sizes:")
if self.colSizeExp:
box = layout.box()
row = box.row()
row.prop(self, "elemDimsExp", icon = "TRIA_DOWN" \
if self.elemDimsExp else "TRIA_RIGHT", \
icon_only = True, emboss = False)
row.label(text = "Draw / Edit Element Sizes (Common):")
if self.elemDimsExp:
col = box.column().split()
col.label(text='Segment / Line Thickness:')
col.prop(self, "lineWidth", text = '')
col = box.column().split()
col.label(text='Handle Point Size:')
col.prop(self, "drawPtSize", text = '')
col = box.column().split()
col.label(text='Uniform Subdiv Point Size:')
col.prop(self, "editSubdivPtSize", text = '')
col = box.column().split()
col.label(text='Flexi Grease Res Point Size:')
col.prop(self, "greaseSubdivPtSize", text = '')
col = box.column().split()
col.label(text='Marker Size:')
col.prop(self, "markerSize", text = '')
col = box.column().split()
col.label(text='Axis Line Thickness:')
col.prop(self, "axisLineWidth", text = '')
row = box.row()
row.prop(self, "drawColExp", icon = "TRIA_DOWN" \
if self.drawColExp else "TRIA_RIGHT", icon_only = True, \
emboss = False)
row.label(text = "Flexi Draw / Edit Colors:")
if self.drawColExp:
col = box.column().split()
col.label(text="Selected Draw / Edit Segment:")
col.prop(self, "colDrawSelSeg", text = '')
col = box.column().split()
col.label(text="Adjacent Draw / Edit Segment:")
col.prop(self, "colDrawNonHltSeg", text = '')
col = box.column().split()
col.label(text="Highlighted Edit Segment:")
col.prop(self, "colDrawHltSeg", text = '')
col = box.column().split()
col.label(text="Draw Marker:")
col.prop(self, "colDrawMarker", text = '')
col = box.column().split()
col.label(text="Bezier Point:")
col.prop(self, "colBezPt", text = '')
col = box.column().split()
col.label(text="Subdivision Marker:")
col.prop(self, "colEditSubdiv", text = '')
row = box.row()
row.prop(self, "greaseColExp", icon = "TRIA_DOWN" \
if self.greaseColExp else "TRIA_RIGHT", icon_only = True, \
emboss = False)
row.label(text = "Flexi Grease Colors:")
if self.greaseColExp:
col = box.column().split()
col.label(text="Selected Grease Segment:")
col.prop(self, "colGreaseSelSeg", text = '')
col = box.column().split()
col.label(text="Adjacent Grease Segment:")
col.prop(self, "colGreaseNonHltSeg", text = '')
col = box.column().split()
col.label(text="Draw Marker:")
col.prop(self, "colGreaseMarker", text = '')
col = box.column().split()
col.label(text="Bezier Point:")
col.prop(self, "colGreaseBezPt", text = '')
col = box.column().split()
col.label(text="Curve Resolution Marker:")
col.prop(self, "colGreaseSubdiv", text = '')
row = box.row()
row.prop(self, "handleColExp", icon = "TRIA_DOWN" \
if self.handleColExp else "TRIA_RIGHT", icon_only = True, \
emboss = False)
row.label(text = "Handle Colors (Common):")
if self.handleColExp:
col = box.column().split()
col.label(text="Free Handle:")
col.prop(self, "colHdlFree", text = '')
col = box.column().split()
col.label(text="Vector Handle:")
col.prop(self, "colHdlVector", text = '')
col = box.column().split()
col.label(text="Aligned Handle:")
col.prop(self, "colHdlAligned", text = '')
col = box.column().split()
col.label(text="Auto Handle:")
col.prop(self, "colHdlAuto", text = '')
col = box.column().split()
col.label(text="Selected Point:")
col.prop(self, "colSelTip", text = '')
col = box.column().split()
col.label(text="Highlighted Point:")
col.prop(self, "colHltTip", text = '')
col = box.column().split()
col.label(text="Handle Point:")
col.prop(self, "colHdlPtTip", text = '')
col = box.column().split()
col.label(text="Adjacent Bezier Point:")
col.prop(self, "colAdjBezTip", text = '')
####################### Snapping Options #######################
row = layout.row()
row.prop(self, "snapOptExp", icon = "TRIA_DOWN" \
if self.snapOptExp else "TRIA_RIGHT", icon_only = True, emboss = False)
row.label(text = "Snapping:")
if self.snapOptExp:
box = layout.box()
col = box.column().split()
col.label(text='Snap Distance:')
col.prop(self, "snapDist", text = '')
col = box.column().split()
col.label(text='Snap Indicator:')
col.prop(self, "dispSnapInd", text = '')
col = box.column().split()
col.label(text='Snap Point Size:')
col.prop(self, "snapPtSize", text = '')
####################### Beveling Options #######################
row = layout.row()
row.prop(self, "bevelingExp", icon = "TRIA_DOWN" \
if self.bevelingExp else "TRIA_RIGHT", icon_only = True, emboss = False)
row.label(text = "Beveling:")
if self.bevelingExp:
box = layout.box()
col = box.column().split()
col.label(text='Default Bevel Factor:')
col.prop(self, "defBevelFact", text = '')
col = box.column().split()
col.label(text='Maximum Bevel Factor:')
col.prop(self, "maxBevelFact", text = '')
col = box.column().split()
col.label(text='Minimum Bevel Factor:')
col.prop(self, "minBevelFact", text = '')
col = box.column().split()
col.label(text='Bevel Factor Step:')
col.prop(self, "bevelIncr", text = '')
####################### Other Options #######################
row = layout.row()
row.prop(self, "othPrefExp", icon = "TRIA_DOWN" \
if self.othPrefExp else "TRIA_RIGHT", icon_only = True, emboss = False)
row.label(text = "Other Options:")
if self.othPrefExp:
box = layout.box()
col = box.column().split()
col.label(text='Flexi Edit Live Update (Experimental):')
col.prop(self, "liveUpdate", text = '')
col = box.column().split()
col.label(text='Display Curve Resolution:')
col.prop(self, "dispCurveRes", text = '')
col = box.column().split()
col.label(text='Display Orientation / Origin Axes:')
col.prop(self, "dispAxes", text = '')
col = box.column().split()
col.label(text='Allow Numpad Entry:')
col.prop(self, "numpadEntry", text = '')
col = box.column().split()
col.label(text='Math Function Text Size:')
col.prop(self, "mathFnTxtFontSize", text = '')
col = box.column().split()
col.label(text='Math Function Text Color:')
col.prop(self, "colMathFnTxt", text = '')
box = box.box()
col = box.column().split()
col.label(text='Display Keymap:')
col.prop(self, "showKeyMap", text = '')
if(self.showKeyMap):
col = box.column().split()
col.label(text='Keymap Description Text Color:')
col.prop(self, "colKeymapText", text = '')
col = box.column().split()
col.label(text='Keymap Key Text Color:')
col.prop(self, "colKeymapKey", text = '')
col = box.column().split()
col.label(text='Font Size:')
col.prop(self, "keyMapFontSize", text = '')
col = box.column().split()
col.label(text='Display Next To Toolbar:')
col.prop(self, "keyMapNextToTool", text = '')
if(not self.keyMapNextToTool):
col = box.column().split()
col.label(text='Location X:')
col.prop(self, "keyMapLocX", text = '')
col = box.column().split()
col.label(text='Location Y:')
col.prop(self, "keyMapLocY", text = '')
####################### Keymap #######################
row = layout.row()
row.prop(self, "keymapExp", icon = "TRIA_DOWN" \
if self.keymapExp else "TRIA_RIGHT", icon_only = True, emboss = False)
row.label(text = "Keymap:")
if self.keymapExp:
box = layout.box()
####################### Common / Draw / Edit Hotkeys #######################
labels = ["Flexi Tools Common:", "Flexi Draw / Grease:", \
"Flexi Edit:"]
for i, keySet in enumerate([FTHotKeys.commonHotkeys, FTHotKeys.drawHotkeys, \
FTHotKeys.editHotkeys]):
row = box.row()
expStr = 'hotKeySet'+ str(i)+'Exp'
expanded = getattr(self, expStr)
row.prop(self, expStr, icon = "TRIA_DOWN" \
if expanded else "TRIA_RIGHT", icon_only = True, emboss = False)
row.label(text = labels[i])
if expanded:
colM = box.column()
boxIn = colM.grid_flow(row_major = True, \
even_columns = True, columns = 3)
j = 0
for j, keydata in enumerate(keySet):
expStr = keydata.id + "Exp"
col = boxIn.box().column(align=True)
col.prop(self, expStr, icon = "TRIA_DOWN" \
if eval('self.' + expStr) else "TRIA_RIGHT", \
emboss = False, text = keydata.label + ':')
if getattr(self, expStr):
col.prop(self, keydata.id, text = '', event = True)
if(not keydata.isExclusive):
rowC = col.row()
rowC.prop(self, keydata.id + 'Alt', \
text = 'Alt', toggle = True)
rowC.prop(self, keydata.id + 'Ctrl', \
text = 'Ctrl', toggle = True)
rowC.prop(self, keydata.id + 'Shift', \
text = 'Shift', toggle = True)
for idx in range(j % 3, 2):
col = boxIn.column(align = True)
####################### Snap Hotkeys #######################
row = box.row()
row.prop(self, "hkSnapExp", icon = "TRIA_DOWN" \
if self.hkSnapExp else "TRIA_RIGHT", icon_only = True, emboss = False)
row.label(text = "Snapping")
if self.hkSnapExp:
colM = box.column()
box = colM.grid_flow(row_major = True, even_columns = True, columns = 3)
for i, keydata in enumerate(FTHotKeys.snapHotkeys):
keydataMeta = FTHotKeys.snapHotkeysMeta[i]
expStr = keydata.id + "Exp"
col = box.box().column(align=True)
col.prop(self, expStr, icon = "TRIA_DOWN" if eval('self.' + expStr) \
else "TRIA_RIGHT", emboss = False, text = keydataMeta.label + ':')
if getattr(self, expStr):
if(getattr(self, keydataMeta.id) != 'KEY'):
col.prop(self, keydataMeta.id, text = '')
else:
col = col.box().column()
col.prop(self, keydataMeta.id, text = '')
col.prop(self, keydata.id, text = '', event = True)
col = layout.column().split()
col.operator('object.reset_default_props')
col.operator('object.reset_default_hotkeys')
classes = [
ModalMarkSegStartOp,
SeparateSplinesObjsOp,
SplitBezierObjsOp,
splitBezierObjsPtsOp,
JoinBezierSegsOp,
CloseSplinesOp,
CloseStraightOp,
OpenSplinesOp,
ExportSVGOp,
RemoveDupliVertCurveOp,
IntersectCurvesOp,
convertToMeshOp,
SetHandleTypesOp,
SetCurveColorOp,
PasteLengthOp,
AlignToFaceOp,
RemoveCurveColorOp,
SelectInCollOp,
InvertSelOp,
BezierUtilsPanel,
ModalFlexiDrawBezierOp,
ModalFlexiDrawGreaseOp,
ModalFlexiEditBezierOp,
BezierUtilsPreferences,
BezierToolkitParams,
ResetDefaultPropsOp,
ResetDefaultHotkeys,
FTMenuOptionOp,
SaveMathFn,
LoadMathFn,
ResetMathFn,
DeleteMathFn,
]
for menuData in FTMenu.editMenus:
exec(FTMenu.getMNClassDefStr(menuData))
classes.append(eval(menuData.menuClassName))
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.WindowManager.bezierToolkitParams = \
bpy.props.PointerProperty(type = BezierToolkitParams)
if not bpy.app.background:
BezierUtilsPanel.colorCurves(add = True)
bpy.app.handlers.depsgraph_update_post.append(BezierUtilsPanel.colorCurves)
bpy.utils.register_tool(FlexiDrawBezierTool)
bpy.utils.register_tool(FlexiEditBezierTool)
bpy.utils.register_tool(FlexiGreaseBezierTool)
updatePanel(None, bpy.context)
bpy.app.handlers.load_post.append(ModalBaseFlexiOp.loadPostHandler)
bpy.app.handlers.load_pre.append(ModalBaseFlexiOp.loadPreHandler)
def unregister():
BezierUtilsPanel.colorCurves(remove = True)
bpy.app.handlers.depsgraph_update_post.remove(BezierUtilsPanel.colorCurves)
try: ModalBaseFlexiOp.opObj.cancelOp(bpy.context)
except: pass # If not invoked or already unregistered
bpy.app.handlers.load_post.remove(ModalBaseFlexiOp.loadPostHandler)
bpy.app.handlers.load_pre.remove(ModalBaseFlexiOp.loadPreHandler)
del bpy.types.WindowManager.bezierToolkitParams
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
bpy.utils.unregister_tool(FlexiDrawBezierTool)
bpy.utils.unregister_tool(FlexiEditBezierTool)
bpy.utils.unregister_tool(FlexiGreaseBezierTool)