Fix: Node Wrangler: new reroutes offset when output hidden
[blender-addons.git] / object_carver / carver_draw.py
blobf5d227f41690b74587affcb2c5dc8b93dba32f22
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 import bpy
4 import blf
5 import bpy_extras
6 import numpy as np
7 import gpu
8 from gpu_extras.batch import batch_for_shader
9 from math import(
10 cos,
11 sin,
12 ceil,
13 floor,
16 from bpy_extras.view3d_utils import (
17 region_2d_to_location_3d,
18 location_3d_to_region_2d,
21 from .carver_utils import (
22 draw_circle,
23 draw_shader,
24 objDiagonal,
25 mini_grid,
28 from mathutils import (
29 Color,
30 Euler,
31 Vector,
32 Quaternion,
35 def get_text_info(self, context, help_txt):
36 """ Return the dimensions of each part of the text """
38 #Extract the longest first option in sublist
39 max_option = max(list(blf.dimensions(0, row[0])[0] for row in help_txt))
41 #Extract the longest key in sublist
42 max_key = max(list(blf.dimensions(0, row[1])[0] for row in help_txt))
44 #Space between option and key with a comma separator (" : ")
45 comma = blf.dimensions(0, "_:_")[0]
47 #Get a default height for all the letters
48 line_height = (blf.dimensions(0, "gM")[1] * 1.45)
50 #Get the total height of the text
51 bloc_height = 0
52 for row in help_txt:
53 bloc_height += line_height
55 return(help_txt, bloc_height, max_option, max_key, comma)
57 def draw_string(self, color1, color2, left, bottom, text, max_option, divide = 1):
58 """ Draw the text like 'option : key' or just 'option' """
60 font_id = 0
61 ui_scale = bpy.context.preferences.system.ui_scale
63 blf.enable(font_id,blf.SHADOW)
64 blf.shadow(font_id, 0, 0.0, 0.0, 0.0, 1.0)
65 blf.shadow_offset(font_id,2,-2)
66 line_height = (blf.dimensions(font_id, "gM")[1] * 1.45)
67 y_offset = 5
69 # Test if the text is a list formatted like : ('option', 'key')
70 if isinstance(text,list):
71 spacer_text = " : "
72 spacer_width = blf.dimensions(font_id, spacer_text)[0]
73 for string in text:
74 blf.position(font_id, (left), (bottom + y_offset), 0)
75 blf.color(font_id, *color1)
76 blf.draw(font_id, string[0])
77 blf.position(font_id, (left + max_option), (bottom + y_offset), 0)
78 blf.draw(font_id, spacer_text)
79 blf.color(font_id, *color2)
80 blf.position(font_id, (left + max_option + spacer_width), (bottom + y_offset), 0)
81 blf.draw(font_id, string[1])
82 y_offset += line_height
83 else:
84 # The text is formatted like : ('option')
85 blf.position(font_id, left, (bottom + y_offset), 0)
86 blf.color(font_id, *color1)
87 blf.draw(font_id, text)
88 y_offset += line_height
90 blf.disable(font_id,blf.SHADOW)
92 # Opengl draw on screen
93 def draw_callback_px(self, context):
94 font_id = 0
95 region = context.region
96 UIColor = (0.992, 0.5518, 0.0, 1.0)
98 # Cut Type
99 RECTANGLE = 0
100 LINE = 1
101 CIRCLE = 2
102 self.carver_prefs = context.preferences.addons[__package__].preferences
104 # Color
105 color1 = (1.0, 1.0, 1.0, 1.0)
106 color2 = UIColor
108 #The mouse is outside the active region
109 if not self.in_view_3d:
110 color1 = color2 = (1.0, 0.2, 0.1, 1.0)
112 # Primitives type
113 PrimitiveType = "Rectangle"
114 if self.CutType == CIRCLE:
115 PrimitiveType = "Circle"
116 if self.CutType == LINE:
117 PrimitiveType = "Line"
119 # Width screen
120 overlap = context.preferences.system.use_region_overlap
122 t_panel_width = 0
123 if overlap:
124 for region in context.area.regions:
125 if region.type == 'TOOLS':
126 t_panel_width = region.width
128 # Initial position
129 region_width = int(region.width / 2.0)
130 y_txt = 10
133 # Draw the center command from bottom to top
135 # Get the size of the text
136 text_size = 18 if region.width >= 850 else 12
137 ui_scale = bpy.context.preferences.system.ui_scale
138 blf.size(0, round(text_size * ui_scale), 72)
140 # Help Display
141 if (self.ObjectMode is False) and (self.ProfileMode is False):
143 # Depth Cursor
144 TypeStr = "Cursor Depth [" + self.carver_prefs.Key_Depth + "]"
145 BoolStr = "(ON)" if self.snapCursor else "(OFF)"
146 help_txt = [[TypeStr, BoolStr]]
148 # Close poygonal shape
149 if self.CreateMode and self.CutType == LINE:
150 TypeStr = "Close [" + self.carver_prefs.Key_Close + "]"
151 BoolStr = "(ON)" if self.Closed else "(OFF)"
152 help_txt += [[TypeStr, BoolStr]]
154 if self.CreateMode is False:
155 # Apply Booleans
156 TypeStr = "Apply Operations [" + self.carver_prefs.Key_Apply + "]"
157 BoolStr = "(OFF)" if self.dont_apply_boolean else "(ON)"
158 help_txt += [[TypeStr, BoolStr]]
160 #Auto update for bevel
161 TypeStr = "Bevel Update [" + self.carver_prefs.Key_Update + "]"
162 BoolStr = "(ON)" if self.Auto_BevelUpdate else "(OFF)"
163 help_txt += [[TypeStr, BoolStr]]
165 # Circle subdivisions
166 if self.CutType == CIRCLE:
167 TypeStr = "Subdivisions [" + self.carver_prefs.Key_Subrem + "][" + self.carver_prefs.Key_Subadd + "]"
168 BoolStr = str((int(360 / self.stepAngle[self.step])))
169 help_txt += [[TypeStr, BoolStr]]
171 if self.CreateMode:
172 help_txt += [["Type [Space]", PrimitiveType]]
173 else:
174 help_txt += [["Cut Type [Space]", PrimitiveType]]
176 else:
177 # Instantiate
178 TypeStr = "Instantiate [" + self.carver_prefs.Key_Instant + "]"
179 BoolStr = "(ON)" if self.Instantiate else "(OFF)"
180 help_txt = [[TypeStr, BoolStr]]
182 # Random rotation
183 if self.alt:
184 TypeStr = "Random Rotation [" + self.carver_prefs.Key_Randrot + "]"
185 BoolStr = "(ON)" if self.RandomRotation else "(OFF)"
186 help_txt += [[TypeStr, BoolStr]]
188 # Thickness
189 if self.BrushSolidify:
190 TypeStr = "Thickness [" + self.carver_prefs.Key_Depth + "]"
191 if self.ProfileMode:
192 BoolStr = str(round(self.ProfileBrush.modifiers["CT_SOLIDIFY"].thickness, 2))
193 if self.ObjectMode:
194 BoolStr = str(round(self.ObjectBrush.modifiers["CT_SOLIDIFY"].thickness, 2))
195 help_txt += [[TypeStr, BoolStr]]
197 # Brush depth
198 if (self.ObjectMode):
199 TypeStr = "Carve Depth [" + self.carver_prefs.Key_Depth + "]"
200 BoolStr = str(round(self.ObjectBrush.data.vertices[0].co.z, 2))
201 help_txt += [[TypeStr, BoolStr]]
203 TypeStr = "Brush Depth [" + self.carver_prefs.Key_BrushDepth + "]"
204 BoolStr = str(round(self.BrushDepthOffset, 2))
205 help_txt += [[TypeStr, BoolStr]]
207 help_txt, bloc_height, max_option, max_key, comma = get_text_info(self, context, help_txt)
208 xCmd = region_width - (max_option + max_key + comma) / 2
209 draw_string(self, color1, color2, xCmd, y_txt, help_txt, max_option, divide = 2)
212 # Separator (Line)
213 LineWidth = (max_option + max_key + comma) / 2
214 if region.width >= 850:
215 LineWidth = 140
217 LineWidth = (max_option + max_key + comma)
218 coords = [(int(region_width - LineWidth/2), y_txt + bloc_height + 8), \
219 (int(region_width + LineWidth/2), y_txt + bloc_height + 8)]
220 draw_shader(self, UIColor, 1, 'LINES', coords, self.carver_prefs.LineWidth)
222 # Command Display
223 if self.CreateMode and ((self.ObjectMode is False) and (self.ProfileMode is False)):
224 BooleanMode = "Create"
225 else:
226 if self.ObjectMode or self.ProfileMode:
227 BooleanType = "Difference) [T]" if self.BoolOps == self.difference else "Union) [T]"
228 BooleanMode = \
229 "Object Brush (" + BooleanType if self.ObjectMode else "Profil Brush (" + BooleanType
230 else:
231 BooleanMode = \
232 "Difference" if (self.shift is False) and (self.ForceRebool is False) else "Rebool"
234 # Display boolean mode
235 text_size = 40 if region.width >= 850 else 20
236 blf.size(0, round(text_size * ui_scale), 72)
238 draw_string(self, color2, color2, region_width - (blf.dimensions(0, BooleanMode)[0]) / 2, \
239 y_txt + bloc_height + 16, BooleanMode, 0, divide = 2)
241 if region.width >= 850:
243 if self.AskHelp is False:
244 # "H for Help" text
245 blf.size(0, round(13 * ui_scale), 72)
246 help_txt = "[" + self.carver_prefs.Key_Help + "] for help"
247 txt_width = blf.dimensions(0, help_txt)[0]
248 txt_height = (blf.dimensions(0, "gM")[1] * 1.45)
250 # Draw a rectangle and put the text "H for Help"
251 xrect = 40
252 yrect = 40
253 rect_vertices = [(xrect - 5, yrect - 5), (xrect + txt_width + 5, yrect - 5), \
254 (xrect + txt_width + 5, yrect + txt_height + 5), (xrect - 5, yrect + txt_height + 5)]
255 draw_shader(self, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', rect_vertices, self.carver_prefs.LineWidth)
256 draw_string(self, color1, color2, xrect, yrect, help_txt, 0)
258 else:
259 #Draw the help text
260 xHelp = 30 + t_panel_width
261 yHelp = 10
263 if self.ObjectMode or self.ProfileMode:
264 if self.ProfileMode:
265 help_txt = [["Object Mode", self.carver_prefs.Key_Brush]]
266 else:
267 help_txt = [["Cut Mode", self.carver_prefs.Key_Brush]]
269 else:
270 help_txt =[
271 ["Profil Brush", self.carver_prefs.Key_Brush],\
272 ["Move Cursor", "Ctrl + LMB"]
275 if (self.ObjectMode is False) and (self.ProfileMode is False):
276 if self.CreateMode is False:
277 help_txt +=[
278 ["Create geometry", self.carver_prefs.Key_Create],\
280 else:
281 help_txt +=[
282 ["Cut", self.carver_prefs.Key_Create],\
284 if self.CutMode == RECTANGLE:
285 help_txt +=[
286 ["Dimension", "MouseMove"],\
287 ["Move all", "Alt"],\
288 ["Validate", "LMB"],\
289 ["Rebool", "Shift"]
292 elif self.CutMode == CIRCLE:
293 help_txt +=[
294 ["Rotation and Radius", "MouseMove"],\
295 ["Move all", "Alt"],\
296 ["Subdivision", self.carver_prefs.Key_Subrem + " " + self.carver_prefs.Key_Subadd],\
297 ["Incremental rotation", "Ctrl"],\
298 ["Rebool", "Shift"]
301 elif self.CutMode == LINE:
302 help_txt +=[
303 ["Dimension", "MouseMove"],\
304 ["Move all", "Alt"],\
305 ["Validate", "Space"],\
306 ["Rebool", "Shift"],\
307 ["Snap", "Ctrl"],\
308 ["Scale Snap", "WheelMouse"],\
310 else:
311 # ObjectMode
312 help_txt +=[
313 ["Difference", "Space"],\
314 ["Rebool", "Shift + Space"],\
315 ["Duplicate", "Alt + Space"],\
316 ["Scale", self.carver_prefs.Key_Scale],\
317 ["Rotation", "LMB + Move"],\
318 ["Step Angle", "CTRL + LMB + Move"],\
321 if self.ProfileMode:
322 help_txt +=[["Previous or Next Profile", self.carver_prefs.Key_Subadd + " " + self.carver_prefs.Key_Subrem]]
324 help_txt +=[
325 ["Create / Delete rows", chr(8597)],\
326 ["Create / Delete cols", chr(8596)],\
327 ["Gap for rows or columns", self.carver_prefs.Key_Gapy + " " + self.carver_prefs.Key_Gapx]
330 blf.size(0, round(15 * ui_scale), 72)
331 help_txt, bloc_height, max_option, max_key, comma = get_text_info(self, context, help_txt)
332 draw_string(self, color1, color2, xHelp, yHelp, help_txt, max_option)
334 if self.ProfileMode:
335 xrect = region.width - t_panel_width - 80
336 yrect = 80
337 coords = [(xrect, yrect), (xrect+60, yrect), (xrect+60, yrect-60), (xrect, yrect-60)]
339 # Draw rectangle background in the lower right
340 draw_shader(self, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', coords, size=self.carver_prefs.LineWidth)
342 # Use numpy to get the vertices and indices of the profile object to draw
343 WidthProfil = 50
344 location = Vector((region.width - t_panel_width - WidthProfil, 50, 0))
345 ProfilScale = 20.0
346 coords = []
347 mesh = bpy.data.meshes[self.Profils[self.nProfil][0]]
348 mesh.calc_loop_triangles()
349 vertices = np.empty((len(mesh.vertices), 3), 'f')
350 indices = np.empty((len(mesh.loop_triangles), 3), 'i')
351 mesh.vertices.foreach_get("co", np.reshape(vertices, len(mesh.vertices) * 3))
352 mesh.loop_triangles.foreach_get("vertices", np.reshape(indices, len(mesh.loop_triangles) * 3))
354 for idx, vals in enumerate(vertices):
355 coords.append([
356 vals[0] * ProfilScale + location.x,
357 vals[1] * ProfilScale + location.y,
358 vals[2] * ProfilScale + location.z
361 #Draw the silhouette of the mesh
362 draw_shader(self, UIColor, 0.5, 'TRIS', coords, size=self.carver_prefs.LineWidth, indices=indices)
365 if self.CutMode:
367 if len(self.mouse_path) > 1:
368 x0 = self.mouse_path[0][0]
369 y0 = self.mouse_path[0][1]
370 x1 = self.mouse_path[1][0]
371 y1 = self.mouse_path[1][1]
373 # Cut rectangle
374 if self.CutType == RECTANGLE:
375 coords = [
376 (x0 + self.xpos, y0 + self.ypos), (x1 + self.xpos, y0 + self.ypos), \
377 (x1 + self.xpos, y1 + self.ypos), (x0 + self.xpos, y1 + self.ypos)
379 indices = ((0, 1, 2), (2, 0, 3))
381 self.rectangle_coord = coords
383 draw_shader(self, UIColor, 1, 'LINE_LOOP', coords, size=self.carver_prefs.LineWidth)
385 #Draw points
386 draw_shader(self, UIColor, 1, 'POINTS', coords, size=3)
388 if self.shift or self.CreateMode:
389 draw_shader(self, UIColor, 0.5, 'TRIS', coords, size=self.carver_prefs.LineWidth, indices=indices)
391 # Draw grid (based on the overlay options) to show the incremental snapping
392 if self.ctrl:
393 mini_grid(self, context, UIColor)
395 # Cut Line
396 elif self.CutType == LINE:
397 coords = []
398 indices = []
399 top_grid = False
401 for idx, vals in enumerate(self.mouse_path):
402 coords.append([vals[0] + self.xpos, vals[1] + self.ypos])
403 indices.append([idx])
405 # Draw lines
406 if self.Closed:
407 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', coords, size=self.carver_prefs.LineWidth)
408 else:
409 draw_shader(self, UIColor, 1.0, 'LINE_STRIP', coords, size=self.carver_prefs.LineWidth)
411 # Draw points
412 draw_shader(self, UIColor, 1.0, 'POINTS', coords, size=3)
414 # Draw polygon
415 if (self.shift) or (self.CreateMode and self.Closed):
416 draw_shader(self, UIColor, 0.5, 'TRI_FAN', coords, size=self.carver_prefs.LineWidth)
418 # Draw grid (based on the overlay options) to show the incremental snapping
419 if self.ctrl:
420 mini_grid(self, context, UIColor)
422 # Circle Cut
423 elif self.CutType == CIRCLE:
424 # Create a circle using a tri fan
425 tris_coords, indices = draw_circle(self, x0, y0)
427 # Remove the vertex in the center to get the outer line of the circle
428 line_coords = tris_coords[1:]
429 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', line_coords, size=self.carver_prefs.LineWidth)
431 if self.shift or self.CreateMode:
432 draw_shader(self, UIColor, 0.5, 'TRIS', tris_coords, size=self.carver_prefs.LineWidth, indices=indices)
434 if (self.ObjectMode or self.ProfileMode) and len(self.CurrentSelection) > 0:
435 if self.ShowCursor:
436 region = context.region
437 rv3d = context.space_data.region_3d
439 if self.ObjectMode:
440 ob = self.ObjectBrush
441 if self.ProfileMode:
442 ob = self.ProfileBrush
443 mat = ob.matrix_world
445 # 50% alpha, 2 pixel width line
446 gpu.state.blend_set('ALPHA')
448 bbox = [mat @ Vector(b) for b in ob.bound_box]
449 objBBDiagonal = objDiagonal(self.CurrentSelection[0])
451 if self.shift:
452 gl_size = 4
453 UIColor = (0.5, 1.0, 0.0, 1.0)
454 else:
455 gl_size = 2
456 UIColor = (1.0, 0.8, 0.0, 1.0)
458 line_coords = []
459 idx = 0
460 CRadius = ((bbox[7] - bbox[0]).length) / 2
461 for i in range(int(len(self.CLR_C) / 3)):
462 vector3d = (self.CLR_C[idx * 3] * CRadius + self.CurLoc.x, \
463 self.CLR_C[idx * 3 + 1] * CRadius + self.CurLoc.y, \
464 self.CLR_C[idx * 3 + 2] * CRadius + self.CurLoc.z)
465 vector2d = bpy_extras.view3d_utils.location_3d_to_region_2d(region, rv3d, vector3d)
466 if vector2d is not None:
467 line_coords.append((vector2d[0], vector2d[1]))
468 idx += 1
469 if len(line_coords) > 0 :
470 draw_shader(self, UIColor, 1.0, 'LINE_LOOP', line_coords, size=gl_size)
472 # Object display
473 if self.quat_rot is not None:
474 ob.location = self.CurLoc
475 v = Vector()
476 v.x = v.y = 0.0
477 v.z = self.BrushDepthOffset
478 ob.location += self.quat_rot @ v
480 e = Euler()
481 e.x = 0.0
482 e.y = 0.0
483 e.z = self.aRotZ / 25.0
485 qe = e.to_quaternion()
486 qRot = self.quat_rot @ qe
487 ob.rotation_mode = 'QUATERNION'
488 ob.rotation_quaternion = qRot
489 ob.rotation_mode = 'XYZ'
491 if self.ProfileMode:
492 if self.ProfileBrush is not None:
493 self.ProfileBrush.location = self.CurLoc
494 self.ProfileBrush.rotation_mode = 'QUATERNION'
495 self.ProfileBrush.rotation_quaternion = qRot
496 self.ProfileBrush.rotation_mode = 'XYZ'
498 # Opengl defaults
499 gpu.state.blend_set('NONE')