Skip to content

Commit 9d5a44a

Browse files
committedMar 17, 2023
added quick mirror
1 parent fc8e94a commit 9d5a44a

File tree

5 files changed

+270
-38
lines changed

5 files changed

+270
-38
lines changed
 

‎__init__.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
from .ui import POLYBLOCKER_MT_pie, POLYBLOCKER_AP_preferences
1818
from .add_mesh import POLYBLOCKER_OT_add_mesh, POLYBLOCKER_OT_make_collection
1919
from .cap_tool import POLYBLOCKER_OT_cap_tool
20+
from .quick_mirror import POLYBLOCKER_OT_quick_mirror
2021

2122
bl_info = {
2223
"name": "PolyBlocker",
2324
"author": "Daniel Boxer",
24-
"description": "Enhanced add mesh menu and cap tool",
25+
"description": "Enhanced add mesh menu, cap tool, and quick mirror",
2526
"blender": (2, 80, 0),
26-
"version": (1, 1, 0),
27-
"location": "View3D > Ctrl Shift A/C",
27+
"version": (1, 2, 0),
28+
"location": "View3D > Ctrl Shift A/C, Alt M",
2829
"category": "Mesh",
2930
}
3031

@@ -34,6 +35,7 @@
3435
POLYBLOCKER_OT_add_mesh,
3536
POLYBLOCKER_OT_make_collection,
3637
POLYBLOCKER_OT_cap_tool,
38+
POLYBLOCKER_OT_quick_mirror,
3739
POLYBLOCKER_MT_pie,
3840
POLYBLOCKER_AP_preferences,
3941
)
@@ -56,6 +58,11 @@ def register():
5658
)
5759
keymaps.append((keymap, keymap_item))
5860

61+
keymap_item = keymap.keymap_items.new(
62+
"polyblocker.quick_mirror", type="M", value="PRESS", alt=True
63+
)
64+
keymaps.append((keymap, keymap_item))
65+
5966

6067
def unregister():
6168
for keymap, keymap_item in keymaps:

‎cap_tool.py

+20-22
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,9 @@ def falloff(segment_idx, max_val):
229229
f" Scale: {self.scale_fac:.2f} Flip Scale: {flip_scale_txt}"
230230
f" Control Length: {control_len_txt} Invert: {invert_txt}"
231231
)
232-
line_draw.remove()
233-
line_draw.add((tuple(self.init_mouse_pos), tuple(current_pos)), (0, 0, 0, 1))
232+
line_draw.draw_guide(
233+
(tuple(self.init_mouse_pos), tuple(current_pos)), (0, 0, 0, 1)
234+
)
234235

235236
def segment(self, segment_edge, old_verts):
236237
def walk(edge):
@@ -302,23 +303,20 @@ def select_first(self):
302303
f.select = True
303304

304305
def finish(self, context, revert=False):
305-
try:
306-
if revert:
307-
# delete new geometry
308-
bmesh.ops.delete(self.bm, geom=[v for loop in self.loops for v in loop])
309-
bmesh.ops.recalc_face_normals(self.bm, faces=self.origin_faces)
310-
for f in self.origin_faces:
311-
f.hide = False
312-
f.select = True
313-
for e in f.edges:
314-
e.hide = False
315-
else:
316-
# delete original faces
317-
bmesh.ops.delete(self.bm, geom=self.origin_faces, context="FACES")
318-
bmesh.update_edit_mesh(context.object.data)
319-
context.area.header_text_set(None)
320-
context.workspace.status_text_set(None)
321-
context.window.cursor_modal_restore()
322-
line_draw.remove()
323-
except Exception as e:
324-
self.report({"ERROR"}, f"Error cleaning up: {str(e)}")
306+
if revert:
307+
# delete new geometry
308+
bmesh.ops.delete(self.bm, geom=[v for loop in self.loops for v in loop])
309+
bmesh.ops.recalc_face_normals(self.bm, faces=self.origin_faces)
310+
for f in self.origin_faces:
311+
f.hide = False
312+
f.select = True
313+
for e in f.edges:
314+
e.hide = False
315+
else:
316+
# delete original faces
317+
bmesh.ops.delete(self.bm, geom=self.origin_faces, context="FACES")
318+
bmesh.update_edit_mesh(context.object.data)
319+
context.area.header_text_set(None)
320+
context.workspace.status_text_set(None)
321+
context.window.cursor_modal_restore()
322+
line_draw.remove("guide")

