Cleanup: use staticmethod where appropriate
[blender.git] / release / scripts / templates_py / gizmo_operator.py
1 # Example of an operator which uses gizmos to control its properties.
2 #
3 # Usage: Run this script, then in mesh edit-mode press F3
4 # to activate the operator "Select Side of Plane"
5 # The gizmos can then be used to adjust the plane in the 3D view.
6 #
7 import bpy
8 import bmesh
9
10 from bpy.types import (
11     Operator,
12     GizmoGroup,
13 )
14
15 from bpy.props import (
16     FloatVectorProperty,
17 )
18
19
20 def main(context, plane_co, plane_no):
21     obj = context.active_object
22     matrix = obj.matrix_world.copy()
23     me = obj.data
24     bm = bmesh.from_edit_mesh(me)
25
26     plane_dot = plane_no.dot(plane_co)
27
28     for v in bm.verts:
29         co = matrix @ v.co
30         v.select = (plane_no.dot(co) > plane_dot)
31     bm.select_flush_mode()
32
33     bmesh.update_edit_mesh(me)
34
35
36 class SelectSideOfPlane(Operator):
37     """UV Operator description"""
38     bl_idname = "mesh.select_side_of_plane"
39     bl_label = "Select Side of Plane"
40     bl_options = {'REGISTER', 'UNDO'}
41
42     plane_co: FloatVectorProperty(
43         size=3,
44         default=(0, 0, 0),
45     )
46     plane_no: FloatVectorProperty(
47         size=3,
48         default=(0, 0, 1),
49     )
50
51     @classmethod
52     def poll(cls, context):
53         return (context.mode == 'EDIT_MESH')
54
55     def invoke(self, context, event):
56
57         if not self.properties.is_property_set("plane_co"):
58             self.plane_co = context.scene.cursor.location
59
60         if not self.properties.is_property_set("plane_no"):
61             if context.space_data.type == 'VIEW_3D':
62                 rv3d = context.space_data.region_3d
63                 view_inv = rv3d.view_matrix.to_3x3()
64                 # view y axis
65                 self.plane_no = view_inv[1].normalized()
66
67         self.execute(context)
68
69         if context.space_data.type == 'VIEW_3D':
70             wm = context.window_manager
71             wm.gizmo_group_type_ensure(SelectSideOfPlaneGizmoGroup.bl_idname)
72
73         return {'FINISHED'}
74
75     def execute(self, context):
76         from mathutils import Vector
77         main(context, Vector(self.plane_co), Vector(self.plane_no))
78         return {'FINISHED'}
79
80
81 # Gizmos for plane_co, plane_no
82 class SelectSideOfPlaneGizmoGroup(GizmoGroup):
83     bl_idname = "MESH_GGT_select_side_of_plane"
84     bl_label = "Side of Plane Gizmo"
85     bl_space_type = 'VIEW_3D'
86     bl_region_type = 'WINDOW'
87     bl_options = {'3D'}
88
89     # Helper functions
90     @staticmethod
91     def my_target_operator(context):
92         wm = context.window_manager
93         op = wm.operators[-1] if wm.operators else None
94         if isinstance(op, SelectSideOfPlane):
95             return op
96         return None
97
98     @staticmethod
99     def my_view_orientation(context):
100         rv3d = context.space_data.region_3d
101         view_inv = rv3d.view_matrix.to_3x3()
102         return view_inv.normalized()
103
104     @classmethod
105     def poll(cls, context):
106         op = cls.my_target_operator(context)
107         if op is None:
108             wm = context.window_manager
109             wm.gizmo_group_type_unlink_delayed(SelectSideOfPlaneGizmoGroup.bl_idname)
110             return False
111         return True
112
113     def setup(self, context):
114         from mathutils import Matrix, Vector
115
116         # ----
117         # Move
118
119         def move_get_cb():
120             op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
121             return op.plane_co
122
123         def move_set_cb(value):
124             op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
125             op.plane_co = value
126             # XXX, this may change!
127             op.execute(context)
128
129         mpr = self.gizmos.new("GIZMO_GT_move_3d")
130         mpr.target_set_handler("offset", get=move_get_cb, set=move_set_cb)
131
132         mpr.use_draw_value = True
133
134         mpr.color = 0.8, 0.8, 0.8
135         mpr.alpha = 0.5
136
137         mpr.color_highlight = 1.0, 1.0, 1.0
138         mpr.alpha_highlight = 1.0
139
140         mpr.scale_basis = 0.2
141
142         self.widget_move = mpr
143
144         # ----
145         # Dial
146
147         def direction_get_cb():
148             op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
149
150             no_a = self.widget_dial.matrix_basis.col[1].xyz
151             no_b = Vector(op.plane_no)
152
153             no_a = (no_a @ self.view_inv).xy.normalized()
154             no_b = (no_b @ self.view_inv).xy.normalized()
155             return no_a.angle_signed(no_b)
156
157         def direction_set_cb(value):
158             op = SelectSideOfPlaneGizmoGroup.my_target_operator(context)
159             matrix_rotate = Matrix.Rotation(-value, 3, self.rotate_axis)
160             no = matrix_rotate @ self.widget_dial.matrix_basis.col[1].xyz
161             op.plane_no = no
162             op.execute(context)
163
164         mpr = self.gizmos.new("GIZMO_GT_dial_3d")
165         mpr.target_set_handler("offset", get=direction_get_cb, set=direction_set_cb)
166         mpr.draw_options = {'ANGLE_START_Y'}
167
168         mpr.use_draw_value = True
169
170         mpr.color = 0.8, 0.8, 0.8
171         mpr.alpha = 0.5
172
173         mpr.color_highlight = 1.0, 1.0, 1.0
174         mpr.alpha_highlight = 1.0
175
176         self.widget_dial = mpr
177
178     def draw_prepare(self, context):
179         from mathutils import Vector
180
181         view_inv = self.my_view_orientation(context)
182
183         self.view_inv = view_inv
184         self.rotate_axis = view_inv[2].xyz
185         self.rotate_up = view_inv[1].xyz
186
187         op = self.my_target_operator(context)
188
189         co = Vector(op.plane_co)
190         no = Vector(op.plane_no).normalized()
191
192         # Move
193         no_z = no
194         no_y = no_z.orthogonal()
195         no_x = no_z.cross(no_y)
196
197         matrix = self.widget_move.matrix_basis
198         matrix.identity()
199         matrix.col[0].xyz = no_x
200         matrix.col[1].xyz = no_y
201         matrix.col[2].xyz = no_z
202         matrix.col[3].xyz = co
203
204         # Dial
205         no_z = self.rotate_axis
206         no_y = (no - (no.project(no_z))).normalized()
207         no_x = self.rotate_axis.cross(no_y)
208
209         matrix = self.widget_dial.matrix_basis
210         matrix.identity()
211         matrix.col[0].xyz = no_x
212         matrix.col[1].xyz = no_y
213         matrix.col[2].xyz = no_z
214         matrix.col[3].xyz = co
215
216
217 classes = (
218     SelectSideOfPlane,
219     SelectSideOfPlaneGizmoGroup,
220 )
221
222
223 def register():
224     for cls in classes:
225         bpy.utils.register_class(cls)
226
227
228 def unregister():
229     for cls in reversed(classes):
230         bpy.utils.unregister_class(cls)
231
232
233 if __name__ == "__main__":
234     register()