Merge branch 'blender-v4.0-release'
[blender-addons.git] / mesh_tools / mesh_edges_length.py
blobcd68e5f39180ebf18cdb2e80db0095d6d5f34b74
1 # SPDX-FileCopyrightText: 2019-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # Author: Giuseppe De Marco [BlenderLab] inspired by NirenYang
7 bl_info = {
8 "name": "Set edges length",
9 "description": "Edges length",
10 "author": "Giuseppe De Marco [BlenderLab] inspired by NirenYang",
11 "version": (0, 1, 0),
12 "blender": (2, 80, 0),
13 "location": "Toolbar > Tools > Mesh Tools: set Length(Shit+Alt+E)",
14 "warning": "",
15 "doc_url": "",
16 "category": "Mesh",
19 import bpy
20 import bmesh
21 from mathutils import Vector
22 from bpy.types import Operator
23 from bpy.props import (
24 FloatProperty,
25 EnumProperty,
28 # GLOBALS
29 edge_length_debug = False
30 _error_message = "Please select at least one edge to fill select history"
31 _error_message_2 = "Edges with shared vertices are not allowed. Please, use scale instead"
33 # Note : Refactor - removed all the operators apart from LengthSet
34 # and merged the other ones as options of length (lijenstina)
37 def get_edge_vector(edge):
38 verts = (edge.verts[0].co, edge.verts[1].co)
39 vector = verts[1] - verts[0]
41 return vector
44 def get_selected(bmesh_obj, geometry_type):
45 # geometry type should be edges, verts or faces
46 selected = []
48 for i in getattr(bmesh_obj, geometry_type):
49 if i.select:
50 selected.append(i)
51 return tuple(selected)
54 def get_center_vector(verts):
55 # verts = [Vector((x,y,z)), Vector((x,y,z))]
57 center_vector = Vector((((verts[1][0] + verts[0][0]) / 2.),
58 ((verts[1][1] + verts[0][1]) / 2.),
59 ((verts[1][2] + verts[0][2]) / 2.)))
60 return center_vector
63 class LengthSet(Operator):
64 bl_idname = "object.mesh_edge_length_set"
65 bl_label = "Set edge length"
66 bl_description = ("Change one selected edge length by a specified target,\n"
67 "existing length and different modes\n"
68 "Note: works only with Edges that not share a vertex")
69 bl_options = {'REGISTER', 'UNDO'}
71 old_length: FloatProperty(
72 name="Original length",
73 options={'HIDDEN'},
75 set_length_type: EnumProperty(
76 items=[
77 ('manual', "Manual",
78 "Input manually the desired Target Length"),
79 ('existing', "Existing Length",
80 "Use existing geometry Edges' characteristics"),
82 name="Set Type of Input",
84 target_length: FloatProperty(
85 name="Target Length",
86 description="Input a value for an Edges Length target",
87 default=1.00,
88 unit='LENGTH',
89 precision=5
91 existing_length: EnumProperty(
92 items=[
93 ('min', "Shortest",
94 "Set all to shortest Edge of selection"),
95 ('max', "Longest",
96 "Set all to the longest Edge of selection"),
97 ('average', "Average",
98 "Set all to the average Edge length of selection"),
99 ('active', "Active",
100 "Set all to the active Edge's one\n"
101 "Needs a selection to be done in Edge Select mode"),
103 name="Existing length"
105 mode: EnumProperty(
106 items=[
107 ('fixed', "Fixed", "Fixed"),
108 ('increment', "Increment", "Increment"),
109 ('decrement', "Decrement", "Decrement"),
111 name="Mode"
113 behaviour: EnumProperty(
114 items=[
115 ('proportional', "Proportional",
116 "Move vertex locations proportionally to the center of the Edge"),
117 ('clockwise', "Clockwise",
118 "Compute the Edges' vertex locations in a clockwise fashion"),
119 ('unclockwise', "Counterclockwise",
120 "Compute the Edges' vertex locations in a counterclockwise fashion"),
122 name="Resize behavior"
125 originary_edge_length_dict = {}
126 edge_lengths = []
127 selected_edges = ()
129 @classmethod
130 def poll(cls, context):
131 return (context.edit_object and context.object.type == 'MESH')
133 def check(self, context):
134 return True
136 def draw(self, context):
137 layout = self.layout
139 layout.label(text="Original Active length is: {:.3f}".format(self.old_length))
141 layout.label(text="Input Mode:")
142 layout.prop(self, "set_length_type", expand=True)
143 if self.set_length_type == 'manual':
144 layout.prop(self, "target_length")
145 else:
146 layout.prop(self, "existing_length", text="")
148 layout.label(text="Mode:")
149 layout.prop(self, "mode", text="")
151 layout.label(text="Resize Behavior:")
152 layout.prop(self, "behaviour", text="")
154 def get_existing_edge_length(self, bm):
155 if self.existing_length != "active":
156 if self.existing_length == "min":
157 return min(self.edge_lengths)
158 if self.existing_length == "max":
159 return max(self.edge_lengths)
160 elif self.existing_length == "average":
161 return sum(self.edge_lengths) / float(len(self.selected_edges))
162 else:
163 bm.edges.ensure_lookup_table()
164 active_edge_length = None
166 for elem in reversed(bm.select_history):
167 if isinstance(elem, bmesh.types.BMEdge):
168 active_edge_length = elem.calc_length()
169 break
170 return active_edge_length
172 return 0.0
174 def invoke(self, context, event):
175 wm = context.window_manager
177 obj = context.edit_object
178 bm = bmesh.from_edit_mesh(obj.data)
180 bpy.ops.mesh.select_mode(type="EDGE")
181 self.selected_edges = get_selected(bm, 'edges')
183 if self.selected_edges:
184 vertex_set = []
186 for edge in self.selected_edges:
187 vector = get_edge_vector(edge)
189 if edge.verts[0].index not in vertex_set:
190 vertex_set.append(edge.verts[0].index)
191 else:
192 self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
193 return {'CANCELLED'}
195 if edge.verts[1].index not in vertex_set:
196 vertex_set.append(edge.verts[1].index)
197 else:
198 self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
199 return {'CANCELLED'}
201 # warning, it's a constant !
202 verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
203 self.originary_edge_length_dict[verts_index] = vector
204 self.edge_lengths.append(vector.length)
205 self.old_length = vector.length
206 else:
207 self.report({'ERROR'}, _error_message)
208 return {'CANCELLED'}
210 if edge_length_debug:
211 self.report({'INFO'}, str(self.originary_edge_length_dict))
213 self.target_length = vector.length
215 return wm.invoke_props_dialog(self)
217 def execute(self, context):
219 bpy.ops.mesh.select_mode(type="EDGE")
220 self.context = context
222 obj = context.edit_object
223 bm = bmesh.from_edit_mesh(obj.data)
225 self.selected_edges = get_selected(bm, 'edges')
227 if not self.selected_edges:
228 self.report({'ERROR'}, _error_message)
229 return {'CANCELLED'}
231 for edge in self.selected_edges:
232 vector = get_edge_vector(edge)
233 # what we should see in original length dialog field
234 self.old_length = vector.length
236 if self.set_length_type == 'manual':
237 vector.length = abs(self.target_length)
238 else:
239 get_lengths = self.get_existing_edge_length(bm)
240 # check for edit mode
241 if not get_lengths:
242 self.report({'WARNING'},
243 "Operation Cancelled. "
244 "Active Edge could not be determined (needs selection in Edit Mode)")
245 return {'CANCELLED'}
247 vector.length = get_lengths
249 if vector.length == 0.0:
250 self.report({'ERROR'}, "Operation cancelled. Target length is set to zero")
251 return {'CANCELLED'}
253 center_vector = get_center_vector((edge.verts[0].co, edge.verts[1].co))
255 verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
257 if edge_length_debug:
258 self.report({'INFO'},
259 ' - '.join(('vector ' + str(vector),
260 'originary_vector ' +
261 str(self.originary_edge_length_dict[verts_index])
263 verts = (edge.verts[0].co, edge.verts[1].co)
265 if edge_length_debug:
266 self.report({'INFO'},
267 '\n edge.verts[0].co ' + str(verts[0]) +
268 '\n edge.verts[1].co ' + str(verts[1]) +
269 '\n vector.length' + str(vector.length))
271 # the clockwise direction have v1 -> v0, unclockwise v0 -> v1
272 if self.target_length >= 0:
273 if self.behaviour == 'proportional':
274 edge.verts[1].co = center_vector + vector / 2
275 edge.verts[0].co = center_vector - vector / 2
277 if self.mode == 'decrement':
278 edge.verts[0].co = (center_vector + vector / 2) - \
279 (self.originary_edge_length_dict[verts_index] / 2)
280 edge.verts[1].co = (center_vector - vector / 2) + \
281 (self.originary_edge_length_dict[verts_index] / 2)
283 elif self.mode == 'increment':
284 edge.verts[1].co = (center_vector + vector / 2) + \
285 self.originary_edge_length_dict[verts_index] / 2
286 edge.verts[0].co = (center_vector - vector / 2) - \
287 self.originary_edge_length_dict[verts_index] / 2
289 elif self.behaviour == 'unclockwise':
290 if self.mode == 'increment':
291 edge.verts[1].co = \
292 verts[0] + (self.originary_edge_length_dict[verts_index] + vector)
293 elif self.mode == 'decrement':
294 edge.verts[0].co = \
295 verts[1] - (self.originary_edge_length_dict[verts_index] - vector)
296 else:
297 edge.verts[1].co = verts[0] + vector
299 else:
300 # clockwise
301 if self.mode == 'increment':
302 edge.verts[0].co = \
303 verts[1] - (self.originary_edge_length_dict[verts_index] + vector)
304 elif self.mode == 'decrement':
305 edge.verts[1].co = \
306 verts[0] + (self.originary_edge_length_dict[verts_index] - vector)
307 else:
308 edge.verts[0].co = verts[1] - vector
311 if edge_length_debug:
312 self.report({'INFO'},
313 '\n edge.verts[0].co' + str(verts[0]) +
314 '\n edge.verts[1].co' + str(verts[1]) +
315 '\n vector' + str(vector) + '\n v1 > v0:' + str((verts[1] >= verts[0]))
317 bmesh.update_edit_mesh(obj.data, loop_triangles=True)
319 return {'FINISHED'}
322 def register():
323 bpy.utils.register_class(LengthSet)
326 def unregister():
327 bpy.utils.unregister_class(LengthSet)
330 if __name__ == "__main__":
331 register()