1 # SPDX-FileCopyrightText: 2013 Adam Newgas
3 # SPDX-License-Identifier: MIT
5 # Blender plugin for generating celtic knot curves from 3d meshes
10 "author": "Adam Newgas",
12 "blender": (2, 80, 0),
13 "location": "View3D > Add > Curve",
15 "doc_url": "https://github.com/BorisTheBrave/celtic-knot/wiki",
16 "category": "Add Curve",
21 from bpy
.types
import Operator
22 from bpy
.props
import (
26 from collections
import defaultdict
33 class CelticKnotOperator(Operator
):
34 bl_idname
= "curve.celtic_links"
35 bl_label
= "Celtic Links"
36 bl_description
= "Select a low poly Mesh Object to cover with Knitted Links"
37 bl_options
= {'REGISTER', 'UNDO', 'PRESET'}
39 weave_up
: FloatProperty(
41 description
="Distance to shift curve upwards over knots",
45 weave_down
: FloatProperty(
47 description
="Distance to shift curve downward under knots",
52 ('ALIGNED', "Aligned", "Points at a fixed crossing angle"),
53 ('AUTO', "Auto", "Automatic control points")
55 handle_type
: EnumProperty(
58 description
="Controls what type the bezier control points use",
62 handle_type_map
= {"AUTO": "AUTOMATIC", "ALIGNED": "ALIGNED"}
64 crossing_angle
: FloatProperty(
65 name
="Crossing Angle",
66 description
="Aligned only: the angle between curves in a knot",
72 crossing_strength
: FloatProperty(
73 name
="Crossing Strength",
74 description
="Aligned only: strength of bezier control points",
79 geo_bDepth
: FloatProperty(
83 description
="Bevel Depth",
87 def poll(cls
, context
):
88 ob
= context
.active_object
89 return ((ob
is not None) and (ob
.mode
== "OBJECT") and
90 (ob
.type == "MESH") and (context
.mode
== "OBJECT"))
92 def draw(self
, context
):
94 layout
.prop(self
, "handle_type")
96 col
= layout
.column(align
=True)
97 col
.prop(self
, "weave_up")
98 col
.prop(self
, "weave_down")
100 col
= layout
.column(align
=True)
101 col
.active
= False if self
.handle_type
== 'AUTO' else True
102 col
.prop(self
, "crossing_angle")
103 col
.prop(self
, "crossing_strength")
105 layout
.prop(self
, "geo_bDepth")
107 def execute(self
, context
):
108 # turn off 'Enter Edit Mode'
109 use_enter_edit_mode
= bpy
.context
.preferences
.edit
.use_enter_edit_mode
110 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= False
113 s
= sin(self
.crossing_angle
) * self
.crossing_strength
114 c
= cos(self
.crossing_angle
) * self
.crossing_strength
115 handle_type
= self
.handle_type
116 weave_up
= self
.weave_up
117 weave_down
= self
.weave_down
119 # Create the new object
120 orig_obj
= obj
= context
.active_object
121 curve
= bpy
.data
.curves
.new("Celtic", "CURVE")
122 curve
.dimensions
= "3D"
123 curve
.twist_mode
= "MINIMUM"
124 curve
.fill_mode
= "FULL"
125 curve
.bevel_depth
= 0.015
126 curve
.extrude
= 0.003
127 curve
.bevel_resolution
= 4
131 # Compute all the midpoints of each edge
132 for e
in obj
.edges
.values():
133 v1
= obj
.vertices
[e
.vertices
[0]]
134 v2
= obj
.vertices
[e
.vertices
[1]]
135 m
= (v1
.co
+ v2
.co
) / 2.0
140 # Stores which loops the curve has already passed through
141 loops_entered
= defaultdict(lambda: False)
142 loops_exited
= defaultdict(lambda: False)
143 # Loops on the boundary of a surface
145 def ignorable_loop(loop
):
146 return len(loop
.link_loops
) == 0
148 # Starting at loop, build a curve one vertex at a time
149 # until we start where we came from
150 # Forward means that for any two edges the loop crosses
151 # sharing a face, it is passing through in clockwise order
154 def make_loop(loop
, forward
):
155 current_spline
= curve
.splines
.new("BEZIER")
156 current_spline
.use_cyclic_u
= True
158 # Data for the spline
159 # It's faster to store in an array and load into blender
166 if loops_exited
[loop
]:
168 loops_exited
[loop
] = True
169 # Follow the face around, ignoring boundary edges
171 loop
= loop
.link_loop_next
172 if not ignorable_loop(loop
):
174 assert loops_entered
[loop
] is False
175 loops_entered
[loop
] = True
178 # Find next radial loop
179 assert loop
.link_loops
[0] != loop
180 loop
= loop
.link_loops
[0]
181 forward
= loop
.vert
.index
== v
183 if loops_entered
[loop
]:
185 loops_entered
[loop
] = True
186 # Follow the face around, ignoring boundary edges
189 loop
= loop
.link_loop_prev
190 if not ignorable_loop(loop
):
192 assert loops_exited
[loop
] is False
193 loops_exited
[loop
] = True
195 # Find next radial loop
196 assert loop
.link_loops
[-1] != loop
197 loop
= loop
.link_loops
[-1]
198 forward
= loop
.vert
.index
== v
201 current_spline
.bezier_points
.add(1)
203 midpoint
= midpoints
[loop
.edge
.index
]
204 normal
= loop
.calc_normal() + prev_loop
.calc_normal()
206 offset
= weave_up
if forward
else weave_down
207 midpoint
= midpoint
+ offset
* normal
210 if handle_type
!= "AUTO":
211 tangent
= loop
.link_loop_next
.vert
.co
- loop
.vert
.co
213 binormal
= normal
.cross(tangent
).normalized()
216 s_binormal
= s
* binormal
217 c_tangent
= c
* tangent
218 handle_left
= midpoint
- s_binormal
- c_tangent
219 handle_right
= midpoint
+ s_binormal
+ c_tangent
220 handle_lefts
.extend(handle_left
)
221 handle_rights
.extend(handle_right
)
223 points
= current_spline
.bezier_points
224 points
.foreach_set("co", cos
)
225 if handle_type
!= "AUTO":
226 points
.foreach_set("handle_left", handle_lefts
)
227 points
.foreach_set("handle_right", handle_rights
)
229 # Attempt to start a loop at each untouched loop in the entire mesh
230 for face
in bm
.faces
:
231 for loop
in face
.loops
:
232 if ignorable_loop(loop
):
234 if not loops_exited
[loop
]:
235 make_loop(loop
, True)
236 if not loops_entered
[loop
]:
237 make_loop(loop
, False)
239 # Create an object from the curve
240 from bpy_extras
import object_utils
241 object_utils
.object_data_add(context
, curve
, operator
=None)
242 # Set the handle type (this is faster than setting it pointwise)
243 bpy
.ops
.object.editmode_toggle()
244 bpy
.ops
.curve
.select_all(action
="SELECT")
245 bpy
.ops
.curve
.handle_type_set(type=self
.handle_type_map
[handle_type
])
246 # Some blender versions lack the default
247 bpy
.ops
.curve
.radius_set(radius
=1.0)
248 bpy
.ops
.object.editmode_toggle()
249 # Restore active selection
250 curve_obj
= context
.active_object
252 # apply the bevel setting since it was unused
254 curve_obj
.data
.bevel_depth
= self
.geo_bDepth
258 bpy
.context
.view_layer
.objects
.active
= orig_obj
260 # restore pre operator state
261 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= use_enter_edit_mode
267 bpy
.utils
.register_class(CelticKnotOperator
)
271 bpy
.utils
.unregister_class(CelticKnotOperator
)
274 if __name__
== "__main__":