Cleanup: tabs -> spaces
[blender-addons.git] / object_carver / carver_draw.py
blobf2a3aadc7831d532afd35d37011666ede5b2a7f9
1 import bpy
2 import bgl
3 import blf
4 import bpy_extras
5 import numpy as np
6 import gpu
7 from gpu_extras.batch import batch_for_shader
8 from math import(
9 cos,
10 sin,
11 ceil,
12 floor,
15 from bpy_extras.view3d_utils import (
16 region_2d_to_location_3d,
17 location_3d_to_region_2d,
20 from .carver_utils import (
21 draw_circle,
22 draw_shader,
23 objDiagonal,
24 mini_grid,
27 from mathutils import (
28 Color,
29 Euler,
30 Vector,
31 Quaternion,
34 def get_text_info(self, context, help_txt):
35 """ Return the dimensions of each part of the text """
37 #Extract the longest first option in sublist
38 max_option = max(list(blf.dimensions(0, row[0])[0] for row in help_txt))
40 #Extract the longest key in sublist
41 max_key = max(list(blf.dimensions(0, row[1])[0] for row in help_txt))
43 #Space between option and key with a comma separator (" : ")
44 comma = blf.dimensions(0, "_:_")[0]
46 #Get a default height for all the letters
47 line_height = (blf.dimensions(0, "gM")[1] * 1.45)
49 #Get the total height of the text
50 bloc_height = 0
51 for row in help_txt:
52 bloc_height += line_height
54 return(help_txt, bloc_height, max_option, max_key, comma)
56 def draw_string(self, color1, color2, left, bottom, text, max_option, divide = 1):
57 """ Draw the text like 'option : key' or just 'option' """
59 font_id = 0
60 ui_scale = bpy.context.preferences.system.ui_scale
62 blf.enable(font_id,blf.SHADOW)
63 blf.shadow(font_id, 0, 0.0, 0.0, 0.0, 1.0)
64 blf.shadow_offset(font_id,2,-2)
65 line_height = (blf.dimensions(font_id, "gM")[1] * 1.45)
66 y_offset = 5
68 # Test if the text is a list formatted like : ('option', 'key')
69 if isinstance(text,list):
70 spacer_text = " : "
71 spacer_width = blf.dimensions(font_id, spacer_text)[0]
72 for string in text:
73 blf.position(font_id, (left), (bottom + y_offset), 0)
74 blf.color(font_id, *color1)
75 blf.draw(font_id, string[0])
76 blf.position(font_id, (left + max_option), (bottom + y_offset), 0)
77 blf.draw(font_id, spacer_text)
78 blf.color(font_id, *color2)
79 blf.position(font_id, (left + max_option + spacer_width), (bottom + y_offset), 0)
80 blf.draw(font_id, string[1])
81 y_offset += line_height
82 else:
83 # The text is formatted like : ('option')
84 blf.position(font_id, left, (bottom + y_offset), 0)
85 blf.color(font_id, *color1)
86 blf.draw(font_id, text)
87 y_offset += line_height
89 blf.disable(font_id,blf.SHADOW)
91 # Opengl draw on screen
92 def draw_callback_px(self, context):
93 font_id = 0
94 region = context.region
95 UIColor = (0.992, 0.5518, 0.0, 1.0)
97 # Cut Type
98 RECTANGLE = 0
99 LINE = 1
100 CIRCLE = 2
101 self.carver_prefs = context.preferences.addons[__package__].preferences
103 # Color
104 color1 = (1.0, 1.0, 1.0, 1.0)
105 color2 = UIColor
107 #The mouse is outside the active region
108 if not self.in_view_3d:
109 color1 = color2 = (1.0, 0.2, 0.1, 1.0)
111 # Primitives type
112 PrimitiveType = "Rectangle"
113 if self.CutType == CIRCLE:
114 PrimitiveType = "Circle"
115 if self.CutType == LINE:
116 PrimitiveType = "Line"
118 # Width screen
119 overlap = context.preferences.system.use_region_overlap
121 t_panel_width = 0
122 if overlap:
123 for region in context.area.regions:
124 if region.type == 'TOOLS':
125 t_panel_width = region.width
127 # Initial position
128 region_width = int(region.width / 2.0)
129 y_txt = 10
132 # Draw the center command from bottom to top
134 # Get the size of the text
135 text_size = 18 if region.width >= 850 else 12
136 ui_scale = bpy.context.preferences.system.ui_scale
137 blf.size(0, round(text_size * ui_scale), 72)
139 # Help Display
140 if (self.ObjectMode is False) and (self.ProfileMode is False):
142 # Depth Cursor
143 TypeStr = "Cursor Depth [" + self.carver_prefs.Key_Depth + "]"
144 BoolStr = "(ON)" if self.snapCursor else "(OFF)"
145 help_txt = [[TypeStr, BoolStr]]
147 # Close poygonal shape
148 if self.CreateMode and self.CutType == LINE:
149 TypeStr = "Close [" + self.carver_prefs.Key_Close + "]"
150 BoolStr = "(ON)" if self.Closed else "(OFF)"
151 help_txt += [[TypeStr, BoolStr]]
153 if self.CreateMode is False:
154 # Apply Booleans
155 TypeStr = "Apply Operations [" + self.carver_prefs.Key_Apply + "]"
156 BoolStr = "(OFF)" if self.dont_apply_boolean else "(ON)"
157 help_txt += [[TypeStr, BoolStr]]
159 #Auto update for bevel
160 TypeStr = "Bevel Update [" + self.carver_prefs.Key_Update + "]"
161 BoolStr = "(ON)" if self.Auto_BevelUpdate else "(OFF)"
162 help_txt += [[TypeStr, BoolStr]]
164 # Circle subdivisions
165 if self.CutType == CIRCLE:
166 TypeStr = "Subdivisions [" + self.carver_prefs.Key_Subrem + "][" + self.carver_prefs.Key_Subadd + "]"
167 BoolStr = str((int(360 / self.stepAngle[self.step])))
168 help_txt += [[TypeStr, BoolStr]]
170 if self.CreateMode:
171 help_txt += [["Type [Space]", PrimitiveType]]
172 else:
173 help_txt += [["Cut Type [Space]", PrimitiveType]]
175 else:
176 # Instantiate
177 TypeStr = "Instantiate [" + self.carver_prefs.Key_Instant + "]"
178 BoolStr = "(ON)" if self.Instantiate else "(OFF)"
179 help_txt = [[TypeStr, BoolStr]]
181 # Random rotation
182 if self.alt:
183 TypeStr = "Random Rotation [" + self.carver_prefs.Key_Randrot + "]"
184 BoolStr = "(ON)" if self.RandomRotation else "(OFF)"
185 help_txt += [[TypeStr, BoolStr]]
187 # Thickness
188 if self.BrushSolidify:
189 TypeStr = "Thickness [" + self.carver_prefs.Key_Depth + "]"
190 if self.ProfileMode:
191 BoolStr = str(round(self.ProfileBrush.modifiers["CT_SOLIDIFY"].thickness, 2))
192 if self.ObjectMode:
193 BoolStr = str(round(self.ObjectBrush.modifiers["CT_SOLIDIFY"].thickness, 2))
194 help_txt += [[TypeStr, BoolStr]]
196 # Brush depth
197 if (self.ObjectMode):
198 TypeStr = "Carve Depth [" + self.carver_prefs.Key_Depth + "]"
199 BoolStr = str(round(self.ObjectBrush.data.vertices[0].co.z, 2))
200 help_txt += [[TypeStr, BoolStr]]
202 TypeStr = "Brush Depth [" + self.carver_prefs.Key_BrushDepth + "]"
203 BoolStr = str(round(self.BrushDepthOffset, 2))
204 help_txt += [[TypeStr, BoolStr]]
206 help_txt, bloc_height, max_option, max_key, comma = get_text_info(self, context, help_txt)
207 xCmd = region_width - (max_option + max_key + comma) / 2
208 draw_string(self, color1, color2, xCmd, y_txt, help_txt, max_option, divide = 2)
211 # Separator (Line)
212 LineWidth = (max_option + max_key + comma) / 2
213 if region.width >= 850:
214 LineWidth = 140
216 LineWidth = (max_option + max_key + comma)
217 coords = [(int(region_width - LineWidth/2), y_txt + bloc_height + 8), \
218 (int(region_width + LineWidth/2), y_txt + bloc_height + 8)]
219 draw_shader(self, UIColor, 1, 'LINES', coords, self.carver_prefs.LineWidth)
221 # Command Display
222 if self.CreateMode and ((self.ObjectMode is False) and (self.ProfileMode is False)):
223 BooleanMode = "Create"
224 else:
225 if self.ObjectMode or self.ProfileMode:
226 BooleanType = "Difference) [T]" if self.BoolOps == self.difference else "Union) [T]"
227 BooleanMode = \
228 "Object Brush (" + BooleanType if self.ObjectMode else "Profil Brush (" + BooleanType
229 else:
230 BooleanMode = \
231 "Difference" if (self.shift is False) and (self.ForceRebool is False) else "Rebool"
233 # Display boolean mode
234 text_size = 40 if region.width >= 850 else 20
235 blf.size(0, round(text_size * ui_scale), 72)
237 draw_string(self, color2, color2, region_width - (blf.dimensions(0, BooleanMode)[0]) / 2, \
238 y_txt + bloc_height + 16, BooleanMode, 0, divide = 2)
240 if region.width >= 850:
242 if self.AskHelp is False:
243 # "H for Help" text
244 blf.size(0, round(13 * ui_scale), 72)
245 help_txt = "[" + self.carver_prefs.Key_Help + "] for help"
246 txt_width = blf.dimensions(0, help_txt)[0]
247 txt_height = (blf.dimensions(0, "gM")[1] * 1.45)
249 # Draw a rectangle and put the text "H for Help"
250 xrect = 40
251 yrect = 40
252 rect_vertices = [(xrect - 5, yrect - 5), (xrect + txt_width + 5, yrect - 5), \
253 (xrect + txt_width + 5, yrect + txt_height + 5), (xrect - 5, yrect + txt_height + 5)]
254 draw_shader(self, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', rect_vertices, self.carver_prefs.LineWidth)
255 draw_string(self, color1, color2, xrect, yrect, help_txt, 0)
257 else:
258 #Draw the help text
259 xHelp = 30 + t_panel_width
260 yHelp = 10
262 if self.ObjectMode or self.ProfileMode:
263 if self.ProfileMode:
264 help_txt = [["Object Mode", self.carver_prefs.Key_Brush]]
265 else:
266 help_txt = [["Cut Mode", self.carver_prefs.Key_Brush]]
268 else:
269 help_txt =[
270 ["Profil Brush", self.carver_prefs.Key_Brush],\
271 ["Move Cursor", "Ctrl + LMB"]
274 if (self.ObjectMode is False) and (self.ProfileMode is False):
275 if self.CreateMode is False:
276 help_txt +=[
277 ["Create geometry", self.carver_prefs.Key_Create],\
279 else:
280 help_txt +=[
281 ["Cut", self.carver_prefs.Key_Create],\
283 if self.CutMode == RECTANGLE:
284 help_txt +=[
285 ["Dimension", "MouseMove"],\
286 ["Move all", "Alt"],\
287 ["Validate", "LMB"],\
288 ["Rebool", "Shift"]
291 elif self.CutMode == CIRCLE:
292 help_txt +=[
293 ["Rotation and Radius", "MouseMove"],\
294 ["Move all", "Alt"],\
295 ["Subdivision", self.carver_prefs.Key_Subrem + " " + self.carver_prefs.Key_Subadd],\
296 ["Incremental rotation", "Ctrl"],\
297 ["Rebool", "Shift"]
300 elif self.CutMode == LINE:
301 help_txt +=[
302 ["Dimension", "MouseMove"],\
303 ["Move all", "Alt"],\
304 ["Validate", "Space"],\
305 ["Rebool", "Shift"],\
306 ["Snap", "Ctrl"],\
307 ["Scale Snap", "WheelMouse"],\
309 else:
310 # ObjectMode
311 help_txt +=[
312 ["Difference", "Space"],\
313 ["Rebool", "Shift + Space"],\
314 ["Duplicate", "Alt + Space"],\
315 ["Scale", self.carver_prefs.Key_Scale],\
316 ["Rotation", "LMB + Move"],\
317 ["Step Angle", "CTRL + LMB + Move"],\
320 if self.ProfileMode:
321 help_txt +=[["Previous or Next Profile", self.carver_prefs.Key_Subadd + " " + self.carver_prefs.Key_Subrem]]
323 help_txt +=[
324 ["Create / Delete rows", chr(8597)],\
325 ["Create / Delete cols", chr(8596)],\
326 ["Gap for rows or columns", self.carver_prefs.Key_Gapy + " " + self.carver_prefs.Key_Gapx]
329 blf.size(0, round(15 * ui_scale), 72)
330 help_txt, bloc_height, max_option, max_key, comma = get_text_info(self, context, help_txt)
331 draw_string(self, color1, color2, xHelp, yHelp, help_txt, max_option)
333 if self.ProfileMode:
334 xrect = region.width - t_panel_width - 80
335 yrect = 80
336 coords = [(xrect, yrect), (xrect+60, yrect), (xrect+60, yrect-60), (xrect, yrect-60)]
338 # Draw rectangle background in the lower right
339 draw_shader(self, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', coords, size=self.carver_prefs.LineWidth)
341 # Use numpy to get the vertices and indices of the profile object to draw
342 WidthProfil = 50
343 location = Vector((region.width - t_panel_width - WidthProfil, 50, 0))
344 ProfilScale = 20.0
345 coords = []
346 mesh = bpy.data.meshes[self.Profils[self.nProfil][0]]
347 mesh.calc_loop_triangles()
348 vertices = np.empty((len(mesh.vertices), 3), 'f')
349 indices = np.empty((len(mesh.loop_triangles), 3), 'i')
350 mesh.vertices.foreach_get("co", np.reshape(vertices, len(mesh.vertices) * 3))
351 mesh.loop_triangles.foreach_get("vertices", np.reshape(indices, len(mesh.loop_triangles) * 3))
353 for idx, vals in enumerate(vertices):
354 coords.append([
355 vals[0] * ProfilScale + location.x,
356 vals[1] * ProfilScale + location.y,
357 vals[2] * ProfilScale + location.z
360 #Draw the silhouette of the mesh
361 draw_shader(self, UIColor, 0.5, 'TRIS', coords, size=self.carver_prefs.LineWidth, indices=indices)
364 if self.CutMode:
366 if len(self.mouse_path) > 1:
367 x0 = self.mouse_path[0][0]
368 y0 = self.mouse_path[0][1]
369 x1 = self.mouse_path[1][0]
370 y1 = self.mouse_path[1][1]
372 # Cut rectangle
373 if self.CutType == RECTANGLE:
374 coords = [
375 (x0 + self.xpos, y0 + self.ypos), (x1 + self.xpos, y0 + self.ypos), \
376 (x1 + self.xpos, y1 + self.ypos), (x0 + self.xpos, y1 + self.ypos)
378 indices = ((0, 1, 2), (2, 0, 3))
380 self.rectangle_coord = coords
382 draw_shader(self, UIColor, 1, 'LINE_LOOP', coords, size=self.carver_prefs.LineWidth)
384 #Draw points
385 draw_shader(self, UIColor, 1, 'POINTS', coords, size=3)
387 if self.shift or self.CreateMode:
388 draw_shader(self, UIColor, 0.5, 'TRIS', coords, size=self.carver_prefs.LineWidth, indices=indices)
390 # Draw grid (based on the overlay options) to show the incremental snapping
391 if self.ctrl:
392 mini_grid(self, context, UIColor)
394 # Cut Line
395 elif self.CutType == LINE:
396 coords = []
397 indices = []
398 top_grid = False
400 for idx, vals in enumerate(self.mouse_path):
401 coords.append([vals[0] + self.xpos, vals[1] + self.ypos])
402 indices.append([idx])
404 # Draw lines
405 if self.Closed:
406 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', coords, size=self.carver_prefs.LineWidth)
407 else:
408 draw_shader(self, UIColor, 1.0, 'LINE_STRIP', coords, size=self.carver_prefs.LineWidth)
410 # Draw points
411 draw_shader(self, UIColor, 1.0, 'POINTS', coords, size=3)
413 # Draw polygon
414 if (self.shift) or (self.CreateMode and self.Closed):
415 draw_shader(self, UIColor, 0.5, 'TRI_FAN', coords, size=self.carver_prefs.LineWidth)
417 # Draw grid (based on the overlay options) to show the incremental snapping
418 if self.ctrl:
419 mini_grid(self, context, UIColor)
421 # Circle Cut
422 elif self.CutType == CIRCLE:
423 # Create a circle using a tri fan
424 tris_coords, indices = draw_circle(self, x0, y0)
426 # Remove the vertex in the center to get the outer line of the circle
427 line_coords = tris_coords[1:]
428 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', line_coords, size=self.carver_prefs.LineWidth)
430 if self.shift or self.CreateMode:
431 draw_shader(self, UIColor, 0.5, 'TRIS', tris_coords, size=self.carver_prefs.LineWidth, indices=indices)
433 if (self.ObjectMode or self.ProfileMode) and len(self.CurrentSelection) > 0:
434 if self.ShowCursor:
435 region = context.region
436 rv3d = context.space_data.region_3d
438 if self.ObjectMode:
439 ob = self.ObjectBrush
440 if self.ProfileMode:
441 ob = self.ProfileBrush
442 mat = ob.matrix_world
444 # 50% alpha, 2 pixel width line
445 bgl.glEnable(bgl.GL_BLEND)
447 bbox = [mat @ Vector(b) for b in ob.bound_box]
448 objBBDiagonal = objDiagonal(self.CurrentSelection[0])
450 if self.shift:
451 gl_size = 4
452 UIColor = (0.5, 1.0, 0.0, 1.0)
453 else:
454 gl_size = 2
455 UIColor = (1.0, 0.8, 0.0, 1.0)
457 line_coords = []
458 idx = 0
459 CRadius = ((bbox[7] - bbox[0]).length) / 2
460 for i in range(int(len(self.CLR_C) / 3)):
461 vector3d = (self.CLR_C[idx * 3] * CRadius + self.CurLoc.x, \
462 self.CLR_C[idx * 3 + 1] * CRadius + self.CurLoc.y, \
463 self.CLR_C[idx * 3 + 2] * CRadius + self.CurLoc.z)
464 vector2d = bpy_extras.view3d_utils.location_3d_to_region_2d(region, rv3d, vector3d)
465 if vector2d is not None:
466 line_coords.append((vector2d[0], vector2d[1]))
467 idx += 1
468 if len(line_coords) > 0 :
469 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', line_coords, size=gl_size)
471 # Object display
472 if self.quat_rot is not None:
473 ob.location = self.CurLoc
474 v = Vector()
475 v.x = v.y = 0.0
476 v.z = self.BrushDepthOffset
477 ob.location += self.quat_rot @ v
479 e = Euler()
480 e.x = 0.0
481 e.y = 0.0
482 e.z = self.aRotZ / 25.0
484 qe = e.to_quaternion()
485 qRot = self.quat_rot @ qe
486 ob.rotation_mode = 'QUATERNION'
487 ob.rotation_quaternion = qRot
488 ob.rotation_mode = 'XYZ'
490 if self.ProfileMode:
491 if self.ProfileBrush is not None:
492 self.ProfileBrush.location = self.CurLoc
493 self.ProfileBrush.rotation_mode = 'QUATERNION'
494 self.ProfileBrush.rotation_quaternion = qRot
495 self.ProfileBrush.rotation_mode = 'XYZ'
497 # Opengl defaults
498 bgl.glLineWidth(1)
499 bgl.glDisable(bgl.GL_BLEND)