Merge branch 'blender-v3.3-release'
[blender-addons.git] / mesh_tools / mesh_edges_length.py
blobb4e91be07922e900d954793afac678427a060686
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Author: Giuseppe De Marco [BlenderLab] inspired by NirenYang
5 bl_info = {
6 "name": "Set edges length",
7 "description": "Edges length",
8 "author": "Giuseppe De Marco [BlenderLab] inspired by NirenYang",
9 "version": (0, 1, 0),
10 "blender": (2, 80, 0),
11 "location": "Toolbar > Tools > Mesh Tools: set Length(Shit+Alt+E)",
12 "warning": "",
13 "doc_url": "",
14 "category": "Mesh",
17 import bpy
18 import bmesh
19 from mathutils import Vector
20 from bpy.types import Operator
21 from bpy.props import (
22 FloatProperty,
23 EnumProperty,
26 # GLOBALS
27 edge_length_debug = False
28 _error_message = "Please select at least one edge to fill select history"
29 _error_message_2 = "Edges with shared vertices are not allowed. Please, use scale instead"
31 # Note : Refactor - removed all the operators apart from LengthSet
32 # and merged the other ones as options of length (lijenstina)
35 def get_edge_vector(edge):
36 verts = (edge.verts[0].co, edge.verts[1].co)
37 vector = verts[1] - verts[0]
39 return vector
42 def get_selected(bmesh_obj, geometry_type):
43 # geometry type should be edges, verts or faces
44 selected = []
46 for i in getattr(bmesh_obj, geometry_type):
47 if i.select:
48 selected.append(i)
49 return tuple(selected)
52 def get_center_vector(verts):
53 # verts = [Vector((x,y,z)), Vector((x,y,z))]
55 center_vector = Vector((((verts[1][0] + verts[0][0]) / 2.),
56 ((verts[1][1] + verts[0][1]) / 2.),
57 ((verts[1][2] + verts[0][2]) / 2.)))
58 return center_vector
61 class LengthSet(Operator):
62 bl_idname = "object.mesh_edge_length_set"
63 bl_label = "Set edge length"
64 bl_description = ("Change one selected edge length by a specified target,\n"
65 "existing length and different modes\n"
66 "Note: works only with Edges that not share a vertex")
67 bl_options = {'REGISTER', 'UNDO'}
69 old_length: FloatProperty(
70 name="Original length",
71 options={'HIDDEN'},
73 set_length_type: EnumProperty(
74 items=[
75 ('manual', "Manual",
76 "Input manually the desired Target Length"),
77 ('existing', "Existing Length",
78 "Use existing geometry Edges' characteristics"),
80 name="Set Type of Input",
82 target_length: FloatProperty(
83 name="Target Length",
84 description="Input a value for an Edges Length target",
85 default=1.00,
86 unit='LENGTH',
87 precision=5
89 existing_length: EnumProperty(
90 items=[
91 ('min', "Shortest",
92 "Set all to shortest Edge of selection"),
93 ('max', "Longest",
94 "Set all to the longest Edge of selection"),
95 ('average', "Average",
96 "Set all to the average Edge length of selection"),
97 ('active', "Active",
98 "Set all to the active Edge's one\n"
99 "Needs a selection to be done in Edge Select mode"),
101 name="Existing length"
103 mode: EnumProperty(
104 items=[
105 ('fixed', "Fixed", "Fixed"),
106 ('increment', "Increment", "Increment"),
107 ('decrement', "Decrement", "Decrement"),
109 name="Mode"
111 behaviour: EnumProperty(
112 items=[
113 ('proportional', "Proportional",
114 "Move vertex locations proportionally to the center of the Edge"),
115 ('clockwise', "Clockwise",
116 "Compute the Edges' vertex locations in a clockwise fashion"),
117 ('unclockwise', "Counterclockwise",
118 "Compute the Edges' vertex locations in a counterclockwise fashion"),
120 name="Resize behavior"
123 originary_edge_length_dict = {}
124 edge_lengths = []
125 selected_edges = ()
127 @classmethod
128 def poll(cls, context):
129 return (context.edit_object and context.object.type == 'MESH')
131 def check(self, context):
132 return True
134 def draw(self, context):
135 layout = self.layout
137 layout.label(text="Original Active length is: {:.3f}".format(self.old_length))
139 layout.label(text="Input Mode:")
140 layout.prop(self, "set_length_type", expand=True)
141 if self.set_length_type == 'manual':
142 layout.prop(self, "target_length")
143 else:
144 layout.prop(self, "existing_length", text="")
146 layout.label(text="Mode:")
147 layout.prop(self, "mode", text="")
149 layout.label(text="Resize Behavior:")
150 layout.prop(self, "behaviour", text="")
152 def get_existing_edge_length(self, bm):
153 if self.existing_length != "active":
154 if self.existing_length == "min":
155 return min(self.edge_lengths)
156 if self.existing_length == "max":
157 return max(self.edge_lengths)
158 elif self.existing_length == "average":
159 return sum(self.edge_lengths) / float(len(self.selected_edges))
160 else:
161 bm.edges.ensure_lookup_table()
162 active_edge_length = None
164 for elem in reversed(bm.select_history):
165 if isinstance(elem, bmesh.types.BMEdge):
166 active_edge_length = elem.calc_length()
167 break
168 return active_edge_length
170 return 0.0
172 def invoke(self, context, event):
173 wm = context.window_manager
175 obj = context.edit_object
176 bm = bmesh.from_edit_mesh(obj.data)
178 bpy.ops.mesh.select_mode(type="EDGE")
179 self.selected_edges = get_selected(bm, 'edges')
181 if self.selected_edges:
182 vertex_set = []
184 for edge in self.selected_edges:
185 vector = get_edge_vector(edge)
187 if edge.verts[0].index not in vertex_set:
188 vertex_set.append(edge.verts[0].index)
189 else:
190 self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
191 return {'CANCELLED'}
193 if edge.verts[1].index not in vertex_set:
194 vertex_set.append(edge.verts[1].index)
195 else:
196 self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
197 return {'CANCELLED'}
199 # warning, it's a constant !
200 verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
201 self.originary_edge_length_dict[verts_index] = vector
202 self.edge_lengths.append(vector.length)
203 self.old_length = vector.length
204 else:
205 self.report({'ERROR'}, _error_message)
206 return {'CANCELLED'}
208 if edge_length_debug:
209 self.report({'INFO'}, str(self.originary_edge_length_dict))
211 self.target_length = vector.length
213 return wm.invoke_props_dialog(self)
215 def execute(self, context):
217 bpy.ops.mesh.select_mode(type="EDGE")
218 self.context = context
220 obj = context.edit_object
221 bm = bmesh.from_edit_mesh(obj.data)
223 self.selected_edges = get_selected(bm, 'edges')
225 if not self.selected_edges:
226 self.report({'ERROR'}, _error_message)
227 return {'CANCELLED'}
229 for edge in self.selected_edges:
230 vector = get_edge_vector(edge)
231 # what we should see in original length dialog field
232 self.old_length = vector.length
234 if self.set_length_type == 'manual':
235 vector.length = abs(self.target_length)
236 else:
237 get_lengths = self.get_existing_edge_length(bm)
238 # check for edit mode
239 if not get_lengths:
240 self.report({'WARNING'},
241 "Operation Cancelled. "
242 "Active Edge could not be determined (needs selection in Edit Mode)")
243 return {'CANCELLED'}
245 vector.length = get_lengths
247 if vector.length == 0.0:
248 self.report({'ERROR'}, "Operation cancelled. Target length is set to zero")
249 return {'CANCELLED'}
251 center_vector = get_center_vector((edge.verts[0].co, edge.verts[1].co))
253 verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
255 if edge_length_debug:
256 self.report({'INFO'},
257 ' - '.join(('vector ' + str(vector),
258 'originary_vector ' +
259 str(self.originary_edge_length_dict[verts_index])
261 verts = (edge.verts[0].co, edge.verts[1].co)
263 if edge_length_debug:
264 self.report({'INFO'},
265 '\n edge.verts[0].co ' + str(verts[0]) +
266 '\n edge.verts[1].co ' + str(verts[1]) +
267 '\n vector.length' + str(vector.length))
269 # the clockwise direction have v1 -> v0, unclockwise v0 -> v1
270 if self.target_length >= 0:
271 if self.behaviour == 'proportional':
272 edge.verts[1].co = center_vector + vector / 2
273 edge.verts[0].co = center_vector - vector / 2
275 if self.mode == 'decrement':
276 edge.verts[0].co = (center_vector + vector / 2) - \
277 (self.originary_edge_length_dict[verts_index] / 2)
278 edge.verts[1].co = (center_vector - vector / 2) + \
279 (self.originary_edge_length_dict[verts_index] / 2)
281 elif self.mode == 'increment':
282 edge.verts[1].co = (center_vector + vector / 2) + \
283 self.originary_edge_length_dict[verts_index] / 2
284 edge.verts[0].co = (center_vector - vector / 2) - \
285 self.originary_edge_length_dict[verts_index] / 2
287 elif self.behaviour == 'unclockwise':
288 if self.mode == 'increment':
289 edge.verts[1].co = \
290 verts[0] + (self.originary_edge_length_dict[verts_index] + vector)
291 elif self.mode == 'decrement':
292 edge.verts[0].co = \
293 verts[1] - (self.originary_edge_length_dict[verts_index] - vector)
294 else:
295 edge.verts[1].co = verts[0] + vector
297 else:
298 # clockwise
299 if self.mode == 'increment':
300 edge.verts[0].co = \
301 verts[1] - (self.originary_edge_length_dict[verts_index] + vector)
302 elif self.mode == 'decrement':
303 edge.verts[1].co = \
304 verts[0] + (self.originary_edge_length_dict[verts_index] - vector)
305 else:
306 edge.verts[0].co = verts[1] - vector
309 if edge_length_debug:
310 self.report({'INFO'},
311 '\n edge.verts[0].co' + str(verts[0]) +
312 '\n edge.verts[1].co' + str(verts[1]) +
313 '\n vector' + str(vector) + '\n v1 > v0:' + str((verts[1] >= verts[0]))
315 bmesh.update_edit_mesh(obj.data, loop_triangles=True)
317 return {'FINISHED'}
320 def register():
321 bpy.utils.register_class(LengthSet)
324 def unregister():
325 bpy.utils.unregister_class(LengthSet)
328 if __name__ == "__main__":
329 register()