‎line_draw.py

+41-12
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,52 @@
1+
# Copyright (C) 2023 Daniel Boxer
2+
13
import bpy
24
import gpu
35
from gpu_extras.batch import batch_for_shader
46

57

6-
SHADER = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
7-
handles = []
8+
SHADER_2D = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
9+
SHADER_3D = gpu.shader.from_builtin("3D_UNIFORM_COLOR")
10+
COLOURS = {"X": (1, 0, 0, 1), "Y": (0, 1, 0, 1), "Z": (0, 0, 1, 1)}
11+
handles = {}
12+
batches = {}
13+
14+
15+
def make_batch(shader, coords):
16+
return batch_for_shader(shader, "LINES", {"pos": coords})
17+
18+
19+
def add_handle(name, callback, type):
20+
handles[name] = bpy.types.SpaceView3D.draw_handler_add(callback, (), "WINDOW", type)
21+
22+
23+
def change_colour(shader, colour):
24+
shader.uniform_float("color", colour)
25+
26+
27+
def draw_guide(coords, colour):
28+
def draw():
29+
change_colour(SHADER_2D, colour)
30+
make_batch(SHADER_2D, coords).draw(SHADER_2D)
31+
32+
remove("guide")
33+
add_handle("guide", draw, "POST_PIXEL")
834

935

10-
def add(coords, colour):
36+
def draw_axis(name, colour, coords=()):
1137
def draw():
12-
SHADER.uniform_float("color", colour)
13-
batch_for_shader(SHADER, "LINES", {"pos": coords}).draw(SHADER)
38+
change_colour(SHADER_3D, colour)
39+
# just change colour if no coords
40+
if coords:
41+
batches[name] = make_batch(SHADER_3D, coords)
42+
batches[name].draw(SHADER_3D)
1443

15-
handles.append(
16-
bpy.types.SpaceView3D.draw_handler_add(draw, (), "WINDOW", "POST_PIXEL")
17-
)
44+
if not coords:
45+
remove(name)
46+
add_handle(name, draw, "POST_VIEW")
1847

1948

20-
def remove():
21-
for handle in handles:
22-
bpy.types.SpaceView3D.draw_handler_remove(handle, "WINDOW")
23-
handles.clear()
49+
def remove(name):
50+
if name in handles:
51+
bpy.types.SpaceView3D.draw_handler_remove(handles[name], "WINDOW")
52+
del handles[name]

