Animall: move to own dir, to add translations later
[blender-addons.git] / add_curve_extra_objects / add_curve_celtic_links.py
blobd6b777deb09bf212ead2dea9256ef432b16b681e
1 # SPDX-License-Identifier: MIT
2 # Copyright 2013 Adam Newgas
4 # Blender plugin for generating celtic knot curves from 3d meshes
6 bl_info = {
7 "name": "Celtic Knot",
8 "description": "",
9 "author": "Adam Newgas",
10 "version": (0, 1, 3),
11 "blender": (2, 80, 0),
12 "location": "View3D > Add > Curve",
13 "warning": "",
14 "doc_url": "https://github.com/BorisTheBrave/celtic-knot/wiki",
15 "category": "Add Curve",
18 import bpy
19 import bmesh
20 from bpy.types import Operator
21 from bpy.props import (
22 EnumProperty,
23 FloatProperty,
25 from collections import defaultdict
26 from math import (
27 pi, sin,
28 cos,
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(
39 name="Weave Up",
40 description="Distance to shift curve upwards over knots",
41 subtype="DISTANCE",
42 unit="LENGTH"
44 weave_down : FloatProperty(
45 name="Weave Down",
46 description="Distance to shift curve downward under knots",
47 subtype="DISTANCE",
48 unit="LENGTH"
50 handle_types = [
51 ('ALIGNED', "Aligned", "Points at a fixed crossing angle"),
52 ('AUTO', "Auto", "Automatic control points")
54 handle_type : EnumProperty(
55 items=handle_types,
56 name="Handle Type",
57 description="Controls what type the bezier control points use",
58 default='AUTO'
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",
66 default=pi / 4,
67 min=0, max=pi / 2,
68 subtype="ANGLE",
69 unit="ROTATION"
71 crossing_strength : FloatProperty(
72 name="Crossing Strength",
73 description="Aligned only: strength of bezier control points",
74 soft_min=0,
75 subtype="DISTANCE",
76 unit="LENGTH"
78 geo_bDepth : FloatProperty(
79 name="Bevel Depth",
80 default=0.04,
81 min=0, soft_min=0,
82 description="Bevel Depth",
85 @classmethod
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):
92 layout = self.layout
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
111 # Cache some values
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
127 obj = obj.data
128 midpoints = []
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
135 midpoints.append(m)
137 bm = bmesh.new()
138 bm.from_mesh(obj)
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
151 # else anticlockwise
153 def make_loop(loop, forward):
154 current_spline = curve.splines.new("BEZIER")
155 current_spline.use_cyclic_u = True
156 first = True
157 # Data for the spline
158 # It's faster to store in an array and load into blender
159 # at once
160 cos = []
161 handle_lefts = []
162 handle_rights = []
163 while True:
164 if forward:
165 if loops_exited[loop]:
166 break
167 loops_exited[loop] = True
168 # Follow the face around, ignoring boundary edges
169 while True:
170 loop = loop.link_loop_next
171 if not ignorable_loop(loop):
172 break
173 assert loops_entered[loop] is False
174 loops_entered[loop] = True
175 v = loop.vert.index
176 prev_loop = loop
177 # Find next radial loop
178 assert loop.link_loops[0] != loop
179 loop = loop.link_loops[0]
180 forward = loop.vert.index == v
181 else:
182 if loops_entered[loop]:
183 break
184 loops_entered[loop] = True
185 # Follow the face around, ignoring boundary edges
186 while True:
187 v = loop.vert.index
188 loop = loop.link_loop_prev
189 if not ignorable_loop(loop):
190 break
191 assert loops_exited[loop] is False
192 loops_exited[loop] = True
193 prev_loop = loop
194 # Find next radial loop
195 assert loop.link_loops[-1] != loop
196 loop = loop.link_loops[-1]
197 forward = loop.vert.index == v
199 if not first:
200 current_spline.bezier_points.add(1)
201 first = False
202 midpoint = midpoints[loop.edge.index]
203 normal = loop.calc_normal() + prev_loop.calc_normal()
204 normal.normalize()
205 offset = weave_up if forward else weave_down
206 midpoint = midpoint + offset * normal
207 cos.extend(midpoint)
209 if handle_type != "AUTO":
210 tangent = loop.link_loop_next.vert.co - loop.vert.co
211 tangent.normalize()
212 binormal = normal.cross(tangent).normalized()
213 if not forward:
214 tangent *= -1
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):
232 continue
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
252 try:
253 curve_obj.data.bevel_depth = self.geo_bDepth
254 except:
255 pass
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
262 return {'FINISHED'}
265 def register():
266 bpy.utils.register_class(CelticKnotOperator)
269 def unregister():
270 bpy.utils.unregister_class(CelticKnotOperator)
273 if __name__ == "__main__":
274 register()