Cleanup: remove "Tweak" event type
[blender-addons.git] / object_carver / carver_draw.py
blobc70007871e44a01f86eb852a2080101f45545dfe
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import bpy
4 import bgl
5 import blf
6 import bpy_extras
7 import numpy as np
8 import gpu
9 from gpu_extras.batch import batch_for_shader
10 from math import(
11 cos,
12 sin,
13 ceil,
14 floor,
17 from bpy_extras.view3d_utils import (
18 region_2d_to_location_3d,
19 location_3d_to_region_2d,
22 from .carver_utils import (
23 draw_circle,
24 draw_shader,
25 objDiagonal,
26 mini_grid,
29 from mathutils import (
30 Color,
31 Euler,
32 Vector,
33 Quaternion,
36 def get_text_info(self, context, help_txt):
37 """ Return the dimensions of each part of the text """
39 #Extract the longest first option in sublist
40 max_option = max(list(blf.dimensions(0, row[0])[0] for row in help_txt))
42 #Extract the longest key in sublist
43 max_key = max(list(blf.dimensions(0, row[1])[0] for row in help_txt))
45 #Space between option and key with a comma separator (" : ")
46 comma = blf.dimensions(0, "_:_")[0]
48 #Get a default height for all the letters
49 line_height = (blf.dimensions(0, "gM")[1] * 1.45)
51 #Get the total height of the text
52 bloc_height = 0
53 for row in help_txt:
54 bloc_height += line_height
56 return(help_txt, bloc_height, max_option, max_key, comma)
58 def draw_string(self, color1, color2, left, bottom, text, max_option, divide = 1):
59 """ Draw the text like 'option : key' or just 'option' """
61 font_id = 0
62 ui_scale = bpy.context.preferences.system.ui_scale
64 blf.enable(font_id,blf.SHADOW)
65 blf.shadow(font_id, 0, 0.0, 0.0, 0.0, 1.0)
66 blf.shadow_offset(font_id,2,-2)
67 line_height = (blf.dimensions(font_id, "gM")[1] * 1.45)
68 y_offset = 5
70 # Test if the text is a list formatted like : ('option', 'key')
71 if isinstance(text,list):
72 spacer_text = " : "
73 spacer_width = blf.dimensions(font_id, spacer_text)[0]
74 for string in text:
75 blf.position(font_id, (left), (bottom + y_offset), 0)
76 blf.color(font_id, *color1)
77 blf.draw(font_id, string[0])
78 blf.position(font_id, (left + max_option), (bottom + y_offset), 0)
79 blf.draw(font_id, spacer_text)
80 blf.color(font_id, *color2)
81 blf.position(font_id, (left + max_option + spacer_width), (bottom + y_offset), 0)
82 blf.draw(font_id, string[1])
83 y_offset += line_height
84 else:
85 # The text is formatted like : ('option')
86 blf.position(font_id, left, (bottom + y_offset), 0)
87 blf.color(font_id, *color1)
88 blf.draw(font_id, text)
89 y_offset += line_height
91 blf.disable(font_id,blf.SHADOW)
93 # Opengl draw on screen
94 def draw_callback_px(self, context):
95 font_id = 0
96 region = context.region
97 UIColor = (0.992, 0.5518, 0.0, 1.0)
99 # Cut Type
100 RECTANGLE = 0
101 LINE = 1
102 CIRCLE = 2
103 self.carver_prefs = context.preferences.addons[__package__].preferences
105 # Color
106 color1 = (1.0, 1.0, 1.0, 1.0)
107 color2 = UIColor
109 #The mouse is outside the active region
110 if not self.in_view_3d:
111 color1 = color2 = (1.0, 0.2, 0.1, 1.0)
113 # Primitives type
114 PrimitiveType = "Rectangle"
115 if self.CutType == CIRCLE:
116 PrimitiveType = "Circle"
117 if self.CutType == LINE:
118 PrimitiveType = "Line"
120 # Width screen
121 overlap = context.preferences.system.use_region_overlap
123 t_panel_width = 0
124 if overlap:
125 for region in context.area.regions:
126 if region.type == 'TOOLS':
127 t_panel_width = region.width
129 # Initial position
130 region_width = int(region.width / 2.0)
131 y_txt = 10
134 # Draw the center command from bottom to top
136 # Get the size of the text
137 text_size = 18 if region.width >= 850 else 12
138 ui_scale = bpy.context.preferences.system.ui_scale
139 blf.size(0, round(text_size * ui_scale), 72)
141 # Help Display
142 if (self.ObjectMode is False) and (self.ProfileMode is False):
144 # Depth Cursor
145 TypeStr = "Cursor Depth [" + self.carver_prefs.Key_Depth + "]"
146 BoolStr = "(ON)" if self.snapCursor else "(OFF)"
147 help_txt = [[TypeStr, BoolStr]]
149 # Close poygonal shape
150 if self.CreateMode and self.CutType == LINE:
151 TypeStr = "Close [" + self.carver_prefs.Key_Close + "]"
152 BoolStr = "(ON)" if self.Closed else "(OFF)"
153 help_txt += [[TypeStr, BoolStr]]
155 if self.CreateMode is False:
156 # Apply Booleans
157 TypeStr = "Apply Operations [" + self.carver_prefs.Key_Apply + "]"
158 BoolStr = "(OFF)" if self.dont_apply_boolean else "(ON)"
159 help_txt += [[TypeStr, BoolStr]]
161 #Auto update for bevel
162 TypeStr = "Bevel Update [" + self.carver_prefs.Key_Update + "]"
163 BoolStr = "(ON)" if self.Auto_BevelUpdate else "(OFF)"
164 help_txt += [[TypeStr, BoolStr]]
166 # Circle subdivisions
167 if self.CutType == CIRCLE:
168 TypeStr = "Subdivisions [" + self.carver_prefs.Key_Subrem + "][" + self.carver_prefs.Key_Subadd + "]"
169 BoolStr = str((int(360 / self.stepAngle[self.step])))
170 help_txt += [[TypeStr, BoolStr]]
172 if self.CreateMode:
173 help_txt += [["Type [Space]", PrimitiveType]]
174 else:
175 help_txt += [["Cut Type [Space]", PrimitiveType]]
177 else:
178 # Instantiate
179 TypeStr = "Instantiate [" + self.carver_prefs.Key_Instant + "]"
180 BoolStr = "(ON)" if self.Instantiate else "(OFF)"
181 help_txt = [[TypeStr, BoolStr]]
183 # Random rotation
184 if self.alt:
185 TypeStr = "Random Rotation [" + self.carver_prefs.Key_Randrot + "]"
186 BoolStr = "(ON)" if self.RandomRotation else "(OFF)"
187 help_txt += [[TypeStr, BoolStr]]
189 # Thickness
190 if self.BrushSolidify:
191 TypeStr = "Thickness [" + self.carver_prefs.Key_Depth + "]"
192 if self.ProfileMode:
193 BoolStr = str(round(self.ProfileBrush.modifiers["CT_SOLIDIFY"].thickness, 2))
194 if self.ObjectMode:
195 BoolStr = str(round(self.ObjectBrush.modifiers["CT_SOLIDIFY"].thickness, 2))
196 help_txt += [[TypeStr, BoolStr]]
198 # Brush depth
199 if (self.ObjectMode):
200 TypeStr = "Carve Depth [" + self.carver_prefs.Key_Depth + "]"
201 BoolStr = str(round(self.ObjectBrush.data.vertices[0].co.z, 2))
202 help_txt += [[TypeStr, BoolStr]]
204 TypeStr = "Brush Depth [" + self.carver_prefs.Key_BrushDepth + "]"
205 BoolStr = str(round(self.BrushDepthOffset, 2))
206 help_txt += [[TypeStr, BoolStr]]
208 help_txt, bloc_height, max_option, max_key, comma = get_text_info(self, context, help_txt)
209 xCmd = region_width - (max_option + max_key + comma) / 2
210 draw_string(self, color1, color2, xCmd, y_txt, help_txt, max_option, divide = 2)
213 # Separator (Line)
214 LineWidth = (max_option + max_key + comma) / 2
215 if region.width >= 850:
216 LineWidth = 140
218 LineWidth = (max_option + max_key + comma)
219 coords = [(int(region_width - LineWidth/2), y_txt + bloc_height + 8), \
220 (int(region_width + LineWidth/2), y_txt + bloc_height + 8)]
221 draw_shader(self, UIColor, 1, 'LINES', coords, self.carver_prefs.LineWidth)
223 # Command Display
224 if self.CreateMode and ((self.ObjectMode is False) and (self.ProfileMode is False)):
225 BooleanMode = "Create"
226 else:
227 if self.ObjectMode or self.ProfileMode:
228 BooleanType = "Difference) [T]" if self.BoolOps == self.difference else "Union) [T]"
229 BooleanMode = \
230 "Object Brush (" + BooleanType if self.ObjectMode else "Profil Brush (" + BooleanType
231 else:
232 BooleanMode = \
233 "Difference" if (self.shift is False) and (self.ForceRebool is False) else "Rebool"
235 # Display boolean mode
236 text_size = 40 if region.width >= 850 else 20
237 blf.size(0, round(text_size * ui_scale), 72)
239 draw_string(self, color2, color2, region_width - (blf.dimensions(0, BooleanMode)[0]) / 2, \
240 y_txt + bloc_height + 16, BooleanMode, 0, divide = 2)
242 if region.width >= 850:
244 if self.AskHelp is False:
245 # "H for Help" text
246 blf.size(0, round(13 * ui_scale), 72)
247 help_txt = "[" + self.carver_prefs.Key_Help + "] for help"
248 txt_width = blf.dimensions(0, help_txt)[0]
249 txt_height = (blf.dimensions(0, "gM")[1] * 1.45)
251 # Draw a rectangle and put the text "H for Help"
252 xrect = 40
253 yrect = 40
254 rect_vertices = [(xrect - 5, yrect - 5), (xrect + txt_width + 5, yrect - 5), \
255 (xrect + txt_width + 5, yrect + txt_height + 5), (xrect - 5, yrect + txt_height + 5)]
256 draw_shader(self, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', rect_vertices, self.carver_prefs.LineWidth)
257 draw_string(self, color1, color2, xrect, yrect, help_txt, 0)
259 else:
260 #Draw the help text
261 xHelp = 30 + t_panel_width
262 yHelp = 10
264 if self.ObjectMode or self.ProfileMode:
265 if self.ProfileMode:
266 help_txt = [["Object Mode", self.carver_prefs.Key_Brush]]
267 else:
268 help_txt = [["Cut Mode", self.carver_prefs.Key_Brush]]
270 else:
271 help_txt =[
272 ["Profil Brush", self.carver_prefs.Key_Brush],\
273 ["Move Cursor", "Ctrl + LMB"]
276 if (self.ObjectMode is False) and (self.ProfileMode is False):
277 if self.CreateMode is False:
278 help_txt +=[
279 ["Create geometry", self.carver_prefs.Key_Create],\
281 else:
282 help_txt +=[
283 ["Cut", self.carver_prefs.Key_Create],\
285 if self.CutMode == RECTANGLE:
286 help_txt +=[
287 ["Dimension", "MouseMove"],\
288 ["Move all", "Alt"],\
289 ["Validate", "LMB"],\
290 ["Rebool", "Shift"]
293 elif self.CutMode == CIRCLE:
294 help_txt +=[
295 ["Rotation and Radius", "MouseMove"],\
296 ["Move all", "Alt"],\
297 ["Subdivision", self.carver_prefs.Key_Subrem + " " + self.carver_prefs.Key_Subadd],\
298 ["Incremental rotation", "Ctrl"],\
299 ["Rebool", "Shift"]
302 elif self.CutMode == LINE:
303 help_txt +=[
304 ["Dimension", "MouseMove"],\
305 ["Move all", "Alt"],\
306 ["Validate", "Space"],\
307 ["Rebool", "Shift"],\
308 ["Snap", "Ctrl"],\
309 ["Scale Snap", "WheelMouse"],\
311 else:
312 # ObjectMode
313 help_txt +=[
314 ["Difference", "Space"],\
315 ["Rebool", "Shift + Space"],\
316 ["Duplicate", "Alt + Space"],\
317 ["Scale", self.carver_prefs.Key_Scale],\
318 ["Rotation", "LMB + Move"],\
319 ["Step Angle", "CTRL + LMB + Move"],\
322 if self.ProfileMode:
323 help_txt +=[["Previous or Next Profile", self.carver_prefs.Key_Subadd + " " + self.carver_prefs.Key_Subrem]]
325 help_txt +=[
326 ["Create / Delete rows", chr(8597)],\
327 ["Create / Delete cols", chr(8596)],\
328 ["Gap for rows or columns", self.carver_prefs.Key_Gapy + " " + self.carver_prefs.Key_Gapx]
331 blf.size(0, round(15 * ui_scale), 72)
332 help_txt, bloc_height, max_option, max_key, comma = get_text_info(self, context, help_txt)
333 draw_string(self, color1, color2, xHelp, yHelp, help_txt, max_option)
335 if self.ProfileMode:
336 xrect = region.width - t_panel_width - 80
337 yrect = 80
338 coords = [(xrect, yrect), (xrect+60, yrect), (xrect+60, yrect-60), (xrect, yrect-60)]
340 # Draw rectangle background in the lower right
341 draw_shader(self, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', coords, size=self.carver_prefs.LineWidth)
343 # Use numpy to get the vertices and indices of the profile object to draw
344 WidthProfil = 50
345 location = Vector((region.width - t_panel_width - WidthProfil, 50, 0))
346 ProfilScale = 20.0
347 coords = []
348 mesh = bpy.data.meshes[self.Profils[self.nProfil][0]]
349 mesh.calc_loop_triangles()
350 vertices = np.empty((len(mesh.vertices), 3), 'f')
351 indices = np.empty((len(mesh.loop_triangles), 3), 'i')
352 mesh.vertices.foreach_get("co", np.reshape(vertices, len(mesh.vertices) * 3))
353 mesh.loop_triangles.foreach_get("vertices", np.reshape(indices, len(mesh.loop_triangles) * 3))
355 for idx, vals in enumerate(vertices):
356 coords.append([
357 vals[0] * ProfilScale + location.x,
358 vals[1] * ProfilScale + location.y,
359 vals[2] * ProfilScale + location.z
362 #Draw the silhouette of the mesh
363 draw_shader(self, UIColor, 0.5, 'TRIS', coords, size=self.carver_prefs.LineWidth, indices=indices)
366 if self.CutMode:
368 if len(self.mouse_path) > 1:
369 x0 = self.mouse_path[0][0]
370 y0 = self.mouse_path[0][1]
371 x1 = self.mouse_path[1][0]
372 y1 = self.mouse_path[1][1]
374 # Cut rectangle
375 if self.CutType == RECTANGLE:
376 coords = [
377 (x0 + self.xpos, y0 + self.ypos), (x1 + self.xpos, y0 + self.ypos), \
378 (x1 + self.xpos, y1 + self.ypos), (x0 + self.xpos, y1 + self.ypos)
380 indices = ((0, 1, 2), (2, 0, 3))
382 self.rectangle_coord = coords
384 draw_shader(self, UIColor, 1, 'LINE_LOOP', coords, size=self.carver_prefs.LineWidth)
386 #Draw points
387 draw_shader(self, UIColor, 1, 'POINTS', coords, size=3)
389 if self.shift or self.CreateMode:
390 draw_shader(self, UIColor, 0.5, 'TRIS', coords, size=self.carver_prefs.LineWidth, indices=indices)
392 # Draw grid (based on the overlay options) to show the incremental snapping
393 if self.ctrl:
394 mini_grid(self, context, UIColor)
396 # Cut Line
397 elif self.CutType == LINE:
398 coords = []
399 indices = []
400 top_grid = False
402 for idx, vals in enumerate(self.mouse_path):
403 coords.append([vals[0] + self.xpos, vals[1] + self.ypos])
404 indices.append([idx])
406 # Draw lines
407 if self.Closed:
408 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', coords, size=self.carver_prefs.LineWidth)
409 else:
410 draw_shader(self, UIColor, 1.0, 'LINE_STRIP', coords, size=self.carver_prefs.LineWidth)
412 # Draw points
413 draw_shader(self, UIColor, 1.0, 'POINTS', coords, size=3)
415 # Draw polygon
416 if (self.shift) or (self.CreateMode and self.Closed):
417 draw_shader(self, UIColor, 0.5, 'TRI_FAN', coords, size=self.carver_prefs.LineWidth)
419 # Draw grid (based on the overlay options) to show the incremental snapping
420 if self.ctrl:
421 mini_grid(self, context, UIColor)
423 # Circle Cut
424 elif self.CutType == CIRCLE:
425 # Create a circle using a tri fan
426 tris_coords, indices = draw_circle(self, x0, y0)
428 # Remove the vertex in the center to get the outer line of the circle
429 line_coords = tris_coords[1:]
430 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', line_coords, size=self.carver_prefs.LineWidth)
432 if self.shift or self.CreateMode:
433 draw_shader(self, UIColor, 0.5, 'TRIS', tris_coords, size=self.carver_prefs.LineWidth, indices=indices)
435 if (self.ObjectMode or self.ProfileMode) and len(self.CurrentSelection) > 0:
436 if self.ShowCursor:
437 region = context.region
438 rv3d = context.space_data.region_3d
440 if self.ObjectMode:
441 ob = self.ObjectBrush
442 if self.ProfileMode:
443 ob = self.ProfileBrush
444 mat = ob.matrix_world
446 # 50% alpha, 2 pixel width line
447 bgl.glEnable(bgl.GL_BLEND)
449 bbox = [mat @ Vector(b) for b in ob.bound_box]
450 objBBDiagonal = objDiagonal(self.CurrentSelection[0])
452 if self.shift:
453 gl_size = 4
454 UIColor = (0.5, 1.0, 0.0, 1.0)
455 else:
456 gl_size = 2
457 UIColor = (1.0, 0.8, 0.0, 1.0)
459 line_coords = []
460 idx = 0
461 CRadius = ((bbox[7] - bbox[0]).length) / 2
462 for i in range(int(len(self.CLR_C) / 3)):
463 vector3d = (self.CLR_C[idx * 3] * CRadius + self.CurLoc.x, \
464 self.CLR_C[idx * 3 + 1] * CRadius + self.CurLoc.y, \
465 self.CLR_C[idx * 3 + 2] * CRadius + self.CurLoc.z)
466 vector2d = bpy_extras.view3d_utils.location_3d_to_region_2d(region, rv3d, vector3d)
467 if vector2d is not None:
468 line_coords.append((vector2d[0], vector2d[1]))
469 idx += 1
470 if len(line_coords) > 0 :
471 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', line_coords, size=gl_size)
473 # Object display
474 if self.quat_rot is not None:
475 ob.location = self.CurLoc
476 v = Vector()
477 v.x = v.y = 0.0
478 v.z = self.BrushDepthOffset
479 ob.location += self.quat_rot @ v
481 e = Euler()
482 e.x = 0.0
483 e.y = 0.0
484 e.z = self.aRotZ / 25.0
486 qe = e.to_quaternion()
487 qRot = self.quat_rot @ qe
488 ob.rotation_mode = 'QUATERNION'
489 ob.rotation_quaternion = qRot
490 ob.rotation_mode = 'XYZ'
492 if self.ProfileMode:
493 if self.ProfileBrush is not None:
494 self.ProfileBrush.location = self.CurLoc
495 self.ProfileBrush.rotation_mode = 'QUATERNION'
496 self.ProfileBrush.rotation_quaternion = qRot
497 self.ProfileBrush.rotation_mode = 'XYZ'
499 # Opengl defaults
500 bgl.glLineWidth(1)
501 bgl.glDisable(bgl.GL_BLEND)