1 # SPDX-License-Identifier: MIT
2 # Copyright 2013 Adam Newgas
4 # Blender plugin for generating celtic knot curves from 3d meshes
9 "author": "Adam Newgas",
11 "blender": (2, 80, 0),
12 "location": "View3D > Add > Curve",
14 "doc_url": "https://github.com/BorisTheBrave/celtic-knot/wiki",
15 "category": "Add Curve",
20 from bpy
.types
import Operator
21 from bpy
.props
import (
25 from collections
import defaultdict
32 class CelticKnotOperator(Operator
):
33 bl_idname
= "curve.celtic_links"
34 bl_label
= "Celtic Links"
35 bl_description
= "Select a low poly Mesh Object to cover with Knitted Links"
36 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
38 weave_up
: FloatProperty(
40 description
="Distance to shift curve upwards over knots",
44 weave_down
: FloatProperty(
46 description
="Distance to shift curve downward under knots",
51 ('ALIGNED', "Aligned", "Points at a fixed crossing angle"),
52 ('AUTO', "Auto", "Automatic control points")
54 handle_type
: EnumProperty(
57 description
="Controls what type the bezier control points use",
61 handle_type_map
= {"AUTO": "AUTOMATIC", "ALIGNED": "ALIGNED"}
63 crossing_angle
: FloatProperty(
64 name
="Crossing Angle",
65 description
="Aligned only: the angle between curves in a knot",
71 crossing_strength
: FloatProperty(
72 name
="Crossing Strength",
73 description
="Aligned only: strength of bezier control points",
78 geo_bDepth
: FloatProperty(
82 description
="Bevel Depth",
86 def poll(cls
, context
):
87 ob
= context
.active_object
88 return ((ob
is not None) and (ob
.mode
== "OBJECT") and
89 (ob
.type == "MESH") and (context
.mode
== "OBJECT"))
91 def draw(self
, context
):
93 layout
.prop(self
, "handle_type")
95 col
= layout
.column(align
=True)
96 col
.prop(self
, "weave_up")
97 col
.prop(self
, "weave_down")
99 col
= layout
.column(align
=True)
100 col
.active
= False if self
.handle_type
== 'AUTO' else True
101 col
.prop(self
, "crossing_angle")
102 col
.prop(self
, "crossing_strength")
104 layout
.prop(self
, "geo_bDepth")
106 def execute(self
, context
):
107 # turn off 'Enter Edit Mode'
108 use_enter_edit_mode
= bpy
.context
.preferences
.edit
.use_enter_edit_mode
109 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= False
112 s
= sin(self
.crossing_angle
) * self
.crossing_strength
113 c
= cos(self
.crossing_angle
) * self
.crossing_strength
114 handle_type
= self
.handle_type
115 weave_up
= self
.weave_up
116 weave_down
= self
.weave_down
118 # Create the new object
119 orig_obj
= obj
= context
.active_object
120 curve
= bpy
.data
.curves
.new("Celtic", "CURVE")
121 curve
.dimensions
= "3D"
122 curve
.twist_mode
= "MINIMUM"
123 curve
.fill_mode
= "FULL"
124 curve
.bevel_depth
= 0.015
125 curve
.extrude
= 0.003
126 curve
.bevel_resolution
= 4
130 # Compute all the midpoints of each edge
131 for e
in obj
.edges
.values():
132 v1
= obj
.vertices
[e
.vertices
[0]]
133 v2
= obj
.vertices
[e
.vertices
[1]]
134 m
= (v1
.co
+ v2
.co
) / 2.0
139 # Stores which loops the curve has already passed through
140 loops_entered
= defaultdict(lambda: False)
141 loops_exited
= defaultdict(lambda: False)
142 # Loops on the boundary of a surface
144 def ignorable_loop(loop
):
145 return len(loop
.link_loops
) == 0
147 # Starting at loop, build a curve one vertex at a time
148 # until we start where we came from
149 # Forward means that for any two edges the loop crosses
150 # sharing a face, it is passing through in clockwise order
153 def make_loop(loop
, forward
):
154 current_spline
= curve
.splines
.new("BEZIER")
155 current_spline
.use_cyclic_u
= True
157 # Data for the spline
158 # It's faster to store in an array and load into blender
165 if loops_exited
[loop
]:
167 loops_exited
[loop
] = True
168 # Follow the face around, ignoring boundary edges
170 loop
= loop
.link_loop_next
171 if not ignorable_loop(loop
):
173 assert loops_entered
[loop
] is False
174 loops_entered
[loop
] = True
177 # Find next radial loop
178 assert loop
.link_loops
[0] != loop
179 loop
= loop
.link_loops
[0]
180 forward
= loop
.vert
.index
== v
182 if loops_entered
[loop
]:
184 loops_entered
[loop
] = True
185 # Follow the face around, ignoring boundary edges
188 loop
= loop
.link_loop_prev
189 if not ignorable_loop(loop
):
191 assert loops_exited
[loop
] is False
192 loops_exited
[loop
] = True
194 # Find next radial loop
195 assert loop
.link_loops
[-1] != loop
196 loop
= loop
.link_loops
[-1]
197 forward
= loop
.vert
.index
== v
200 current_spline
.bezier_points
.add(1)
202 midpoint
= midpoints
[loop
.edge
.index
]
203 normal
= loop
.calc_normal() + prev_loop
.calc_normal()
205 offset
= weave_up
if forward
else weave_down
206 midpoint
= midpoint
+ offset
* normal
209 if handle_type
!= "AUTO":
210 tangent
= loop
.link_loop_next
.vert
.co
- loop
.vert
.co
212 binormal
= normal
.cross(tangent
).normalized()
215 s_binormal
= s
* binormal
216 c_tangent
= c
* tangent
217 handle_left
= midpoint
- s_binormal
- c_tangent
218 handle_right
= midpoint
+ s_binormal
+ c_tangent
219 handle_lefts
.extend(handle_left
)
220 handle_rights
.extend(handle_right
)
222 points
= current_spline
.bezier_points
223 points
.foreach_set("co", cos
)
224 if handle_type
!= "AUTO":
225 points
.foreach_set("handle_left", handle_lefts
)
226 points
.foreach_set("handle_right", handle_rights
)
228 # Attempt to start a loop at each untouched loop in the entire mesh
229 for face
in bm
.faces
:
230 for loop
in face
.loops
:
231 if ignorable_loop(loop
):
233 if not loops_exited
[loop
]:
234 make_loop(loop
, True)
235 if not loops_entered
[loop
]:
236 make_loop(loop
, False)
238 # Create an object from the curve
239 from bpy_extras
import object_utils
240 object_utils
.object_data_add(context
, curve
, operator
=None)
241 # Set the handle type (this is faster than setting it pointwise)
242 bpy
.ops
.object.editmode_toggle()
243 bpy
.ops
.curve
.select_all(action
="SELECT")
244 bpy
.ops
.curve
.handle_type_set(type=self
.handle_type_map
[handle_type
])
245 # Some blender versions lack the default
246 bpy
.ops
.curve
.radius_set(radius
=1.0)
247 bpy
.ops
.object.editmode_toggle()
248 # Restore active selection
249 curve_obj
= context
.active_object
251 # apply the bevel setting since it was unused
253 curve_obj
.data
.bevel_depth
= self
.geo_bDepth
257 bpy
.context
.view_layer
.objects
.active
= orig_obj
259 # restore pre operator state
260 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= use_enter_edit_mode
266 bpy
.utils
.register_class(CelticKnotOperator
)
270 bpy
.utils
.unregister_class(CelticKnotOperator
)
273 if __name__
== "__main__":