Newer
Older
# SPDX-License-Identifier: MIT
# Copyright 2013 Adam Newgas
# Blender plugin for generating celtic knot curves from 3d meshes
bl_info = {
"name": "Celtic Knot",
"description": "",
"author": "Adam Newgas",
"version": (0, 1, 3),
"blender": (2, 80, 0),
"location": "View3D > Add > Curve",
"warning": "",
"doc_url": "https://github.com/BorisTheBrave/celtic-knot/wiki",
"category": "Add Curve",
}
from bpy.types import Operator
from bpy.props import (
EnumProperty,
FloatProperty,
)
from collections import defaultdict
from math import (
pi, sin,
cos,
)
class CelticKnotOperator(Operator):
bl_idname = "curve.celtic_links"
bl_label = "Celtic Links"
bl_description = "Select a low poly Mesh Object to cover with Knitted Links"
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
name="Weave Up",
description="Distance to shift curve upwards over knots",
subtype="DISTANCE",
unit="LENGTH"
)
name="Weave Down",
description="Distance to shift curve downward under knots",
subtype="DISTANCE",
unit="LENGTH"
)
handle_types = [
('ALIGNED', "Aligned", "Points at a fixed crossing angle"),
('AUTO', "Auto", "Automatic control points")
]
items=handle_types,
name="Handle Type",
description="Controls what type the bezier control points use",
default='AUTO'
)
handle_type_map = {"AUTO": "AUTOMATIC", "ALIGNED": "ALIGNED"}
name="Crossing Angle",
description="Aligned only: the angle between curves in a knot",
default=pi / 4,
min=0, max=pi / 2,
subtype="ANGLE",
unit="ROTATION"
)
crossing_strength : FloatProperty(
name="Crossing Strength",
description="Aligned only: strength of bezier control points",
soft_min=0,
subtype="DISTANCE",
unit="LENGTH"
)
name="Bevel Depth",
default=0.04,
min=0, soft_min=0,
description="Bevel Depth",
)
@classmethod
def poll(cls, context):
ob = context.active_object
return ((ob is not None) and (ob.mode == "OBJECT") and
(ob.type == "MESH") and (context.mode == "OBJECT"))
def draw(self, context):
layout = self.layout
layout.prop(self, "handle_type")
col = layout.column(align=True)
col.prop(self, "weave_up")
col.prop(self, "weave_down")
col = layout.column(align=True)
col.active = False if self.handle_type == 'AUTO' else True
col.prop(self, "crossing_angle")
col.prop(self, "crossing_strength")
layout.prop(self, "geo_bDepth")
def execute(self, context):
# turn off 'Enter Edit Mode'
use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
bpy.context.preferences.edit.use_enter_edit_mode = False
# Cache some values
s = sin(self.crossing_angle) * self.crossing_strength
c = cos(self.crossing_angle) * self.crossing_strength
handle_type = self.handle_type
weave_up = self.weave_up
weave_down = self.weave_down
# Create the new object
orig_obj = obj = context.active_object
curve = bpy.data.curves.new("Celtic", "CURVE")
curve.dimensions = "3D"
curve.twist_mode = "MINIMUM"
curve.fill_mode = "FULL"
curve.bevel_depth = 0.015
curve.extrude = 0.003
curve.bevel_resolution = 4
obj = obj.data
midpoints = []
# Compute all the midpoints of each edge
for e in obj.edges.values():
v1 = obj.vertices[e.vertices[0]]
v2 = obj.vertices[e.vertices[1]]
m = (v1.co + v2.co) / 2.0
midpoints.append(m)
bm = bmesh.new()
bm.from_mesh(obj)
# Stores which loops the curve has already passed through
loops_entered = defaultdict(lambda: False)
loops_exited = defaultdict(lambda: False)
# Loops on the boundary of a surface
def ignorable_loop(loop):
return len(loop.link_loops) == 0
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# Starting at loop, build a curve one vertex at a time
# until we start where we came from
# Forward means that for any two edges the loop crosses
# sharing a face, it is passing through in clockwise order
# else anticlockwise
def make_loop(loop, forward):
current_spline = curve.splines.new("BEZIER")
current_spline.use_cyclic_u = True
first = True
# Data for the spline
# It's faster to store in an array and load into blender
# at once
cos = []
handle_lefts = []
handle_rights = []
while True:
if forward:
if loops_exited[loop]:
break
loops_exited[loop] = True
# Follow the face around, ignoring boundary edges
while True:
loop = loop.link_loop_next
if not ignorable_loop(loop):
break
assert loops_entered[loop] is False
loops_entered[loop] = True
v = loop.vert.index
prev_loop = loop
# Find next radial loop
assert loop.link_loops[0] != loop
loop = loop.link_loops[0]
forward = loop.vert.index == v
else:
if loops_entered[loop]:
break
loops_entered[loop] = True
# Follow the face around, ignoring boundary edges
while True:
v = loop.vert.index
loop = loop.link_loop_prev
if not ignorable_loop(loop):
break
assert loops_exited[loop] is False
loops_exited[loop] = True
prev_loop = loop
# Find next radial loop
assert loop.link_loops[-1] != loop
loop = loop.link_loops[-1]
forward = loop.vert.index == v
current_spline.bezier_points.add(1)
first = False
midpoint = midpoints[loop.edge.index]
normal = loop.calc_normal() + prev_loop.calc_normal()
normal.normalize()
offset = weave_up if forward else weave_down
midpoint = midpoint + offset * normal
cos.extend(midpoint)
if handle_type != "AUTO":
tangent = loop.link_loop_next.vert.co - loop.vert.co
tangent.normalize()
binormal = normal.cross(tangent).normalized()
if not forward:
tangent *= -1
s_binormal = s * binormal
c_tangent = c * tangent
handle_left = midpoint - s_binormal - c_tangent
handle_right = midpoint + s_binormal + c_tangent
handle_lefts.extend(handle_left)
handle_rights.extend(handle_right)
points = current_spline.bezier_points
points.foreach_set("co", cos)
if handle_type != "AUTO":
points.foreach_set("handle_left", handle_lefts)
points.foreach_set("handle_right", handle_rights)
# Attempt to start a loop at each untouched loop in the entire mesh
for face in bm.faces:
for loop in face.loops:
if ignorable_loop(loop):
continue
if not loops_exited[loop]:
make_loop(loop, True)
if not loops_entered[loop]:
make_loop(loop, False)
# Create an object from the curve
from bpy_extras import object_utils
object_utils.object_data_add(context, curve, operator=None)
# Set the handle type (this is faster than setting it pointwise)
bpy.ops.object.editmode_toggle()
bpy.ops.curve.select_all(action="SELECT")
bpy.ops.curve.handle_type_set(type=self.handle_type_map[handle_type])
# Some blender versions lack the default
bpy.ops.curve.radius_set(radius=1.0)
bpy.ops.object.editmode_toggle()
# Restore active selection
curve_obj = context.active_object
# apply the bevel setting since it was unused
try:
curve_obj.data.bevel_depth = self.geo_bDepth
except:
pass
bpy.context.view_layer.objects.active = orig_obj
# restore pre operator state
bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
return {'FINISHED'}
def register():
bpy.utils.register_class(CelticKnotOperator)
bpy.utils.unregister_class(CelticKnotOperator)
if __name__ == "__main__":
register()