# SPDX-License-Identifier: GPL-2.0-or-later import bpy import bpy_extras import sys from bpy.props import ( BoolProperty, IntProperty, PointerProperty, StringProperty, EnumProperty, ) from mathutils import ( Vector, ) from bpy_extras.view3d_utils import ( region_2d_to_vector_3d, region_2d_to_origin_3d, region_2d_to_location_3d, location_3d_to_region_2d, ) from .carver_profils import ( Profils ) from .carver_utils import ( duplicateObject, UndoListUpdate, createMeshFromData, SelectObject, Selection_Save_Restore, Selection_Save, Selection_Restore, update_grid, objDiagonal, Undo, UndoAdd, Pick, rot_axis_quat, MoveCursor, Picking, CreateCutSquare, CreateCutCircle, CreateCutLine, boolean_operation, update_bevel, CreateBevel, Rebool, Snap_Cursor, ) from .carver_draw import draw_callback_px # Modal Operator class CARVER_OT_operator(bpy.types.Operator): bl_idname = "carver.operator" bl_label = "Carver" bl_description = "Cut or create Meshes in Object mode" bl_options = {'REGISTER', 'UNDO'} def __init__(self): context = bpy.context # Carve mode: Cut, Object, Profile self.CutMode = False self.CreateMode = False self.ObjectMode = False self.ProfileMode = False # Create mode self.ExclusiveCreateMode = False if len(context.selected_objects) == 0: self.ExclusiveCreateMode = True self.CreateMode = True # Cut type (Rectangle, Circle, Line) self.rectangle = 0 self.line = 1 self.circle = 2 # Cut Rectangle coordinates self.rectangle_coord = [] # Selected type of cut self.CutType = 0 # Boolean operation self.difference = 0 self.union = 1 self.BoolOps = self.difference self.CurrentSelection = context.selected_objects.copy() self.CurrentActive = context.active_object self.all_sel_obj_list = context.selected_objects.copy() self.save_active_obj = None args = (self, context) self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') self.mouse_path = [(0, 0), (0, 0)] # Keyboard event self.shift = False self.ctrl = False self.alt = False self.dont_apply_boolean = context.scene.mesh_carver.DontApply self.Auto_BevelUpdate = True # Circle variables self.stepAngle = [2, 4, 5, 6, 9, 10, 15, 20, 30, 40, 45, 60, 72, 90] self.step = 4 # Primitives Position self.xpos = 0 self.ypos = 0 self.InitPosition = False # Close polygonal shape self.Closed = False # Depth Cursor self.snapCursor = context.scene.mesh_carver.DepthCursor # Help self.AskHelp = False # Working object self.OpsObj = context.active_object # Rebool forced (cut line) self.ForceRebool = False self.ViewVector = Vector() self.CurrentObj = None # Brush self.BrushSolidify = False self.WidthSolidify = False self.CarveDepth = False self.BrushDepth = False self.BrushDepthOffset = 0.0 self.snap = False self.ObjectScale = False #Init create circle primitive self.CLR_C = [] # Cursor location self.CurLoc = Vector((0.0, 0.0, 0.0)) self.SavCurLoc = Vector((0.0, 0.0, 0.0)) # Mouse region self.mouse_region = -1, -1 self.SavMousePos = None self.xSavMouse = 0 # Scale, rotate object self.ascale = 0 self.aRotZ = 0 self.nRotZ = 0 self.quat_rot_axis = None self.quat_rot = None self.RandomRotation = context.scene.mesh_carver.ORandom self.ShowCursor = True self.Instantiate = context.scene.mesh_carver.OInstanciate self.ProfileBrush = None self.ObjectBrush = None self.InitBrush = { 'location' : None, 'scale' : None, 'rotation_quaternion' : None, 'rotation_euler' : None, 'display_type' : 'WIRE', 'show_in_front' : False } # Array variables self.nbcol = 1 self.nbrow = 1 self.gapx = 0 self.gapy = 0 self.scale_x = 1 self.scale_y = 1 self.GridScaleX = False self.GridScaleY = False @classmethod def poll(cls, context): ob = None if len(context.selected_objects) > 0: ob = context.selected_objects[0] # Test if selected object or none (for create mode) return ( (ob and ob.type == 'MESH' and context.mode == 'OBJECT') or (context.mode == 'OBJECT' and ob is None) or (context.mode == 'EDIT_MESH')) def modal(self, context, event): PI = 3.14156 region_types = {'WINDOW', 'UI'} win = context.window # Find the limit of the view3d region self.check_region(context,event) for area in win.screen.areas: if area.type == 'VIEW_3D': for region in area.regions: if not region_types or region.type in region_types: region.tag_redraw() # Change the snap increment value using the wheel mouse if self.CutMode: if self.alt is False: if self.ctrl and (self.CutType in (self.line, self.rectangle)): # Get the VIEW3D area for i, a in enumerate(context.screen.areas): if a.type == 'VIEW_3D': space = context.screen.areas[i].spaces.active grid_scale = space.overlay.grid_scale grid_subdivisions = space.overlay.grid_subdivisions if event.type == 'WHEELUPMOUSE': space.overlay.grid_subdivisions += 1 elif event.type == 'WHEELDOWNMOUSE': space.overlay.grid_subdivisions -= 1 if event.type in { 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9', 'NUMPAD_5'}: return {'PASS_THROUGH'} try: # [Shift] self.shift = True if event.shift else False # [Ctrl] self.ctrl = True if event.ctrl else False # [Alt] self.alt = False # [Alt] press : Init position variable before moving the cut brush with LMB if event.alt: if self.InitPosition is False: self.xpos = 0 self.ypos = 0 self.last_mouse_region_x = event.mouse_region_x self.last_mouse_region_y = event.mouse_region_y self.InitPosition = True self.alt = True # [Alt] release : update the coordinates if self.InitPosition and self.alt is False: for i in range(0, len(self.mouse_path)): l = list(self.mouse_path[i]) l[0] += self.xpos l[1] += self.ypos self.mouse_path[i] = tuple(l) self.xpos = self.ypos = 0 self.InitPosition = False if event.type == 'SPACE' and event.value == 'PRESS': # If object or profile mode is TRUE : Confirm the cut if self.ObjectMode or self.ProfileMode: # If array, remove double with intersect meshes if ((self.nbcol + self.nbrow) > 3): # Go in edit mode mode bpy.ops.object.mode_set(mode='EDIT') # Remove duplicate vertices bpy.ops.mesh.remove_doubles() # Return in object mode bpy.ops.object.mode_set(mode='OBJECT') if self.alt: # Save selected objects self.all_sel_obj_list = context.selected_objects.copy() if len(context.selected_objects) > 0: bpy.ops.object.select_all(action='TOGGLE') if self.ObjectMode: SelectObject(self, self.ObjectBrush) else: SelectObject(self, self.ProfileBrush) duplicateObject(self) else: # Brush Cut self.Cut() # Save selected objects if self.ObjectMode: if len(self.ObjectBrush.children) > 0: self.all_sel_obj_list = context.selected_objects.copy() if len(context.selected_objects) > 0: bpy.ops.object.select_all(action='TOGGLE') if self.ObjectMode: SelectObject(self, self.ObjectBrush) else: SelectObject(self, self.ProfileBrush) duplicateObject(self) UndoListUpdate(self) # Save cursor position self.SavMousePos = self.CurLoc else: if self.CutMode is False: # Cut Mode self.CutType += 1 if self.CutType > 2: self.CutType = 0 else: if self.CutType == self.line: # Cuts creation CreateCutLine(self, context) if self.CreateMode: # Object creation self.CreateGeometry() bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') # Cursor Snap context.scene.mesh_carver.DepthCursor = self.snapCursor # Object Instantiate context.scene.mesh_carver.OInstanciate = self.Instantiate # Random rotation context.scene.mesh_carver.ORandom = self.RandomRotation return {'FINISHED'} else: self.Cut() UndoListUpdate(self) #----------------------------------------------------- # Object creation #----------------------------------------------------- # Object creation if event.type == self.carver_prefs.Key_Create and event.value == 'PRESS': if self.ExclusiveCreateMode is False: self.CreateMode = not self.CreateMode # Auto Bevel Update if event.type == self.carver_prefs.Key_Update and event.value == 'PRESS': self.Auto_BevelUpdate = not self.Auto_BevelUpdate # Boolean operation type if event.type == self.carver_prefs.Key_Bool and event.value == 'PRESS': if (self.ProfileMode is True) or (self.ObjectMode is True): if self.BoolOps == self.difference: self.BoolOps = self.union else: self.BoolOps = self.difference # Brush Mode if event.type == self.carver_prefs.Key_Brush and event.value == 'PRESS': self.dont_apply_boolean = False if (self.ProfileMode is False) and (self.ObjectMode is False): self.ProfileMode = True else: self.ProfileMode = False if self.ObjectBrush is not None: if self.ObjectMode is False: self.ObjectMode = True self.BrushSolidify = False self.CList = self.OB_List Selection_Save_Restore(self) context.scene.mesh_carver.nProfile = self.nProfil else: self.ObjectMode = False else: self.BrushSolidify = False Selection_Save_Restore(self) if self.ProfileMode: createMeshFromData(self) self.ProfileBrush = bpy.data.objects["CT_Profil"] Selection_Save(self) self.BrushSolidify = True bpy.ops.object.select_all(action='TOGGLE') self.ProfileBrush.select_set(True) context.view_layer.objects.active = self.ProfileBrush # Set xRay self.ProfileBrush.show_in_front = True bpy.ops.object.modifier_add(type='SOLIDIFY') context.object.modifiers["Solidify"].name = "CT_SOLIDIFY" context.object.modifiers["CT_SOLIDIFY"].thickness = 0.1 Selection_Restore(self) self.CList = self.CurrentSelection else: if self.ObjectBrush is not None: if self.ObjectMode is False: if self.ObjectBrush is not None: self.ObjectBrush.location = self.InitBrush['location'] self.ObjectBrush.scale = self.InitBrush['scale'] self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion'] self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler'] self.ObjectBrush.display_type = self.InitBrush['display_type'] self.ObjectBrush.show_in_front = self.InitBrush['show_in_front'] #Store active and selected objects Selection_Save(self) #Remove Carver modifier self.BrushSolidify = False bpy.ops.object.select_all(action='TOGGLE') self.ObjectBrush.select_set(True) context.view_layer.objects.active = self.ObjectBrush bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY") #Restore selected and active object Selection_Restore(self) else: if self.SolidifyPossible: #Store active and selected objects Selection_Save(self) self.BrushSolidify = True bpy.ops.object.select_all(action='TOGGLE') self.ObjectBrush.select_set(True) context.view_layer.objects.active = self.ObjectBrush # Set xRay self.ObjectBrush.show_in_front = True bpy.ops.object.modifier_add(type='SOLIDIFY') context.object.modifiers["Solidify"].name = "CT_SOLIDIFY" context.object.modifiers["CT_SOLIDIFY"].thickness = 0.1 #Restore selected and active object Selection_Restore(self) # Help display if event.type == self.carver_prefs.Key_Help and event.value == 'PRESS': self.AskHelp = not self.AskHelp # Instantiate object if event.type == self.carver_prefs.Key_Instant and event.value == 'PRESS': self.Instantiate = not self.Instantiate # Close polygonal shape if event.type == self.carver_prefs.Key_Close and event.value == 'PRESS': if self.CreateMode: self.Closed = not self.Closed if event.type == self.carver_prefs.Key_Apply and event.value == 'PRESS': self.dont_apply_boolean = not self.dont_apply_boolean # Scale object if event.type == self.carver_prefs.Key_Scale and event.value == 'PRESS': if self.ObjectScale is False: self.mouse_region = event.mouse_region_x, event.mouse_region_y self.ObjectScale = True # Grid : Snap on grid if event.type == self.carver_prefs.Key_Snap and event.value == 'PRESS': self.snap = not self.snap # Array : Add column if event.type == 'UP_ARROW' and event.value == 'PRESS': self.nbrow += 1 update_grid(self, context) # Array : Delete column elif event.type == 'DOWN_ARROW' and event.value == 'PRESS': self.nbrow -= 1 update_grid(self, context) # Array : Add row elif event.type == 'RIGHT_ARROW' and event.value == 'PRESS': self.nbcol += 1 update_grid(self, context) # Array : Delete row elif event.type == 'LEFT_ARROW' and event.value == 'PRESS': self.nbcol -= 1 update_grid(self, context) # Array : Scale gap between columns if event.type == self.carver_prefs.Key_Gapy and event.value == 'PRESS': if self.GridScaleX is False: self.mouse_region = event.mouse_region_x, event.mouse_region_y self.GridScaleX = True # Array : Scale gap between rows if event.type == self.carver_prefs.Key_Gapx and event.value == 'PRESS': if self.GridScaleY is False: self.mouse_region = event.mouse_region_x, event.mouse_region_y self.GridScaleY = True # Cursor depth or solidify pattern if event.type == self.carver_prefs.Key_Depth and event.value == 'PRESS': if (self.ObjectMode is False) and (self.ProfileMode is False): self.snapCursor = not self.snapCursor else: # Solidify if (self.ObjectMode or self.ProfileMode) and (self.SolidifyPossible): solidify = True if self.ObjectMode: z = self.ObjectBrush.data.vertices[0].co.z ErrorMarge = 0.01 for v in self.ObjectBrush.data.vertices: if abs(v.co.z - z) > ErrorMarge: solidify = False self.CarveDepth = True self.mouse_region = event.mouse_region_x, event.mouse_region_y break if solidify: if self.ObjectMode: for mb in self.ObjectBrush.modifiers: if mb.type == 'SOLIDIFY': AlreadySoldify = True else: for mb in self.ProfileBrush.modifiers: if mb.type == 'SOLIDIFY': AlreadySoldify = True if AlreadySoldify is False: Selection_Save(self) self.BrushSolidify = True bpy.ops.object.select_all(action='TOGGLE') if self.ObjectMode: self.ObjectBrush.select_set(True) context.view_layer.objects.active = self.ObjectBrush # Active le xray self.ObjectBrush.show_in_front = True else: self.ProfileBrush.select_set(True) context.view_layer.objects.active = self.ProfileBrush # Active le xray self.ProfileBrush.show_in_front = True bpy.ops.object.modifier_add(type='SOLIDIFY') context.object.modifiers["Solidify"].name = "CT_SOLIDIFY" context.object.modifiers["CT_SOLIDIFY"].thickness = 0.1 Selection_Restore(self) self.WidthSolidify = not self.WidthSolidify self.mouse_region = event.mouse_region_x, event.mouse_region_y if event.type == self.carver_prefs.Key_BrushDepth and event.value == 'PRESS': if self.ObjectMode: self.CarveDepth = False self.BrushDepth = True self.mouse_region = event.mouse_region_x, event.mouse_region_y # Random rotation if event.type == 'R' and event.value == 'PRESS': self.RandomRotation = not self.RandomRotation # Undo if event.type == 'Z' and event.value == 'PRESS': if self.ctrl: if (self.CutType == self.line) and (self.CutMode): if len(self.mouse_path) > 1: self.mouse_path[len(self.mouse_path) - 1:] = [] else: Undo(self) # Mouse move if event.type == 'MOUSEMOVE' : if self.ObjectMode or self.ProfileMode: fac = 50.0 if self.shift: fac = 500.0 if self.WidthSolidify: if self.ObjectMode: bpy.data.objects[self.ObjectBrush.name].modifiers[ "CT_SOLIDIFY"].thickness += (event.mouse_region_x - self.mouse_region[0]) / fac elif self.ProfileMode: bpy.data.objects[self.ProfileBrush.name].modifiers[ "CT_SOLIDIFY"].thickness += (event.mouse_region_x - self.mouse_region[0]) / fac self.mouse_region = event.mouse_region_x, event.mouse_region_y elif self.CarveDepth: for v in self.ObjectBrush.data.vertices: v.co.z += (event.mouse_region_x - self.mouse_region[0]) / fac self.mouse_region = event.mouse_region_x, event.mouse_region_y elif self.BrushDepth: self.BrushDepthOffset += (event.mouse_region_x - self.mouse_region[0]) / fac self.mouse_region = event.mouse_region_x, event.mouse_region_y else: if (self.GridScaleX): self.gapx += (event.mouse_region_x - self.mouse_region[0]) / 50 self.mouse_region = event.mouse_region_x, event.mouse_region_y update_grid(self, context) return {'RUNNING_MODAL'} elif (self.GridScaleY): self.gapy += (event.mouse_region_x - self.mouse_region[0]) / 50 self.mouse_region = event.mouse_region_x, event.mouse_region_y update_grid(self, context) return {'RUNNING_MODAL'} elif self.ObjectScale: self.ascale = -(event.mouse_region_x - self.mouse_region[0]) self.mouse_region = event.mouse_region_x, event.mouse_region_y if self.ObjectMode: self.ObjectBrush.scale.x -= float(self.ascale) / 150.0 if self.ObjectBrush.scale.x <= 0.0: self.ObjectBrush.scale.x = 0.0 self.ObjectBrush.scale.y -= float(self.ascale) / 150.0 if self.ObjectBrush.scale.y <= 0.0: self.ObjectBrush.scale.y = 0.0 self.ObjectBrush.scale.z -= float(self.ascale) / 150.0 if self.ObjectBrush.scale.z <= 0.0: self.ObjectBrush.scale.z = 0.0 elif self.ProfileMode: if self.ProfileBrush is not None: self.ProfileBrush.scale.x -= float(self.ascale) / 150.0 self.ProfileBrush.scale.y -= float(self.ascale) / 150.0 self.ProfileBrush.scale.z -= float(self.ascale) / 150.0 else: if self.LMB: if self.ctrl: self.aRotZ = - \ ((int((event.mouse_region_x - self.xSavMouse) / 10.0) * PI / 4.0) * 25.0) else: self.aRotZ -= event.mouse_region_x - self.mouse_region[0] self.ascale = 0.0 self.mouse_region = event.mouse_region_x, event.mouse_region_y else: target_hit, target_normal, target_eul_rotation = Pick(context, event, self) if target_hit is not None: self.ShowCursor = True up_vector = Vector((0.0, 0.0, 1.0)) quat_rot_axis = rot_axis_quat(up_vector, target_normal) self.quat_rot = target_eul_rotation @ quat_rot_axis MoveCursor(quat_rot_axis, target_hit, self) self.SavCurLoc = target_hit if self.ctrl: if self.SavMousePos is not None: xEcart = abs(self.SavMousePos.x - self.SavCurLoc.x) yEcart = abs(self.SavMousePos.y - self.SavCurLoc.y) zEcart = abs(self.SavMousePos.z - self.SavCurLoc.z) if (xEcart > yEcart) and (xEcart > zEcart): self.CurLoc = Vector( (target_hit.x, self.SavMousePos.y, self.SavMousePos.z)) if (yEcart > xEcart) and (yEcart > zEcart): self.CurLoc = Vector( (self.SavMousePos.x, target_hit.y, self.SavMousePos.z)) if (zEcart > xEcart) and (zEcart > yEcart): self.CurLoc = Vector( (self.SavMousePos.x, self.SavMousePos.y, target_hit.z)) else: self.CurLoc = target_hit else: self.CurLoc = target_hit else: if self.CutMode: if self.alt is False: if self.ctrl : # Find the closest position on the overlay grid and snap the mouse on it # Draw a mini grid around the cursor mouse_pos = [[event.mouse_region_x, event.mouse_region_y]] Snap_Cursor(self, context, event, mouse_pos) else: if len(self.mouse_path) > 0: self.mouse_path[len(self.mouse_path) - 1] = (event.mouse_region_x, event.mouse_region_y) else: # [ALT] press, update position self.xpos += (event.mouse_region_x - self.last_mouse_region_x) self.ypos += (event.mouse_region_y - self.last_mouse_region_y) self.last_mouse_region_x = event.mouse_region_x self.last_mouse_region_y = event.mouse_region_y elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': if self.ObjectMode or self.ProfileMode: if self.LMB is False: target_hit, target_normal, target_eul_rotation = Pick(context, event, self) if target_hit is not None: up_vector = Vector((0.0, 0.0, 1.0)) self.quat_rot_axis = rot_axis_quat(up_vector, target_normal) self.quat_rot = target_eul_rotation @ self.quat_rot_axis self.mouse_region = event.mouse_region_x, event.mouse_region_y self.xSavMouse = event.mouse_region_x if self.ctrl: self.nRotZ = int((self.aRotZ / 25.0) / (PI / 4.0)) self.aRotZ = self.nRotZ * (PI / 4.0) * 25.0 self.LMB = True # LEFTMOUSE elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE' and self.in_view_3d: if self.ObjectMode or self.ProfileMode: # Rotation and scale self.LMB = False if self.ObjectScale is True: self.ObjectScale = False if self.GridScaleX is True: self.GridScaleX = False if self.GridScaleY is True: self.GridScaleY = False if self.WidthSolidify: self.WidthSolidify = False if self.CarveDepth is True: self.CarveDepth = False if self.BrushDepth is True: self.BrushDepth = False else: if self.CutMode is False: if self.ctrl: Picking(context, event) else: if self.CutType == self.line: if self.CutMode is False: self.mouse_path.clear() self.mouse_path.append((event.mouse_region_x, event.mouse_region_y)) self.mouse_path.append((event.mouse_region_x, event.mouse_region_y)) else: self.mouse_path[0] = (event.mouse_region_x, event.mouse_region_y) self.mouse_path[1] = (event.mouse_region_x, event.mouse_region_y) self.CutMode = True else: if self.CutType != self.line: # Cut creation if self.CutType == self.rectangle: CreateCutSquare(self, context) if self.CutType == self.circle: CreateCutCircle(self, context) if self.CutType == self.line: CreateCutLine(self, context) if self.CreateMode: # Object creation self.CreateGeometry() bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') # Depth Cursor context.scene.mesh_carver.DepthCursor = self.snapCursor # Instantiate object context.scene.mesh_carver.OInstanciate = self.Instantiate # Random rotation context.scene.mesh_carver.ORandom = self.RandomRotation # Apply operation context.scene.mesh_carver.DontApply = self.dont_apply_boolean # if Object mode, set initiale state if self.ObjectBrush is not None: self.ObjectBrush.location = self.InitBrush['location'] self.ObjectBrush.scale = self.InitBrush['scale'] self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion'] self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler'] self.ObjectBrush.display_type = self.InitBrush['display_type'] self.ObjectBrush.show_in_front = self.InitBrush['show_in_front'] # remove solidify Selection_Save(self) self.BrushSolidify = False bpy.ops.object.select_all(action='TOGGLE') self.ObjectBrush.select_set(True) context.view_layer.objects.active = self.ObjectBrush bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY") Selection_Restore(self) context.scene.mesh_carver.nProfile = self.nProfil return {'FINISHED'} else: self.Cut() UndoListUpdate(self) else: # Line self.mouse_path.append((event.mouse_region_x, event.mouse_region_y)) # Change brush profil or circle subdivisions elif (event.type == 'COMMA' and event.value == 'PRESS') or \ (event.type == self.carver_prefs.Key_Subrem and event.value == 'PRESS'): # Brush profil if self.ProfileMode: self.nProfil += 1 if self.nProfil >= self.MaxProfil: self.nProfil = 0 createMeshFromData(self) # Circle subdivisions if self.CutType == self.circle: self.step += 1 if self.step >= len(self.stepAngle): self.step = len(self.stepAngle) - 1 # Change brush profil or circle subdivisions elif (event.type == 'PERIOD' and event.value == 'PRESS') or \ (event.type == self.carver_prefs.Key_Subadd and event.value == 'PRESS'): # Brush profil if self.ProfileMode: self.nProfil -= 1 if self.nProfil < 0: self.nProfil = self.MaxProfil - 1 createMeshFromData(self) # Circle subdivisions if self.CutType == self.circle: if self.step > 0: self.step -= 1 # Quit elif event.type in {'RIGHTMOUSE', 'ESC'}: # Depth Cursor context.scene.mesh_carver.DepthCursor = self.snapCursor # Instantiate object context.scene.mesh_carver.OInstanciate = self.Instantiate # Random Rotation context.scene.mesh_carver.ORandom = self.RandomRotation # Apply boolean operation context.scene.mesh_carver.DontApply = self.dont_apply_boolean # Reset Object if self.ObjectBrush is not None: self.ObjectBrush.location = self.InitBrush['location'] self.ObjectBrush.scale = self.InitBrush['scale'] self.ObjectBrush.rotation_quaternion = self.InitBrush['rotation_quaternion'] self.ObjectBrush.rotation_euler = self.InitBrush['rotation_euler'] self.ObjectBrush.display_type = self.InitBrush['display_type'] self.ObjectBrush.show_in_front = self.InitBrush['show_in_front'] # Remove solidify modifier Selection_Save(self) self.BrushSolidify = False bpy.ops.object.select_all(action='TOGGLE') self.ObjectBrush.select_set(True) context.view_layer.objects.active = self.ObjectBrush bpy.ops.object.modifier_remove(modifier="CT_SOLIDIFY") bpy.ops.object.select_all(action='TOGGLE') Selection_Restore(self) Selection_Save_Restore(self) context.view_layer.objects.active = self.CurrentActive context.scene.mesh_carver.nProfile = self.nProfil bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') # Remove Copy Object Brush if bpy.data.objects.get("CarverBrushCopy") is not None: brush = bpy.data.objects["CarverBrushCopy"] self.ObjectBrush.data = bpy.data.meshes[brush.data.name] bpy.ops.object.select_all(action='DESELECT') bpy.data.objects["CarverBrushCopy"].select_set(True) bpy.ops.object.delete() return {'FINISHED'} return {'RUNNING_MODAL'} except: print("\n[Carver MT ERROR]\n") import traceback traceback.print_exc() context.window.cursor_modal_set("DEFAULT") context.area.header_text_set(None) bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') self.report({'WARNING'}, "Operation finished. Failure during Carving (Check the console for more info)") return {'FINISHED'} def cancel(self, context): # Note: used to prevent memory leaks on quitting Blender while the modal operator # is still running, gets called on return {"CANCELLED"} bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') def invoke(self, context, event): if context.area.type != 'VIEW_3D': self.report({'WARNING'}, "View3D not found or not currently active. Operation Cancelled") self.cancel(context) return {'CANCELLED'} # test if some other object types are selected that are not meshes for obj in context.selected_objects: if obj.type != "MESH": self.report({'WARNING'}, "Some selected objects are not of the Mesh type. Operation Cancelled") self.cancel(context) return {'CANCELLED'} if context.mode == 'EDIT_MESH': bpy.ops.object.mode_set(mode='OBJECT') #Load the Carver preferences self.carver_prefs = bpy.context.preferences.addons[__package__].preferences # Get default patterns self.Profils = [] for p in Profils: self.Profils.append((p[0], p[1], p[2], p[3])) for o in context.scene.objects: if not o.name.startswith(self.carver_prefs.ProfilePrefix): continue # In-scene profiles may have changed, remove them to refresh for m in bpy.data.meshes: if m.name.startswith(self.carver_prefs.ProfilePrefix): bpy.data.meshes.remove(m) vertices = [] for v in o.data.vertices: vertices.append((v.co.x, v.co.y, v.co.z)) faces = [] for f in o.data.polygons: face = [] for v in f.vertices: face.append(v) faces.append(face) self.Profils.append( (o.name, Vector((o.location.x, o.location.y, o.location.z)), vertices, faces) ) self.nProfil = context.scene.mesh_carver.nProfile self.MaxProfil = len(self.Profils) # reset selected profile if last profile exceeds length of array if self.nProfil >= self.MaxProfil: self.nProfil = context.scene.mesh_carver.nProfile = 0 if len(context.selected_objects) > 1: self.ObjectBrush = context.active_object # Copy the brush object ob = bpy.data.objects.new("CarverBrushCopy", context.object.data.copy()) ob.location = self.ObjectBrush.location context.collection.objects.link(ob) context.view_layer.update() # Save default variables self.InitBrush['location'] = self.ObjectBrush.location.copy() self.InitBrush['scale'] = self.ObjectBrush.scale.copy() self.InitBrush['rotation_quaternion'] = self.ObjectBrush.rotation_quaternion.copy() self.InitBrush['rotation_euler'] = self.ObjectBrush.rotation_euler.copy() self.InitBrush['display_type'] = self.ObjectBrush.display_type self.InitBrush['show_in_front'] = self.ObjectBrush.show_in_front # Test if flat object z = self.ObjectBrush.data.vertices[0].co.z ErrorMarge = 0.01 self.SolidifyPossible = True for v in self.ObjectBrush.data.vertices: if abs(v.co.z - z) > ErrorMarge: self.SolidifyPossible = False break self.CList = [] self.OPList = [] self.RList = [] self.OB_List = [] for obj in context.selected_objects: if obj != self.ObjectBrush: self.OB_List.append(obj) # Left button self.LMB = False # Undo Variables self.undo_index = 0 self.undo_limit = context.preferences.edit.undo_steps self.undo_list = [] # Boolean operations type self.BooleanType = 0 self.UList = [] self.UList_Index = -1 self.UndoOps = [] context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} #Get the region area where the operator is used def check_region(self,context,event): if context.area != None: if context.area.type == "VIEW_3D" : for region in context.area.regions: if region.type == "TOOLS": t_panel = region elif region.type == "UI": ui_panel = region view_3d_region_x = Vector((context.area.x + t_panel.width, context.area.x + context.area.width - ui_panel.width)) view_3d_region_y = Vector((context.region.y, context.region.y+context.region.height)) if (event.mouse_x > view_3d_region_x[0] and event.mouse_x < view_3d_region_x[1] \ and event.mouse_y > view_3d_region_y[0] and event.mouse_y < view_3d_region_y[1]): self.in_view_3d = True else: self.in_view_3d = False else: self.in_view_3d = False def CreateGeometry(self): context = bpy.context in_local_view = False for area in context.screen.areas: if area.type == 'VIEW_3D': if area.spaces[0].local_view is not None: in_local_view = True if in_local_view: bpy.ops.view3d.localview() if self.ExclusiveCreateMode: # Default width objBBDiagonal = 0.5 else: ActiveObj = self.CurrentSelection[0] if ActiveObj is not None: # Object dimensions objBBDiagonal = objDiagonal(ActiveObj) / 4 subdivisions = 2 if len(context.selected_objects) > 0: bpy.ops.object.select_all(action='TOGGLE') context.view_layer.objects.active = self.CurrentObj bpy.data.objects[self.CurrentObj.name].select_set(True) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.select_mode(type="EDGE") if self.snapCursor is False: bpy.ops.transform.translate(value=self.ViewVector * objBBDiagonal * subdivisions) bpy.ops.mesh.extrude_region_move( TRANSFORM_OT_translate={"value": -self.ViewVector * objBBDiagonal * subdivisions * 2}) bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent() bpy.ops.object.mode_set(mode='OBJECT') saved_location_0 = context.scene.cursor.location.copy() bpy.ops.view3d.snap_cursor_to_active() saved_location = context.scene.cursor.location.copy() bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) context.scene.cursor.location = saved_location bpy.ops.object.origin_set(type='ORIGIN_CURSOR') context.scene.cursor.location = saved_location_0 bpy.data.objects[self.CurrentObj.name].select_set(True) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') for o in self.all_sel_obj_list: bpy.data.objects[o.name].select_set(True) if in_local_view: bpy.ops.view3d.localview() self.CutMode = False self.mouse_path.clear() self.mouse_path = [(0, 0), (0, 0)] def Cut(self): context = bpy.context # Local view ? in_local_view = False for area in context.screen.areas: if area.type == 'VIEW_3D': if area.spaces[0].local_view is not None: in_local_view = True if in_local_view: bpy.ops.view3d.localview() # Save cursor position CursorLocation = context.scene.cursor.location.copy() #List of selected objects selected_obj_list = [] #Cut Mode with line if (self.ObjectMode is False) and (self.ProfileMode is False): #Compute the bounding Box objBBDiagonal = objDiagonal(self.CurrentSelection[0]) if self.dont_apply_boolean: subdivisions = 1 else: subdivisions = 32 # Get selected objects selected_obj_list = context.selected_objects.copy() bpy.ops.object.select_all(action='TOGGLE') context.view_layer.objects.active = self.CurrentObj bpy.data.objects[self.CurrentObj.name].select_set(True) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.select_mode(type="EDGE") #Translate the created mesh away from the view if (self.snapCursor is False) or (self.ForceRebool): bpy.ops.transform.translate(value=self.ViewVector * objBBDiagonal * subdivisions) #Extrude the mesh region and move the result bpy.ops.mesh.extrude_region_move( TRANSFORM_OT_translate={"value": -self.ViewVector * objBBDiagonal * subdivisions * 2}) bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.normals_make_consistent() bpy.ops.object.mode_set(mode='OBJECT') else: # Create list if self.ObjectMode: for o in self.CurrentSelection: if o != self.ObjectBrush: selected_obj_list.append(o) self.CurrentObj = self.ObjectBrush else: selected_obj_list = self.CurrentSelection self.CurrentObj = self.ProfileBrush for obj in self.CurrentSelection: UndoAdd(self, "MESH", obj) # List objects create with rebool lastSelected = [] for ActiveObj in selected_obj_list: context.scene.cursor.location = CursorLocation if len(context.selected_objects) > 0: bpy.ops.object.select_all(action='TOGGLE') # Select cut object bpy.data.objects[self.CurrentObj.name].select_set(True) context.view_layer.objects.active = self.CurrentObj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.object.mode_set(mode='OBJECT') # Select object to cut bpy.data.objects[ActiveObj.name].select_set(True) context.view_layer.objects.active = ActiveObj bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') # Boolean operation if (self.shift is False) and (self.ForceRebool is False): if self.ObjectMode or self.ProfileMode: if self.BoolOps == self.union: boolean_operation(bool_type="UNION") else: boolean_operation(bool_type="DIFFERENCE") else: boolean_operation(bool_type="DIFFERENCE") # Apply booleans if self.dont_apply_boolean is False: BMname = "CT_" + self.CurrentObj.name for mb in ActiveObj.modifiers: if (mb.type == 'BOOLEAN') and (mb.name == BMname): try: bpy.ops.object.modifier_apply(modifier=BMname) except: bpy.ops.object.modifier_remove(modifier=BMname) exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) bpy.ops.object.select_all(action='TOGGLE') else: if self.ObjectMode or self.ProfileMode: for mb in self.CurrentObj.modifiers: if (mb.type == 'SOLIDIFY') and (mb.name == "CT_SOLIDIFY"): try: bpy.ops.object.modifier_apply(modifier="CT_SOLIDIFY") except: exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) # Rebool Rebool(context, self) # Test if not empty object if context.selected_objects[0]: rebool_RT = context.selected_objects[0] if len(rebool_RT.data.vertices) > 0: # Create Bevel for new objects CreateBevel(context, context.selected_objects[0]) UndoAdd(self, "REBOOL", context.selected_objects[0]) context.scene.cursor.location = ActiveObj.location bpy.ops.object.origin_set(type='ORIGIN_CURSOR') else: bpy.ops.object.delete(use_global=False) context.scene.cursor.location = CursorLocation if self.ObjectMode: context.view_layer.objects.active = self.ObjectBrush if self.ProfileMode: context.view_layer.objects.active = self.ProfileBrush if self.dont_apply_boolean is False: # Apply booleans BMname = "CT_" + self.CurrentObj.name for mb in ActiveObj.modifiers: if (mb.type == 'BOOLEAN') and (mb.name == BMname): try: bpy.ops.object.modifier_apply(modifier=BMname) except: bpy.ops.object.modifier_remove(modifier=BMname) exc_type, exc_value, exc_traceback = sys.exc_info() self.report({'ERROR'}, str(exc_value)) # Get new objects created with rebool operations if len(context.selected_objects) > 0: if self.shift is True: # Get the last object selected lastSelected.append(context.selected_objects[0]) context.scene.cursor.location = CursorLocation if self.dont_apply_boolean is False: # Remove cut object if (self.ObjectMode is False) and (self.ProfileMode is False): if len(context.selected_objects) > 0: bpy.ops.object.select_all(action='TOGGLE') bpy.data.objects[self.CurrentObj.name].select_set(True) bpy.ops.object.delete(use_global=False) else: if self.ObjectMode: self.ObjectBrush.display_type = self.InitBrush['display_type'] if len(context.selected_objects) > 0: bpy.ops.object.select_all(action='TOGGLE') # Select cut objects for obj in lastSelected: bpy.data.objects[obj.name].select_set(True) for ActiveObj in selected_obj_list: bpy.data.objects[ActiveObj.name].select_set(True) context.view_layer.objects.active = ActiveObj # Update bevel list_act_obj = context.selected_objects.copy() if self.Auto_BevelUpdate: update_bevel(context) # Re-select initial objects bpy.ops.object.select_all(action='TOGGLE') if self.ObjectMode: # Re-select brush self.ObjectBrush.select_set(True) for ActiveObj in selected_obj_list: bpy.data.objects[ActiveObj.name].select_set(True) context.view_layer.objects.active = ActiveObj # If object has children, set "Wire" draw type if self.ObjectBrush is not None: if len(self.ObjectBrush.children) > 0: self.ObjectBrush.display_type = "WIRE" if self.ProfileMode: self.ProfileBrush.display_type = "WIRE" if in_local_view: bpy.ops.view3d.localview() # Reset variables self.CutMode = False self.mouse_path.clear() self.mouse_path = [(0, 0), (0, 0)] self.ForceRebool = False # bpy.ops.mesh.customdata_custom_splitnormals_clear() class CarverProperties(bpy.types.PropertyGroup): DepthCursor: BoolProperty( name="DepthCursor", default=False ) OInstanciate: BoolProperty( name="Obj_Instantiate", default=False ) ORandom: BoolProperty( name="Random_Rotation", default=False ) DontApply: BoolProperty( name="Dont_Apply", default=False ) nProfile: IntProperty( name="Num_Profile", default=0 ) def register(): from bpy.utils import register_class bpy.utils.register_class(CARVER_OT_operator) bpy.utils.register_class(CarverProperties) bpy.types.Scene.mesh_carver = bpy.props.PointerProperty(type=CarverProperties) def unregister(): from bpy.utils import unregister_class bpy.utils.unregister_class(CarverProperties) bpy.utils.unregister_class(CARVER_OT_operator) del bpy.types.Scene.mesh_carver