Merge branch 'master' into blender2.8
[blender-addons.git] / mesh_extra_tools / mesh_edges_length.py
blob0c1f3cb43552d4f6efd6523e06b4609d7793cda0
1 # gpl author: Giuseppe De Marco [BlenderLab] inspired by NirenYang
3 bl_info = {
4 "name": "Set edges length",
5 "description": "Edges length",
6 "author": "Giuseppe De Marco [BlenderLab] inspired by NirenYang",
7 "version": (0, 1, 0),
8 "blender": (2, 7, 1),
9 "location": "Toolbar > Tools > Mesh Tools: set Length(Shit+Alt+E)",
10 "warning": "",
11 "wiki_url": "",
12 "category": "Mesh",
15 import bpy
16 import bmesh
17 from mathutils import Vector
18 from bpy.types import Operator
19 from bpy.props import (
20 FloatProperty,
21 EnumProperty,
24 # GLOBALS
25 edge_length_debug = False
26 _error_message = "Please select at least one edge to fill select history"
27 _error_message_2 = "Edges with shared vertices are not allowed. Please, use scale instead"
29 # Note : Refactor - removed all the operators apart from LengthSet
30 # and merged the other ones as options of length (lijenstina)
33 def get_edge_vector(edge):
34 verts = (edge.verts[0].co, edge.verts[1].co)
35 vector = verts[1] - verts[0]
37 return vector
40 def get_selected(bmesh_obj, geometry_type):
41 # geometry type should be edges, verts or faces
42 selected = []
44 for i in getattr(bmesh_obj, geometry_type):
45 if i.select:
46 selected.append(i)
47 return tuple(selected)
50 def get_center_vector(verts):
51 # verts = [Vector((x,y,z)), Vector((x,y,z))]
53 center_vector = Vector((((verts[1][0] + verts[0][0]) / 2.),
54 ((verts[1][1] + verts[0][1]) / 2.),
55 ((verts[1][2] + verts[0][2]) / 2.)))
56 return center_vector
59 class LengthSet(Operator):
60 bl_idname = "object.mesh_edge_length_set"
61 bl_label = "Set edge length"
62 bl_description = ("Change one selected edge length by a specified target,\n"
63 "existing length and different modes\n"
64 "Note: works only with Edges that not share a vertex")
65 bl_options = {'REGISTER', 'UNDO'}
67 old_length = FloatProperty(
68 name="Original length",
69 options={'HIDDEN'},
71 set_length_type = EnumProperty(
72 items=[
73 ('manual', "Manual",
74 "Input manually the desired Target Length"),
75 ('existing', "Existing Length",
76 "Use existing geometry Edges' characteristics"),
78 name="Set Type of Input",
80 target_length = FloatProperty(
81 name="Target Length",
82 description="Input a value for an Edges Length target",
83 default=1.00,
84 unit='LENGTH',
85 precision=5
87 existing_length = EnumProperty(
88 items=[
89 ('min', "Shortest",
90 "Set all to shortest Edge of selection"),
91 ('max', "Longest",
92 "Set all to the longest Edge of selection"),
93 ('average', "Average",
94 "Set all to the average Edge length of selection"),
95 ('active', "Active",
96 "Set all to the active Edge's one\n"
97 "Needs a selection to be done in Edge Select mode"),
99 name="Existing length"
101 mode = EnumProperty(
102 items=[
103 ('fixed', "Fixed", "Fixed"),
104 ('increment', "Increment", "Increment"),
105 ('decrement', "Decrement", "Decrement"),
107 name="Mode"
109 behaviour = EnumProperty(
110 items=[
111 ('proportional', "Proportional",
112 "Move vertex locations proportionally to the center of the Edge"),
113 ('clockwise', "Clockwise",
114 "Compute the Edges' vertex locations in a clockwise fashion"),
115 ('unclockwise', "Counterclockwise",
116 "Compute the Edges' vertex locations in a counterclockwise fashion"),
118 name="Resize behavior"
121 originary_edge_length_dict = {}
122 edge_lengths = []
123 selected_edges = ()
125 @classmethod
126 def poll(cls, context):
127 return (context.edit_object and context.object.type == 'MESH')
129 def check(self, context):
130 return True
132 def draw(self, context):
133 layout = self.layout
135 layout.label("Original Active length is: {:.3f}".format(self.old_length))
137 layout.label("Input Mode:")
138 layout.prop(self, "set_length_type", expand=True)
139 if self.set_length_type == 'manual':
140 layout.prop(self, "target_length")
141 else:
142 layout.prop(self, "existing_length", text="")
144 layout.label("Mode:")
145 layout.prop(self, "mode", text="")
147 layout.label("Resize Behavior:")
148 layout.prop(self, "behaviour", text="")
150 def get_existing_edge_length(self, bm):
151 if self.existing_length != "active":
152 if self.existing_length == "min":
153 return min(self.edge_lengths)
154 if self.existing_length == "max":
155 return max(self.edge_lengths)
156 elif self.existing_length == "average":
157 return sum(self.edge_lengths) / float(len(self.selected_edges))
158 else:
159 bm.edges.ensure_lookup_table()
160 active_edge_length = None
162 for elem in reversed(bm.select_history):
163 if isinstance(elem, bmesh.types.BMEdge):
164 active_edge_length = elem.calc_length()
165 break
166 return active_edge_length
168 return 0.0
170 def invoke(self, context, event):
171 wm = context.window_managerlength
173 obj = context.edit_object
174 bm = bmesh.from_edit_mesh(obj.data)
176 bpy.ops.mesh.select_mode(type="EDGE")
177 self.selected_edges = get_selected(bm, 'edges')
179 if self.selected_edges:
180 vertex_set = []
182 for edge in self.selected_edges:
183 vector = get_edge_vector(edge)
185 if edge.verts[0].index not in vertex_set:
186 vertex_set.append(edge.verts[0].index)
187 else:
188 self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
189 return {'CANCELLED'}
191 if edge.verts[1].index not in vertex_set:
192 vertex_set.append(edge.verts[1].index)
193 else:
194 self.report({'ERROR_INVALID_INPUT'}, _error_message_2)
195 return {'CANCELLED'}
197 # warning, it's a constant !
198 verts_index = ''.join((str(edge.verts[0].index), str(edge.verts[1].index)))
199 self.originary_edge_length_dict[verts_index] = vector
200 self.edge_lengths.append(vector.length)
201 self.old_length = vector.length
202 else:
203 self.report({'ERROR'}, _error_message)
204 return {'CANCELLED'}
206 if edge_length_debug:
207 self.report({'INFO'}, str(self.originary_edge_length_dict))
209 if bpy.context.scene.unit_settings.system == 'IMPERIAL':
210 # imperial to metric conversion
211 vector.length = (0.9144 * vector.length) / 3
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
310 if bpy.context.scene.unit_settings.system == 'IMPERIAL':
312 # yards to metric conversion
313 vector.length = ( 3. * vector.length ) / 0.9144
314 # metric to yards conversion
315 vector.length = ( 0.9144 * vector.length ) / 3.
317 for mvert in edge.verts:
318 # school time: 0.9144 : 3 = X : mvert
319 mvert.co = (0.9144 * mvert.co) / 3
321 if edge_length_debug:
322 self.report({'INFO'},
323 '\n edge.verts[0].co' + str(verts[0]) +
324 '\n edge.verts[1].co' + str(verts[1]) +
325 '\n vector' + str(vector) + '\n v1 > v0:' + str((verts[1] >= verts[0]))
327 bmesh.update_edit_mesh(obj.data, True)
329 return {'FINISHED'}
332 def register():
333 bpy.utils.register_class(LengthSet)
336 def unregister():
337 bpy.utils.unregister_class(LengthSet)
340 if __name__ == "__main__":
341 register()