Node Wrangler: do not rely on image name to detect Viewer Node image
[blender-addons.git] / object_carver / carver_operator.py
blob6e8e5fffad259154a04d18961c17831cf723fc85
1 # SPDX-FileCopyrightText: 2019-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 import bpy_extras
7 import sys
8 from bpy.props import (
9 BoolProperty,
10 IntProperty,
11 PointerProperty,
12 StringProperty,
13 EnumProperty,
15 from mathutils import (
16 Vector,
19 from bpy_extras.view3d_utils import (
20 region_2d_to_vector_3d,
21 region_2d_to_origin_3d,
22 region_2d_to_location_3d,
23 location_3d_to_region_2d,
25 from .carver_profils import (
26 Profils
29 from .carver_utils import (
30 duplicateObject,
31 UndoListUpdate,
32 createMeshFromData,
33 SelectObject,
34 Selection_Save_Restore,
35 Selection_Save,
36 Selection_Restore,
37 update_grid,
38 objDiagonal,
39 Undo,
40 UndoAdd,
41 Pick,
42 rot_axis_quat,
43 MoveCursor,
44 Picking,
45 CreateCutSquare,
46 CreateCutCircle,
47 CreateCutLine,
48 boolean_operation,
49 update_bevel,
50 CreateBevel,
51 Rebool,
52 Snap_Cursor,
55 from .carver_draw import draw_callback_px
57 # Modal Operator
58 class CARVER_OT_operator(bpy.types.Operator):
59 bl_idname = "carver.operator"
60 bl_label = "Carver"
61 bl_description = "Cut or create Meshes in Object mode"
62 bl_options = {'REGISTER', 'UNDO'}
64 def __init__(self):
65 context = bpy.context
66 # Carve mode: Cut, Object, Profile
67 self.CutMode = False
68 self.CreateMode = False
69 self.ObjectMode = False
70 self.ProfileMode = False
72 # Create mode
73 self.ExclusiveCreateMode = False
74 if len(context.selected_objects) == 0:
75 self.ExclusiveCreateMode = True
76 self.CreateMode = True
78 # Cut type (Rectangle, Circle, Line)
79 self.rectangle = 0
80 self.line = 1
81 self.circle = 2
83 # Cut Rectangle coordinates
84 self.rectangle_coord = []
86 # Selected type of cut
87 self.CutType = 0
89 # Boolean operation
90 self.difference = 0
91 self.union = 1
93 self.BoolOps = self.difference
95 self.CurrentSelection = context.selected_objects.copy()
96 self.CurrentActive = context.active_object
97 self.all_sel_obj_list = context.selected_objects.copy()
98 self.save_active_obj = None
100 args = (self, context)
101 self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
103 self.mouse_path = [(0, 0), (0, 0)]
105 # Keyboard event
106 self.shift = False
107 self.ctrl = False
108 self.alt = False
110 self.dont_apply_boolean = context.scene.mesh_carver.DontApply
111 self.Auto_BevelUpdate = True
113 # Circle variables
114 self.stepAngle = [2, 4, 5, 6, 9, 10, 15, 20, 30, 40, 45, 60, 72, 90]
115 self.step = 4
117 # Primitives Position
118 self.xpos = 0
119 self.ypos = 0
120 self.InitPosition = False
122 # Close polygonal shape
123 self.Closed = False
125 # Depth Cursor
126 self.snapCursor = context.scene.mesh_carver.DepthCursor
128 # Help
129 self.AskHelp = False
131 # Working object
132 self.OpsObj = context.active_object
134 # Rebool forced (cut line)
135 self.ForceRebool = False
137 self.ViewVector = Vector()
138 self.CurrentObj = None
140 # Brush
141 self.BrushSolidify = False
142 self.WidthSolidify = False
143 self.CarveDepth = False
144 self.BrushDepth = False
145 self.BrushDepthOffset = 0.0
146 self.snap = False
148 self.ObjectScale = False
150 #Init create circle primitive
151 self.CLR_C = []
153 # Cursor location
154 self.CurLoc = Vector((0.0, 0.0, 0.0))
155 self.SavCurLoc = Vector((0.0, 0.0, 0.0))
157 # Mouse region
158 self.mouse_region = -1, -1
159 self.SavMousePos = None
160 self.xSavMouse = 0
162 # Scale, rotate object
163 self.ascale = 0
164 self.aRotZ = 0
165 self.nRotZ = 0
166 self.quat_rot_axis = None
167 self.quat_rot = None
169 self.RandomRotation = context.scene.mesh_carver.ORandom
171 self.ShowCursor = True
173 self.Instantiate = context.scene.mesh_carver.OInstanciate
175 self.ProfileBrush = None
176 self.ObjectBrush = None
178 self.InitBrush = {
179 'location' : None,
180 'scale' : None,
181 'rotation_quaternion' : None,
182 'rotation_euler' : None,
183 'display_type' : 'WIRE',
184 'show_in_front' : False
187 # Array variables
188 self.nbcol = 1
189 self.nbrow = 1
190 self.gapx = 0
191 self.gapy = 0
192 self.scale_x = 1
193 self.scale_y = 1
194 self.GridScaleX = False
195 self.GridScaleY = False
197 @classmethod
198 def poll(cls, context):
199 ob = None
200 if len(context.selected_objects) > 0:
201 ob = context.selected_objects[0]
202 # Test if selected object or none (for create mode)
203 return (
204 (ob and ob.type == 'MESH' and context.mode == 'OBJECT') or
205 (context.mode == 'OBJECT' and ob is None) or
206 (context.mode == 'EDIT_MESH'))
208 def modal(self, context, event):
209 PI = 3.14156
210 region_types = {'WINDOW', 'UI'}
211 win = context.window
213 # Find the limit of the view3d region
214 self.check_region(context,event)
216 for area in win.screen.areas:
217 if area.type == 'VIEW_3D':
218 for region in area.regions:
219 if not region_types or region.type in region_types:
220 region.tag_redraw()
222 # Change the snap increment value using the wheel mouse
223 if self.CutMode:
224 if self.alt is False:
225 if self.ctrl and (self.CutType in (self.line, self.rectangle)):
226 # Get the VIEW3D area
227 for i, a in enumerate(context.screen.areas):
228 if a.type == 'VIEW_3D':
229 space = context.screen.areas[i].spaces.active
230 grid_scale = space.overlay.grid_scale
231 grid_subdivisions = space.overlay.grid_subdivisions
233 if event.type == 'WHEELUPMOUSE':
234 space.overlay.grid_subdivisions += 1
235 elif event.type == 'WHEELDOWNMOUSE':
236 space.overlay.grid_subdivisions -= 1
238 if event.type in {
239 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
240 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_6',
241 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_5'}:
242 return {'PASS_THROUGH'}
244 try:
245 # [Shift]
246 self.shift = True if event.shift else False
248 # [Ctrl]
249 self.ctrl = True if event.ctrl else False
251 # [Alt]
252 self.alt = False
254 # [Alt] press : Init position variable before moving the cut brush with LMB
255 if event.alt:
256 if self.InitPosition is False:
257 self.xpos = 0
258 self.ypos = 0
259 self.last_mouse_region_x = event.mouse_region_x
260 self.last_mouse_region_y = event.mouse_region_y
261 self.InitPosition = True
262 self.alt = True
264 # [Alt] release : update the coordinates
265 if self.InitPosition and self.alt is False:
266 for i in range(0, len(self.mouse_path)):
267 l = list(self.mouse_path[i])
268 l[0] += self.xpos
269 l[1] += self.ypos
270 self.mouse_path[i] = tuple(l)
272 self.xpos = self.ypos = 0
273 self.InitPosition = False
275 if event.type == 'SPACE' and event.value == 'PRESS':
276 # If object or profile mode is TRUE : Confirm the cut
277 if self.ObjectMode or self.ProfileMode:
278 # If array, remove double with intersect meshes
279 if ((self.nbcol + self.nbrow) > 3):
280 # Go in edit mode mode
281 bpy.ops.object.mode_set(mode='EDIT')
282 # Remove duplicate vertices
283 bpy.ops.mesh.remove_doubles()
284 # Return in object mode
285 bpy.ops.object.mode_set(mode='OBJECT')
287 if self.alt:
288 # Save selected objects
289 self.all_sel_obj_list = context.selected_objects.copy()
290 if len(context.selected_objects) > 0:
291 bpy.ops.object.select_all(action='TOGGLE')
293 if self.ObjectMode:
294 SelectObject(self, self.ObjectBrush)
295 else:
296 SelectObject(self, self.ProfileBrush)
297 duplicateObject(self)
298 else:
299 # Brush Cut
300 self.Cut()
301 # Save selected objects
302 if self.ObjectMode:
303 if len(self.ObjectBrush.children) > 0:
304 self.all_sel_obj_list = context.selected_objects.copy()
305 if len(context.selected_objects) > 0:
306 bpy.ops.object.select_all(action='TOGGLE')
308 if self.ObjectMode:
309 SelectObject(self, self.ObjectBrush)
310 else:
311 SelectObject(self, self.ProfileBrush)
312 duplicateObject(self)
314 UndoListUpdate(self)
316 # Save cursor position
317 self.SavMousePos = self.CurLoc
318 else:
319 if self.CutMode is False:
320 # Cut Mode
321 self.CutType += 1
322 if self.CutType > 2:
323 self.CutType = 0
324 else:
325 if self.CutType == self.line:
326 # Cuts creation
327 CreateCutLine(self, context)
328 if self.CreateMode:
329 # Object creation
330 self.CreateGeometry()
331 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
332 # Cursor Snap
333 context.scene.mesh_carver.DepthCursor = self.snapCursor
334 # Object Instantiate
335 context.scene.mesh_carver.OInstanciate = self.Instantiate
336 # Random rotation
337 context.scene.mesh_carver.ORandom = self.RandomRotation
339 return {'FINISHED'}
340 else:
341 self.Cut()
342 UndoListUpdate(self)
345 #-----------------------------------------------------
346 # Object creation
347 #-----------------------------------------------------
350 # Object creation
351 if event.type == self.carver_prefs.Key_Create and event.value == 'PRESS':
352 if self.ExclusiveCreateMode is False:
353 self.CreateMode = not self.CreateMode
355 # Auto Bevel Update
356 if event.type == self.carver_prefs.Key_Update and event.value == 'PRESS':
357 self.Auto_BevelUpdate = not self.Auto_BevelUpdate
359 # Boolean operation type
360 if event.type == self.carver_prefs.Key_Bool and event.value == 'PRESS':
361 if (self.ProfileMode is True) or (self.ObjectMode is True):
362 if self.BoolOps == self.difference:
363 self.BoolOps = self.union
364 else:
365 self.BoolOps = self.difference
367 # Brush Mode
368 if event.type == self.carver_prefs.Key_Brush and event.value == 'PRESS':
369 self.dont_apply_boolean = False
370 if (self.ProfileMode is False) and (self.ObjectMode is False):
371 self.ProfileMode = True
372 else:
373 self.ProfileMode = False
374 if self.ObjectBrush is not None:
375 if self.ObjectMode is False:
376 self.ObjectMode = True
377 self.BrushSolidify = False
378 self.CList = self.OB_List
380 Selection_Save_Restore(self)
381 context.scene.mesh_carver.nProfile = self.nProfil
382 else:
383 self.ObjectMode = False
384 else:
385 self.BrushSolidify = False
386 Selection_Save_Restore(self)
388 if self.ProfileMode:
389 createMeshFromData(self)
390 self.ProfileBrush = bpy.data.objects["CT_Profil"]
391 Selection_Save(self)
392 self.BrushSolidify = True
394 bpy.ops.object.select_all(action='TOGGLE')
395 self.ProfileBrush.select_set(True)
396 context.view_layer.objects.active = self.ProfileBrush
397 # Set xRay
398 self.ProfileBrush.show_in_front = True
400 solidify_modifier = context.object.modifiers.new("CT_SOLIDIFY",
401 'SOLIDIFY')
402 solidify_modifier.thickness = 0.1
404 Selection_Restore(self)
406 self.CList = self.CurrentSelection
407 else:
408 if self.ObjectBrush is not None:
409 if self.ObjectMode is False:
410 if self.ObjectBrush is not None:
411 self.ObjectBrush.location = self.InitBrush['location']
412 self.ObjectBrush.scale = self.InitBrush['scale']
413 self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion']
414 self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler']
415 self.ObjectBrush.display_type = self.InitBrush['display_type']
416 self.ObjectBrush.show_in_front = self.InitBrush['show_in_front']
418 #Store active and selected objects
419 Selection_Save(self)
421 #Remove Carver modifier
422 self.BrushSolidify = False
423 bpy.ops.object.select_all(action='TOGGLE')
424 self.ObjectBrush.select_set(True)
425 context.view_layer.objects.active = self.ObjectBrush
426 bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY")
428 #Restore selected and active object
429 Selection_Restore(self)
430 else:
431 if self.SolidifyPossible:
432 #Store active and selected objects
433 Selection_Save(self)
434 self.BrushSolidify = True
435 bpy.ops.object.select_all(action='TOGGLE')
436 self.ObjectBrush.select_set(True)
437 context.view_layer.objects.active = self.ObjectBrush
438 # Set xRay
439 self.ObjectBrush.show_in_front = True
440 solidify_modifier = context.object.modifiers.new("CT_SOLIDIFY",
441 'SOLIDIFY')
442 solidify_modifier.thickness = 0.1
444 #Restore selected and active object
445 Selection_Restore(self)
447 # Help display
448 if event.type == self.carver_prefs.Key_Help and event.value == 'PRESS':
449 self.AskHelp = not self.AskHelp
451 # Instantiate object
452 if event.type == self.carver_prefs.Key_Instant and event.value == 'PRESS':
453 self.Instantiate = not self.Instantiate
455 # Close polygonal shape
456 if event.type == self.carver_prefs.Key_Close and event.value == 'PRESS':
457 if self.CreateMode:
458 self.Closed = not self.Closed
460 if event.type == self.carver_prefs.Key_Apply and event.value == 'PRESS':
461 self.dont_apply_boolean = not self.dont_apply_boolean
463 # Scale object
464 if event.type == self.carver_prefs.Key_Scale and event.value == 'PRESS':
465 if self.ObjectScale is False:
466 self.mouse_region = event.mouse_region_x, event.mouse_region_y
467 self.ObjectScale = True
469 # Grid : Snap on grid
470 if event.type == self.carver_prefs.Key_Snap and event.value == 'PRESS':
471 self.snap = not self.snap
473 # Array : Add column
474 if event.type == 'UP_ARROW' and event.value == 'PRESS':
475 self.nbrow += 1
476 update_grid(self, context)
478 # Array : Delete column
479 elif event.type == 'DOWN_ARROW' and event.value == 'PRESS':
480 self.nbrow -= 1
481 update_grid(self, context)
483 # Array : Add row
484 elif event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
485 self.nbcol += 1
486 update_grid(self, context)
488 # Array : Delete row
489 elif event.type == 'LEFT_ARROW' and event.value == 'PRESS':
490 self.nbcol -= 1
491 update_grid(self, context)
493 # Array : Scale gap between columns
494 if event.type == self.carver_prefs.Key_Gapy and event.value == 'PRESS':
495 if self.GridScaleX is False:
496 self.mouse_region = event.mouse_region_x, event.mouse_region_y
497 self.GridScaleX = True
499 # Array : Scale gap between rows
500 if event.type == self.carver_prefs.Key_Gapx and event.value == 'PRESS':
501 if self.GridScaleY is False:
502 self.mouse_region = event.mouse_region_x, event.mouse_region_y
503 self.GridScaleY = True
505 # Cursor depth or solidify pattern
506 if event.type == self.carver_prefs.Key_Depth and event.value == 'PRESS':
507 if (self.ObjectMode is False) and (self.ProfileMode is False):
508 self.snapCursor = not self.snapCursor
509 else:
510 # Solidify
512 if (self.ObjectMode or self.ProfileMode) and (self.SolidifyPossible):
513 solidify = True
515 if self.ObjectMode:
516 z = self.ObjectBrush.data.vertices[0].co.z
517 ErrorMarge = 0.01
518 for v in self.ObjectBrush.data.vertices:
519 if abs(v.co.z - z) > ErrorMarge:
520 solidify = False
521 self.CarveDepth = True
522 self.mouse_region = event.mouse_region_x, event.mouse_region_y
523 break
525 if solidify:
526 if self.ObjectMode:
527 for mb in self.ObjectBrush.modifiers:
528 if mb.type == 'SOLIDIFY':
529 AlreadySoldify = True
530 else:
531 for mb in self.ProfileBrush.modifiers:
532 if mb.type == 'SOLIDIFY':
533 AlreadySoldify = True
535 if AlreadySoldify is False:
536 Selection_Save(self)
537 self.BrushSolidify = True
539 bpy.ops.object.select_all(action='TOGGLE')
540 if self.ObjectMode:
541 self.ObjectBrush.select_set(True)
542 context.view_layer.objects.active = self.ObjectBrush
543 # Active le xray
544 self.ObjectBrush.show_in_front = True
545 else:
546 self.ProfileBrush.select_set(True)
547 context.view_layer.objects.active = self.ProfileBrush
548 # Active le xray
549 self.ProfileBrush.show_in_front = True
551 solidify_modifier = context.object.modifiers.new("CT_SOLIDIFY",
552 'SOLIDIFY')
553 solidify_modifier.thickness = 0.1
555 Selection_Restore(self)
557 self.WidthSolidify = not self.WidthSolidify
558 self.mouse_region = event.mouse_region_x, event.mouse_region_y
560 if event.type == self.carver_prefs.Key_BrushDepth and event.value == 'PRESS':
561 if self.ObjectMode:
562 self.CarveDepth = False
564 self.BrushDepth = True
565 self.mouse_region = event.mouse_region_x, event.mouse_region_y
567 # Random rotation
568 if event.type == 'R' and event.value == 'PRESS':
569 self.RandomRotation = not self.RandomRotation
571 # Undo
572 if event.type == 'Z' and event.value == 'PRESS':
573 if self.ctrl:
574 if (self.CutType == self.line) and (self.CutMode):
575 if len(self.mouse_path) > 1:
576 self.mouse_path[len(self.mouse_path) - 1:] = []
577 else:
578 Undo(self)
580 # Mouse move
581 if event.type == 'MOUSEMOVE' :
582 if self.ObjectMode or self.ProfileMode:
583 fac = 50.0
584 if self.shift:
585 fac = 500.0
586 if self.WidthSolidify:
587 if self.ObjectMode:
588 bpy.data.objects[self.ObjectBrush.name].modifiers[
589 "CT_SOLIDIFY"].thickness += (event.mouse_region_x - self.mouse_region[0]) / fac
590 elif self.ProfileMode:
591 bpy.data.objects[self.ProfileBrush.name].modifiers[
592 "CT_SOLIDIFY"].thickness += (event.mouse_region_x - self.mouse_region[0]) / fac
593 self.mouse_region = event.mouse_region_x, event.mouse_region_y
594 elif self.CarveDepth:
595 for v in self.ObjectBrush.data.vertices:
596 v.co.z += (event.mouse_region_x - self.mouse_region[0]) / fac
597 self.mouse_region = event.mouse_region_x, event.mouse_region_y
598 elif self.BrushDepth:
599 self.BrushDepthOffset += (event.mouse_region_x - self.mouse_region[0]) / fac
600 self.mouse_region = event.mouse_region_x, event.mouse_region_y
601 else:
602 if (self.GridScaleX):
603 self.gapx += (event.mouse_region_x - self.mouse_region[0]) / 50
604 self.mouse_region = event.mouse_region_x, event.mouse_region_y
605 update_grid(self, context)
606 return {'RUNNING_MODAL'}
608 elif (self.GridScaleY):
609 self.gapy += (event.mouse_region_x - self.mouse_region[0]) / 50
610 self.mouse_region = event.mouse_region_x, event.mouse_region_y
611 update_grid(self, context)
612 return {'RUNNING_MODAL'}
614 elif self.ObjectScale:
615 self.ascale = -(event.mouse_region_x - self.mouse_region[0])
616 self.mouse_region = event.mouse_region_x, event.mouse_region_y
618 if self.ObjectMode:
619 self.ObjectBrush.scale.x -= float(self.ascale) / 150.0
620 if self.ObjectBrush.scale.x <= 0.0:
621 self.ObjectBrush.scale.x = 0.0
622 self.ObjectBrush.scale.y -= float(self.ascale) / 150.0
623 if self.ObjectBrush.scale.y <= 0.0:
624 self.ObjectBrush.scale.y = 0.0
625 self.ObjectBrush.scale.z -= float(self.ascale) / 150.0
626 if self.ObjectBrush.scale.z <= 0.0:
627 self.ObjectBrush.scale.z = 0.0
629 elif self.ProfileMode:
630 if self.ProfileBrush is not None:
631 self.ProfileBrush.scale.x -= float(self.ascale) / 150.0
632 self.ProfileBrush.scale.y -= float(self.ascale) / 150.0
633 self.ProfileBrush.scale.z -= float(self.ascale) / 150.0
634 else:
635 if self.LMB:
636 if self.ctrl:
637 self.aRotZ = - \
638 ((int((event.mouse_region_x - self.xSavMouse) / 10.0) * PI / 4.0) * 25.0)
639 else:
640 self.aRotZ -= event.mouse_region_x - self.mouse_region[0]
641 self.ascale = 0.0
643 self.mouse_region = event.mouse_region_x, event.mouse_region_y
644 else:
645 target_hit, target_normal, target_eul_rotation = Pick(context, event, self)
646 if target_hit is not None:
647 self.ShowCursor = True
648 up_vector = Vector((0.0, 0.0, 1.0))
649 quat_rot_axis = rot_axis_quat(up_vector, target_normal)
650 self.quat_rot = target_eul_rotation @ quat_rot_axis
651 MoveCursor(quat_rot_axis, target_hit, self)
652 self.SavCurLoc = target_hit
653 if self.ctrl:
654 if self.SavMousePos is not None:
655 xEcart = abs(self.SavMousePos.x - self.SavCurLoc.x)
656 yEcart = abs(self.SavMousePos.y - self.SavCurLoc.y)
657 zEcart = abs(self.SavMousePos.z - self.SavCurLoc.z)
658 if (xEcart > yEcart) and (xEcart > zEcart):
659 self.CurLoc = Vector(
660 (target_hit.x, self.SavMousePos.y, self.SavMousePos.z))
661 if (yEcart > xEcart) and (yEcart > zEcart):
662 self.CurLoc = Vector(
663 (self.SavMousePos.x, target_hit.y, self.SavMousePos.z))
664 if (zEcart > xEcart) and (zEcart > yEcart):
665 self.CurLoc = Vector(
666 (self.SavMousePos.x, self.SavMousePos.y, target_hit.z))
667 else:
668 self.CurLoc = target_hit
669 else:
670 self.CurLoc = target_hit
671 else:
672 if self.CutMode:
673 if self.alt is False:
674 if self.ctrl :
675 # Find the closest position on the overlay grid and snap the mouse on it
676 # Draw a mini grid around the cursor
677 mouse_pos = [[event.mouse_region_x, event.mouse_region_y]]
678 Snap_Cursor(self, context, event, mouse_pos)
680 else:
681 if len(self.mouse_path) > 0:
682 self.mouse_path[len(self.mouse_path) -
683 1] = (event.mouse_region_x, event.mouse_region_y)
684 else:
685 # [ALT] press, update position
686 self.xpos += (event.mouse_region_x - self.last_mouse_region_x)
687 self.ypos += (event.mouse_region_y - self.last_mouse_region_y)
689 self.last_mouse_region_x = event.mouse_region_x
690 self.last_mouse_region_y = event.mouse_region_y
692 elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
693 if self.ObjectMode or self.ProfileMode:
694 if self.LMB is False:
695 target_hit, target_normal, target_eul_rotation = Pick(context, event, self)
696 if target_hit is not None:
697 up_vector = Vector((0.0, 0.0, 1.0))
698 self.quat_rot_axis = rot_axis_quat(up_vector, target_normal)
699 self.quat_rot = target_eul_rotation @ self.quat_rot_axis
700 self.mouse_region = event.mouse_region_x, event.mouse_region_y
701 self.xSavMouse = event.mouse_region_x
703 if self.ctrl:
704 self.nRotZ = int((self.aRotZ / 25.0) / (PI / 4.0))
705 self.aRotZ = self.nRotZ * (PI / 4.0) * 25.0
707 self.LMB = True
709 # LEFTMOUSE
710 elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE' and self.in_view_3d:
711 if self.ObjectMode or self.ProfileMode:
712 # Rotation and scale
713 self.LMB = False
714 if self.ObjectScale is True:
715 self.ObjectScale = False
717 if self.GridScaleX is True:
718 self.GridScaleX = False
720 if self.GridScaleY is True:
721 self.GridScaleY = False
723 if self.WidthSolidify:
724 self.WidthSolidify = False
726 if self.CarveDepth is True:
727 self.CarveDepth = False
729 if self.BrushDepth is True:
730 self.BrushDepth = False
732 else:
733 if self.CutMode is False:
734 if self.ctrl:
735 Picking(context, event)
737 else:
739 if self.CutType == self.line:
740 if self.CutMode is False:
741 self.mouse_path.clear()
742 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
743 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
744 else:
745 self.mouse_path[0] = (event.mouse_region_x, event.mouse_region_y)
746 self.mouse_path[1] = (event.mouse_region_x, event.mouse_region_y)
747 self.CutMode = True
748 else:
749 if self.CutType != self.line:
750 # Cut creation
751 if self.CutType == self.rectangle:
752 CreateCutSquare(self, context)
753 if self.CutType == self.circle:
754 CreateCutCircle(self, context)
755 if self.CutType == self.line:
756 CreateCutLine(self, context)
758 if self.CreateMode:
759 # Object creation
760 self.CreateGeometry()
761 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
762 # Depth Cursor
763 context.scene.mesh_carver.DepthCursor = self.snapCursor
764 # Instantiate object
765 context.scene.mesh_carver.OInstanciate = self.Instantiate
766 # Random rotation
767 context.scene.mesh_carver.ORandom = self.RandomRotation
768 # Apply operation
769 context.scene.mesh_carver.DontApply = self.dont_apply_boolean
771 # if Object mode, set initiale state
772 if self.ObjectBrush is not None:
773 self.ObjectBrush.location = self.InitBrush['location']
774 self.ObjectBrush.scale = self.InitBrush['scale']
775 self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion']
776 self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler']
777 self.ObjectBrush.display_type = self.InitBrush['display_type']
778 self.ObjectBrush.show_in_front = self.InitBrush['show_in_front']
780 # remove solidify
781 Selection_Save(self)
782 self.BrushSolidify = False
784 bpy.ops.object.select_all(action='TOGGLE')
785 self.ObjectBrush.select_set(True)
786 context.view_layer.objects.active = self.ObjectBrush
788 bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY")
790 Selection_Restore(self)
792 context.scene.mesh_carver.nProfile = self.nProfil
794 return {'FINISHED'}
795 else:
796 self.Cut()
797 UndoListUpdate(self)
798 else:
799 # Line
800 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
802 # Change brush profil or circle subdivisions
803 elif (event.type == 'COMMA' and event.value == 'PRESS') or \
804 (event.type == self.carver_prefs.Key_Subrem and event.value == 'PRESS'):
805 # Brush profil
806 if self.ProfileMode:
807 self.nProfil += 1
808 if self.nProfil >= self.MaxProfil:
809 self.nProfil = 0
810 createMeshFromData(self)
811 # Circle subdivisions
812 if self.CutType == self.circle:
813 self.step += 1
814 if self.step >= len(self.stepAngle):
815 self.step = len(self.stepAngle) - 1
816 # Change brush profil or circle subdivisions
817 elif (event.type == 'PERIOD' and event.value == 'PRESS') or \
818 (event.type == self.carver_prefs.Key_Subadd and event.value == 'PRESS'):
819 # Brush profil
820 if self.ProfileMode:
821 self.nProfil -= 1
822 if self.nProfil < 0:
823 self.nProfil = self.MaxProfil - 1
824 createMeshFromData(self)
825 # Circle subdivisions
826 if self.CutType == self.circle:
827 if self.step > 0:
828 self.step -= 1
829 # Quit
830 elif event.type in {'RIGHTMOUSE', 'ESC'}:
831 # Depth Cursor
832 context.scene.mesh_carver.DepthCursor = self.snapCursor
833 # Instantiate object
834 context.scene.mesh_carver.OInstanciate = self.Instantiate
835 # Random Rotation
836 context.scene.mesh_carver.ORandom = self.RandomRotation
837 # Apply boolean operation
838 context.scene.mesh_carver.DontApply = self.dont_apply_boolean
840 # Reset Object
841 if self.ObjectBrush is not None:
842 self.ObjectBrush.location = self.InitBrush['location']
843 self.ObjectBrush.scale = self.InitBrush['scale']
844 self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion']
845 self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler']
846 self.ObjectBrush.display_type = self.InitBrush['display_type']
847 self.ObjectBrush.show_in_front = self.InitBrush['show_in_front']
849 # Remove solidify modifier
850 Selection_Save(self)
851 self.BrushSolidify = False
853 bpy.ops.object.select_all(action='TOGGLE')
854 self.ObjectBrush.select_set(True)
855 context.view_layer.objects.active = self.ObjectBrush
857 bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY")
858 bpy.ops.object.select_all(action='TOGGLE')
860 Selection_Restore(self)
862 Selection_Save_Restore(self)
863 context.view_layer.objects.active = self.CurrentActive
864 context.scene.mesh_carver.nProfile = self.nProfil
866 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
868 # Remove Copy Object Brush
869 if bpy.data.objects.get("CarverBrushCopy") is not None:
870 brush = bpy.data.objects["CarverBrushCopy"]
871 self.ObjectBrush.data = bpy.data.meshes[brush.data.name]
872 bpy.ops.object.select_all(action='DESELECT')
873 bpy.data.objects["CarverBrushCopy"].select_set(True)
874 bpy.ops.object.delete()
876 return {'FINISHED'}
878 return {'RUNNING_MODAL'}
880 except:
881 print("\n[Carver MT ERROR]\n")
882 import traceback
883 traceback.print_exc()
885 context.window.cursor_modal_set("DEFAULT")
886 context.area.header_text_set(None)
887 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
889 self.report({'WARNING'},
890 "Operation finished. Failure during Carving (Check the console for more info)")
892 return {'FINISHED'}
894 def cancel(self, context):
895 # Note: used to prevent memory leaks on quitting Blender while the modal operator
896 # is still running, gets called on return {"CANCELLED"}
897 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
899 def invoke(self, context, event):
900 if context.area.type != 'VIEW_3D':
901 self.report({'WARNING'},
902 "View3D not found or not currently active. Operation Cancelled")
903 self.cancel(context)
904 return {'CANCELLED'}
906 # test if some other object types are selected that are not meshes
907 for obj in context.selected_objects:
908 if obj.type != "MESH":
909 self.report({'WARNING'},
910 "Some selected objects are not of the Mesh type. Operation Cancelled")
911 self.cancel(context)
912 return {'CANCELLED'}
914 if context.mode == 'EDIT_MESH':
915 bpy.ops.object.mode_set(mode='OBJECT')
917 #Load the Carver preferences
918 self.carver_prefs = bpy.context.preferences.addons[__package__].preferences
920 # Get default patterns
921 self.Profils = []
922 for p in Profils:
923 self.Profils.append((p[0], p[1], p[2], p[3]))
925 for o in context.scene.objects:
926 if not o.name.startswith(self.carver_prefs.ProfilePrefix):
927 continue
928 # In-scene profiles may have changed, remove them to refresh
929 for m in bpy.data.meshes:
930 if m.name.startswith(self.carver_prefs.ProfilePrefix):
931 bpy.data.meshes.remove(m)
933 vertices = []
934 for v in o.data.vertices:
935 vertices.append((v.co.x, v.co.y, v.co.z))
937 faces = []
938 for f in o.data.polygons:
939 face = []
940 for v in f.vertices:
941 face.append(v)
943 faces.append(face)
945 self.Profils.append(
946 (o.name,
947 Vector((o.location.x, o.location.y, o.location.z)),
948 vertices, faces)
951 self.nProfil = context.scene.mesh_carver.nProfile
952 self.MaxProfil = len(self.Profils)
955 # reset selected profile if last profile exceeds length of array
956 if self.nProfil >= self.MaxProfil:
957 self.nProfil = context.scene.mesh_carver.nProfile = 0
959 if len(context.selected_objects) > 1:
960 self.ObjectBrush = context.active_object
962 # Copy the brush object
963 ob = bpy.data.objects.new("CarverBrushCopy", context.object.data.copy())
964 ob.location = self.ObjectBrush.location
965 context.collection.objects.link(ob)
966 context.view_layer.update()
968 # Save default variables
969 self.InitBrush['location'] = self.ObjectBrush.location.copy()
970 self.InitBrush['scale'] = self.ObjectBrush.scale.copy()
971 self.InitBrush['rotation_quaternion'] = self.ObjectBrush.rotation_quaternion.copy()
972 self.InitBrush['rotation_euler'] = self.ObjectBrush.rotation_euler.copy()
973 self.InitBrush['display_type'] = self.ObjectBrush.display_type
974 self.InitBrush['show_in_front'] = self.ObjectBrush.show_in_front
976 # Test if flat object
977 z = self.ObjectBrush.data.vertices[0].co.z
978 ErrorMarge = 0.01
979 self.SolidifyPossible = True
980 for v in self.ObjectBrush.data.vertices:
981 if abs(v.co.z - z) > ErrorMarge:
982 self.SolidifyPossible = False
983 break
985 self.CList = []
986 self.OPList = []
987 self.RList = []
988 self.OB_List = []
990 for obj in context.selected_objects:
991 if obj != self.ObjectBrush:
992 self.OB_List.append(obj)
994 # Left button
995 self.LMB = False
997 # Undo Variables
998 self.undo_index = 0
999 self.undo_limit = context.preferences.edit.undo_steps
1000 self.undo_list = []
1002 # Boolean operations type
1003 self.BooleanType = 0
1005 self.UList = []
1006 self.UList_Index = -1
1007 self.UndoOps = []
1009 context.window_manager.modal_handler_add(self)
1010 return {'RUNNING_MODAL'}
1012 #Get the region area where the operator is used
1013 def check_region(self,context,event):
1014 if context.area != None:
1015 if context.area.type == "VIEW_3D" :
1016 for region in context.area.regions:
1017 if region.type == "TOOLS":
1018 t_panel = region
1019 elif region.type == "UI":
1020 ui_panel = region
1022 view_3d_region_x = Vector((context.area.x + t_panel.width, context.area.x + context.area.width - ui_panel.width))
1023 view_3d_region_y = Vector((context.region.y, context.region.y+context.region.height))
1025 if (event.mouse_x > view_3d_region_x[0] and event.mouse_x < view_3d_region_x[1] \
1026 and event.mouse_y > view_3d_region_y[0] and event.mouse_y < view_3d_region_y[1]):
1027 self.in_view_3d = True
1028 else:
1029 self.in_view_3d = False
1030 else:
1031 self.in_view_3d = False
1033 def CreateGeometry(self):
1034 context = bpy.context
1035 in_local_view = False
1037 for area in context.screen.areas:
1038 if area.type == 'VIEW_3D':
1039 if area.spaces[0].local_view is not None:
1040 in_local_view = True
1042 if in_local_view:
1043 bpy.ops.view3d.localview()
1045 if self.ExclusiveCreateMode:
1046 # Default width
1047 objBBDiagonal = 0.5
1048 else:
1049 ActiveObj = self.CurrentSelection[0]
1050 if ActiveObj is not None:
1051 # Object dimensions
1052 objBBDiagonal = objDiagonal(ActiveObj) / 4
1053 subdivisions = 2
1055 if len(context.selected_objects) > 0:
1056 bpy.ops.object.select_all(action='TOGGLE')
1058 context.view_layer.objects.active = self.CurrentObj
1060 bpy.data.objects[self.CurrentObj.name].select_set(True)
1061 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
1063 bpy.ops.object.mode_set(mode='EDIT')
1064 bpy.ops.mesh.select_all(action='SELECT')
1065 bpy.ops.mesh.select_mode(type="EDGE")
1066 if self.snapCursor is False:
1067 bpy.ops.transform.translate(value=self.ViewVector * objBBDiagonal * subdivisions)
1068 bpy.ops.mesh.extrude_region_move(
1069 TRANSFORM_OT_translate={"value": -self.ViewVector * objBBDiagonal * subdivisions * 2})
1071 bpy.ops.mesh.select_all(action='SELECT')
1072 bpy.ops.mesh.normals_make_consistent()
1073 bpy.ops.object.mode_set(mode='OBJECT')
1075 saved_location_0 = context.scene.cursor.location.copy()
1076 bpy.ops.view3d.snap_cursor_to_active()
1077 saved_location = context.scene.cursor.location.copy()
1078 bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
1079 context.scene.cursor.location = saved_location
1080 bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
1081 context.scene.cursor.location = saved_location_0
1083 bpy.data.objects[self.CurrentObj.name].select_set(True)
1084 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
1086 for o in self.all_sel_obj_list:
1087 bpy.data.objects[o.name].select_set(True)
1089 if in_local_view:
1090 bpy.ops.view3d.localview()
1092 self.CutMode = False
1093 self.mouse_path.clear()
1094 self.mouse_path = [(0, 0), (0, 0)]
1096 def Cut(self):
1097 context = bpy.context
1099 # Local view ?
1100 in_local_view = False
1101 for area in context.screen.areas:
1102 if area.type == 'VIEW_3D':
1103 if area.spaces[0].local_view is not None:
1104 in_local_view = True
1106 if in_local_view:
1107 bpy.ops.view3d.localview()
1109 # Save cursor position
1110 CursorLocation = context.scene.cursor.location.copy()
1112 #List of selected objects
1113 selected_obj_list = []
1115 #Cut Mode with line
1116 if (self.ObjectMode is False) and (self.ProfileMode is False):
1118 #Compute the bounding Box
1119 objBBDiagonal = objDiagonal(self.CurrentSelection[0])
1120 if self.dont_apply_boolean:
1121 subdivisions = 1
1122 else:
1123 subdivisions = 32
1125 # Get selected objects
1126 selected_obj_list = context.selected_objects.copy()
1128 bpy.ops.object.select_all(action='TOGGLE')
1130 context.view_layer.objects.active = self.CurrentObj
1132 bpy.data.objects[self.CurrentObj.name].select_set(True)
1133 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
1135 bpy.ops.object.mode_set(mode='EDIT')
1136 bpy.ops.mesh.select_all(action='SELECT')
1137 bpy.ops.mesh.select_mode(type="EDGE")
1138 #Translate the created mesh away from the view
1139 if (self.snapCursor is False) or (self.ForceRebool):
1140 bpy.ops.transform.translate(value=self.ViewVector * objBBDiagonal * subdivisions)
1141 #Extrude the mesh region and move the result
1142 bpy.ops.mesh.extrude_region_move(
1143 TRANSFORM_OT_translate={"value": -self.ViewVector * objBBDiagonal * subdivisions * 2})
1144 bpy.ops.mesh.select_all(action='SELECT')
1145 bpy.ops.mesh.normals_make_consistent()
1146 bpy.ops.object.mode_set(mode='OBJECT')
1147 else:
1148 # Create list
1149 if self.ObjectMode:
1150 for o in self.CurrentSelection:
1151 if o != self.ObjectBrush:
1152 selected_obj_list.append(o)
1153 self.CurrentObj = self.ObjectBrush
1154 else:
1155 selected_obj_list = self.CurrentSelection
1156 self.CurrentObj = self.ProfileBrush
1158 for obj in self.CurrentSelection:
1159 UndoAdd(self, "MESH", obj)
1161 # List objects create with rebool
1162 lastSelected = []
1164 for ActiveObj in selected_obj_list:
1165 context.scene.cursor.location = CursorLocation
1167 if len(context.selected_objects) > 0:
1168 bpy.ops.object.select_all(action='TOGGLE')
1170 # Select cut object
1171 bpy.data.objects[self.CurrentObj.name].select_set(True)
1172 context.view_layer.objects.active = self.CurrentObj
1174 bpy.ops.object.mode_set(mode='EDIT')
1175 bpy.ops.mesh.select_all(action='SELECT')
1176 bpy.ops.object.mode_set(mode='OBJECT')
1178 # Select object to cut
1179 bpy.data.objects[ActiveObj.name].select_set(True)
1180 context.view_layer.objects.active = ActiveObj
1182 bpy.ops.object.mode_set(mode='EDIT')
1183 bpy.ops.mesh.select_all(action='DESELECT')
1184 bpy.ops.object.mode_set(mode='OBJECT')
1186 # Boolean operation
1187 if (self.shift is False) and (self.ForceRebool is False):
1188 if self.ObjectMode or self.ProfileMode:
1189 if self.BoolOps == self.union:
1190 boolean_operation(bool_type="UNION")
1191 else:
1192 boolean_operation(bool_type="DIFFERENCE")
1193 else:
1194 boolean_operation(bool_type="DIFFERENCE")
1196 # Apply booleans
1197 if self.dont_apply_boolean is False:
1198 BMname = "CT_" + self.CurrentObj.name
1199 for mb in ActiveObj.modifiers:
1200 if (mb.type == 'BOOLEAN') and (mb.name == BMname):
1201 try:
1202 bpy.ops.object.modifier_apply(modifier=BMname)
1203 except:
1204 bpy.ops.object.modifier_remove(modifier=BMname)
1205 exc_type, exc_value, exc_traceback = sys.exc_info()
1206 self.report({'ERROR'}, str(exc_value))
1208 bpy.ops.object.select_all(action='TOGGLE')
1209 else:
1210 if self.ObjectMode or self.ProfileMode:
1211 for mb in self.CurrentObj.modifiers:
1212 if (mb.type == 'SOLIDIFY') and (mb.name == "CT_SOLIDIFY"):
1213 try:
1214 bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY")
1215 except:
1216 exc_type, exc_value, exc_traceback = sys.exc_info()
1217 self.report({'ERROR'}, str(exc_value))
1219 # Rebool
1220 Rebool(context, self)
1222 # Test if not empty object
1223 if context.selected_objects[0]:
1224 rebool_RT = context.selected_objects[0]
1225 if len(rebool_RT.data.vertices) > 0:
1226 # Create Bevel for new objects
1227 CreateBevel(context, context.selected_objects[0])
1229 UndoAdd(self, "REBOOL", context.selected_objects[0])
1231 context.scene.cursor.location = ActiveObj.location
1232 bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
1233 else:
1234 bpy.ops.object.delete(use_global=False)
1236 context.scene.cursor.location = CursorLocation
1238 if self.ObjectMode:
1239 context.view_layer.objects.active = self.ObjectBrush
1240 if self.ProfileMode:
1241 context.view_layer.objects.active = self.ProfileBrush
1243 if self.dont_apply_boolean is False:
1244 # Apply booleans
1245 BMname = "CT_" + self.CurrentObj.name
1246 for mb in ActiveObj.modifiers:
1247 if (mb.type == 'BOOLEAN') and (mb.name == BMname):
1248 try:
1249 bpy.ops.object.modifier_apply(modifier=BMname)
1250 except:
1251 bpy.ops.object.modifier_remove(modifier=BMname)
1252 exc_type, exc_value, exc_traceback = sys.exc_info()
1253 self.report({'ERROR'}, str(exc_value))
1254 # Get new objects created with rebool operations
1255 if len(context.selected_objects) > 0:
1256 if self.shift is True:
1257 # Get the last object selected
1258 lastSelected.append(context.selected_objects[0])
1260 context.scene.cursor.location = CursorLocation
1262 if self.dont_apply_boolean is False:
1263 # Remove cut object
1264 if (self.ObjectMode is False) and (self.ProfileMode is False):
1265 if len(context.selected_objects) > 0:
1266 bpy.ops.object.select_all(action='TOGGLE')
1267 bpy.data.objects[self.CurrentObj.name].select_set(True)
1268 bpy.ops.object.delete(use_global=False)
1269 else:
1270 if self.ObjectMode:
1271 self.ObjectBrush.display_type = self.InitBrush['display_type']
1273 if len(context.selected_objects) > 0:
1274 bpy.ops.object.select_all(action='TOGGLE')
1276 # Select cut objects
1277 for obj in lastSelected:
1278 bpy.data.objects[obj.name].select_set(True)
1280 for ActiveObj in selected_obj_list:
1281 bpy.data.objects[ActiveObj.name].select_set(True)
1282 context.view_layer.objects.active = ActiveObj
1283 # Update bevel
1284 list_act_obj = context.selected_objects.copy()
1285 if self.Auto_BevelUpdate:
1286 update_bevel(context)
1288 # Re-select initial objects
1289 bpy.ops.object.select_all(action='TOGGLE')
1290 if self.ObjectMode:
1291 # Re-select brush
1292 self.ObjectBrush.select_set(True)
1293 for ActiveObj in selected_obj_list:
1294 bpy.data.objects[ActiveObj.name].select_set(True)
1295 context.view_layer.objects.active = ActiveObj
1297 # If object has children, set "Wire" draw type
1298 if self.ObjectBrush is not None:
1299 if len(self.ObjectBrush.children) > 0:
1300 self.ObjectBrush.display_type = "WIRE"
1301 if self.ProfileMode:
1302 self.ProfileBrush.display_type = "WIRE"
1304 if in_local_view:
1305 bpy.ops.view3d.localview()
1307 # Reset variables
1308 self.CutMode = False
1309 self.mouse_path.clear()
1310 self.mouse_path = [(0, 0), (0, 0)]
1312 self.ForceRebool = False
1314 # bpy.ops.mesh.customdata_custom_splitnormals_clear()
1317 class CarverProperties(bpy.types.PropertyGroup):
1318 DepthCursor: BoolProperty(
1319 name="DepthCursor",
1320 default=False
1322 OInstanciate: BoolProperty(
1323 name="Obj_Instantiate",
1324 default=False
1326 ORandom: BoolProperty(
1327 name="Random_Rotation",
1328 default=False
1330 DontApply: BoolProperty(
1331 name="Dont_Apply",
1332 default=False
1334 nProfile: IntProperty(
1335 name="Num_Profile",
1336 default=0
1340 def register():
1341 from bpy.utils import register_class
1342 bpy.utils.register_class(CARVER_OT_operator)
1343 bpy.utils.register_class(CarverProperties)
1344 bpy.types.Scene.mesh_carver = bpy.props.PointerProperty(type=CarverProperties)
1346 def unregister():
1347 from bpy.utils import unregister_class
1348 bpy.utils.unregister_class(CarverProperties)
1349 bpy.utils.unregister_class(CARVER_OT_operator)
1350 del bpy.types.Scene.mesh_carver