|
20 | 20 | "name": "Merge Tool", |
21 | 21 | "description": "An interactive tool for merging vertices and edges.", |
22 | 22 | "author": "Andreas Strømberg, Chris Kohl", |
23 | | - "version": (1, 4, 0), |
| 23 | + "version": (1, 5, 0), |
24 | 24 | "blender": (2, 93, 0), |
25 | 25 | "location": "View3D > TOOLS > Merge Tool", |
26 | 26 | "warning": "", |
|
31 | 31 |
|
32 | 32 |
|
33 | 33 | import bpy |
34 | | -import gpu |
35 | 34 | import bmesh |
36 | 35 | import os |
37 | 36 | from mathutils import Vector |
38 | | -from gpu_extras.presets import draw_circle_2d |
39 | | -from gpu_extras.batch import batch_for_shader |
| 37 | + |
| 38 | +from importlib import reload |
| 39 | +if 'shaders' in globals(): |
| 40 | + reload(shaders) |
| 41 | + |
| 42 | +from .shaders import draw_callback_3d, draw_callback_2d |
| 43 | +from .util import find_center, set_component |
| 44 | + |
40 | 45 | from bpy.props import ( |
41 | 46 | EnumProperty, |
42 | 47 | StringProperty, |
|
49 | 54 | icon_dir = os.path.join(os.path.dirname(__file__), "icons") |
50 | 55 | t_cursor = 'PAINT_CROSS' |
51 | 56 |
|
52 | | -# Blender versions higher than 4.0 don't support 3D_UNIFORM_COLOR but versions below 3.4 require it |
53 | | -if bpy.app.version[0] >= 4: |
54 | | - shader_type = 'UNIFORM_COLOR' |
55 | | -elif bpy.app.version[0] == 3 and bpy.app.version[1] >= 4: |
56 | | - shader_type = 'UNIFORM_COLOR' |
57 | | -else: |
58 | | - shader_type = '3D_UNIFORM_COLOR' |
59 | 57 |
|
60 | 58 | classes = [] |
61 | 59 |
|
@@ -104,7 +102,7 @@ class MergeToolPreferences(bpy.types.AddonPreferences): |
104 | 102 | description="Size of the circle cursor (VISUAL ONLY)", |
105 | 103 | default=12.0, |
106 | 104 | min=6.0, |
107 | | - max=100, |
| 105 | + max=100.0, |
108 | 106 | step=1, |
109 | 107 | precision=2) |
110 | 108 |
|
@@ -158,247 +156,6 @@ def draw(self, context): |
158 | 156 | classes.append(MergeToolPreferences) |
159 | 157 |
|
160 | 158 |
|
161 | | -vertex_shader = ''' |
162 | | - uniform mat4 u_ViewProjectionMatrix; |
163 | | -
|
164 | | - in vec3 position; |
165 | | - in float arcLength; |
166 | | -
|
167 | | - out float v_ArcLength; |
168 | | -
|
169 | | - void main() |
170 | | - { |
171 | | - v_ArcLength = arcLength; |
172 | | - gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f); |
173 | | - } |
174 | | -''' |
175 | | - |
176 | | -fragment_shader = ''' |
177 | | - uniform float u_Scale; |
178 | | - uniform vec4 u_Color; |
179 | | -
|
180 | | - in float v_ArcLength; |
181 | | - out vec4 FragColor; |
182 | | -
|
183 | | - void main() |
184 | | - { |
185 | | - if (step(sin(v_ArcLength * u_Scale), 0.5) == 1) discard; |
186 | | - FragColor = vec4(u_Color); |
187 | | - } |
188 | | -''' |
189 | | - |
190 | | - |
191 | | -class DrawPoint(): |
192 | | - def __init__(self, *args, **kwargs): |
193 | | - super().__init__(*args, **kwargs) |
194 | | - self.shader = None |
195 | | - self.coords = None |
196 | | - self.color = None |
197 | | - |
198 | | - def draw(self): |
199 | | - batch = batch_for_shader(self.shader, 'POINTS', {"pos": self.coords}) |
200 | | - self.shader.bind() |
201 | | - self.shader.uniform_float("color", self.color) |
202 | | - batch.draw(self.shader) |
203 | | - |
204 | | - def add(self, shader, coords, color): |
205 | | - self.shader = shader |
206 | | - if isinstance(coords, Vector): |
207 | | - self.coords = [coords] |
208 | | - else: |
209 | | - self.coords = coords |
210 | | - self.color = color |
211 | | - self.draw() |
212 | | - |
213 | | - |
214 | | -class DrawLine(): |
215 | | - def __init__(self, *args, **kwargs): |
216 | | - super().__init__(*args, **kwargs) |
217 | | - self.shader = None |
218 | | - self.coords = None |
219 | | - self.color = None |
220 | | - |
221 | | - def draw(self): |
222 | | - batch = batch_for_shader(self.shader, 'LINES', {"pos": self.coords}) |
223 | | - self.shader.bind() |
224 | | - self.shader.uniform_float("color", self.color) |
225 | | - batch.draw(self.shader) |
226 | | - |
227 | | - def add(self, shader, coords, color): |
228 | | - self.shader = shader |
229 | | - self.coords = coords |
230 | | - self.color = color |
231 | | - self.draw() |
232 | | - |
233 | | - |
234 | | -class DrawLineDashed(): |
235 | | - def __init__(self, *args, **kwargs): |
236 | | - super().__init__(*args, **kwargs) |
237 | | - self.shader = None |
238 | | - self.coords = None |
239 | | - self.color = None |
240 | | - self.arc_lengths = None |
241 | | - |
242 | | - def draw(self): |
243 | | - batch = batch_for_shader(self.shader, 'LINES', {"position": self.coords, "arcLength": self.arc_lengths}) |
244 | | - self.shader.bind() |
245 | | - matrix = bpy.context.region_data.perspective_matrix |
246 | | - self.shader.uniform_float("u_ViewProjectionMatrix", matrix) |
247 | | - self.shader.uniform_float("u_Scale", 50) |
248 | | - self.shader.uniform_float("u_Color", self.color) |
249 | | - batch.draw(self.shader) |
250 | | - |
251 | | - def add(self, shader, coords, color): |
252 | | - self.shader = shader |
253 | | - self.coords = coords |
254 | | - self.color = color |
255 | | - self.arc_lengths = [0] |
256 | | - for a, b in zip(self.coords[:-1], self.coords[1:]): |
257 | | - self.arc_lengths.append(self.arc_lengths[-1] + (a - b).length) |
258 | | - self.draw() |
259 | | - |
260 | | - |
261 | | -def draw_callback_3d(self, context): |
262 | | - if self.started and self.start_comp is not None: |
263 | | - gpu.state.point_size_set(self.prefs.point_size) |
264 | | - shader = gpu.shader.from_builtin(shader_type) |
265 | | - if self.end_comp is not None and self.end_comp != self.start_comp: |
266 | | - gpu.state.line_width_set(self.prefs.line_width) |
267 | | - if not self.multi_merge: |
268 | | - line_coords = [self.start_comp_transformed, self.end_comp_transformed] |
269 | | - else: |
270 | | - line_coords = [] |
271 | | - vert_coords = [] |
272 | | - if self.merge_location == 'CENTER': |
273 | | - vert_list = [v.co for v in self.start_sel] |
274 | | - if self.end_comp not in self.start_sel: |
275 | | - vert_list.append(self.end_comp.co) |
276 | | - for v in self.start_sel: |
277 | | - line_coords.append(self.world_matrix @ v.co) |
278 | | - line_coords.append(self.world_matrix @ find_center(vert_list)) |
279 | | - vert_coords.append(self.world_matrix @ v.co) |
280 | | - line_coords.append(self.end_comp_transformed) |
281 | | - line_coords.append(self.world_matrix @ find_center(vert_list)) |
282 | | - elif self.merge_location == 'LAST': |
283 | | - for v in self.start_sel: |
284 | | - line_coords.append(self.world_matrix @ v.co) |
285 | | - line_coords.append(self.end_comp_transformed) |
286 | | - vert_coords.append(self.world_matrix @ v.co) |
287 | | - elif self.merge_location == 'FIRST': |
288 | | - for v in self.start_sel: |
289 | | - line_coords.append(self.world_matrix @ v.co) |
290 | | - line_coords.append(self.start_comp_transformed) |
291 | | - vert_coords.append(self.world_matrix @ v.co) |
292 | | - line_coords.append(self.end_comp_transformed) |
293 | | - line_coords.append(self.start_comp_transformed) |
294 | | - |
295 | | - # Line that connects the start and end position (draw first so it's beneath the vertices) |
296 | | - if not self.multi_merge: |
297 | | - tool_line = DrawLine() |
298 | | - tool_line.add(shader, line_coords, self.prefs.line_color) |
299 | | - else: |
300 | | - shader_dashed = gpu.types.GPUShader(vertex_shader, fragment_shader) |
301 | | - tool_line = DrawLineDashed() |
302 | | - tool_line.add(shader_dashed, line_coords, self.prefs.line_color) |
303 | | - |
304 | | - # Ending edge |
305 | | - if self.sel_mode == 'EDGE': |
306 | | - gpu.state.line_width_set(self.prefs.edge_width) |
307 | | - e1v = [self.world_matrix @ v.co for v in self.end_comp.verts] |
308 | | - |
309 | | - end_edge = DrawLine() |
310 | | - if self.merge_location in ('FIRST', 'CENTER'): |
311 | | - end_edge.add(shader, e1v, self.prefs.start_color) |
312 | | - else: |
313 | | - end_edge.add(shader, e1v, self.prefs.end_color) |
314 | | - |
315 | | - # Ending point |
316 | | - end_point = DrawPoint() |
317 | | - if self.multi_merge: |
318 | | - end_point.add(shader, vert_coords, self.prefs.start_color) |
319 | | - if self.merge_location in ('FIRST', 'CENTER'): |
320 | | - end_point.add(shader, self.end_comp_transformed, self.prefs.start_color) |
321 | | - else: |
322 | | - end_point.add(shader, self.end_comp_transformed, self.prefs.end_color) |
323 | | - |
324 | | - # Middle point |
325 | | - if self.merge_location == 'CENTER': |
326 | | - if self.sel_mode == 'VERT': |
327 | | - if self.multi_merge: |
328 | | - midpoint = self.world_matrix @ find_center(vert_list) |
329 | | - else: |
330 | | - midpoint = self.world_matrix @ find_center([self.start_comp, self.end_comp]) |
331 | | - elif self.sel_mode == 'EDGE': |
332 | | - midpoint = self.world_matrix @ \ |
333 | | - find_center([find_center(self.start_comp), find_center(self.end_comp)]) |
334 | | - |
335 | | - mid_point = DrawPoint() |
336 | | - mid_point.add(shader, midpoint, self.prefs.end_color) |
337 | | - |
338 | | - # Starting edge |
339 | | - if self.sel_mode == 'EDGE': |
340 | | - gpu.state.line_width_set(self.prefs.edge_width) |
341 | | - e0v = [self.world_matrix @ v.co for v in self.start_comp.verts] |
342 | | - |
343 | | - start_edge = DrawLine() |
344 | | - if self.merge_location == 'FIRST': |
345 | | - start_edge.add(shader, e0v, self.prefs.end_color) |
346 | | - else: |
347 | | - start_edge.add(shader, e0v, self.prefs.start_color) |
348 | | - |
349 | | - # Starting point |
350 | | - start_point = DrawPoint() |
351 | | - if self.merge_location == 'FIRST': |
352 | | - start_point.add(shader, self.start_comp_transformed, self.prefs.end_color) |
353 | | - else: |
354 | | - start_point.add(shader, self.start_comp_transformed, self.prefs.start_color) |
355 | | - |
356 | | - gpu.state.line_width_set(1) |
357 | | - gpu.state.point_size_set(1) |
358 | | - |
359 | | - |
360 | | -def draw_callback_2d(self, context): |
361 | | - # Have to add 1 for some reason in order to get proper number of segments. |
362 | | - # This could potentially also be a ratio with the radius. |
363 | | - circ_segments = 8 + 1 |
364 | | - draw_circle_2d(self.m_coord, self.prefs.circ_color, self.prefs.circ_radius, segments=circ_segments) |
365 | | - |
366 | | - |
367 | | -def find_center(source): |
368 | | - """Assumes that the input is an Edge or an ordered object holding vertices or Vectors""" |
369 | | - coords = [] |
370 | | - if isinstance(source, bmesh.types.BMEdge): |
371 | | - coords = [source.verts[0].co, source.verts[1].co] |
372 | | - elif isinstance(source[0], bmesh.types.BMVert): |
373 | | - coords = [v.co for v in source] |
374 | | - elif isinstance(source[0], Vector): |
375 | | - coords = [v for v in source] |
376 | | - |
377 | | - offset = Vector((0.0, 0.0, 0.0)) |
378 | | - for v in coords: |
379 | | - offset = offset + v |
380 | | - return offset / len(coords) |
381 | | - |
382 | | - |
383 | | -def set_component(self, mode): |
384 | | - selected_comp = None |
385 | | - selected_comp = self.bm.select_history.active |
386 | | - |
387 | | - if selected_comp: |
388 | | - if mode == 'START': |
389 | | - self.start_comp = selected_comp # Set the start component |
390 | | - if self.sel_mode == 'VERT': |
391 | | - self.start_comp_transformed = self.world_matrix @ self.start_comp.co |
392 | | - elif self.sel_mode == 'EDGE': |
393 | | - self.start_comp_transformed = self.world_matrix @ find_center(self.start_comp) |
394 | | - if mode == 'END': |
395 | | - self.end_comp = selected_comp # Set the end component |
396 | | - if self.sel_mode == 'VERT': |
397 | | - self.end_comp_transformed = self.world_matrix @ self.end_comp.co |
398 | | - elif self.sel_mode == 'EDGE': |
399 | | - self.end_comp_transformed = self.world_matrix @ find_center(self.end_comp) |
400 | | - |
401 | | - |
402 | 159 | def main(self, context, event): |
403 | 160 | """Run this function on left mouse, execute the ray cast""" |
404 | 161 | self.m_coord = event.mouse_region_x, event.mouse_region_y |
@@ -535,6 +292,8 @@ def modal(self, context, event): |
535 | 292 | elif self.sel_mode == 'EDGE': |
536 | 293 | # Case of two fully separate edges |
537 | 294 | if not any([v for v in self.start_comp.verts if v in self.end_comp.verts]): |
| 295 | + # Bridge is a hack to let Blender deal with deciding |
| 296 | + # which vertices connect to each other so we don't have to |
538 | 297 | bridge = bmesh.ops.bridge_loops(self.bm, edges=(self.start_comp, self.end_comp)) |
539 | 298 | new_e0 = bridge['edges'][0] |
540 | 299 | new_e1 = bridge['edges'][1] |
@@ -628,6 +387,7 @@ def invoke(self, context, event): |
628 | 387 | self.world_matrix = bpy.context.object.matrix_world |
629 | 388 | self.bm = bmesh.from_edit_mesh(self.me) |
630 | 389 |
|
| 390 | + # Get starting selection, if any. |
631 | 391 | if self.sel_mode == 'VERT' and context.object.data.total_vert_sel > 1: |
632 | 392 | self.start_sel = [v for v in self.bm.verts if v.select] |
633 | 393 | elif self.sel_mode == 'EDGE' and context.object.data.total_edge_sel > 1: |
@@ -682,6 +442,8 @@ class WorkSpaceMergeTool(bpy.types.WorkSpaceTool): |
682 | 442 | bl_cursor = t_cursor |
683 | 443 | bl_widget = None |
684 | 444 | bl_keymap = ( |
| 445 | + ("mesh.merge_tool", {"ctrl": 1, "type": 'LEFTMOUSE', "value": 'PRESS'}, |
| 446 | + {"properties": [("merge_location", 'CENTER')]}), |
685 | 447 | ("mesh.merge_tool", {"type": 'LEFTMOUSE', "value": 'PRESS'}, |
686 | 448 | {"properties": [("wait_for_input", False)]}), |
687 | 449 | ) |
|
0 commit comments