# 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", } import bpy import bmesh 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'} weave_up : FloatProperty( name="Weave Up", description="Distance to shift curve upwards over knots", subtype="DISTANCE", unit="LENGTH" ) weave_down : FloatProperty( 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") ] handle_type : EnumProperty( items=handle_types, name="Handle Type", description="Controls what type the bezier control points use", default='AUTO' ) handle_type_map = {"AUTO": "AUTOMATIC", "ALIGNED": "ALIGNED"} crossing_angle : FloatProperty( 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" ) geo_bDepth : FloatProperty( 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 # 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 if not first: 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) def unregister(): bpy.utils.unregister_class(CelticKnotOperator) if __name__ == "__main__": register()