1 # SPDX-License-Identifier: GPL-2.0-or-later
8 from gpu_extras
.batch
import batch_for_shader
16 from bpy_extras
.view3d_utils
import (
17 region_2d_to_location_3d
,
18 location_3d_to_region_2d
,
21 from .carver_utils
import (
28 from mathutils
import (
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
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' """
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)
69 # Test if the text is a list formatted like : ('option', 'key')
70 if isinstance(text
,list):
72 spacer_width
= blf
.dimensions(font_id
, spacer_text
)[0]
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
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
):
95 region
= context
.region
96 UIColor
= (0.992, 0.5518, 0.0, 1.0)
102 self
.carver_prefs
= context
.preferences
.addons
[__package__
].preferences
105 color1
= (1.0, 1.0, 1.0, 1.0)
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)
113 PrimitiveType
= "Rectangle"
114 if self
.CutType
== CIRCLE
:
115 PrimitiveType
= "Circle"
116 if self
.CutType
== LINE
:
117 PrimitiveType
= "Line"
120 overlap
= context
.preferences
.system
.use_region_overlap
124 for region
in context
.area
.regions
:
125 if region
.type == 'TOOLS':
126 t_panel_width
= region
.width
129 region_width
= int(region
.width
/ 2.0)
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)
141 if (self
.ObjectMode
is False) and (self
.ProfileMode
is False):
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:
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
]]
172 help_txt
+= [["Type [Space]", PrimitiveType
]]
174 help_txt
+= [["Cut Type [Space]", PrimitiveType
]]
178 TypeStr
= "Instantiate [" + self
.carver_prefs
.Key_Instant
+ "]"
179 BoolStr
= "(ON)" if self
.Instantiate
else "(OFF)"
180 help_txt
= [[TypeStr
, BoolStr
]]
184 TypeStr
= "Random Rotation [" + self
.carver_prefs
.Key_Randrot
+ "]"
185 BoolStr
= "(ON)" if self
.RandomRotation
else "(OFF)"
186 help_txt
+= [[TypeStr
, BoolStr
]]
189 if self
.BrushSolidify
:
190 TypeStr
= "Thickness [" + self
.carver_prefs
.Key_Depth
+ "]"
192 BoolStr
= str(round(self
.ProfileBrush
.modifiers
["CT_SOLIDIFY"].thickness
, 2))
194 BoolStr
= str(round(self
.ObjectBrush
.modifiers
["CT_SOLIDIFY"].thickness
, 2))
195 help_txt
+= [[TypeStr
, BoolStr
]]
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)
213 LineWidth
= (max_option
+ max_key
+ comma
) / 2
214 if region
.width
>= 850:
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
)
223 if self
.CreateMode
and ((self
.ObjectMode
is False) and (self
.ProfileMode
is False)):
224 BooleanMode
= "Create"
226 if self
.ObjectMode
or self
.ProfileMode
:
227 BooleanType
= "Difference) [T]" if self
.BoolOps
== self
.difference
else "Union) [T]"
229 "Object Brush (" + BooleanType
if self
.ObjectMode
else "Profil Brush (" + BooleanType
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:
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"
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)
260 xHelp
= 30 + t_panel_width
263 if self
.ObjectMode
or self
.ProfileMode
:
265 help_txt
= [["Object Mode", self
.carver_prefs
.Key_Brush
]]
267 help_txt
= [["Cut Mode", self
.carver_prefs
.Key_Brush
]]
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:
278 ["Create geometry", self
.carver_prefs
.Key_Create
],\
282 ["Cut", self
.carver_prefs
.Key_Create
],\
284 if self
.CutMode
== RECTANGLE
:
286 ["Dimension", "MouseMove"],\
287 ["Move all", "Alt"],\
288 ["Validate", "LMB"],\
292 elif self
.CutMode
== CIRCLE
:
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"],\
301 elif self
.CutMode
== LINE
:
303 ["Dimension", "MouseMove"],\
304 ["Move all", "Alt"],\
305 ["Validate", "Space"],\
306 ["Rebool", "Shift"],\
308 ["Scale Snap", "WheelMouse"],\
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"],\
322 help_txt
+=[["Previous or Next Profile", self
.carver_prefs
.Key_Subadd
+ " " + self
.carver_prefs
.Key_Subrem
]]
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
)
335 xrect
= region
.width
- t_panel_width
- 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
344 location
= Vector((region
.width
- t_panel_width
- WidthProfil
, 50, 0))
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
):
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
)
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]
374 if self
.CutType
== RECTANGLE
:
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
)
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
393 mini_grid(self
, context
, UIColor
)
396 elif self
.CutType
== LINE
:
401 for idx
, vals
in enumerate(self
.mouse_path
):
402 coords
.append([vals
[0] + self
.xpos
, vals
[1] + self
.ypos
])
403 indices
.append([idx
])
407 draw_shader(self
, UIColor
, 1.0, 'LINE_LOOP', coords
, size
=self
.carver_prefs
.LineWidth
)
409 draw_shader(self
, UIColor
, 1.0, 'LINE_STRIP', coords
, size
=self
.carver_prefs
.LineWidth
)
412 draw_shader(self
, UIColor
, 1.0, 'POINTS', coords
, size
=3)
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
420 mini_grid(self
, context
, UIColor
)
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:
436 region
= context
.region
437 rv3d
= context
.space_data
.region_3d
440 ob
= self
.ObjectBrush
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])
453 UIColor
= (0.5, 1.0, 0.0, 1.0)
456 UIColor
= (1.0, 0.8, 0.0, 1.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]))
469 if len(line_coords
) > 0 :
470 draw_shader(self
, UIColor
, 1.0, 'LINE_LOOP', line_coords
, size
=gl_size
)
473 if self
.quat_rot
is not None:
474 ob
.location
= self
.CurLoc
477 v
.z
= self
.BrushDepthOffset
478 ob
.location
+= self
.quat_rot
@ v
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'
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'
499 gpu
.state
.blend_set('NONE')