Merge branch 'blender-v2.81-release'
[blender-addons.git] / object_carver / carver_operator.py
blobc75a5ab7640efcc61650c1147d8416177dd71023
1 import bpy
2 import bpy_extras
3 import sys
4 from bpy.props import (
5 BoolProperty,
6 IntProperty,
7 PointerProperty,
8 StringProperty,
9 EnumProperty,
11 from mathutils import (
12 Vector,
15 from bpy_extras.view3d_utils import (
16 region_2d_to_vector_3d,
17 region_2d_to_origin_3d,
18 region_2d_to_location_3d,
19 location_3d_to_region_2d,
21 from .carver_profils import (
22 Profils
25 from .carver_utils import (
26 duplicateObject,
27 UndoListUpdate,
28 createMeshFromData,
29 SelectObject,
30 Selection_Save_Restore,
31 Selection_Save,
32 Selection_Restore,
33 update_grid,
34 objDiagonal,
35 Undo,
36 UndoAdd,
37 Pick,
38 rot_axis_quat,
39 MoveCursor,
40 Picking,
41 CreateCutSquare,
42 CreateCutCircle,
43 CreateCutLine,
44 boolean_operation,
45 update_bevel,
46 CreateBevel,
47 Rebool,
48 Snap_Cursor,
51 from .carver_draw import draw_callback_px
53 # Modal Operator
54 class CARVER_OT_operator(bpy.types.Operator):
55 bl_idname = "carver.operator"
56 bl_label = "Carver"
57 bl_description = "Cut or create Meshes in Object mode"
58 bl_options = {'REGISTER', 'UNDO'}
60 def __init__(self):
61 context = bpy.context
62 # Carve mode: Cut, Object, Profile
63 self.CutMode = False
64 self.CreateMode = False
65 self.ObjectMode = False
66 self.ProfileMode = False
68 # Create mode
69 self.ExclusiveCreateMode = False
70 if len(context.selected_objects) == 0:
71 self.ExclusiveCreateMode = True
72 self.CreateMode = True
74 # Cut type (Rectangle, Circle, Line)
75 self.rectangle = 0
76 self.line = 1
77 self.circle = 2
79 # Cut Rectangle coordinates
80 self.rectangle_coord = []
82 # Selected type of cut
83 self.CutType = 0
85 # Boolean operation
86 self.difference = 0
87 self.union = 1
89 self.BoolOps = self.difference
91 self.CurrentSelection = context.selected_objects.copy()
92 self.CurrentActive = context.active_object
93 self.all_sel_obj_list = context.selected_objects.copy()
94 self.save_active_obj = None
96 args = (self, context)
97 self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
99 self.mouse_path = [(0, 0), (0, 0)]
101 # Keyboard event
102 self.shift = False
103 self.ctrl = False
104 self.alt = False
106 self.dont_apply_boolean = context.scene.mesh_carver.DontApply
107 self.Auto_BevelUpdate = True
109 # Circle variables
110 self.stepAngle = [2, 4, 5, 6, 9, 10, 15, 20, 30, 40, 45, 60, 72, 90]
111 self.step = 4
113 # Primitives Position
114 self.xpos = 0
115 self.ypos = 0
116 self.InitPosition = False
118 # Close polygonal shape
119 self.Closed = False
121 # Depth Cursor
122 self.snapCursor = context.scene.mesh_carver.DepthCursor
124 # Help
125 self.AskHelp = False
127 # Working object
128 self.OpsObj = context.active_object
130 # Rebool forced (cut line)
131 self.ForceRebool = False
133 self.ViewVector = Vector()
134 self.CurrentObj = None
136 # Brush
137 self.BrushSolidify = False
138 self.WidthSolidify = False
139 self.CarveDepth = False
140 self.BrushDepth = False
141 self.BrushDepthOffset = 0.0
142 self.snap = False
144 self.ObjectScale = False
146 #Init create circle primitive
147 self.CLR_C = []
149 # Cursor location
150 self.CurLoc = Vector((0.0, 0.0, 0.0))
151 self.SavCurLoc = Vector((0.0, 0.0, 0.0))
153 # Mouse region
154 self.mouse_region = -1, -1
155 self.SavMousePos = None
156 self.xSavMouse = 0
158 # Scale, rotate object
159 self.ascale = 0
160 self.aRotZ = 0
161 self.nRotZ = 0
162 self.quat_rot_axis = None
163 self.quat_rot = None
165 self.RandomRotation = context.scene.mesh_carver.ORandom
167 self.ShowCursor = True
169 self.Instantiate = context.scene.mesh_carver.OInstanciate
171 self.ProfileBrush = None
172 self.ObjectBrush = None
174 self.InitBrush = {
175 'location' : None,
176 'scale' : None,
177 'rotation_quaternion' : None,
178 'rotation_euler' : None,
179 'display_type' : 'WIRE',
180 'show_in_front' : False
183 # Array variables
184 self.nbcol = 1
185 self.nbrow = 1
186 self.gapx = 0
187 self.gapy = 0
188 self.scale_x = 1
189 self.scale_y = 1
190 self.GridScaleX = False
191 self.GridScaleY = False
193 @classmethod
194 def poll(cls, context):
195 ob = None
196 if len(context.selected_objects) > 0:
197 ob = context.selected_objects[0]
198 # Test if selected object or none (for create mode)
199 return (
200 (ob and ob.type == 'MESH' and context.mode == 'OBJECT') or
201 (context.mode == 'OBJECT' and ob is None) or
202 (context.mode == 'EDIT_MESH'))
204 def modal(self, context, event):
205 PI = 3.14156
206 region_types = {'WINDOW', 'UI'}
207 win = context.window
209 # Find the limit of the view3d region
210 self.check_region(context,event)
212 for area in win.screen.areas:
213 if area.type == 'VIEW_3D':
214 for region in area.regions:
215 if not region_types or region.type in region_types:
216 region.tag_redraw()
218 # Change the snap increment value using the wheel mouse
219 if self.CutMode:
220 if self.alt is False:
221 if self.ctrl and (self.CutType in (self.line, self.rectangle)):
222 # Get the VIEW3D area
223 for i, a in enumerate(context.screen.areas):
224 if a.type == 'VIEW_3D':
225 space = context.screen.areas[i].spaces.active
226 grid_scale = space.overlay.grid_scale
227 grid_subdivisions = space.overlay.grid_subdivisions
229 if event.type == 'WHEELUPMOUSE':
230 space.overlay.grid_subdivisions += 1
231 elif event.type == 'WHEELDOWNMOUSE':
232 space.overlay.grid_subdivisions -= 1
234 if event.type in {
235 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
236 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_6',
237 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_5'}:
238 return {'PASS_THROUGH'}
240 try:
241 # [Shift]
242 self.shift = True if event.shift else False
244 # [Ctrl]
245 self.ctrl = True if event.ctrl else False
247 # [Alt]
248 self.alt = False
250 # [Alt] press : Init position variable before moving the cut brush with LMB
251 if event.alt:
252 if self.InitPosition is False:
253 self.xpos = 0
254 self.ypos = 0
255 self.last_mouse_region_x = event.mouse_region_x
256 self.last_mouse_region_y = event.mouse_region_y
257 self.InitPosition = True
258 self.alt = True
260 # [Alt] release : update the coordinates
261 if self.InitPosition and self.alt is False:
262 for i in range(0, len(self.mouse_path)):
263 l = list(self.mouse_path[i])
264 l[0] += self.xpos
265 l[1] += self.ypos
266 self.mouse_path[i] = tuple(l)
268 self.xpos = self.ypos = 0
269 self.InitPosition = False
271 if event.type == 'SPACE' and event.value == 'PRESS':
272 # If object or profile mode is TRUE : Confirm the cut
273 if self.ObjectMode or self.ProfileMode:
274 # If array, remove double with intersect meshes
275 if ((self.nbcol + self.nbrow) > 3):
276 # Go in edit mode mode
277 bpy.ops.object.mode_set(mode='EDIT')
278 # Remove duplicate vertices
279 bpy.ops.mesh.remove_doubles()
280 # Return in object mode
281 bpy.ops.object.mode_set(mode='OBJECT')
283 if self.alt:
284 # Save selected objects
285 self.all_sel_obj_list = context.selected_objects.copy()
286 if len(context.selected_objects) > 0:
287 bpy.ops.object.select_all(action='TOGGLE')
289 if self.ObjectMode:
290 SelectObject(self, self.ObjectBrush)
291 else:
292 SelectObject(self, self.ProfileBrush)
293 duplicateObject(self)
294 else:
295 # Brush Cut
296 self.Cut()
297 # Save selected objects
298 if self.ObjectMode:
299 if len(self.ObjectBrush.children) > 0:
300 self.all_sel_obj_list = context.selected_objects.copy()
301 if len(context.selected_objects) > 0:
302 bpy.ops.object.select_all(action='TOGGLE')
304 if self.ObjectMode:
305 SelectObject(self, self.ObjectBrush)
306 else:
307 SelectObject(self, self.ProfileBrush)
308 duplicateObject(self)
310 UndoListUpdate(self)
312 # Save cursor position
313 self.SavMousePos = self.CurLoc
314 else:
315 if self.CutMode is False:
316 # Cut Mode
317 self.CutType += 1
318 if self.CutType > 2:
319 self.CutType = 0
320 else:
321 if self.CutType == self.line:
322 # Cuts creation
323 CreateCutLine(self, context)
324 if self.CreateMode:
325 # Object creation
326 self.CreateGeometry()
327 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
328 # Cursor Snap
329 context.scene.mesh_carver.DepthCursor = self.snapCursor
330 # Object Instantiate
331 context.scene.mesh_carver.OInstanciate = self.Instantiate
332 # Random rotation
333 context.scene.mesh_carver.ORandom = self.RandomRotation
335 return {'FINISHED'}
336 else:
337 self.Cut()
338 UndoListUpdate(self)
341 #-----------------------------------------------------
342 # Object creation
343 #-----------------------------------------------------
346 # Object creation
347 if event.type == self.carver_prefs.Key_Create and event.value == 'PRESS':
348 if self.ExclusiveCreateMode is False:
349 self.CreateMode = not self.CreateMode
351 # Auto Bevel Update
352 if event.type == self.carver_prefs.Key_Update and event.value == 'PRESS':
353 self.Auto_BevelUpdate = not self.Auto_BevelUpdate
355 # Boolean operation type
356 if event.type == self.carver_prefs.Key_Bool and event.value == 'PRESS':
357 if (self.ProfileMode is True) or (self.ObjectMode is True):
358 if self.BoolOps == self.difference:
359 self.BoolOps = self.union
360 else:
361 self.BoolOps = self.difference
363 # Brush Mode
364 if event.type == self.carver_prefs.Key_Brush and event.value == 'PRESS':
365 self.dont_apply_boolean = False
366 if (self.ProfileMode is False) and (self.ObjectMode is False):
367 self.ProfileMode = True
368 else:
369 self.ProfileMode = False
370 if self.ObjectBrush is not None:
371 if self.ObjectMode is False:
372 self.ObjectMode = True
373 self.BrushSolidify = False
374 self.CList = self.OB_List
376 Selection_Save_Restore(self)
377 context.scene.mesh_carver.nProfile = self.nProfil
378 else:
379 self.ObjectMode = False
380 else:
381 self.BrushSolidify = False
382 Selection_Save_Restore(self)
384 if self.ProfileMode:
385 createMeshFromData(self)
386 self.ProfileBrush = bpy.data.objects["CT_Profil"]
387 Selection_Save(self)
388 self.BrushSolidify = True
390 bpy.ops.object.select_all(action='TOGGLE')
391 self.ProfileBrush.select_set(True)
392 context.view_layer.objects.active = self.ProfileBrush
393 # Set xRay
394 self.ProfileBrush.show_in_front = True
396 bpy.ops.object.modifier_add(type='SOLIDIFY')
397 context.object.modifiers["Solidify"].name = "CT_SOLIDIFY"
398 context.object.modifiers["CT_SOLIDIFY"].thickness = 0.1
400 Selection_Restore(self)
402 self.CList = self.CurrentSelection
403 else:
404 if self.ObjectBrush is not None:
405 if self.ObjectMode is False:
406 if self.ObjectBrush is not None:
407 self.ObjectBrush.location = self.InitBrush['location']
408 self.ObjectBrush.scale = self.InitBrush['scale']
409 self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion']
410 self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler']
411 self.ObjectBrush.display_type = self.InitBrush['display_type']
412 self.ObjectBrush.show_in_front = self.InitBrush['show_in_front']
414 #Store active and selected objects
415 Selection_Save(self)
417 #Remove Carver modifier
418 self.BrushSolidify = False
419 bpy.ops.object.select_all(action='TOGGLE')
420 self.ObjectBrush.select_set(True)
421 context.view_layer.objects.active = self.ObjectBrush
422 bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY")
424 #Restore selected and active object
425 Selection_Restore(self)
426 else:
427 if self.SolidifyPossible:
428 #Store active and selected objects
429 Selection_Save(self)
430 self.BrushSolidify = True
431 bpy.ops.object.select_all(action='TOGGLE')
432 self.ObjectBrush.select_set(True)
433 context.view_layer.objects.active = self.ObjectBrush
434 # Set xRay
435 self.ObjectBrush.show_in_front = True
436 bpy.ops.object.modifier_add(type='SOLIDIFY')
437 context.object.modifiers["Solidify"].name = "CT_SOLIDIFY"
438 context.object.modifiers["CT_SOLIDIFY"].thickness = 0.1
440 #Restore selected and active object
441 Selection_Restore(self)
443 # Help display
444 if event.type == self.carver_prefs.Key_Help and event.value == 'PRESS':
445 self.AskHelp = not self.AskHelp
447 # Instantiate object
448 if event.type == self.carver_prefs.Key_Instant and event.value == 'PRESS':
449 self.Instantiate = not self.Instantiate
451 # Close polygonal shape
452 if event.type == self.carver_prefs.Key_Close and event.value == 'PRESS':
453 if self.CreateMode:
454 self.Closed = not self.Closed
456 if event.type == self.carver_prefs.Key_Apply and event.value == 'PRESS':
457 self.dont_apply_boolean = not self.dont_apply_boolean
459 # Scale object
460 if event.type == self.carver_prefs.Key_Scale and event.value == 'PRESS':
461 if self.ObjectScale is False:
462 self.mouse_region = event.mouse_region_x, event.mouse_region_y
463 self.ObjectScale = True
465 # Grid : Snap on grid
466 if event.type == self.carver_prefs.Key_Snap and event.value == 'PRESS':
467 self.snap = not self.snap
469 # Array : Add column
470 if event.type == 'UP_ARROW' and event.value == 'PRESS':
471 self.nbrow += 1
472 update_grid(self, context)
474 # Array : Delete column
475 elif event.type == 'DOWN_ARROW' and event.value == 'PRESS':
476 self.nbrow -= 1
477 update_grid(self, context)
479 # Array : Add row
480 elif event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
481 self.nbcol += 1
482 update_grid(self, context)
484 # Array : Delete row
485 elif event.type == 'LEFT_ARROW' and event.value == 'PRESS':
486 self.nbcol -= 1
487 update_grid(self, context)
489 # Array : Scale gap between columns
490 if event.type == self.carver_prefs.Key_Gapy and event.value == 'PRESS':
491 if self.GridScaleX is False:
492 self.mouse_region = event.mouse_region_x, event.mouse_region_y
493 self.GridScaleX = True
495 # Array : Scale gap between rows
496 if event.type == self.carver_prefs.Key_Gapx and event.value == 'PRESS':
497 if self.GridScaleY is False:
498 self.mouse_region = event.mouse_region_x, event.mouse_region_y
499 self.GridScaleY = True
501 # Cursor depth or solidify pattern
502 if event.type == self.carver_prefs.Key_Depth and event.value == 'PRESS':
503 if (self.ObjectMode is False) and (self.ProfileMode is False):
504 self.snapCursor = not self.snapCursor
505 else:
506 # Solidify
508 if (self.ObjectMode or self.ProfileMode) and (self.SolidifyPossible):
509 solidify = True
511 if self.ObjectMode:
512 z = self.ObjectBrush.data.vertices[0].co.z
513 ErrorMarge = 0.01
514 for v in self.ObjectBrush.data.vertices:
515 if abs(v.co.z - z) > ErrorMarge:
516 solidify = False
517 self.CarveDepth = True
518 self.mouse_region = event.mouse_region_x, event.mouse_region_y
519 break
521 if solidify:
522 if self.ObjectMode:
523 for mb in self.ObjectBrush.modifiers:
524 if mb.type == 'SOLIDIFY':
525 AlreadySoldify = True
526 else:
527 for mb in self.ProfileBrush.modifiers:
528 if mb.type == 'SOLIDIFY':
529 AlreadySoldify = True
531 if AlreadySoldify is False:
532 Selection_Save(self)
533 self.BrushSolidify = True
535 bpy.ops.object.select_all(action='TOGGLE')
536 if self.ObjectMode:
537 self.ObjectBrush.select_set(True)
538 context.view_layer.objects.active = self.ObjectBrush
539 # Active le xray
540 self.ObjectBrush.show_in_front = True
541 else:
542 self.ProfileBrush.select_set(True)
543 context.view_layer.objects.active = self.ProfileBrush
544 # Active le xray
545 self.ProfileBrush.show_in_front = True
547 bpy.ops.object.modifier_add(type='SOLIDIFY')
548 context.object.modifiers["Solidify"].name = "CT_SOLIDIFY"
550 context.object.modifiers["CT_SOLIDIFY"].thickness = 0.1
552 Selection_Restore(self)
554 self.WidthSolidify = not self.WidthSolidify
555 self.mouse_region = event.mouse_region_x, event.mouse_region_y
557 if event.type == self.carver_prefs.Key_BrushDepth and event.value == 'PRESS':
558 if self.ObjectMode:
559 self.CarveDepth = False
561 self.BrushDepth = True
562 self.mouse_region = event.mouse_region_x, event.mouse_region_y
564 # Random rotation
565 if event.type == 'R' and event.value == 'PRESS':
566 self.RandomRotation = not self.RandomRotation
568 # Undo
569 if event.type == 'Z' and event.value == 'PRESS':
570 if self.ctrl:
571 if (self.CutType == self.line) and (self.CutMode):
572 if len(self.mouse_path) > 1:
573 self.mouse_path[len(self.mouse_path) - 1:] = []
574 else:
575 Undo(self)
577 # Mouse move
578 if event.type == 'MOUSEMOVE' :
579 if self.ObjectMode or self.ProfileMode:
580 fac = 50.0
581 if self.shift:
582 fac = 500.0
583 if self.WidthSolidify:
584 if self.ObjectMode:
585 bpy.data.objects[self.ObjectBrush.name].modifiers[
586 "CT_SOLIDIFY"].thickness += (event.mouse_region_x - self.mouse_region[0]) / fac
587 elif self.ProfileMode:
588 bpy.data.objects[self.ProfileBrush.name].modifiers[
589 "CT_SOLIDIFY"].thickness += (event.mouse_region_x - self.mouse_region[0]) / fac
590 self.mouse_region = event.mouse_region_x, event.mouse_region_y
591 elif self.CarveDepth:
592 for v in self.ObjectBrush.data.vertices:
593 v.co.z += (event.mouse_region_x - self.mouse_region[0]) / fac
594 self.mouse_region = event.mouse_region_x, event.mouse_region_y
595 elif self.BrushDepth:
596 self.BrushDepthOffset += (event.mouse_region_x - self.mouse_region[0]) / fac
597 self.mouse_region = event.mouse_region_x, event.mouse_region_y
598 else:
599 if (self.GridScaleX):
600 self.gapx += (event.mouse_region_x - self.mouse_region[0]) / 50
601 self.mouse_region = event.mouse_region_x, event.mouse_region_y
602 update_grid(self, context)
603 return {'RUNNING_MODAL'}
605 elif (self.GridScaleY):
606 self.gapy += (event.mouse_region_x - self.mouse_region[0]) / 50
607 self.mouse_region = event.mouse_region_x, event.mouse_region_y
608 update_grid(self, context)
609 return {'RUNNING_MODAL'}
611 elif self.ObjectScale:
612 self.ascale = -(event.mouse_region_x - self.mouse_region[0])
613 self.mouse_region = event.mouse_region_x, event.mouse_region_y
615 if self.ObjectMode:
616 self.ObjectBrush.scale.x -= float(self.ascale) / 150.0
617 if self.ObjectBrush.scale.x <= 0.0:
618 self.ObjectBrush.scale.x = 0.0
619 self.ObjectBrush.scale.y -= float(self.ascale) / 150.0
620 if self.ObjectBrush.scale.y <= 0.0:
621 self.ObjectBrush.scale.y = 0.0
622 self.ObjectBrush.scale.z -= float(self.ascale) / 150.0
623 if self.ObjectBrush.scale.z <= 0.0:
624 self.ObjectBrush.scale.z = 0.0
626 elif self.ProfileMode:
627 if self.ProfileBrush is not None:
628 self.ProfileBrush.scale.x -= float(self.ascale) / 150.0
629 self.ProfileBrush.scale.y -= float(self.ascale) / 150.0
630 self.ProfileBrush.scale.z -= float(self.ascale) / 150.0
631 else:
632 if self.LMB:
633 if self.ctrl:
634 self.aRotZ = - \
635 ((int((event.mouse_region_x - self.xSavMouse) / 10.0) * PI / 4.0) * 25.0)
636 else:
637 self.aRotZ -= event.mouse_region_x - self.mouse_region[0]
638 self.ascale = 0.0
640 self.mouse_region = event.mouse_region_x, event.mouse_region_y
641 else:
642 target_hit, target_normal, target_eul_rotation = Pick(context, event, self)
643 if target_hit is not None:
644 self.ShowCursor = True
645 up_vector = Vector((0.0, 0.0, 1.0))
646 quat_rot_axis = rot_axis_quat(up_vector, target_normal)
647 self.quat_rot = target_eul_rotation @ quat_rot_axis
648 MoveCursor(quat_rot_axis, target_hit, self)
649 self.SavCurLoc = target_hit
650 if self.ctrl:
651 if self.SavMousePos is not None:
652 xEcart = abs(self.SavMousePos.x - self.SavCurLoc.x)
653 yEcart = abs(self.SavMousePos.y - self.SavCurLoc.y)
654 zEcart = abs(self.SavMousePos.z - self.SavCurLoc.z)
655 if (xEcart > yEcart) and (xEcart > zEcart):
656 self.CurLoc = Vector(
657 (target_hit.x, self.SavMousePos.y, self.SavMousePos.z))
658 if (yEcart > xEcart) and (yEcart > zEcart):
659 self.CurLoc = Vector(
660 (self.SavMousePos.x, target_hit.y, self.SavMousePos.z))
661 if (zEcart > xEcart) and (zEcart > yEcart):
662 self.CurLoc = Vector(
663 (self.SavMousePos.x, self.SavMousePos.y, target_hit.z))
664 else:
665 self.CurLoc = target_hit
666 else:
667 self.CurLoc = target_hit
668 else:
669 if self.CutMode:
670 if self.alt is False:
671 if self.ctrl :
672 # Find the closest position on the overlay grid and snap the mouse on it
673 # Draw a mini grid around the cursor
674 mouse_pos = [[event.mouse_region_x, event.mouse_region_y]]
675 Snap_Cursor(self, context, event, mouse_pos)
677 else:
678 if len(self.mouse_path) > 0:
679 self.mouse_path[len(self.mouse_path) -
680 1] = (event.mouse_region_x, event.mouse_region_y)
681 else:
682 # [ALT] press, update position
683 self.xpos += (event.mouse_region_x - self.last_mouse_region_x)
684 self.ypos += (event.mouse_region_y - self.last_mouse_region_y)
686 self.last_mouse_region_x = event.mouse_region_x
687 self.last_mouse_region_y = event.mouse_region_y
689 elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
690 if self.ObjectMode or self.ProfileMode:
691 if self.LMB is False:
692 target_hit, target_normal, target_eul_rotation = Pick(context, event, self)
693 if target_hit is not None:
694 up_vector = Vector((0.0, 0.0, 1.0))
695 self.quat_rot_axis = rot_axis_quat(up_vector, target_normal)
696 self.quat_rot = target_eul_rotation @ self.quat_rot_axis
697 self.mouse_region = event.mouse_region_x, event.mouse_region_y
698 self.xSavMouse = event.mouse_region_x
700 if self.ctrl:
701 self.nRotZ = int((self.aRotZ / 25.0) / (PI / 4.0))
702 self.aRotZ = self.nRotZ * (PI / 4.0) * 25.0
704 self.LMB = True
706 # LEFTMOUSE
707 elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE' and self.in_view_3d:
708 if self.ObjectMode or self.ProfileMode:
709 # Rotation and scale
710 self.LMB = False
711 if self.ObjectScale is True:
712 self.ObjectScale = False
714 if self.GridScaleX is True:
715 self.GridScaleX = False
717 if self.GridScaleY is True:
718 self.GridScaleY = False
720 if self.WidthSolidify:
721 self.WidthSolidify = False
723 if self.CarveDepth is True:
724 self.CarveDepth = False
726 if self.BrushDepth is True:
727 self.BrushDepth = False
729 else:
730 if self.CutMode is False:
731 if self.ctrl:
732 Picking(context, event)
734 else:
736 if self.CutType == self.line:
737 if self.CutMode is False:
738 self.mouse_path.clear()
739 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
740 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
741 else:
742 self.mouse_path[0] = (event.mouse_region_x, event.mouse_region_y)
743 self.mouse_path[1] = (event.mouse_region_x, event.mouse_region_y)
744 self.CutMode = True
745 else:
746 if self.CutType != self.line:
747 # Cut creation
748 if self.CutType == self.rectangle:
749 CreateCutSquare(self, context)
750 if self.CutType == self.circle:
751 CreateCutCircle(self, context)
752 if self.CutType == self.line:
753 CreateCutLine(self, context)
755 if self.CreateMode:
756 # Object creation
757 self.CreateGeometry()
758 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
759 # Depth Cursor
760 context.scene.mesh_carver.DepthCursor = self.snapCursor
761 # Instantiate object
762 context.scene.mesh_carver.OInstanciate = self.Instantiate
763 # Random rotation
764 context.scene.mesh_carver.ORandom = self.RandomRotation
765 # Apply operation
766 context.scene.mesh_carver.DontApply = self.dont_apply_boolean
768 # if Object mode, set initiale state
769 if self.ObjectBrush is not None:
770 self.ObjectBrush.location = self.InitBrush['location']
771 self.ObjectBrush.scale = self.InitBrush['scale']
772 self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion']
773 self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler']
774 self.ObjectBrush.display_type = self.InitBrush['display_type']
775 self.ObjectBrush.show_in_front = self.InitBrush['show_in_front']
777 # remove solidify
778 Selection_Save(self)
779 self.BrushSolidify = False
781 bpy.ops.object.select_all(action='TOGGLE')
782 self.ObjectBrush.select_set(True)
783 context.view_layer.objects.active = self.ObjectBrush
785 bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY")
787 Selection_Restore(self)
789 context.scene.mesh_carver.nProfile = self.nProfil
791 return {'FINISHED'}
792 else:
793 self.Cut()
794 UndoListUpdate(self)
795 else:
796 # Line
797 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
799 # Change brush profil or circle subdivisions
800 elif (event.type == 'COMMA' and event.value == 'PRESS') or \
801 (event.type == self.carver_prefs.Key_Subrem and event.value == 'PRESS'):
802 # Brush profil
803 if self.ProfileMode:
804 self.nProfil += 1
805 if self.nProfil >= self.MaxProfil:
806 self.nProfil = 0
807 createMeshFromData(self)
808 # Circle subdivisions
809 if self.CutType == self.circle:
810 self.step += 1
811 if self.step >= len(self.stepAngle):
812 self.step = len(self.stepAngle) - 1
813 # Change brush profil or circle subdivisions
814 elif (event.type == 'PERIOD' and event.value == 'PRESS') or \
815 (event.type == self.carver_prefs.Key_Subadd and event.value == 'PRESS'):
816 # Brush profil
817 if self.ProfileMode:
818 self.nProfil -= 1
819 if self.nProfil < 0:
820 self.nProfil = self.MaxProfil - 1
821 createMeshFromData(self)
822 # Circle subdivisions
823 if self.CutType == self.circle:
824 if self.step > 0:
825 self.step -= 1
826 # Quit
827 elif event.type in {'RIGHTMOUSE', 'ESC'}:
828 # Depth Cursor
829 context.scene.mesh_carver.DepthCursor = self.snapCursor
830 # Instantiate object
831 context.scene.mesh_carver.OInstanciate = self.Instantiate
832 # Random Rotation
833 context.scene.mesh_carver.ORandom = self.RandomRotation
834 # Apply boolean operation
835 context.scene.mesh_carver.DontApply = self.dont_apply_boolean
837 # Reset Object
838 if self.ObjectBrush is not None:
839 self.ObjectBrush.location = self.InitBrush['location']
840 self.ObjectBrush.scale = self.InitBrush['scale']
841 self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion']
842 self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler']
843 self.ObjectBrush.display_type = self.InitBrush['display_type']
844 self.ObjectBrush.show_in_front = self.InitBrush['show_in_front']
846 # Remove solidify modifier
847 Selection_Save(self)
848 self.BrushSolidify = False
850 bpy.ops.object.select_all(action='TOGGLE')
851 self.ObjectBrush.select_set(True)
852 context.view_layer.objects.active = self.ObjectBrush
854 bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY")
855 bpy.ops.object.select_all(action='TOGGLE')
857 Selection_Restore(self)
859 Selection_Save_Restore(self)
860 context.view_layer.objects.active = self.CurrentActive
861 context.scene.mesh_carver.nProfile = self.nProfil
863 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
865 # Remove Copy Object Brush
866 if bpy.data.objects.get("CarverBrushCopy") is not None:
867 brush = bpy.data.objects["CarverBrushCopy"]
868 self.ObjectBrush.data = bpy.data.meshes[brush.data.name]
869 bpy.ops.object.select_all(action='DESELECT')
870 bpy.data.objects["CarverBrushCopy"].select_set(True)
871 bpy.ops.object.delete()
873 return {'FINISHED'}
875 return {'RUNNING_MODAL'}
877 except:
878 print("\n[Carver MT ERROR]\n")
879 import traceback
880 traceback.print_exc()
882 context.window.cursor_modal_set("DEFAULT")
883 context.area.header_text_set(None)
884 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
886 self.report({'WARNING'},
887 "Operation finished. Failure during Carving (Check the console for more info)")
889 return {'FINISHED'}
891 def cancel(self, context):
892 # Note: used to prevent memory leaks on quitting Blender while the modal operator
893 # is still running, gets called on return {"CANCELLED"}
894 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
896 def invoke(self, context, event):
897 if context.area.type != 'VIEW_3D':
898 self.report({'WARNING'},
899 "View3D not found or not currently active. Operation Cancelled")
900 self.cancel(context)
901 return {'CANCELLED'}
903 # test if some other object types are selected that are not meshes
904 for obj in context.selected_objects:
905 if obj.type != "MESH":
906 self.report({'WARNING'},
907 "Some selected objects are not of the Mesh type. Operation Cancelled")
908 self.cancel(context)
909 return {'CANCELLED'}
911 if context.mode == 'EDIT_MESH':
912 bpy.ops.object.mode_set(mode='OBJECT')
914 #Load the Carver preferences
915 self.carver_prefs = bpy.context.preferences.addons[__package__].preferences
917 # Get default patterns
918 self.Profils = []
919 for p in Profils:
920 self.Profils.append((p[0], p[1], p[2], p[3]))
922 for o in context.scene.objects:
923 if not o.name.startswith(self.carver_prefs.ProfilePrefix):
924 continue
925 # In-scene profiles may have changed, remove them to refresh
926 for m in bpy.data.meshes:
927 if m.name.startswith(self.carver_prefs.ProfilePrefix):
928 bpy.data.meshes.remove(m)
930 vertices = []
931 for v in o.data.vertices:
932 vertices.append((v.co.x, v.co.y, v.co.z))
934 faces = []
935 for f in o.data.polygons:
936 face = []
937 for v in f.vertices:
938 face.append(v)
940 faces.append(face)
942 self.Profils.append(
943 (o.name,
944 Vector((o.location.x, o.location.y, o.location.z)),
945 vertices, faces)
948 self.nProfil = context.scene.mesh_carver.nProfile
949 self.MaxProfil = len(self.Profils)
952 # reset selected profile if last profile exceeds length of array
953 if self.nProfil >= self.MaxProfil:
954 self.nProfil = context.scene.mesh_carver.nProfile = 0
956 if len(context.selected_objects) > 1:
957 self.ObjectBrush = context.active_object
959 # Copy the brush object
960 ob = bpy.data.objects.new("CarverBrushCopy", context.object.data.copy())
961 ob.location = self.ObjectBrush.location
962 context.collection.objects.link(ob)
963 context.view_layer.update()
965 # Save default variables
966 self.InitBrush['location'] = self.ObjectBrush.location.copy()
967 self.InitBrush['scale'] = self.ObjectBrush.scale.copy()
968 self.InitBrush['rotation_quaternion'] = self.ObjectBrush.rotation_quaternion.copy()
969 self.InitBrush['rotation_euler'] = self.ObjectBrush.rotation_euler.copy()
970 self.InitBrush['display_type'] = self.ObjectBrush.display_type
971 self.InitBrush['show_in_front'] = self.ObjectBrush.show_in_front
973 # Test if flat object
974 z = self.ObjectBrush.data.vertices[0].co.z
975 ErrorMarge = 0.01
976 self.SolidifyPossible = True
977 for v in self.ObjectBrush.data.vertices:
978 if abs(v.co.z - z) > ErrorMarge:
979 self.SolidifyPossible = False
980 break
982 self.CList = []
983 self.OPList = []
984 self.RList = []
985 self.OB_List = []
987 for obj in context.selected_objects:
988 if obj != self.ObjectBrush:
989 self.OB_List.append(obj)
991 # Left button
992 self.LMB = False
994 # Undo Variables
995 self.undo_index = 0
996 self.undo_limit = context.preferences.edit.undo_steps
997 self.undo_list = []
999 # Boolean operations type
1000 self.BooleanType = 0
1002 self.UList = []
1003 self.UList_Index = -1
1004 self.UndoOps = []
1006 context.window_manager.modal_handler_add(self)
1007 return {'RUNNING_MODAL'}
1009 #Get the region area where the operator is used
1010 def check_region(self,context,event):
1011 if context.area != None:
1012 if context.area.type == "VIEW_3D" :
1013 for region in context.area.regions:
1014 if region.type == "TOOLS":
1015 t_panel = region
1016 elif region.type == "UI":
1017 ui_panel = region
1019 view_3d_region_x = Vector((context.area.x + t_panel.width, context.area.x + context.area.width - ui_panel.width))
1020 view_3d_region_y = Vector((context.region.y, context.region.y+context.region.height))
1022 if (event.mouse_x > view_3d_region_x[0] and event.mouse_x < view_3d_region_x[1] \
1023 and event.mouse_y > view_3d_region_y[0] and event.mouse_y < view_3d_region_y[1]):
1024 self.in_view_3d = True
1025 else:
1026 self.in_view_3d = False
1027 else:
1028 self.in_view_3d = False
1030 def CreateGeometry(self):
1031 context = bpy.context
1032 in_local_view = False
1034 for area in context.screen.areas:
1035 if area.type == 'VIEW_3D':
1036 if area.spaces[0].local_view is not None:
1037 in_local_view = True
1039 if in_local_view:
1040 bpy.ops.view3d.localview()
1042 if self.ExclusiveCreateMode:
1043 # Default width
1044 objBBDiagonal = 0.5
1045 else:
1046 ActiveObj = self.CurrentSelection[0]
1047 if ActiveObj is not None:
1048 # Object dimensions
1049 objBBDiagonal = objDiagonal(ActiveObj) / 4
1050 subdivisions = 2
1052 if len(context.selected_objects) > 0:
1053 bpy.ops.object.select_all(action='TOGGLE')
1055 context.view_layer.objects.active = self.CurrentObj
1057 bpy.data.objects[self.CurrentObj.name].select_set(True)
1058 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
1060 bpy.ops.object.mode_set(mode='EDIT')
1061 bpy.ops.mesh.select_all(action='SELECT')
1062 bpy.ops.mesh.select_mode(type="EDGE")
1063 if self.snapCursor is False:
1064 bpy.ops.transform.translate(value=self.ViewVector * objBBDiagonal * subdivisions)
1065 bpy.ops.mesh.extrude_region_move(
1066 TRANSFORM_OT_translate={"value": -self.ViewVector * objBBDiagonal * subdivisions * 2})
1068 bpy.ops.mesh.select_all(action='SELECT')
1069 bpy.ops.mesh.normals_make_consistent()
1070 bpy.ops.object.mode_set(mode='OBJECT')
1072 saved_location_0 = context.scene.cursor.location.copy()
1073 bpy.ops.view3d.snap_cursor_to_active()
1074 saved_location = context.scene.cursor.location.copy()
1075 bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
1076 context.scene.cursor.location = saved_location
1077 bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
1078 context.scene.cursor.location = saved_location_0
1080 bpy.data.objects[self.CurrentObj.name].select_set(True)
1081 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
1083 for o in self.all_sel_obj_list:
1084 bpy.data.objects[o.name].select_set(True)
1086 if in_local_view:
1087 bpy.ops.view3d.localview()
1089 self.CutMode = False
1090 self.mouse_path.clear()
1091 self.mouse_path = [(0, 0), (0, 0)]
1093 def Cut(self):
1094 context = bpy.context
1096 # Local view ?
1097 in_local_view = False
1098 for area in context.screen.areas:
1099 if area.type == 'VIEW_3D':
1100 if area.spaces[0].local_view is not None:
1101 in_local_view = True
1103 if in_local_view:
1104 bpy.ops.view3d.localview()
1106 # Save cursor position
1107 CursorLocation = context.scene.cursor.location.copy()
1109 #List of selected objects
1110 selected_obj_list = []
1112 #Cut Mode with line
1113 if (self.ObjectMode is False) and (self.ProfileMode is False):
1115 #Compute the bounding Box
1116 objBBDiagonal = objDiagonal(self.CurrentSelection[0])
1117 if self.dont_apply_boolean:
1118 subdivisions = 1
1119 else:
1120 subdivisions = 32
1122 # Get selected objects
1123 selected_obj_list = context.selected_objects.copy()
1125 bpy.ops.object.select_all(action='TOGGLE')
1127 context.view_layer.objects.active = self.CurrentObj
1129 bpy.data.objects[self.CurrentObj.name].select_set(True)
1130 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
1132 bpy.ops.object.mode_set(mode='EDIT')
1133 bpy.ops.mesh.select_all(action='SELECT')
1134 bpy.ops.mesh.select_mode(type="EDGE")
1135 #Translate the created mesh away from the view
1136 if (self.snapCursor is False) or (self.ForceRebool):
1137 bpy.ops.transform.translate(value=self.ViewVector * objBBDiagonal * subdivisions)
1138 #Extrude the mesh region and move the result
1139 bpy.ops.mesh.extrude_region_move(
1140 TRANSFORM_OT_translate={"value": -self.ViewVector * objBBDiagonal * subdivisions * 2})
1141 bpy.ops.mesh.select_all(action='SELECT')
1142 bpy.ops.mesh.normals_make_consistent()
1143 bpy.ops.object.mode_set(mode='OBJECT')
1144 else:
1145 # Create list
1146 if self.ObjectMode:
1147 for o in self.CurrentSelection:
1148 if o != self.ObjectBrush:
1149 selected_obj_list.append(o)
1150 self.CurrentObj = self.ObjectBrush
1151 else:
1152 selected_obj_list = self.CurrentSelection
1153 self.CurrentObj = self.ProfileBrush
1155 for obj in self.CurrentSelection:
1156 UndoAdd(self, "MESH", obj)
1158 # List objects create with rebool
1159 lastSelected = []
1161 for ActiveObj in selected_obj_list:
1162 context.scene.cursor.location = CursorLocation
1164 if len(context.selected_objects) > 0:
1165 bpy.ops.object.select_all(action='TOGGLE')
1167 # Select cut object
1168 bpy.data.objects[self.CurrentObj.name].select_set(True)
1169 context.view_layer.objects.active = self.CurrentObj
1171 bpy.ops.object.mode_set(mode='EDIT')
1172 bpy.ops.mesh.select_all(action='SELECT')
1173 bpy.ops.object.mode_set(mode='OBJECT')
1175 # Select object to cut
1176 bpy.data.objects[ActiveObj.name].select_set(True)
1177 context.view_layer.objects.active = ActiveObj
1179 bpy.ops.object.mode_set(mode='EDIT')
1180 bpy.ops.mesh.select_all(action='DESELECT')
1181 bpy.ops.object.mode_set(mode='OBJECT')
1183 # Boolean operation
1184 if (self.shift is False) and (self.ForceRebool is False):
1185 if self.ObjectMode or self.ProfileMode:
1186 if self.BoolOps == self.union:
1187 boolean_operation(bool_type="UNION")
1188 else:
1189 boolean_operation(bool_type="DIFFERENCE")
1190 else:
1191 boolean_operation(bool_type="DIFFERENCE")
1193 # Apply booleans
1194 if self.dont_apply_boolean is False:
1195 BMname = "CT_" + self.CurrentObj.name
1196 for mb in ActiveObj.modifiers:
1197 if (mb.type == 'BOOLEAN') and (mb.name == BMname):
1198 try:
1199 bpy.ops.object.modifier_apply(apply_as='DATA', modifier=BMname)
1200 except:
1201 bpy.ops.object.modifier_remove(modifier=BMname)
1202 exc_type, exc_value, exc_traceback = sys.exc_info()
1203 self.report({'ERROR'}, str(exc_value))
1205 bpy.ops.object.select_all(action='TOGGLE')
1206 else:
1207 if self.ObjectMode or self.ProfileMode:
1208 for mb in self.CurrentObj.modifiers:
1209 if (mb.type == 'SOLIDIFY') and (mb.name == "CT_SOLIDIFY"):
1210 try:
1211 bpy.ops.object.modifier_apply(apply_as='DATA', modifier="CT_SOLIDIFY")
1212 except:
1213 exc_type, exc_value, exc_traceback = sys.exc_info()
1214 self.report({'ERROR'}, str(exc_value))
1216 # Rebool
1217 Rebool(context, self)
1219 # Test if not empty object
1220 if context.selected_objects[0]:
1221 rebool_RT = context.selected_objects[0]
1222 if len(rebool_RT.data.vertices) > 0:
1223 # Create Bevel for new objects
1224 CreateBevel(context, context.selected_objects[0])
1226 UndoAdd(self, "REBOOL", context.selected_objects[0])
1228 context.scene.cursor.location = ActiveObj.location
1229 bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
1230 else:
1231 bpy.ops.object.delete(use_global=False)
1233 context.scene.cursor.location = CursorLocation
1235 if self.ObjectMode:
1236 context.view_layer.objects.active = self.ObjectBrush
1237 if self.ProfileMode:
1238 context.view_layer.objects.active = self.ProfileBrush
1240 if self.dont_apply_boolean is False:
1241 # Apply booleans
1242 BMname = "CT_" + self.CurrentObj.name
1243 for mb in ActiveObj.modifiers:
1244 if (mb.type == 'BOOLEAN') and (mb.name == BMname):
1245 try:
1246 bpy.ops.object.modifier_apply(apply_as='DATA', modifier=BMname)
1247 except:
1248 bpy.ops.object.modifier_remove(modifier=BMname)
1249 exc_type, exc_value, exc_traceback = sys.exc_info()
1250 self.report({'ERROR'}, str(exc_value))
1251 # Get new objects created with rebool operations
1252 if len(context.selected_objects) > 0:
1253 if self.shift is True:
1254 # Get the last object selected
1255 lastSelected.append(context.selected_objects[0])
1257 context.scene.cursor.location = CursorLocation
1259 if self.dont_apply_boolean is False:
1260 # Remove cut object
1261 if (self.ObjectMode is False) and (self.ProfileMode is False):
1262 if len(context.selected_objects) > 0:
1263 bpy.ops.object.select_all(action='TOGGLE')
1264 bpy.data.objects[self.CurrentObj.name].select_set(True)
1265 bpy.ops.object.delete(use_global=False)
1266 else:
1267 if self.ObjectMode:
1268 self.ObjectBrush.display_type = self.InitBrush['display_type']
1270 if len(context.selected_objects) > 0:
1271 bpy.ops.object.select_all(action='TOGGLE')
1273 # Select cut objects
1274 for obj in lastSelected:
1275 bpy.data.objects[obj.name].select_set(True)
1277 for ActiveObj in selected_obj_list:
1278 bpy.data.objects[ActiveObj.name].select_set(True)
1279 context.view_layer.objects.active = ActiveObj
1280 # Update bevel
1281 list_act_obj = context.selected_objects.copy()
1282 if self.Auto_BevelUpdate:
1283 update_bevel(context)
1285 # Re-select initial objects
1286 bpy.ops.object.select_all(action='TOGGLE')
1287 if self.ObjectMode:
1288 # Re-select brush
1289 self.ObjectBrush.select_set(True)
1290 for ActiveObj in selected_obj_list:
1291 bpy.data.objects[ActiveObj.name].select_set(True)
1292 context.view_layer.objects.active = ActiveObj
1294 # If object has children, set "Wire" draw type
1295 if self.ObjectBrush is not None:
1296 if len(self.ObjectBrush.children) > 0:
1297 self.ObjectBrush.display_type = "WIRE"
1298 if self.ProfileMode:
1299 self.ProfileBrush.display_type = "WIRE"
1301 if in_local_view:
1302 bpy.ops.view3d.localview()
1304 # Reset variables
1305 self.CutMode = False
1306 self.mouse_path.clear()
1307 self.mouse_path = [(0, 0), (0, 0)]
1309 self.ForceRebool = False
1311 # bpy.ops.mesh.customdata_custom_splitnormals_clear()
1314 class CarverProperties(bpy.types.PropertyGroup):
1315 DepthCursor: BoolProperty(
1316 name="DepthCursor",
1317 default=False
1319 OInstanciate: BoolProperty(
1320 name="Obj_Instantiate",
1321 default=False
1323 ORandom: BoolProperty(
1324 name="Random_Rotation",
1325 default=False
1327 DontApply: BoolProperty(
1328 name="Dont_Apply",
1329 default=False
1331 nProfile: IntProperty(
1332 name="Num_Profile",
1333 default=0
1337 def register():
1338 from bpy.utils import register_class
1339 bpy.utils.register_class(CARVER_OT_operator)
1340 bpy.utils.register_class(CarverProperties)
1341 bpy.types.Scene.mesh_carver = bpy.props.PointerProperty(type=CarverProperties)
1343 def unregister():
1344 from bpy.utils import unregister_class
1345 bpy.utils.unregister_class(CarverProperties)
1346 bpy.utils.unregister_class(CARVER_OT_operator)
1347 del bpy.types.Scene.mesh_carver