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