1 # SPDX-FileCopyrightText: 2019-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
10 from gpu_extras
.batch
import batch_for_shader
18 from bpy_extras
.view3d_utils
import (
19 region_2d_to_location_3d
,
20 location_3d_to_region_2d
,
23 from .carver_utils
import (
30 from mathutils
import (
37 def get_text_info(self
, context
, help_txt
):
38 """ Return the dimensions of each part of the text """
40 #Extract the longest first option in sublist
41 max_option
= max(list(blf
.dimensions(0, row
[0])[0] for row
in help_txt
))
43 #Extract the longest key in sublist
44 max_key
= max(list(blf
.dimensions(0, row
[1])[0] for row
in help_txt
))
46 #Space between option and key with a comma separator (" : ")
47 comma
= blf
.dimensions(0, "_:_")[0]
49 #Get a default height for all the letters
50 line_height
= (blf
.dimensions(0, "gM")[1] * 1.45)
52 #Get the total height of the text
55 bloc_height
+= line_height
57 return(help_txt
, bloc_height
, max_option
, max_key
, comma
)
59 def draw_string(self
, color1
, color2
, left
, bottom
, text
, max_option
, divide
= 1):
60 """ Draw the text like 'option : key' or just 'option' """
63 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
65 blf
.enable(font_id
,blf
.SHADOW
)
66 blf
.shadow(font_id
, 0, 0.0, 0.0, 0.0, 1.0)
67 blf
.shadow_offset(font_id
,2,-2)
68 line_height
= (blf
.dimensions(font_id
, "gM")[1] * 1.45)
71 # Test if the text is a list formatted like : ('option', 'key')
72 if isinstance(text
,list):
74 spacer_width
= blf
.dimensions(font_id
, spacer_text
)[0]
76 blf
.position(font_id
, (left
), (bottom
+ y_offset
), 0)
77 blf
.color(font_id
, *color1
)
78 blf
.draw(font_id
, string
[0])
79 blf
.position(font_id
, (left
+ max_option
), (bottom
+ y_offset
), 0)
80 blf
.draw(font_id
, spacer_text
)
81 blf
.color(font_id
, *color2
)
82 blf
.position(font_id
, (left
+ max_option
+ spacer_width
), (bottom
+ y_offset
), 0)
83 blf
.draw(font_id
, string
[1])
84 y_offset
+= line_height
86 # The text is formatted like : ('option')
87 blf
.position(font_id
, left
, (bottom
+ y_offset
), 0)
88 blf
.color(font_id
, *color1
)
89 blf
.draw(font_id
, text
)
90 y_offset
+= line_height
92 blf
.disable(font_id
,blf
.SHADOW
)
94 # Opengl draw on screen
95 def draw_callback_px(self
, context
):
97 region
= context
.region
98 UIColor
= (0.992, 0.5518, 0.0, 1.0)
104 self
.carver_prefs
= context
.preferences
.addons
[__package__
].preferences
107 color1
= (1.0, 1.0, 1.0, 1.0)
110 #The mouse is outside the active region
111 if not self
.in_view_3d
:
112 color1
= color2
= (1.0, 0.2, 0.1, 1.0)
115 PrimitiveType
= "Rectangle"
116 if self
.CutType
== CIRCLE
:
117 PrimitiveType
= "Circle"
118 if self
.CutType
== LINE
:
119 PrimitiveType
= "Line"
122 overlap
= context
.preferences
.system
.use_region_overlap
126 for region
in context
.area
.regions
:
127 if region
.type == 'TOOLS':
128 t_panel_width
= region
.width
131 region_width
= int(region
.width
/ 2.0)
135 # Draw the center command from bottom to top
137 # Get the size of the text
138 text_size
= 18 if region
.width
>= 850 else 12
139 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
140 blf
.size(0, round(text_size
* ui_scale
))
143 if (self
.ObjectMode
is False) and (self
.ProfileMode
is False):
146 TypeStr
= "Cursor Depth [" + self
.carver_prefs
.Key_Depth
+ "]"
147 BoolStr
= "(ON)" if self
.snapCursor
else "(OFF)"
148 help_txt
= [[TypeStr
, BoolStr
]]
150 # Close poygonal shape
151 if self
.CreateMode
and self
.CutType
== LINE
:
152 TypeStr
= "Close [" + self
.carver_prefs
.Key_Close
+ "]"
153 BoolStr
= "(ON)" if self
.Closed
else "(OFF)"
154 help_txt
+= [[TypeStr
, BoolStr
]]
156 if self
.CreateMode
is False:
158 TypeStr
= "Apply Operations [" + self
.carver_prefs
.Key_Apply
+ "]"
159 BoolStr
= "(OFF)" if self
.dont_apply_boolean
else "(ON)"
160 help_txt
+= [[TypeStr
, BoolStr
]]
162 #Auto update for bevel
163 TypeStr
= "Bevel Update [" + self
.carver_prefs
.Key_Update
+ "]"
164 BoolStr
= "(ON)" if self
.Auto_BevelUpdate
else "(OFF)"
165 help_txt
+= [[TypeStr
, BoolStr
]]
167 # Circle subdivisions
168 if self
.CutType
== CIRCLE
:
169 TypeStr
= "Subdivisions [" + self
.carver_prefs
.Key_Subrem
+ "][" + self
.carver_prefs
.Key_Subadd
+ "]"
170 BoolStr
= str((int(360 / self
.stepAngle
[self
.step
])))
171 help_txt
+= [[TypeStr
, BoolStr
]]
174 help_txt
+= [["Type [Space]", PrimitiveType
]]
176 help_txt
+= [["Cut Type [Space]", PrimitiveType
]]
180 TypeStr
= "Instantiate [" + self
.carver_prefs
.Key_Instant
+ "]"
181 BoolStr
= "(ON)" if self
.Instantiate
else "(OFF)"
182 help_txt
= [[TypeStr
, BoolStr
]]
186 TypeStr
= "Random Rotation [" + self
.carver_prefs
.Key_Randrot
+ "]"
187 BoolStr
= "(ON)" if self
.RandomRotation
else "(OFF)"
188 help_txt
+= [[TypeStr
, BoolStr
]]
191 if self
.BrushSolidify
:
192 TypeStr
= "Thickness [" + self
.carver_prefs
.Key_Depth
+ "]"
194 BoolStr
= str(round(self
.ProfileBrush
.modifiers
["CT_SOLIDIFY"].thickness
, 2))
196 BoolStr
= str(round(self
.ObjectBrush
.modifiers
["CT_SOLIDIFY"].thickness
, 2))
197 help_txt
+= [[TypeStr
, BoolStr
]]
200 if (self
.ObjectMode
):
201 TypeStr
= "Carve Depth [" + self
.carver_prefs
.Key_Depth
+ "]"
202 BoolStr
= str(round(self
.ObjectBrush
.data
.vertices
[0].co
.z
, 2))
203 help_txt
+= [[TypeStr
, BoolStr
]]
205 TypeStr
= "Brush Depth [" + self
.carver_prefs
.Key_BrushDepth
+ "]"
206 BoolStr
= str(round(self
.BrushDepthOffset
, 2))
207 help_txt
+= [[TypeStr
, BoolStr
]]
209 help_txt
, bloc_height
, max_option
, max_key
, comma
= get_text_info(self
, context
, help_txt
)
210 xCmd
= region_width
- (max_option
+ max_key
+ comma
) / 2
211 draw_string(self
, color1
, color2
, xCmd
, y_txt
, help_txt
, max_option
, divide
= 2)
215 LineWidth
= (max_option
+ max_key
+ comma
) / 2
216 if region
.width
>= 850:
219 LineWidth
= (max_option
+ max_key
+ comma
)
220 coords
= [(int(region_width
- LineWidth
/2), y_txt
+ bloc_height
+ 8), \
221 (int(region_width
+ LineWidth
/2), y_txt
+ bloc_height
+ 8)]
222 draw_shader(self
, UIColor
, 1, 'LINES', coords
, self
.carver_prefs
.LineWidth
)
225 if self
.CreateMode
and ((self
.ObjectMode
is False) and (self
.ProfileMode
is False)):
226 BooleanMode
= "Create"
228 if self
.ObjectMode
or self
.ProfileMode
:
229 BooleanType
= "Difference) [T]" if self
.BoolOps
== self
.difference
else "Union) [T]"
231 "Object Brush (" + BooleanType
if self
.ObjectMode
else "Profil Brush (" + BooleanType
234 "Difference" if (self
.shift
is False) and (self
.ForceRebool
is False) else "Rebool"
236 # Display boolean mode
237 text_size
= 40 if region
.width
>= 850 else 20
238 blf
.size(0, round(text_size
* ui_scale
))
240 draw_string(self
, color2
, color2
, region_width
- (blf
.dimensions(0, BooleanMode
)[0]) / 2, \
241 y_txt
+ bloc_height
+ 16, BooleanMode
, 0, divide
= 2)
243 if region
.width
>= 850:
245 if self
.AskHelp
is False:
247 blf
.size(0, round(13 * ui_scale
))
248 help_txt
= "[" + self
.carver_prefs
.Key_Help
+ "] for help"
249 txt_width
= blf
.dimensions(0, help_txt
)[0]
250 txt_height
= (blf
.dimensions(0, "gM")[1] * 1.45)
252 # Draw a rectangle and put the text "H for Help"
255 rect_vertices
= [(xrect
- 5, yrect
- 5), (xrect
+ txt_width
+ 5, yrect
- 5), \
256 (xrect
+ txt_width
+ 5, yrect
+ txt_height
+ 5), (xrect
- 5, yrect
+ txt_height
+ 5)]
257 draw_shader(self
, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', rect_vertices
, self
.carver_prefs
.LineWidth
)
258 draw_string(self
, color1
, color2
, xrect
, yrect
, help_txt
, 0)
262 xHelp
= 30 + t_panel_width
265 if self
.ObjectMode
or self
.ProfileMode
:
267 help_txt
= [["Object Mode", self
.carver_prefs
.Key_Brush
]]
269 help_txt
= [["Cut Mode", self
.carver_prefs
.Key_Brush
]]
273 ["Profil Brush", self
.carver_prefs
.Key_Brush
],\
274 ["Move Cursor", "Ctrl + LMB"]
277 if (self
.ObjectMode
is False) and (self
.ProfileMode
is False):
278 if self
.CreateMode
is False:
280 ["Create geometry", self
.carver_prefs
.Key_Create
],\
284 ["Cut", self
.carver_prefs
.Key_Create
],\
286 if self
.CutMode
== RECTANGLE
:
288 ["Dimension", "MouseMove"],\
289 ["Move all", "Alt"],\
290 ["Validate", "LMB"],\
294 elif self
.CutMode
== CIRCLE
:
296 ["Rotation and Radius", "MouseMove"],\
297 ["Move all", "Alt"],\
298 ["Subdivision", self
.carver_prefs
.Key_Subrem
+ " " + self
.carver_prefs
.Key_Subadd
],\
299 ["Incremental rotation", "Ctrl"],\
303 elif self
.CutMode
== LINE
:
305 ["Dimension", "MouseMove"],\
306 ["Move all", "Alt"],\
307 ["Validate", "Space"],\
308 ["Rebool", "Shift"],\
310 ["Scale Snap", "WheelMouse"],\
315 ["Difference", "Space"],\
316 ["Rebool", "Shift + Space"],\
317 ["Duplicate", "Alt + Space"],\
318 ["Scale", self
.carver_prefs
.Key_Scale
],\
319 ["Rotation", "LMB + Move"],\
320 ["Step Angle", "CTRL + LMB + Move"],\
324 help_txt
+=[["Previous or Next Profile", self
.carver_prefs
.Key_Subadd
+ " " + self
.carver_prefs
.Key_Subrem
]]
327 ["Create / Delete rows", chr(8597)],\
328 ["Create / Delete cols", chr(8596)],\
329 ["Gap for rows or columns", self
.carver_prefs
.Key_Gapy
+ " " + self
.carver_prefs
.Key_Gapx
]
332 blf
.size(0, round(15 * ui_scale
))
333 help_txt
, bloc_height
, max_option
, max_key
, comma
= get_text_info(self
, context
, help_txt
)
334 draw_string(self
, color1
, color2
, xHelp
, yHelp
, help_txt
, max_option
)
337 xrect
= region
.width
- t_panel_width
- 80
339 coords
= [(xrect
, yrect
), (xrect
+60, yrect
), (xrect
+60, yrect
-60), (xrect
, yrect
-60)]
341 # Draw rectangle background in the lower right
342 draw_shader(self
, (0.0, 0.0, 0.0), 0.3, 'TRI_FAN', coords
, size
=self
.carver_prefs
.LineWidth
)
344 # Use numpy to get the vertices and indices of the profile object to draw
346 location
= Vector((region
.width
- t_panel_width
- WidthProfil
, 50, 0))
349 mesh
= bpy
.data
.meshes
[self
.Profils
[self
.nProfil
][0]]
350 mesh
.calc_loop_triangles()
351 vertices
= np
.empty((len(mesh
.vertices
), 3), 'f')
352 indices
= np
.empty((len(mesh
.loop_triangles
), 3), 'i')
353 mesh
.vertices
.foreach_get("co", np
.reshape(vertices
, len(mesh
.vertices
) * 3))
354 mesh
.loop_triangles
.foreach_get("vertices", np
.reshape(indices
, len(mesh
.loop_triangles
) * 3))
356 for idx
, vals
in enumerate(vertices
):
358 vals
[0] * ProfilScale
+ location
.x
,
359 vals
[1] * ProfilScale
+ location
.y
,
360 vals
[2] * ProfilScale
+ location
.z
363 #Draw the silhouette of the mesh
364 draw_shader(self
, UIColor
, 0.5, 'TRIS', coords
, size
=self
.carver_prefs
.LineWidth
, indices
=indices
)
369 if len(self
.mouse_path
) > 1:
370 x0
= self
.mouse_path
[0][0]
371 y0
= self
.mouse_path
[0][1]
372 x1
= self
.mouse_path
[1][0]
373 y1
= self
.mouse_path
[1][1]
376 if self
.CutType
== RECTANGLE
:
378 (x0
+ self
.xpos
, y0
+ self
.ypos
), (x1
+ self
.xpos
, y0
+ self
.ypos
), \
379 (x1
+ self
.xpos
, y1
+ self
.ypos
), (x0
+ self
.xpos
, y1
+ self
.ypos
)
381 indices
= ((0, 1, 2), (2, 0, 3))
383 self
.rectangle_coord
= coords
385 draw_shader(self
, UIColor
, 1, 'LINE_LOOP', coords
, size
=self
.carver_prefs
.LineWidth
)
388 draw_shader(self
, UIColor
, 1, 'POINTS', coords
, size
=3)
390 if self
.shift
or self
.CreateMode
:
391 draw_shader(self
, UIColor
, 0.5, 'TRIS', coords
, size
=self
.carver_prefs
.LineWidth
, indices
=indices
)
393 # Draw grid (based on the overlay options) to show the incremental snapping
395 mini_grid(self
, context
, UIColor
)
398 elif self
.CutType
== LINE
:
403 for idx
, vals
in enumerate(self
.mouse_path
):
404 coords
.append([vals
[0] + self
.xpos
, vals
[1] + self
.ypos
])
405 indices
.append([idx
])
409 draw_shader(self
, UIColor
, 1.0, 'LINE_LOOP', coords
, size
=self
.carver_prefs
.LineWidth
)
411 draw_shader(self
, UIColor
, 1.0, 'LINE_STRIP', coords
, size
=self
.carver_prefs
.LineWidth
)
414 draw_shader(self
, UIColor
, 1.0, 'POINTS', coords
, size
=3)
417 if (self
.shift
) or (self
.CreateMode
and self
.Closed
):
418 draw_shader(self
, UIColor
, 0.5, 'TRI_FAN', coords
, size
=self
.carver_prefs
.LineWidth
)
420 # Draw grid (based on the overlay options) to show the incremental snapping
422 mini_grid(self
, context
, UIColor
)
425 elif self
.CutType
== CIRCLE
:
426 # Create a circle using a tri fan
427 tris_coords
, indices
= draw_circle(self
, x0
, y0
)
429 # Remove the vertex in the center to get the outer line of the circle
430 line_coords
= tris_coords
[1:]
431 draw_shader(self
, UIColor
, 1.0, 'LINE_LOOP', line_coords
, size
=self
.carver_prefs
.LineWidth
)
433 if self
.shift
or self
.CreateMode
:
434 draw_shader(self
, UIColor
, 0.5, 'TRIS', tris_coords
, size
=self
.carver_prefs
.LineWidth
, indices
=indices
)
436 if (self
.ObjectMode
or self
.ProfileMode
) and len(self
.CurrentSelection
) > 0:
438 region
= context
.region
439 rv3d
= context
.space_data
.region_3d
442 ob
= self
.ObjectBrush
444 ob
= self
.ProfileBrush
445 mat
= ob
.matrix_world
447 # 50% alpha, 2 pixel width line
448 gpu
.state
.blend_set('ALPHA')
450 bbox
= [mat
@ Vector(b
) for b
in ob
.bound_box
]
451 objBBDiagonal
= objDiagonal(self
.CurrentSelection
[0])
455 UIColor
= (0.5, 1.0, 0.0, 1.0)
458 UIColor
= (1.0, 0.8, 0.0, 1.0)
462 CRadius
= ((bbox
[7] - bbox
[0]).length
) / 2
463 for i
in range(int(len(self
.CLR_C
) / 3)):
464 vector3d
= (self
.CLR_C
[idx
* 3] * CRadius
+ self
.CurLoc
.x
, \
465 self
.CLR_C
[idx
* 3 + 1] * CRadius
+ self
.CurLoc
.y
, \
466 self
.CLR_C
[idx
* 3 + 2] * CRadius
+ self
.CurLoc
.z
)
467 vector2d
= bpy_extras
.view3d_utils
.location_3d_to_region_2d(region
, rv3d
, vector3d
)
468 if vector2d
is not None:
469 line_coords
.append((vector2d
[0], vector2d
[1]))
471 if len(line_coords
) > 0 :
472 draw_shader(self
, UIColor
, 1.0, 'LINE_LOOP', line_coords
, size
=gl_size
)
475 if self
.quat_rot
is not None:
476 ob
.location
= self
.CurLoc
479 v
.z
= self
.BrushDepthOffset
480 ob
.location
+= self
.quat_rot
@ v
485 e
.z
= self
.aRotZ
/ 25.0
487 qe
= e
.to_quaternion()
488 qRot
= self
.quat_rot
@ qe
489 ob
.rotation_mode
= 'QUATERNION'
490 ob
.rotation_quaternion
= qRot
491 ob
.rotation_mode
= 'XYZ'
494 if self
.ProfileBrush
is not None:
495 self
.ProfileBrush
.location
= self
.CurLoc
496 self
.ProfileBrush
.rotation_mode
= 'QUATERNION'
497 self
.ProfileBrush
.rotation_quaternion
= qRot
498 self
.ProfileBrush
.rotation_mode
= 'XYZ'
501 gpu
.state
.blend_set('NONE')