‎quick_mirror.py

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Copyright (C) 2023 Daniel Boxer
2+
3+
import bpy
4+
from mathutils import Vector, Matrix, geometry
5+
from bpy_extras import view3d_utils
6+
from . import line_draw
7+
8+
9+
class POLYBLOCKER_OT_quick_mirror(bpy.types.Operator):
10+
bl_idname = "polyblocker.quick_mirror"
11+
bl_label = "Quick Mirror"
12+
bl_description = "Quick mirror"
13+
bl_options = {"UNDO", "BLOCKING"}
14+
15+
axis_map = {0: "X", 1: "Y", 2: "Z"}
16+
17+
@classmethod
18+
def poll(cls, context):
19+
obj = context.object
20+
return obj is not None and obj.type == "MESH"
21+
22+
def invoke(self, context, event):
23+
self.input = []
24+
self.mirror_objs = []
25+
self.hover_axis = None
26+
self.holding = False
27+
self.empty = None
28+
29+
prefs = context.preferences.addons[__package__].preferences
30+
selected = context.selected_objects
31+
target = context.object
32+
is_single_obj = len(selected) == 1
33+
is_no_target = not context.object in selected
34+
if is_single_obj or is_no_target:
35+
if prefs.origin_method == "EMPTY":
36+
# make empty for mirror target
37+
self.empty = bpy.data.objects.new("Quick Mirror", None)
38+
target = self.empty
39+
context.scene.collection.objects.link(target)
40+
else:
41+
target = None
42+
for obj in selected:
43+
# set origin to 0
44+
mw = obj.matrix_world
45+
obj.data.transform(Matrix.Translation(-(mw.inverted() @ Vector())))
46+
mw.translation -= mw.translation
47+
48+
# setup mirror mod
49+
for obj in selected:
50+
if target != obj or is_single_obj:
51+
m = obj.modifiers.new("Mirror", "MIRROR")
52+
m.use_axis[0] = False
53+
m.mirror_object = target
54+
self.mirror_objs.append({"obj": obj, "mod": m})
55+
56+
# choose position of axis guide
57+
center_objs = [target]
58+
if is_single_obj:
59+
center_objs = [selected[0]]
60+
elif is_no_target:
61+
center_objs = selected
62+
center_sum = Vector()
63+
for obj in center_objs:
64+
local_center = sum((Vector(co) for co in obj.bound_box), Vector()) / 8
65+
center_sum += obj.matrix_world @ local_center
66+
center = center_sum / len(center_objs)
67+
68+
self.axis_lines = [None] * 3
69+
self.axis_lines_2d = [None] * 3
70+
for axis in range(3):
71+
# get point offset from center
72+
def axis_point(dist, direction):
73+
p = center.copy()
74+
p[axis] += dist * direction
75+
return p
76+
77+
# 10000 or any large number works
78+
self.axis_lines[axis] = (axis_point(10000, 1), axis_point(10000, -1))
79+
80+
r = context.region
81+
r3d = context.space_data.region_3d
82+
# use axis of length 1 for 2d to fit in viewport better
83+
l1_2d = view3d_utils.location_3d_to_region_2d(r, r3d, axis_point(1, 1))
84+
l2_2d = view3d_utils.location_3d_to_region_2d(r, r3d, axis_point(1, -1))
85+
if l1_2d is None or l2_2d is None:
86+
self.report({"ERROR"}, "Selected object is not visible")
87+
self.finish(context, revert=True)
88+
return {"CANCELLED"}
89+
self.axis_lines_2d[axis] = (l1_2d, l2_2d)
90+
91+
a_str = self.axis_map[axis]
92+
line_draw.draw_axis(a_str, line_draw.COLOURS[a_str], self.axis_lines[axis])
93+
94+
# redraw fixes bug with single obj
95+
self.redraw_v3d(context)
96+
context.window.cursor_modal_set("SCROLL_XY")
97+
context.area.header_text_set(f"Axes: [ ]")
98+
context.workspace.status_text_set(
99+
"Left Click/Hold: Select Axes and Confirm Right Click/Esc: Cancel"
100+
" Scroll Up: Remove Axis"
101+
)
102+
context.window_manager.modal_handler_add(self)
103+
return {"RUNNING_MODAL"}
104+
105+
def modal(self, context, event):
106+
try:
107+
if event.type == "MOUSEMOVE":
108+
m_pos = Vector((event.mouse_region_x, event.mouse_region_y))
109+
110+
# find closest axis to mouse
111+
min_dist = float("inf")
112+
axis = None
113+
for a in range(3):
114+
l1, l2 = self.axis_lines_2d[a]
115+
inter = geometry.intersect_point_line(m_pos, l1, l2)[0] - m_pos
116+
dist = inter.length
117+
if dist < min_dist:
118+
min_dist = dist
119+
axis = self.axis_map[a]
120+
121+
old_axis = self.hover_axis
122+
if old_axis != axis:
123+
# remove mirror preview or if dragging backwards
124+
if not self.holding or axis in self.input:
125+
self.remove(context)
126+
127+
line_draw.draw_axis(axis, (1, 1, 1, 1))
128+
self.redraw_v3d(context)
129+
self.add(context, axis)
130+
self.hover_axis = axis
131+
132+
elif event.type == "WHEELDOWNMOUSE" and event.value == "PRESS":
133+
self.remove(context)
134+
elif event.type == "LEFTMOUSE" and event.value == "PRESS":
135+
self.holding = True
136+
elif event.type == "LEFTMOUSE" and event.value == "RELEASE":
137+
self.finish(context)
138+
return {"FINISHED"}
139+
elif event.type in {"RIGHTMOUSE", "ESC"}:
140+
self.finish(context, revert=True)
141+
return {"FINISHED"}
142+
except Exception as e:
143+
self.report({"ERROR"}, f"Error: {str(e)}")
144+
self.finish(context, revert=True)
145+
return {"CANCELLED"}
146+
return {"RUNNING_MODAL"}
147+
148+
def update(self, context):
149+
axes = [
150+
True if "X" in self.input else False,
151+
True if "Y" in self.input else False,
152+
True if "Z" in self.input else False,
153+
]
154+
for m_obj in self.mirror_objs:
155+
m_obj["mod"].use_axis = axes
156+
s = f"{'X' if axes[0] else ''}{'Y' if axes[1] else ''}{'Z' if axes[2] else ''}"
157+
context.area.header_text_set(f"Axes: [ {s} ]")
158+
159+
def redraw_v3d(self, context):
160+
context.view_layer.objects.active = context.view_layer.objects.active
161+
162+
def add(self, context, axis):
163+
if axis not in self.input:
164+
self.input.append(axis)
165+
self.update(context)
166+
167+
def remove(self, context):
168+
if len(self.input) > 0:
169+
axis = self.input.pop()
170+
# set old axis colour
171+
line_draw.draw_axis(axis, line_draw.COLOURS[axis])
172+
self.update(context)
173+
174+
def finish(self, context, revert=False):
175+
if revert:
176+
for m_obj in self.mirror_objs:
177+
m_obj["obj"].modifiers.remove(m_obj["mod"])
178+
if self.empty is not None:
179+
bpy.data.objects.remove(self.empty, do_unlink=True)
180+
context.area.header_text_set(None)
181+
context.workspace.status_text_set(None)
182+
context.window.cursor_modal_restore()
183+
for axis in range(3):
184+
line_draw.remove(self.axis_map[axis])
185+
self.redraw_v3d(context)

‎ui.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,29 @@ def draw(self, context):
6363
class POLYBLOCKER_AP_preferences(bpy.types.AddonPreferences):
6464
bl_idname = __package__
6565

66+
# add mesh menu
6667
auto_coll: bpy.props.BoolProperty(
6768
name="Auto Collection",
6869
description="Automatically add meshes to new collections",
6970
)
7071
obj_number: bpy.props.IntProperty(
7172
name="Number", description="", min=1, max=10, default=3
7273
)
74+
# quick mirror
75+
origin_method: bpy.props.EnumProperty(
76+
items=[("EMPTY", "Empty", ""), ("ORIGIN", "Set Origin", "")]
77+
)
7378

7479
def draw(self, context):
75-
row = self.layout.row()
80+
layout = self.layout
81+
box = layout.box()
82+
box.label(text="Add Mesh Menu")
83+
row = box.row()
7684
row.prop(self, "auto_coll")
7785
if self.auto_coll:
7886
row.prop(self, "obj_number")
87+
box = layout.box()
88+
box.label(text="Quick Mirror")
89+
row = box.row()
90+
row.label(text="Origin Method")
91+
row.prop(self, "origin_method", expand=True)

0 commit comments

Comments
 (0)
Please sign in to comment.