├── source ├── assets.blend ├── icons │ ├── tool_icons │ │ ├── ops.object.carver_box.dat │ │ ├── ops.object.carver_circle.dat │ │ └── ops.object.carver_polyline.dat │ ├── __init__.py │ ├── cpu.svg │ └── measure.svg ├── functions │ ├── types.py │ ├── math.py │ ├── mesh.py │ ├── list.py │ ├── draw.py │ ├── object.py │ ├── poll.py │ └── modifier.py ├── operators │ ├── __init__.py │ ├── select.py │ ├── canvas.py │ ├── boolean.py │ └── cutter.py ├── blender_manifest.toml ├── __init__.py ├── properties.py ├── versioning.py ├── tools │ ├── __init__.py │ ├── carver_circle.py │ ├── common │ │ ├── ui.py │ │ ├── properties.py │ │ └── types.py │ ├── carver_box.py │ └── carver_polyline.py ├── manual.py ├── preferences.py └── ui.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── FUNDING.yml ├── .gitignore └── LICENSE /source/assets.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickberckley/bool_tool/HEAD/source/assets.blend -------------------------------------------------------------------------------- /source/icons/tool_icons/ops.object.carver_box.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickberckley/bool_tool/HEAD/source/icons/tool_icons/ops.object.carver_box.dat -------------------------------------------------------------------------------- /source/icons/tool_icons/ops.object.carver_circle.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickberckley/bool_tool/HEAD/source/icons/tool_icons/ops.object.carver_circle.dat -------------------------------------------------------------------------------- /source/icons/tool_icons/ops.object.carver_polyline.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickberckley/bool_tool/HEAD/source/icons/tool_icons/ops.object.carver_polyline.dat -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature_request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature you'd like to see** 11 | A clear and concise description of the feature you would like to see implemented 12 | 13 | **Comparisons** 14 | Are there examples of the feature you're asking for? Point them to us so we can take a look. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us catch bugs more easily 4 | title: 'Bug: ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Bug description** 11 | A clear and concise description of what the bug is. 12 | If applicable, add screenshots or videos to help explain your problem. 13 | 14 | **How to reproduce** 15 | Steps to reproduce the behavior: 16 | 1. __ 17 | 2. __ 18 | 3. __ 19 | 20 | **Versions used** 21 | - Blender version: __ 22 | - Add-on version: __ 23 | -------------------------------------------------------------------------------- /source/icons/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import bpy.utils.previews 4 | 5 | 6 | #### ------------------------------ REGISTRATION ------------------------------ #### 7 | 8 | svg_icons = {} 9 | icons = bpy.utils.previews.new() 10 | dir = os.path.join(os.path.dirname(__file__)) 11 | 12 | icons.load("MEASURE", os.path.join(dir, "measure.svg"), 'IMAGE') 13 | icons.load("CPU", os.path.join(dir, "cpu.svg"), 'IMAGE') 14 | svg_icons["main"] = icons 15 | 16 | 17 | def register(): 18 | ... 19 | 20 | def unregister(): 21 | # ICONS 22 | for pcoll in svg_icons.values(): 23 | bpy.utils.previews.remove(pcoll) 24 | svg_icons.clear() 25 | -------------------------------------------------------------------------------- /source/functions/types.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector, Matrix 3 | 4 | 5 | #### ------------------------------ CLASSES ------------------------------ #### 6 | 7 | class Ray: 8 | """Class object for storing raycast results.""" 9 | 10 | def __init__(self, 11 | hit: bool, 12 | location: Vector, 13 | normal: Vector, 14 | index: int, 15 | obj, 16 | matrix: Matrix): 17 | self.hit = hit 18 | self.location = location 19 | self.normal = normal 20 | self.index = index 21 | self.obj = obj 22 | self.matrix = matrix 23 | -------------------------------------------------------------------------------- /source/icons/cpu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/operators/__init__.py: -------------------------------------------------------------------------------- 1 | if "bpy" in locals(): 2 | import importlib 3 | for mod in [boolean, 4 | canvas, 5 | cutter, 6 | select, 7 | ]: 8 | importlib.reload(mod) 9 | else: 10 | import bpy 11 | from . import ( 12 | boolean, 13 | canvas, 14 | cutter, 15 | select, 16 | ) 17 | 18 | 19 | #### ------------------------------ REGISTRATION ------------------------------ #### 20 | 21 | modules = [ 22 | boolean, 23 | canvas, 24 | cutter, 25 | select, 26 | ] 27 | 28 | def register(): 29 | for module in modules: 30 | module.register() 31 | 32 | def unregister(): 33 | for module in reversed(modules): 34 | module.unregister() 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: nickberckley 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /source/blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | 3 | id = "bool_tool" 4 | name = "Bool Tool" 5 | version = "2.0.0" 6 | tagline = "Quick boolean operators and tools for hard surface modeling" 7 | type = "add-on" 8 | 9 | maintainer = "Nika Kutsniashvili " 10 | website = "https://github.com/nickberckley/bool_tool" 11 | tags = ["Modeling", "Object"] 12 | 13 | blender_version_min = "4.5.0" 14 | 15 | # License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) 16 | # https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html 17 | license = [ 18 | "SPDX:GPL-3.0-or-later", 19 | ] 20 | copyright = [ 21 | "2024 Nika Kutsniashvili", 22 | "2024 Vitor Balbio", 23 | "2024 Mikhail Rachinskiy", 24 | "2024 TynkaTopi", 25 | "2024 Meta-Androcto", 26 | "2024 Simon Appelt", 27 | "2024 Pixivore", 28 | "2024 Cedric LEPILLER", 29 | "2024 Ted Milker", 30 | "2024 Clarkx", 31 | ] 32 | 33 | [build] 34 | paths_exclude_pattern = [ 35 | "/.git/", 36 | "__pycache__/", 37 | ".*", 38 | ] 39 | -------------------------------------------------------------------------------- /source/__init__.py: -------------------------------------------------------------------------------- 1 | if "bpy" in locals(): 2 | import importlib 3 | for mod in [icons, 4 | operators, 5 | tools, 6 | manual, 7 | preferences, 8 | properties, 9 | ui, 10 | versioning, 11 | ]: 12 | importlib.reload(mod) 13 | print("Add-on Reloaded: Bool Tool") 14 | else: 15 | import bpy 16 | from . import ( 17 | icons, 18 | operators, 19 | tools, 20 | manual, 21 | preferences, 22 | properties, 23 | ui, 24 | versioning, 25 | ) 26 | 27 | 28 | #### ------------------------------ REGISTRATION ------------------------------ #### 29 | 30 | modules = [ 31 | icons, 32 | operators, 33 | tools, 34 | manual, 35 | preferences, 36 | properties, 37 | ui, 38 | versioning, 39 | ] 40 | 41 | def register(): 42 | for module in modules: 43 | module.register() 44 | 45 | preferences.update_sidebar_category(bpy.context.preferences.addons[__package__].preferences, bpy.context) 46 | 47 | 48 | def unregister(): 49 | for module in reversed(modules): 50 | module.unregister() 51 | -------------------------------------------------------------------------------- /source/properties.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | #### ------------------------------ PROPERTIES ------------------------------ #### 5 | 6 | class OBJECT_PG_booleans(bpy.types.PropertyGroup): 7 | # OBJECT-level Properties 8 | 9 | canvas: bpy.props.BoolProperty( 10 | name = "Boolean Canvas", 11 | options = set(), 12 | default = False, 13 | ) 14 | cutter: bpy.props.StringProperty( 15 | name = "Boolean Cutter", 16 | options = set(), 17 | ) 18 | slice: bpy.props.BoolProperty( 19 | name = "Boolean Slice", 20 | options = set(), 21 | default = False, 22 | ) 23 | 24 | slice_of: bpy.props.PointerProperty( 25 | name = "Slice of...", 26 | type = bpy.types.Object, 27 | options = set(), 28 | ) 29 | carver: bpy.props.BoolProperty( 30 | name = "Is Carver Cutter", 31 | options = set(), 32 | default = False, 33 | ) 34 | 35 | 36 | 37 | #### ------------------------------ REGISTRATION ------------------------------ #### 38 | 39 | classes = [ 40 | OBJECT_PG_booleans, 41 | ] 42 | 43 | def register(): 44 | for cls in classes: 45 | bpy.utils.register_class(cls) 46 | 47 | # PROPERTY 48 | bpy.types.Object.booleans = bpy.props.PointerProperty(type=OBJECT_PG_booleans, name="Booleans") 49 | 50 | 51 | def unregister(): 52 | for cls in reversed(classes): 53 | bpy.utils.unregister_class(cls) 54 | 55 | # PROPERTY 56 | del bpy.types.Object.booleans 57 | -------------------------------------------------------------------------------- /source/icons/measure.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | -------------------------------------------------------------------------------- /source/versioning.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | #### ------------------------------ FUNCTIONS ------------------------------ #### 5 | 6 | @bpy.app.handlers.persistent 7 | def populate_boolean_properties(scene): 8 | prefs = bpy.context.preferences.addons[__package__].preferences 9 | if prefs.versioning: 10 | for obj in bpy.data.objects: 11 | if not obj.get("BoolToolRoot"): 12 | continue 13 | 14 | # Convert Canvas 15 | if obj.get("BoolToolRoot"): 16 | obj.booleans.canvas = True 17 | del obj["BoolToolRoot"] 18 | if obj.get("BoolTool_FTransform"): 19 | del obj["BoolTool_FTransform"] 20 | 21 | for mod in obj.modifiers: 22 | if mod.type == 'BOOLEAN' and "BTool_" in mod.name: 23 | mod.name = "boolean_" + mod.object.name 24 | cutter = mod.object 25 | 26 | # Convert Canvases 27 | if cutter.get("BoolToolBrush"): 28 | cutter.booleans.cutter = cutter.get("BoolToolBrush") 29 | del cutter["BoolToolBrush"] 30 | if cutter.get("BoolTool_FTransform"): 31 | del cutter["BoolTool_FTransform"] 32 | 33 | 34 | 35 | #### ------------------------------ REGISTRATION ------------------------------ #### 36 | 37 | def register(): 38 | # HANDLERS 39 | bpy.app.handlers.load_post.append(populate_boolean_properties) 40 | 41 | def unregister(): 42 | # HANDLERS 43 | bpy.app.handlers.load_post.remove(populate_boolean_properties) 44 | -------------------------------------------------------------------------------- /source/tools/__init__.py: -------------------------------------------------------------------------------- 1 | if "bpy" in locals(): 2 | import importlib 3 | for mod in [carver_box, 4 | carver_circle, 5 | carver_polyline, 6 | ui, 7 | ]: 8 | importlib.reload(mod) 9 | else: 10 | import bpy 11 | from . import ( 12 | carver_box, 13 | carver_circle, 14 | carver_polyline, 15 | ) 16 | from .common import ( 17 | ui, 18 | ) 19 | 20 | 21 | #### ------------------------------ REGISTRATION ------------------------------ #### 22 | 23 | """NOTE: Order of modules is important because of dependancies. Don't change without a reason.""" 24 | modules = [ 25 | carver_circle, 26 | carver_box, 27 | carver_polyline, 28 | ui, 29 | ] 30 | 31 | main_tools = [ 32 | carver_box.OBJECT_WT_carve_box, 33 | carver_box.MESH_WT_carve_box, 34 | ] 35 | secondary_tools = [ 36 | carver_circle.OBJECT_WT_carve_circle, 37 | carver_circle.MESH_WT_carve_circle, 38 | carver_polyline.OBJECT_WT_carve_polyline, 39 | carver_polyline.MESH_WT_carve_polyline, 40 | ] 41 | 42 | 43 | def register(): 44 | for module in modules: 45 | module.register() 46 | 47 | for tool in main_tools: 48 | bpy.utils.register_tool(tool, separator=False, after="builtin.primitive_cube_add", group=True) 49 | for tool in secondary_tools: 50 | bpy.utils.register_tool(tool, separator=False, after="object.carve_box", group=False) 51 | 52 | 53 | def unregister(): 54 | for module in reversed(modules): 55 | module.unregister() 56 | 57 | for tool in main_tools: 58 | bpy.utils.unregister_tool(tool) 59 | for tool in secondary_tools: 60 | bpy.utils.unregister_tool(tool) 61 | -------------------------------------------------------------------------------- /source/manual.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | #### ------------------------------ FUNCTIONS ------------------------------ #### 5 | 6 | def bool_tool_manual_map(): 7 | url_manual_prefix = "https://github.com/nickberckley/bool_tool/wiki/" 8 | 9 | # Carver 10 | url_manual_mapping = (("bpy.ops.object.carve", "Carver"), 11 | # Brush Boolean 12 | ("bpy.ops.object.boolean_brush_union", "Boolean-Operators"), 13 | ("bpy.ops.object.boolean_brush_intersect", "Boolean-Operators"), 14 | ("bpy.ops.object.boolean_brush_difference", "Boolean-Operators"), 15 | ("bpy.ops.object.boolean_brush_slice", "Boolean-Operators"), 16 | # Auto Boolean 17 | ("bpy.ops.object.boolean_auto_union", "Boolean-Operators#auto-boolean-operators"), 18 | ("bpy.ops.object.boolean_auto_intersect", "Boolean-Operators#auto-boolean-operators"), 19 | ("bpy.ops.object.boolean_auto_difference", "Boolean-Operators#auto-boolean-operators"), 20 | ("bpy.ops.object.boolean_auto_slice", "Boolean-Operators#auto-boolean-operators"), 21 | # Cutter Utilities 22 | ("bpy.ops.object.boolean_toggle_cutter", "Utility-Operators#toggle-cutter"), 23 | ("bpy.ops.object.boolean_remove_cutter", "Utility-Operators#remove-cutter"), 24 | ("bpy.ops.object.boolean_apply_cutter", "Utility-Operators#apply-cutter"), 25 | # Canvas Utilities 26 | ("bpy.ops.object.boolean_toggle_all", "Utility-Operators#toggle-all-cutters"), 27 | ("bpy.ops.object.boolean_remove_all", "Utility-Operators#remove-all-cutters"), 28 | ("bpy.ops.object.boolean_apply_all", "Utility-Operators#apply-all-cutters"), 29 | # Select 30 | ("bpy.ops.object.select_cutter_canvas", "Utility-Operators#select-operators"), 31 | ("bpy.ops.object.boolean_select_all", "Utility-Operators#select-operators"), 32 | ) 33 | 34 | return url_manual_prefix, url_manual_mapping 35 | 36 | 37 | 38 | #### ------------------------------ REGISTRATION ------------------------------ #### 39 | 40 | def register(): 41 | # MANUAL 42 | bpy.utils.register_manual_map(bool_tool_manual_map) 43 | 44 | def unregister(): 45 | # MANUAL 46 | bpy.utils.unregister_manual_map(bool_tool_manual_map) 47 | -------------------------------------------------------------------------------- /source/functions/math.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import mathutils 3 | from mathutils import Vector 4 | from bpy_extras import view3d_utils 5 | 6 | 7 | #### ------------------------------ FUNCTIONS ------------------------------ #### 8 | 9 | def distance_from_point_to_segment(point, start, end) -> float: 10 | """ 11 | Calculates the shortest distance between a point and a segment. 12 | All three inputs should be `mathutils.Vector` objects. 13 | This is an alternative to `mathutils.geometry.intersect_point_line`. 14 | Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout). 15 | """ 16 | 17 | segment = end - start 18 | start_to_point = point - start 19 | 20 | # projection_along_segment 21 | c1 = start_to_point.dot(segment) 22 | if c1 <= 0: 23 | return (point - start).length 24 | 25 | # segment_length_squared 26 | c2 = segment.dot(segment) 27 | if c2 <= c1: 28 | return (point - end).length 29 | 30 | t = c1 / c2 31 | closest_point = start + t * segment 32 | distance = (point - closest_point).length 33 | 34 | return distance 35 | 36 | 37 | def region_2d_to_line_3d(region, rv3d, point_2d: Vector, line_origin: Vector, line_direction: Vector) -> tuple[Vector, Vector]: 38 | """ 39 | Converts a 2D screen-space point into a 3D ray and finds closest 40 | points between that ray and a given 3D line. 41 | """ 42 | 43 | if line_origin is None or line_direction is None: 44 | return None, None 45 | 46 | # Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction). 47 | ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d) 48 | ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d) 49 | 50 | # Find closest points to each other on each line (second line being a ray). 51 | closest_points = mathutils.geometry.intersect_line_line(ray_origin, 52 | ray_origin + ray_direction, 53 | line_origin, 54 | line_origin + line_direction) 55 | 56 | return closest_points 57 | 58 | 59 | def region_2d_to_plane_3d(region, rv3d, point_2d: Vector, plane: tuple[Vector]) -> Vector: 60 | """ 61 | Converts a 2D screen-space point into a 3D point on a plane in world-space. 62 | Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout). 63 | """ 64 | 65 | location, normal = plane 66 | 67 | # Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction). 68 | p3_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d) 69 | p3_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d) 70 | 71 | # Intersect the point with the plane. 72 | p3_on_plane = mathutils.geometry.intersect_line_plane(p3_origin, # First point of line. 73 | p3_origin + p3_direction, # Second point of line. 74 | location, # `plane_co` (a point on the plane). 75 | normal) # `plane_no` (the direction the plane is facing). 76 | 77 | return p3_on_plane 78 | -------------------------------------------------------------------------------- /source/tools/carver_circle.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from .. import __file__ as base_file 4 | 5 | from .common.ui import ( 6 | carver_ui_common, 7 | ) 8 | from .carver_box import OBJECT_OT_carve_box 9 | 10 | 11 | description = "Cut primitive shapes into mesh objects with brush" 12 | 13 | #### ------------------------------ TOOLS ------------------------------ #### 14 | 15 | class OBJECT_WT_carve_circle(bpy.types.WorkSpaceTool): 16 | bl_idname = "object.carve_circle" 17 | bl_label = "Circle Carve" 18 | bl_description = description 19 | 20 | bl_space_type = 'VIEW_3D' 21 | bl_context_mode = 'OBJECT' 22 | 23 | bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_circle") 24 | bl_keymap = ( 25 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}), 26 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}), 27 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": None}), 28 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": None}), 29 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": None}), 30 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": None}), 31 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": None}), 32 | ("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": None}), 33 | ) 34 | 35 | def draw_settings(context, layout, tool): 36 | props = tool.operator_properties("object.carve_circle") 37 | carver_ui_common(context, layout, props) 38 | 39 | class MESH_WT_carve_circle(OBJECT_WT_carve_circle): 40 | bl_context_mode = 'EDIT_MESH' 41 | 42 | 43 | 44 | #### ------------------------------ OPERATORS ------------------------------ #### 45 | 46 | class OBJECT_OT_carve_circle(OBJECT_OT_carve_box): 47 | bl_idname = "object.carve_circle" 48 | bl_label = "Box Carve" 49 | bl_description = description 50 | 51 | # SHAPE-properties 52 | shape = 'CIRCLE' 53 | 54 | subdivision: bpy.props.IntProperty( 55 | name = "Circle Subdivisions", 56 | description = "Number of vertices that will make up the circular shape that will be extruded into a cylinder", 57 | min = 3, soft_max = 128, 58 | default = 16, 59 | ) 60 | aspect: bpy.props.EnumProperty( 61 | name = "Aspect", 62 | description = "The initial aspect", 63 | items = (('FREE', "Free", "Use an unconstrained aspect"), 64 | ('FIXED', "Fixed", "Use a fixed 1:1 aspect")), 65 | default = 'FIXED', 66 | ) 67 | origin: bpy.props.EnumProperty( 68 | name = "Origin", 69 | description = "The initial position for placement", 70 | items = (('EDGE', "Edge", ""), 71 | ('CENTER', "Center", "")), 72 | default = 'CENTER', 73 | ) 74 | 75 | 76 | 77 | #### ------------------------------ REGISTRATION ------------------------------ #### 78 | 79 | classes = [ 80 | OBJECT_OT_carve_circle, 81 | ] 82 | 83 | def register(): 84 | for cls in classes: 85 | bpy.utils.register_class(cls) 86 | 87 | def unregister(): 88 | for cls in reversed(classes): 89 | bpy.utils.unregister_class(cls) 90 | -------------------------------------------------------------------------------- /source/functions/mesh.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import mathutils 4 | import math 5 | from bpy_extras import view3d_utils 6 | 7 | from .object import hide_objects 8 | from .types import Ray 9 | 10 | 11 | #### ------------------------------ FUNCTIONS ------------------------------ #### 12 | 13 | def extrude_face(bm, face): 14 | """Extrudes cutter face (created by carve operation) along view vector to create a non-manifold mesh""" 15 | 16 | bm.faces.ensure_lookup_table() 17 | 18 | # Extrude 19 | result = bmesh.ops.extrude_face_region(bm, geom=[bm.faces[face.index]]) 20 | 21 | # Offset extruded vertices. 22 | extruded_verts = [v for v in result['geom'] if isinstance(v, bmesh.types.BMVert)] 23 | extruded_edges = [e for e in result['geom'] if isinstance(e, bmesh.types.BMEdge)] 24 | extruded_faces = [f for f in result['geom'] if isinstance(f, bmesh.types.BMFace)] 25 | 26 | return extruded_verts, extruded_edges, extruded_faces 27 | 28 | 29 | def shade_smooth_by_angle(bm, mesh, angle=30): 30 | """Replication of "Auto Smooth" functionality: Marks faces as smooth, sharp edges (by angle) as sharp""" 31 | 32 | for f in bm.faces: 33 | f.smooth = True 34 | 35 | for edge in bm.edges: 36 | if len(edge.link_faces) != 2: 37 | continue 38 | 39 | face1, face2 = edge.link_faces 40 | if face1.normal.length <= 0 or face2.normal.length <= 0:\ 41 | continue 42 | 43 | edge_angle = math.degrees(face1.normal.angle(face2.normal)) 44 | if edge_angle < 0: 45 | continue 46 | if edge_angle < angle: 47 | continue 48 | 49 | edge.smooth = False 50 | 51 | bm.to_mesh(mesh) 52 | 53 | 54 | def are_intersecting(obj_a, obj_b): 55 | """Checks if bounding boxes of two given objects intersect.""" 56 | 57 | def world_bounds(obj): 58 | corners = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box] 59 | xs = [c.x for c in corners] 60 | ys = [c.y for c in corners] 61 | zs = [c.z for c in corners] 62 | return (min(xs), max(xs)), (min(ys), max(ys)), (min(zs), max(zs)) 63 | 64 | (ax0, ax1), (ay0, ay1), (az0, az1) = world_bounds(obj_a) 65 | (bx0, bx1), (by0, by1), (bz0, bz1) = world_bounds(obj_b) 66 | 67 | return ( 68 | ax1 >= bx0 and ax0 <= bx1 and 69 | ay1 >= by0 and ay0 <= by1 and 70 | az1 >= bz0 and az0 <= bz1 71 | ) 72 | 73 | 74 | def ensure_attribute(bm, name, domain): 75 | """Ensure that the attribute with the given name and domain exists on mesh.""" 76 | 77 | if domain == 'EDGE': 78 | attr = bm.edges.layers.float.get(name) 79 | if not attr: 80 | attr = bm.edges.layers.float.new(name) 81 | 82 | elif domain == 'VERTEX': 83 | attr = bm.verts.layers.float.get(name) 84 | if not attr: 85 | attr = bm.verts.layers.float.new(name) 86 | 87 | return attr 88 | 89 | 90 | def raycast(context, position, objects): 91 | """Cast a ray in the scene to get the surface on any of the given objects.""" 92 | 93 | region = context.region 94 | rv3d = context.region_data 95 | depsgraph = context.view_layer.depsgraph 96 | 97 | origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, position) 98 | direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, position) 99 | 100 | # Cast Ray 101 | with hide_objects(context, exceptions=objects): 102 | hit, location, normal, index, object, matrix = context.scene.ray_cast(depsgraph, origin, direction) 103 | ray = Ray(hit, location, normal, index, object, matrix) 104 | 105 | return ray 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /source/operators/select.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. import __package__ as base_package 3 | 4 | from ..functions.poll import ( 5 | basic_poll, 6 | active_modifier_poll, 7 | is_canvas, 8 | ) 9 | from ..functions.list import ( 10 | list_selected_cutters, 11 | list_selected_canvases, 12 | list_canvas_cutters, 13 | list_cutter_users, 14 | ) 15 | 16 | 17 | #### ------------------------------ OPERATORS ------------------------------ #### 18 | 19 | # Select Cutter Canvas 20 | class OBJECT_OT_select_cutter_canvas(bpy.types.Operator): 21 | bl_idname = "object.select_cutter_canvas" 22 | bl_label = "Select Boolean Canvas" 23 | bl_description = "Select all the objects that use selected objects as boolean cutters" 24 | bl_options = {'UNDO'} 25 | 26 | @classmethod 27 | def poll(cls, context): 28 | return basic_poll(cls, context) and context.active_object.booleans.cutter 29 | 30 | def execute(self, context): 31 | cutters = list_selected_cutters(context) 32 | canvases = list_cutter_users(cutters) 33 | 34 | # Select Canvases 35 | bpy.ops.object.select_all(action='DESELECT') 36 | for canvas in canvases: 37 | canvas.select_set(True) 38 | 39 | return {'FINISHED'} 40 | 41 | 42 | # Select All Cutters 43 | class OBJECT_OT_boolean_select_all(bpy.types.Operator): 44 | bl_idname = "object.boolean_select_all" 45 | bl_label = "Select Boolean Cutters" 46 | bl_description = "Select all boolean cutters affecting active object" 47 | bl_options = {'UNDO'} 48 | 49 | @classmethod 50 | def poll(cls, context): 51 | return basic_poll(cls, context) and is_canvas(context.active_object) 52 | 53 | def execute(self, context): 54 | canvases = list_selected_canvases(context) 55 | cutters, __ = list_canvas_cutters(canvases) 56 | 57 | # select_cutters 58 | bpy.ops.object.select_all(action='DESELECT') 59 | for cutter in cutters: 60 | cutter.select_set(True) 61 | 62 | return {'FINISHED'} 63 | 64 | 65 | # Select Modifier Object 66 | class OBJECT_OT_boolean_select_cutter(bpy.types.Operator): 67 | bl_idname = "object.boolean_select_cutter" 68 | bl_label = "Select Boolean Cutter" 69 | bl_description = "Select object that is used as boolean cutter by this modifier" 70 | bl_options = {'REGISTER', 'UNDO'} 71 | 72 | @classmethod 73 | def poll(cls, context): 74 | prefs = context.preferences.addons[base_package].preferences 75 | return (basic_poll(cls, context) and active_modifier_poll(context.active_object) and 76 | context.area.type == 'PROPERTIES' and context.space_data.context == 'MODIFIER' and 77 | prefs.double_click) 78 | 79 | def execute(self, context): 80 | modifier = context.object.modifiers.active 81 | if modifier and modifier.type == "BOOLEAN": 82 | cutter = modifier.object 83 | 84 | bpy.ops.object.select_all(action='DESELECT') 85 | cutter.select_set(True) 86 | context.view_layer.objects.active = cutter 87 | 88 | return {'FINISHED'} 89 | 90 | 91 | 92 | #### ------------------------------ REGISTRATION ------------------------------ #### 93 | 94 | addon_keymaps = [] 95 | 96 | classes = [ 97 | OBJECT_OT_select_cutter_canvas, 98 | OBJECT_OT_boolean_select_all, 99 | OBJECT_OT_boolean_select_cutter, 100 | ] 101 | 102 | 103 | def register(): 104 | for cls in classes: 105 | bpy.utils.register_class(cls) 106 | 107 | # KEYMAP 108 | addon = bpy.context.window_manager.keyconfigs.addon 109 | km = addon.keymaps.new(name="Property Editor", space_type='PROPERTIES') 110 | 111 | kmi = km.keymap_items.new("object.boolean_select_cutter", type='LEFTMOUSE', value='DOUBLE_CLICK') 112 | kmi.active = True 113 | addon_keymaps.append((km, kmi)) 114 | 115 | 116 | def unregister(): 117 | for cls in reversed(classes): 118 | bpy.utils.unregister_class(cls) 119 | 120 | # KEYMAP 121 | for km, kmi in addon_keymaps: 122 | km.keymap_items.remove(kmi) 123 | addon_keymaps.clear() 124 | -------------------------------------------------------------------------------- /source/functions/list.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | 4 | #### ------------------------------ /all/ ------------------------------ #### 5 | 6 | def list_canvases(): 7 | """List all canvases in the scene""" 8 | 9 | canvas = [] 10 | for obj in bpy.context.scene.objects: 11 | if obj.booleans.canvas: 12 | canvas.append(obj) 13 | 14 | return canvas 15 | 16 | 17 | 18 | #### ------------------------------ /selected/ ------------------------------ #### 19 | 20 | def list_selected_cutters(context): 21 | """List selected cutters""" 22 | 23 | cutters = [] 24 | active_object = context.active_object 25 | selected_objects = context.selected_objects 26 | 27 | if selected_objects: 28 | for obj in selected_objects: 29 | if obj != active_object and obj.type == 'MESH': 30 | if obj.booleans.cutter: 31 | cutters.append(obj) 32 | 33 | if active_object: 34 | if active_object.booleans.cutter: 35 | cutters.append(active_object) 36 | 37 | return cutters 38 | 39 | 40 | def list_selected_canvases(context): 41 | """List selected canvases""" 42 | 43 | canvases = [] 44 | active_object = context.active_object 45 | selected_objects = context.selected_objects 46 | 47 | if selected_objects: 48 | for obj in selected_objects: 49 | if obj != active_object and obj.type == 'MESH': 50 | if obj.booleans.canvas: 51 | canvases.append(obj) 52 | 53 | if active_object: 54 | if active_object.booleans.canvas: 55 | canvases.append(active_object) 56 | 57 | return canvases 58 | 59 | 60 | 61 | #### ------------------------------ /users/ ------------------------------ #### 62 | 63 | def list_canvas_cutters(canvases): 64 | """List cutters that are used by specified canvases""" 65 | 66 | cutters = [] 67 | modifiers = [] 68 | for canvas in canvases: 69 | for mod in canvas.modifiers: 70 | if mod.type == 'BOOLEAN' and "boolean_" in mod.name: 71 | if mod.object: 72 | cutters.append(mod.object) 73 | modifiers.append(mod) 74 | 75 | return cutters, modifiers 76 | 77 | 78 | def list_canvas_slices(canvases): 79 | """Returns list of slices for specified canvases""" 80 | 81 | slices = [] 82 | for obj in bpy.context.scene.objects: 83 | if obj.booleans.slice: 84 | if obj.booleans.slice_of in canvases: 85 | slices.append(obj) 86 | 87 | return slices 88 | 89 | 90 | def list_cutter_users(cutters): 91 | """List canvases that use specified cutters""" 92 | 93 | cutter_users = [] 94 | 95 | for cutter in cutters: 96 | object = bpy.data.objects.get(cutter.name) 97 | 98 | for key, values in bpy.data.user_map(subset=[object]).items(): 99 | for value in values: 100 | # filter_only_object_type_users 101 | if value.id_type == 'OBJECT': 102 | for mod in value.modifiers: 103 | if mod.type == 'BOOLEAN': 104 | if mod.object and mod.object == cutter: 105 | cutter_users.append(value) 106 | 107 | return cutter_users 108 | 109 | 110 | def list_cutter_modifiers(canvases, cutters): 111 | """List modifiers on specified canvases that use specified cutters""" 112 | 113 | if not canvases: 114 | canvases = list_canvases() 115 | 116 | modifiers = [] 117 | for canvas in canvases: 118 | for mod in canvas.modifiers: 119 | if mod.type == 'BOOLEAN': 120 | if mod.object in cutters: 121 | modifiers.append(mod) 122 | 123 | return modifiers 124 | 125 | 126 | def list_unused_cutters(cutters, *canvases, do_leftovers=False): 127 | """Takes in list of cutters and returns only those that have no other user besides specified canvas""" 128 | """When `include_visible` is True it will return cutters that aren't used by any visible modifiers""" 129 | 130 | other_canvases = list_canvases() 131 | original_cutters = cutters[:] 132 | 133 | for obj in other_canvases: 134 | if obj in canvases: 135 | return 136 | 137 | if any(mod.object in cutters for mod in obj.modifiers if mod.type == 'BOOLEAN'): 138 | cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]] 139 | 140 | leftovers = [] 141 | # return_cutters_that_do_have_other_users_(so_that_parents_can_be_reassigned) 142 | if do_leftovers: 143 | leftovers = [cutter for cutter in original_cutters if cutter not in cutters] 144 | 145 | return cutters, leftovers 146 | 147 | 148 | def list_pre_boolean_modifiers(obj) -> list: 149 | """Returns a list of boolean modifiers & modifiers that come before last boolean modifier""" 150 | 151 | # Find the index of a last boolean modifier 152 | last_boolean_index = -1 153 | for i in reversed(range(len(obj.modifiers))): 154 | if obj.modifiers[i].type == 'BOOLEAN': 155 | last_boolean_index = i 156 | break 157 | 158 | # If boolean modifier is found, list all modifiers that come before it. 159 | if last_boolean_index != -1: 160 | return [mod for mod in obj.modifiers[:last_boolean_index + 1]] 161 | else: 162 | return [] 163 | -------------------------------------------------------------------------------- /source/functions/draw.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import gpu 3 | import math 4 | import mathutils 5 | from bpy_extras import view3d_utils 6 | from mathutils import Vector 7 | from gpu_extras.batch import batch_for_shader 8 | 9 | 10 | #### ------------------------------ FUNCTIONS ------------------------------ #### 11 | 12 | def draw_shader(type, color, alpha, coords, size=1, indices=None): 13 | """Creates a batch for a draw type""" 14 | 15 | gpu.state.blend_set('ALPHA') 16 | 17 | if type == 'POINTS': 18 | gpu.state.program_point_size_set(False) 19 | gpu.state.point_size_set(size) 20 | shader = gpu.shader.from_builtin('UNIFORM_COLOR') 21 | shader.uniform_float("color", (color[0], color[1], color[2], alpha)) 22 | batch = batch_for_shader(shader, 'POINTS', {"pos": coords}, indices=indices) 23 | 24 | elif type in 'LINES': 25 | gpu.state.line_width_set(size) 26 | shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR') 27 | shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:]) 28 | shader.uniform_float("lineWidth", size) 29 | shader.uniform_float("color", (color[0], color[1], color[2], alpha)) 30 | batch = batch_for_shader(shader, 'LINES', {"pos": coords}, indices=indices) 31 | 32 | elif type in 'LINE_LOOP': 33 | shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR') 34 | shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:]) 35 | shader.uniform_float("lineWidth", size) 36 | shader.uniform_float("color", (color[0], color[1], color[2], alpha)) 37 | batch = batch_for_shader(shader, 'LINE_LOOP', {"pos": coords}) 38 | 39 | if type == 'SOLID': 40 | gpu.state.depth_test_set('NONE') 41 | shader = gpu.shader.from_builtin('UNIFORM_COLOR') 42 | shader.uniform_float("color", (color[0], color[1], color[2], alpha)) 43 | batch = batch_for_shader(shader, 'TRIS', {"pos": coords}, indices=indices) 44 | 45 | batch.draw(shader) 46 | gpu.state.point_size_set(1.0) 47 | gpu.state.line_width_set(1.0) 48 | gpu.state.blend_set('NONE') 49 | 50 | 51 | def draw_bmesh_faces(faces, world_matrix): 52 | """ 53 | Get world-space vertex pairs and indices from `bmesh` face. To be used in GPU batch. 54 | Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout). 55 | """ 56 | 57 | if not faces: 58 | return None, None 59 | 60 | vertices = [] 61 | indices = [] 62 | 63 | vert_index_map = {} 64 | vert_count = 0 65 | for face in faces: 66 | face_indices = [] 67 | 68 | # Collect unique vertices only (avoid storing verts that are shared by faces multiple times). 69 | # (Iterating over face corners because unlike `face.verts` they're ordered). 70 | for loop in face.loops: 71 | vert = loop.vert 72 | co = world_matrix @ Vector(vert.co) 73 | 74 | if vert not in vert_index_map: 75 | vertices.append(co) 76 | vert_index_map[vert] = vert_count 77 | face_indices.append(vert_count) 78 | vert_count += 1 79 | else: 80 | face_indices.append(vert_index_map[vert]) 81 | 82 | # Triangulate face and map local indices to global vertex indices. 83 | if len(face_indices) >= 3: 84 | try: 85 | face_verts_co = [vertices[idx] for idx in face_indices] 86 | tris = mathutils.geometry.tessellate_polygon([face_verts_co]) 87 | for tri in tris: 88 | indices.append((face_indices[tri[0]], face_indices[tri[1]], face_indices[tri[2]])) 89 | except: 90 | # Fallback to simple fan triangulation if tessellation fails. 91 | for i in range(1, len(face_indices) - 1): 92 | indices.append((face_indices[0], face_indices[i], face_indices[i + 1])) 93 | 94 | return vertices, indices 95 | 96 | 97 | def draw_bmesh_edges(edges, world_matrix): 98 | """Convert bmesh edges into world-space vertex pairs to be used in GPU batch.""" 99 | 100 | if not edges: 101 | return None 102 | 103 | vertices = [] 104 | for edge in edges: 105 | v1 = world_matrix @ edge.verts[0].co 106 | v2 = world_matrix @ edge.verts[1].co 107 | vertices.append(v1) 108 | vertices.append(v2) 109 | 110 | return vertices 111 | 112 | 113 | def draw_circle_around_point(context, obj, vert, radius, segments): 114 | """ 115 | Draws the screen-aligned circle around given vertex of the object. 116 | Returns the list of vertices for GPU batch. 117 | """ 118 | 119 | region = context.region 120 | rv3d = context.region_data 121 | vert_world = obj.matrix_world @ vert.co 122 | radius = min(radius, 25) 123 | 124 | vertices = [] 125 | for i in range(segments + 1): 126 | angle = i * (2 * math.pi / segments) 127 | 128 | # Calculate offset and vertex position in screen-space. 129 | offset_x = radius * math.cos(angle) 130 | offset_y = radius * math.sin(angle) 131 | vert_screen = view3d_utils.location_3d_to_region_2d(region, rv3d, vert_world) 132 | 133 | if vert_screen: 134 | # Add offset in screen-space and convert back to world-space. 135 | circle_screen = Vector((vert_screen.x + offset_x, vert_screen.y + offset_y)) 136 | circle_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, circle_screen, vert_world) 137 | vertices.append(circle_3d) 138 | 139 | return vertices 140 | -------------------------------------------------------------------------------- /source/functions/object.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import mathutils 4 | from contextlib import contextmanager 5 | from .. import __package__ as base_package 6 | 7 | 8 | #### ------------------------------ FUNCTIONS ------------------------------ #### 9 | 10 | def set_cutter_properties(context, cutter, mode, display='BOUNDS', collection=True): 11 | """Ensures cutter is properly set: has right properties, is hidden, in a collection & parented""" 12 | 13 | # Hide Cutters 14 | cutter.hide_render = True 15 | cutter.display_type = display 16 | cutter.lineart.usage = 'EXCLUDE' 17 | object_visibility_set(cutter, value=False) 18 | 19 | # Cutters Collection 20 | if collection: 21 | cutters_collection = ensure_collection(context) 22 | if cutters_collection not in cutter.users_collection: 23 | cutters_collection.objects.link(cutter) 24 | 25 | # add_boolean_property 26 | cutter.booleans.cutter = mode.capitalize() 27 | 28 | 29 | def object_visibility_set(obj, value=False): 30 | "Sets object visibility properties to either True or False" 31 | 32 | obj.visible_camera = value 33 | obj.visible_diffuse = value 34 | obj.visible_glossy = value 35 | obj.visible_shadow = value 36 | obj.visible_transmission = value 37 | obj.visible_volume_scatter = value 38 | 39 | 40 | def convert_to_mesh(context, obj): 41 | "Converts active object into mesh (applying all modifiers and shape keys in process)" 42 | 43 | # store_selection 44 | stored_active = context.active_object 45 | stored_selection = context.selected_objects 46 | bpy.ops.object.select_all(action='DESELECT') 47 | 48 | # Convert 49 | obj.select_set(True) 50 | context.view_layer.objects.active = obj 51 | bpy.ops.object.convert(target='MESH') 52 | 53 | # restore_selection 54 | for obj in stored_selection: 55 | obj.select_set(True) 56 | context.view_layer.objects.active = stored_active 57 | 58 | 59 | def ensure_collection(context): 60 | """Checks the existance of boolean cutters collection and creates it if it doesn't exist""" 61 | 62 | prefs = context.preferences.addons[base_package].preferences 63 | 64 | collection_name = prefs.collection_name 65 | cutters_collection = bpy.data.collections.get(collection_name) 66 | 67 | if cutters_collection is None: 68 | cutters_collection = bpy.data.collections.new(collection_name) 69 | context.scene.collection.children.link(cutters_collection) 70 | cutters_collection.hide_render = True 71 | cutters_collection.color_tag = 'COLOR_01' 72 | # cutters_collection.hide_viewport = True 73 | # context.view_layer.layer_collection.children[collection_name].exclude = True 74 | 75 | return cutters_collection 76 | 77 | 78 | def delete_empty_collection(): 79 | """Removes boolean cutters collection if it has no more objects in it""" 80 | 81 | prefs = bpy.context.preferences.addons[base_package].preferences 82 | 83 | collection = bpy.data.collections.get(prefs.collection_name) 84 | if collection and not collection.objects: 85 | bpy.data.collections.remove(collection) 86 | 87 | 88 | def delete_cutter(cutter): 89 | """Deletes cutter object and purges it's mesh data""" 90 | 91 | orphaned_mesh = cutter.data 92 | bpy.data.objects.remove(cutter) 93 | if orphaned_mesh.users == 0: 94 | bpy.data.meshes.remove(orphaned_mesh) 95 | 96 | 97 | def change_parent(obj, parent, force=False, inverse=False): 98 | """Changes or removes parent from cutter object while keeping the transformation""" 99 | 100 | if obj.parent is not None: 101 | if not force: 102 | return 103 | 104 | matrix_copy = obj.matrix_world.copy() 105 | obj.parent = parent 106 | if inverse: 107 | obj.matrix_parent_inverse = parent.matrix_world.inverted() 108 | obj.matrix_world = matrix_copy 109 | 110 | 111 | def create_slice(context, canvas, modifier=False): 112 | """Creates copy of canvas to be used as slice""" 113 | 114 | slice = canvas.copy() 115 | slice.data = canvas.data.copy() 116 | slice.name = slice.data.name = canvas.name + "_slice" 117 | change_parent(slice, canvas) 118 | 119 | # Set Boolean Properties 120 | if modifier == True: 121 | slice.booleans.canvas = True 122 | slice.booleans.slice = True 123 | slice.booleans.slice_of = canvas 124 | 125 | # Add to Canvas Collections 126 | for coll in canvas.users_collection: 127 | coll.objects.link(slice) 128 | 129 | # add_slices_to_local_view 130 | if context.space_data.local_view: 131 | slice.local_view_set(context.space_data, True) 132 | 133 | return slice 134 | 135 | 136 | def set_object_origin(obj, bm, point='CENTER', custom=None): 137 | """Sets object origin to given position by shifting vertices""" 138 | 139 | # Center of the bounding box. 140 | if point == 'CENTER_OBJ': 141 | position_local = 0.125 * sum((mathutils.Vector(b) for b in obj.bound_box), mathutils.Vector()) 142 | position_world = obj.matrix_world @ position_local 143 | 144 | # Center of the geometry. 145 | elif point == 'CENTER_MESH': 146 | if len(bm.verts) > 0: 147 | position_local = sum((v.co for v in bm.verts), mathutils.Vector()) / len(bm.verts) 148 | else: 149 | position_local = mathutils.Vector((0, 0, 0)) 150 | position_world = obj.matrix_world @ position_local 151 | 152 | # Custom origin point (should be local Vector). 153 | elif point == 'CUSTOM': 154 | position_local = custom 155 | position_world = obj.matrix_world @ custom 156 | 157 | mat = mathutils.Matrix.Translation(position_local) 158 | bmesh.ops.transform(bm, matrix=mat.inverted(), verts=bm.verts) 159 | bm.to_mesh(obj.data) 160 | 161 | obj.location = position_world 162 | 163 | 164 | @contextmanager 165 | def hide_objects(context, exceptions: list): 166 | """Hides objects during the context, and restores their visibility afterwards.""" 167 | 168 | hidden_objects = [] 169 | for obj in context.scene.objects: 170 | if obj in exceptions: 171 | continue 172 | if obj.hide_get() == False: 173 | hidden_objects.append(obj) 174 | obj.hide_set(True) 175 | 176 | try: 177 | yield 178 | 179 | finally: 180 | for obj in hidden_objects: 181 | obj.hide_set(False) 182 | -------------------------------------------------------------------------------- /source/functions/poll.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from .list import ( 4 | list_canvas_cutters, 5 | list_cutter_users, 6 | ) 7 | from .object import ( 8 | convert_to_mesh, 9 | ) 10 | 11 | 12 | #### ------------------------------ FUNCTIONS ------------------------------ #### 13 | 14 | def basic_poll(cls, context, check_linked=False): 15 | """Basic poll for boolean operators.""" 16 | 17 | if context.mode != 'OBJECT': 18 | return False 19 | if context.active_object is None: 20 | return False 21 | 22 | obj = context.active_object 23 | if obj.type != 'MESH': 24 | cls.poll_message_set("Boolean operators can only be used for mesh objects") 25 | return False 26 | 27 | if check_linked and is_linked(context, obj) == True: 28 | cls.poll_message_set("Boolean operators can not be executed on linked objects") 29 | return False 30 | 31 | return True 32 | 33 | 34 | def is_linked(context, obj): 35 | """Checks whether the object is linked from an external .blend file (including library-overrides).""" 36 | 37 | if obj not in context.editable_objects: 38 | if obj.library: 39 | return True 40 | else: 41 | return False 42 | else: 43 | if obj.override_library: 44 | return True 45 | else: 46 | return False 47 | 48 | 49 | def is_canvas(obj): 50 | """Checks whether the object is a boolean canvas (i.e. has boolean cutters).""" 51 | 52 | if obj.booleans.canvas == False: 53 | return False 54 | else: 55 | # Even if object is marked as canvas, check if it actually has any cutters 56 | cutters, __ = list_canvas_cutters([obj]) 57 | if len(cutters) > 0: 58 | return True 59 | else: 60 | return False 61 | 62 | 63 | def is_instanced_data(obj): 64 | """Checks if `obj.data` has more than one users, i.e. is instanced.""" 65 | """Function only considers object types as users, and excludes pointers.""" 66 | 67 | data = bpy.data.meshes.get(obj.data.name) 68 | users = 0 69 | 70 | for key, values in bpy.data.user_map(subset=[data]).items(): 71 | for value in values: 72 | if value.id_type == 'OBJECT': 73 | users += 1 74 | 75 | if users > 1: 76 | return True 77 | else: 78 | return False 79 | 80 | 81 | def active_modifier_poll(obj): 82 | """Checks whether the active modifier for active object is a boolean.""" 83 | 84 | # Check if active modifier exists. 85 | if len(obj.modifiers) == 0: 86 | return False 87 | if obj.modifiers.active is None: 88 | return False 89 | 90 | # Check if active modifier is a boolean with a valid object. 91 | modifier = obj.modifiers.active 92 | if modifier.type != "BOOLEAN": 93 | return False 94 | if modifier.object is None: 95 | return False 96 | 97 | return True 98 | 99 | 100 | def has_evaluated_mesh(context, obj): 101 | """Checks if an object (non-mesh type) has an evaluated mesh created by Geometry Nodes modifiers.""" 102 | 103 | depsgraph = context.view_layer.depsgraph 104 | obj_eval = depsgraph.id_eval_get(obj) 105 | geometry = obj_eval.evaluated_geometry() 106 | 107 | if geometry.mesh: 108 | return True 109 | else: 110 | return False 111 | 112 | 113 | def list_candidate_objects(self, context, canvas): 114 | """Filter out objects from the selection that can't be used as a cutter.""" 115 | 116 | cutters = [] 117 | for obj in context.selected_objects: 118 | if obj == context.active_object: 119 | continue 120 | if is_linked(context, obj): 121 | self.report({'WARNING'}, f"{obj.name} is linked and can not be used as a cutter") 122 | continue 123 | 124 | if obj.type == 'MESH': 125 | # Exclude if object is already a cutter for canvas. 126 | if canvas in list_cutter_users([obj]): 127 | continue 128 | # Exclude if canvas is cutting the object (avoid dependancy loop). 129 | if obj in list_cutter_users([canvas]): 130 | self.report({'WARNING'}, f"{obj.name} can not cut its own cutter (dependancy loop)") 131 | continue 132 | 133 | cutters.append(obj) 134 | 135 | elif obj.type in ('CURVE', 'FONT'): 136 | if has_evaluated_mesh(context, obj): 137 | convert_to_mesh(context, obj) 138 | cutters.append(obj) 139 | 140 | return cutters 141 | 142 | 143 | def destructive_op_confirmation(self, context, event, canvases: list, title="Boolean Operation"): 144 | """ 145 | Creates & returns the confirmation pop-up window for destructive boolean operators.\n 146 | Confirmation window is triggered by canvas objects that have instanced object data or shape keys.\n 147 | If none of the canvas objects have them the operator is executed without any confirmation. 148 | """ 149 | 150 | has_instanced_data = any(obj for obj in canvases if is_instanced_data(obj)) 151 | has_shape_keys = any(obj for obj in canvases if obj.data.shape_keys) 152 | 153 | if has_instanced_data or has_shape_keys: 154 | # Instanced data message. 155 | if has_instanced_data and not has_shape_keys: 156 | message = ("Object(s) you're trying to cut have instanced object data.\n" 157 | "In order to apply modifiers, they need to be made single-user.\n" 158 | "Do you proceed?") 159 | 160 | # Shape keys message. 161 | if has_shape_keys and not has_instanced_data: 162 | message = ("Object(s) you're trying to cut have shape keys.\n" 163 | "In order to apply modifiers shape keys need to be applied as well.\n" 164 | "Do you proceed?") 165 | 166 | # Combined message. 167 | if has_instanced_data and has_shape_keys: 168 | message = ("Object(s) you're trying to cut have shape keys and instanced object data.\n" 169 | "In order to apply modifiers shape keys need to be applied, and object data made single user.\n" 170 | "Do you proceed?") 171 | 172 | popup = context.window_manager.invoke_confirm(self, event, title=title, 173 | confirm_text="Yes", icon='WARNING', 174 | message=message) 175 | 176 | return popup 177 | 178 | # Execute without confirmation window. 179 | else: 180 | return self.execute(context) 181 | -------------------------------------------------------------------------------- /source/tools/common/ui.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from ... import __package__ as base_package 3 | 4 | 5 | #### ------------------------------ /toolbar/ ------------------------------ #### 6 | 7 | def carver_ui_common(context, layout, props): 8 | """Common tool properties for all Carver tools""" 9 | 10 | if context.region.type == 'TOOL_HEADER': 11 | layout.prop(props, "mode", text="") 12 | layout.prop(props, "alignment", text="") 13 | layout.prop(props, "depth", text="") 14 | layout.prop(props, "solver", expand=True) 15 | 16 | else: 17 | # Use labels for Properties editor/sidebar. 18 | layout.prop(props, "mode", text="Mode") 19 | layout.prop(props, "alignment", text="Alignment") 20 | layout.prop(props, "depth", text="Depth") 21 | row = layout.row() 22 | row.prop(props, "solver", expand=True) 23 | layout.separator() 24 | 25 | # Popovers 26 | layout.popover("TOPBAR_PT_carver_shape", text="Shape") 27 | layout.popover("TOPBAR_PT_carver_effects", text="Effects") 28 | layout.popover("TOPBAR_PT_carver_cutter", text="Cutter") 29 | 30 | 31 | 32 | #### ------------------------------ /popovers/ ------------------------------ #### 33 | 34 | class TOPBAR_PT_carver_shape(bpy.types.Panel): 35 | bl_label = "Cutter Shape" 36 | bl_idname = "TOPBAR_PT_carver_shape" 37 | bl_region_type = 'HEADER' 38 | bl_space_type = 'TOPBAR' 39 | bl_category = 'Tool' 40 | 41 | def draw(self, context): 42 | layout = self.layout 43 | layout.use_property_split = True 44 | layout.use_property_decorate = False 45 | 46 | tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH') 47 | 48 | # Box & Circle 49 | if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle": 50 | if tool.idname == "object.carve_box": 51 | props = tool.operator_properties("object.carve_box") 52 | else: 53 | props = tool.operator_properties("object.carve_circle") 54 | 55 | if tool.idname == "object.carve_circle": 56 | layout.prop(props, "subdivision", text="Vertices") 57 | layout.prop(props, "rotation") 58 | layout.prop(props, "aspect", expand=True) 59 | layout.prop(props, "origin", expand=True) 60 | 61 | if props.alignment == 'SURFACE': 62 | layout.prop(props, "orientation") 63 | layout.prop(props, "offset", text="Offset") 64 | layout.prop(props, "align_to_all") 65 | if props.alignment == 'CURSOR': 66 | layout.prop(props, "alignment_axis", text="Align to", expand=True) 67 | 68 | # Polyline 69 | elif tool.idname == "object.carve_polyline": 70 | props = tool.operator_properties("object.carve_polyline") 71 | if props.alignment == 'SURFACE': 72 | layout.prop(props, "offset", text="Offset") 73 | layout.prop(props, "align_to_all") 74 | 75 | 76 | class TOPBAR_PT_carver_effects(bpy.types.Panel): 77 | bl_label = "Cutter Effects" 78 | bl_idname = "TOPBAR_PT_carver_effects" 79 | bl_region_type = 'HEADER' 80 | bl_space_type = 'TOPBAR' 81 | bl_category = 'Tool' 82 | 83 | def draw(self, context): 84 | layout = self.layout 85 | layout.use_property_split = True 86 | layout.use_property_decorate = False 87 | 88 | tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH') 89 | if tool.idname == "object.carve_box": 90 | props = tool.operator_properties("object.carve_box") 91 | elif tool.idname == "object.carve_circle": 92 | props = tool.operator_properties("object.carve_circle") 93 | elif tool.idname == "object.carve_polyline": 94 | props = tool.operator_properties("object.carve_polyline") 95 | 96 | # Bevel 97 | if tool.idname == 'object.carve_box': 98 | header, panel = layout.panel("OBJECT_OT_carver_effects_bevel", default_closed=False) 99 | header.label(text="Bevel") 100 | if panel: 101 | panel.prop(props, "use_bevel", text="Side Bevel") 102 | col = panel.column(align=True) 103 | col.prop(props, "bevel_segments", text="Segments") 104 | col.prop(props, "bevel_width", text="Radius") 105 | col.prop(props, "bevel_profile", text="Profile", slider=True) 106 | 107 | if props.use_bevel == False: 108 | col.enabled = False 109 | 110 | # Array 111 | header, panel = layout.panel("OBJECT_OT_carver_effects_array", default_closed=False) 112 | header.label(text="Array") 113 | if panel: 114 | col = panel.column(align=True) 115 | col.prop(props, "columns") 116 | col.prop(props, "rows") 117 | col.prop(props, "gap") 118 | 119 | class TOPBAR_PT_carver_cutter(bpy.types.Panel): 120 | bl_label = "Carver Cutter" 121 | bl_idname = "TOPBAR_PT_carver_cutter" 122 | bl_region_type = 'HEADER' 123 | bl_space_type = 'TOPBAR' 124 | bl_category = 'Tool' 125 | 126 | def draw(self, context): 127 | layout = self.layout 128 | layout.use_property_split = True 129 | layout.use_property_decorate = False 130 | 131 | tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH') 132 | if tool.idname == "object.carve_box": 133 | props = tool.operator_properties("object.carve_box") 134 | elif tool.idname == "object.carve_circle": 135 | props = tool.operator_properties("object.carve_circle") 136 | elif tool.idname == "object.carve_polyline": 137 | props = tool.operator_properties("object.carve_polyline") 138 | 139 | # modifier_&_cutter 140 | col = layout.column() 141 | row = col.row() 142 | row.prop(props, "display", text="Display", expand=True) 143 | col.prop(props, "pin", text="Pin Modifier") 144 | if props.mode == 'MODIFIER': 145 | col.prop(props, "parent") 146 | col.prop(props, "hide") 147 | col.prop(props, "cutter_origin", text="Origin") 148 | 149 | # auto_smooth 150 | layout.separator() 151 | col = layout.column(align=True) 152 | col.prop(props, "auto_smooth", text="Auto Smooth") 153 | col1 = layout.column() 154 | col1.prop(props, "sharp_angle") 155 | if not props.auto_smooth: 156 | col1.enabled = False 157 | 158 | 159 | 160 | #### ------------------------------ REGISTRATION ------------------------------ #### 161 | 162 | classes = [ 163 | TOPBAR_PT_carver_shape, 164 | TOPBAR_PT_carver_effects, 165 | TOPBAR_PT_carver_cutter, 166 | ] 167 | 168 | def register(): 169 | for cls in classes: 170 | bpy.utils.register_class(cls) 171 | 172 | def unregister(): 173 | for cls in reversed(classes): 174 | bpy.utils.unregister_class(cls) 175 | -------------------------------------------------------------------------------- /source/functions/modifier.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from contextlib import contextmanager 4 | from .. import __package__ as base_package 5 | 6 | from ..functions.list import ( 7 | list_pre_boolean_modifiers, 8 | ) 9 | from .object import ( 10 | convert_to_mesh, 11 | ) 12 | from .poll import ( 13 | is_instanced_data, 14 | ) 15 | 16 | 17 | #### ------------------------------ FUNCTIONS ------------------------------ #### 18 | 19 | def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, redo=True): 20 | "Adds boolean modifier with specified cutter and properties to a single object" 21 | 22 | if bpy.app.version < (5, 0, 0) and solver == 'FLOAT': 23 | solver = 'FAST' 24 | 25 | prefs = context.preferences.addons[base_package].preferences 26 | 27 | modifier = obj.modifiers.new("boolean_" + cutter.name.replace("boolean_", ""), 'BOOLEAN') 28 | modifier.operation = mode 29 | modifier.object = cutter 30 | modifier.solver = solver 31 | 32 | # Set solver options (inherited from operator properties). 33 | if redo: 34 | modifier.material_mode = self.material_mode 35 | modifier.use_self = self.use_self 36 | modifier.use_hole_tolerant = self.use_hole_tolerant 37 | modifier.double_threshold = self.double_threshold 38 | 39 | if prefs.show_in_editmode: 40 | modifier.show_in_editmode = True 41 | 42 | # Move modifier to the index 0 (make it first in the stack). 43 | if pin: 44 | index = obj.modifiers.find(modifier.name) 45 | obj.modifiers.move(index, 0) 46 | 47 | return modifier 48 | 49 | 50 | def apply_modifiers(context, obj, modifiers: list, force_clean=False): 51 | """ 52 | Apply modifiers on object. 53 | Instead of using `bpy.ops.object.modifier_apply`, this function uses 54 | `bpy.data.meshes.new_from_object` built-in function to create a temporary 55 | mesh from the evaluated object (basically with visible modifiers applied). 56 | Temporary mesh is then transferred to objects mesh with `bmesh`. 57 | 58 | This method is up to 2x faster, although it's considered experimental 59 | and may fail in some cases, so a fallback to `bpy.ops.object.modifier_apply` is kept. 60 | """ 61 | 62 | prefs = context.preferences.addons[base_package].preferences 63 | 64 | # Make object data unique if it's instanced. 65 | if is_instanced_data(obj): 66 | context.active_object.data = context.active_object.data.copy() 67 | 68 | try: 69 | # Don't use this method if it's not enabled by user in preferences, unless caller forces it. 70 | if not prefs.fast_modifier_apply: 71 | if not force_clean: 72 | raise Exception() 73 | 74 | with hide_modifiers(obj, excluding=modifiers): 75 | # Create a temporary mesh from evaluated object. 76 | evaluated_obj = obj.evaluated_get(context.evaluated_depsgraph_get()) 77 | temp_data = bpy.data.meshes.new_from_object(evaluated_obj) 78 | 79 | # Create `bmesh` from temporary mesh and update edit mesh. 80 | if context.mode == 'EDIT_MESH': 81 | bm = bmesh.from_edit_mesh(obj.data) 82 | bm.clear() 83 | bm.from_mesh(temp_data) 84 | bmesh.update_edit_mesh(obj.data) 85 | else: 86 | bm = bmesh.new() 87 | bm.from_mesh(temp_data) 88 | bm.to_mesh(obj.data) 89 | bm.free() 90 | evaluated_obj.to_mesh_clear() 91 | 92 | # Remove modifiers and purge temporary mesh. 93 | bpy.data.meshes.remove(temp_data) 94 | for mod in modifiers: 95 | obj.modifiers.remove(mod) 96 | 97 | # Remove shape keys if there are any. 98 | # (after above operations none of the shape keys have any effect). 99 | if obj.data.shape_keys: 100 | obj.shape_key_clear() 101 | 102 | # Use `bpy.ops` operator to apply modifiers if above fails. 103 | except Exception as e: 104 | # print("Error applying modifiers with `bmesh` method:", e, "falling back to `bpy.ops` method") 105 | 106 | context_override = {"active_object": obj, "mode": 'OBJECT'} 107 | with context.temp_override(**context_override): 108 | # Apply shape keys if there are any. 109 | if obj.data.shape_keys: 110 | bpy.ops.object.shape_key_remove(all=True, apply_mix=True) 111 | 112 | # If all modifiers need to be applied convert to Mesh. 113 | if modifiers == obj.modifiers.values(): 114 | print("Applying all modifiers by converting to Mesh") 115 | convert_to_mesh(context, obj) 116 | return 117 | 118 | for mod in modifiers: 119 | bpy.ops.object.modifier_apply(modifier=mod.name) 120 | 121 | 122 | @contextmanager 123 | def hide_modifiers(obj, excluding: list): 124 | """Hides all modifiers of a given object in viewport except those in excluding list""" 125 | 126 | visible_modifiers = [] 127 | for mod in obj.modifiers: 128 | if mod in excluding: 129 | continue 130 | if mod.show_viewport == True: 131 | visible_modifiers.append(mod) 132 | mod.show_viewport = False 133 | 134 | try: 135 | yield 136 | finally: 137 | for mod in visible_modifiers: 138 | mod.show_viewport = True 139 | 140 | 141 | def add_modifier_asset(obj, path: str, asset: str): 142 | """Loads the node group asset and adds a Geometry Nodes modifier using it.""" 143 | 144 | try: 145 | # Load the node group. 146 | if bpy.app.version >= (5, 0, 0): 147 | with bpy.data.libraries.load(path, link=True, pack=True) as (data_from, data_to): 148 | if asset in data_from.node_groups: 149 | data_to.node_groups = [asset] 150 | 151 | else: 152 | with bpy.data.libraries.load(path) as (data_from, data_to): 153 | if asset in data_from.node_groups: 154 | data_to.node_groups = [asset] 155 | 156 | node_group = bpy.data.node_groups[asset] 157 | 158 | # Add modifier on the object. 159 | mod = obj.modifiers.new(asset, type='NODES') 160 | mod.node_group = node_group 161 | mod.show_group_selector = False 162 | mod.show_manage_panel = False 163 | 164 | return mod 165 | 166 | except Exception as e: 167 | print("Modifier node group could not be loaded:", e) 168 | return None 169 | 170 | 171 | def get_modifiers_to_apply(context, obj, new_modifiers) -> list: 172 | """Returns the list of modifiers that need to be applied based on add-on preferences.""" 173 | 174 | prefs = context.preferences.addons[base_package].preferences 175 | 176 | if prefs.apply_order == 'ALL': 177 | modifiers = [mod for mod in obj.modifiers] 178 | elif prefs.apply_order == 'BOOLEANS': 179 | modifiers = new_modifiers 180 | elif prefs.apply_order == 'BEFORE': 181 | modifiers = list_pre_boolean_modifiers(obj) 182 | 183 | return modifiers 184 | -------------------------------------------------------------------------------- /source/tools/common/properties.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | 4 | 5 | # Import Custom Icons 6 | from ... import icons 7 | svg_icons = icons.svg_icons["main"] 8 | icon_measure = svg_icons["MEASURE"].icon_id 9 | icon_cpu = svg_icons["CPU"].icon_id 10 | 11 | 12 | #### ------------------------------ PROPERTIES ------------------------------ #### 13 | 14 | class CarverPropsOperator(): 15 | # OPERATOR-properties 16 | mode: bpy.props.EnumProperty( 17 | name = "Mode", 18 | items = (('DESTRUCTIVE', "Destructive", 19 | "Boolean cutters are immediatelly applied and removed after the cut", 'MESH_DATA', 0), 20 | ('MODIFIER', "Modifier", 21 | "Cuts are stored as boolean modifiers and cutters are placed inside the collection", 'MODIFIER_DATA', 1)), 22 | default = 'MODIFIER', 23 | ) 24 | alignment: bpy.props.EnumProperty( 25 | name = "Alignment", 26 | items = (('SURFACE', "Surface", "Align cutters to the surface normal of the mesh under the mouse", 'SNAP_NORMAL', 0), 27 | ('VIEW', "View", "Align cutters to the current view", 'VIEW_CAMERA_UNSELECTED', 1), 28 | ('CURSOR', "3D Cursor", "Align cutters to the 3D cursor orientation", 'ORIENTATION_CURSOR', 2), 29 | ('GRID', "Grid", "Align cutters to the world grid", 'GRID', 3)), 30 | default = 'SURFACE', 31 | ) 32 | depth: bpy.props.EnumProperty( 33 | name = "Depth", 34 | items = (('MANUAL', "Manual", "Depth can be manually set after creating a cutter shape", icon_measure, 0), 35 | ('AUTO', "Auto", "Depth is set automatically to cover selected objects entirely", icon_cpu, 1), 36 | ('CURSOR', "3D Cursor", "Depth is set to 3D cursors location", 'PIVOT_CURSOR', 2)), 37 | default = 'MANUAL', 38 | ) 39 | 40 | 41 | class CarverPropsShape(): 42 | # SHAPE-properties 43 | orientation: bpy.props.EnumProperty( 44 | name = "Orientation", 45 | description = "Orientation method for the shape placement", 46 | items = (('FACE', "Face Normal", "Orient the shape along the normal of the face"), 47 | ('CLOSEST_EDGE', "Closest Edge", "Orient the shape along the closest edge of the face"), 48 | ('LONGEST_EDGE', "Longest Edge", "Orient the shape along the longest edge of the face")), 49 | default = 'CLOSEST_EDGE', 50 | ) 51 | offset: bpy.props.FloatProperty( 52 | name = "Offset from Surface", 53 | description = ("Distance between the shape and the surface of the mesh.\n" 54 | "Offset is important for avoiding Z-fighting issues and solver failures"), 55 | min = 0.0, soft_max = 0.1, 56 | default = 0.01, 57 | ) 58 | align_to_all: bpy.props.BoolProperty( 59 | name = "Align to Anything", 60 | description = "Use all visible objects for surface alignment, not just selected objects", 61 | default = True, 62 | ) 63 | alignment_axis: bpy.props.EnumProperty( 64 | name = "Alignment Axis", 65 | description = "Which axis of the world grid or 3D cursor should be used for workplane alignment", 66 | items = (('X', "X", ""), 67 | ('Y', "Y", ""), 68 | ('Z', "Z", "")), 69 | default = 'Z', 70 | ) 71 | 72 | flip_direction: bpy.props.BoolProperty( 73 | name = "Flip Direction", 74 | description = "Change which way the geometry is extruded", 75 | options = {'SKIP_SAVE', 'HIDDEN', 'SKIP_PRESET', }, 76 | default = False, 77 | ) 78 | 79 | 80 | class CarverPropsModifier(): 81 | # MODIFIER-properties 82 | solver: bpy.props.EnumProperty( 83 | name = "Solver", 84 | items = [('FLOAT', "Float", ""), 85 | ('EXACT', "Exact", ""), 86 | ('MANIFOLD', "Manifold", "")], 87 | default = 'FLOAT', 88 | ) 89 | pin: bpy.props.BoolProperty( 90 | name = "Pin Boolean Modifier", 91 | description = ("Boolean modifier will be placed first in modifier stack, above other modifier (if there are any).\n" 92 | "NOTE: Order of modifiers can drastically affect the result (especially in destructive mode)"), 93 | default = True, 94 | ) 95 | 96 | 97 | class CarverPropsCutter(): 98 | # CUTTER-properties 99 | hide: bpy.props.BoolProperty( 100 | name = "Hide Cutter", 101 | description = ("Hide cutter objects in the viewport after they're created."), 102 | default = True, 103 | ) 104 | parent: bpy.props.BoolProperty( 105 | name = "Parent to Canvas", 106 | description = ("Cutters will be parented to active object being cut, even if cutting multiple objects.\n" 107 | "If there is no active object in selection cutters parent might be chosen seemingly randomly"), 108 | default = True, 109 | ) 110 | display: bpy.props.EnumProperty( 111 | name = "Cutter Display", 112 | items = (('WIRE', "Wire", "Display the cutter object as a wireframe"), 113 | ('BOUNDS', "Bounds", "Display only the bounds of the cutter object")), 114 | default = 'BOUNDS' 115 | ) 116 | cutter_origin: bpy.props.EnumProperty( 117 | name = "Cutter Origin Point", 118 | items = (('CENTER_OBJ', "Bounding Box", "Put the object origin at the center of the cutters bounding box"), 119 | ('CENTER_MESH', "Geometry", "Put the object origin at the center of the cutters geometry (not including effects)"), 120 | ('FACE_CENTER', "First Face", "Put the object origin at the center of cutters first face (i.e. shape)"), 121 | ('MOUSE_INITIAL', "Mouse Click", "Put the object origin at the point where mouse was first clicked"), 122 | ('CANVAS', "Same as Canvas", "Put the object origin of the cutter to the origin point of the cutter")), 123 | default = 'CENTER_MESH', 124 | ) 125 | 126 | auto_smooth: bpy.props.BoolProperty( 127 | name = "Shade Auto Smooth", 128 | description = ("Cutter object will be shaded smooth with sharp edges (above specified degrees) marked as sharp\n" 129 | "NOTE: This is a one time operator. 'Smooth by Angle' modifier will not be added on cutter"), 130 | default = True, 131 | ) 132 | sharp_angle: bpy.props.FloatProperty( 133 | name = "Angle", 134 | description = "Maximum face angle for sharp edges", 135 | subtype = "ANGLE", 136 | min = 0, max = math.pi, 137 | default = 0.523599, 138 | ) 139 | 140 | 141 | class CarverPropsArray(): 142 | # ARRAY-properties 143 | rows: bpy.props.IntProperty( 144 | name = "Rows", 145 | description = "Number of times shape is duplicated horizontally", 146 | min = 1, soft_max = 16, 147 | default = 1, 148 | ) 149 | columns: bpy.props.IntProperty( 150 | name = "Columns", 151 | description = "Number of times shape is duplicated vertically", 152 | min = 1, soft_max = 16, 153 | default = 1, 154 | ) 155 | gap: bpy.props.FloatProperty( 156 | name = "Gap", 157 | description = "Spacing between duplicates, both in rows and columns (relative unit)", 158 | min = 1, soft_max = 10, 159 | default = 1.1, 160 | ) 161 | 162 | 163 | class CarverPropsBevel(): 164 | # BEVEL-properties 165 | use_bevel: bpy.props.BoolProperty( 166 | name = "Bevel Cutter", 167 | description = "Bevel each side edge of the cutter", 168 | default = False, 169 | ) 170 | bevel_segments: bpy.props.IntProperty( 171 | name = "Bevel Segments", 172 | description = "Segments for curved edge", 173 | min = 1, soft_max = 32, 174 | default = 8, 175 | ) 176 | bevel_width: bpy.props.FloatProperty( 177 | name = "Bevel Width", 178 | min = 0, soft_max = 5, 179 | default = 0.1, 180 | ) 181 | bevel_profile: bpy.props.FloatProperty( 182 | name = "Bevel Profile", 183 | description = "The bevel profile shape (0.5 = round)", 184 | min = 0, max = 1, 185 | default = 0.5, 186 | ) 187 | -------------------------------------------------------------------------------- /source/preferences.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import ui 3 | 4 | 5 | #### ------------------------------ FUNCTIONS ------------------------------ #### 6 | 7 | def update_sidebar_category(self, context): 8 | """Change sidebar category of add-ons panel.""" 9 | 10 | panel_classes = [ 11 | ui.VIEW3D_PT_boolean, 12 | ui.VIEW3D_PT_boolean_properties, 13 | ui.VIEW3D_PT_boolean_cutters, 14 | ] 15 | 16 | for cls in panel_classes: 17 | try: 18 | bpy.utils.unregister_class(cls) 19 | except: 20 | pass 21 | cls.bl_category = self.sidebar_category 22 | bpy.utils.register_class(cls) 23 | 24 | 25 | 26 | #### ------------------------------ PREFERENCES ------------------------------ #### 27 | 28 | class BoolToolPreferences(bpy.types.AddonPreferences): 29 | bl_idname = __package__ 30 | 31 | # UI 32 | show_in_sidebar: bpy.props.BoolProperty( 33 | name = "Show Addon Panel in Sidebar", 34 | description = "Add a sidebar panel in 3D Viewport with add-ons operators and properties", 35 | default = True, 36 | ) 37 | sidebar_category: bpy.props.StringProperty( 38 | name = "Category Name", 39 | description = "Sidebar category name. Using the name of the existing category will add panel there", 40 | default = "Edit", 41 | update = update_sidebar_category, 42 | ) 43 | 44 | # Defaults 45 | solver: bpy.props.EnumProperty( 46 | name = "Boolean Solver", 47 | description = "Which solver to use for automatic and brush booleans", 48 | items = [('FLOAT', "Float", ""), 49 | ('EXACT', "Exact", ""), 50 | ('MANIFOLD', "Manifold", "")], 51 | default = 'FLOAT', 52 | ) 53 | wireframe: bpy.props.BoolProperty( 54 | name = "Display Cutters as Wireframe", 55 | description = ("When enabled cutters will be displayed as wireframes, instead of bounding boxes.\n" 56 | "It's better for visualizating the shape, but might be harder to see and have performance cost"), 57 | default = False, 58 | ) 59 | show_in_editmode: bpy.props.BoolProperty( 60 | name = "Enable 'Show in Edit Mode' by Default", 61 | description = "Every new boolean modifier created with brush boolean wil have 'Show in Edit Mode' enabled by default", 62 | default = True, 63 | ) 64 | 65 | # Advanced 66 | use_collection: bpy.props.BoolProperty( 67 | name = "Put Cutters in Collection", 68 | description = ("Brush boolean operators will put all cutters in same collection, and create one if it doesn't exist.\n" 69 | "Useful for scene management, and quickly selecting and removing all clutter when needed"), 70 | default = True, 71 | ) 72 | collection_name: bpy.props.StringProperty( 73 | name = "Collection Name", 74 | default = "boolean_cutters", 75 | ) 76 | parent: bpy.props.BoolProperty( 77 | name = "Parent Cutters to Object", 78 | description = ("Cutters will be parented to first canvas they're applied to. Works best when one cutter is used one canvas.\n" 79 | "NOTE: This doesn't affect Carver tool, which has its own property for this"), 80 | default = True, 81 | ) 82 | apply_order: bpy.props.EnumProperty( 83 | name = "When Applying Cutters...", 84 | description = ("What happens when boolean cutters are applied on object.\n" 85 | "Either when performing auto-boolean, using 'Apply All Cutters' operator.\n" 86 | "NOTE: This doesn't apply to Carver tool on 'Destructive' mode; or when applying individual cutters"), 87 | items = (('ALL', "Apply All Modifiers", "All modifiers on object will be applied (this includes shape keys as well)"), 88 | ('BEFORE', "Apply Booleans & Everything Before", "Alongside boolean modifiers all modifiers will be applied that come before the last boolean"), 89 | ('BOOLEANS', "Only Apply Booleans", "Only apply boolean modifiers. This method will fail if object has shape keys")), 90 | default = 'ALL', 91 | ) 92 | pin: bpy.props.BoolProperty( 93 | name = "Pin Boolean Modifiers", 94 | description = ("When enabled boolean modifiers will be placed above every other modifier on the object (if there are any).\n" 95 | "Order of modifiers can drastically affect the result (especially when performing auto boolean).\n" 96 | "NOTE: This doesn't affect Carver tool, which has its own property for this"), 97 | default = False, 98 | ) 99 | 100 | # Features 101 | fast_modifier_apply: bpy.props.BoolProperty( 102 | name = "Faster Destructive Booleans", 103 | description = ("Experimental method of applying modifiers that results in 30-50% faster destructive booleans.\n" 104 | "Performance improvements also affect the add-ons operators that apply cutters.\n" 105 | "However, changing modifier properties in the redo panel (like material transfer)\n" 106 | "is not available for this method yet."), 107 | default = False, 108 | ) 109 | double_click: bpy.props.BoolProperty( 110 | name = "Double-click Select", 111 | description = ("Select boolean cutters by dbl-clicking on the boolean modifier.\n" 112 | "This feature works in entire modifier properties area, not just on boolean modifier header,\n" 113 | "therefore can result in lot of misclicks and unintended selections."), 114 | default = False, 115 | ) 116 | 117 | # Debug 118 | versioning: bpy.props.BoolProperty( 119 | name = "Versioning", 120 | description = ("Because of the drastic changes in add-on data, it's necessary to do versioning when loading old files\n" 121 | "where Bool Tool cutters(brushes) are not applied. If you don't have files like that, you can ignore this") 122 | ) 123 | experimental: bpy.props.BoolProperty( 124 | name = "Experimental", 125 | description = "Enable experimental features", 126 | default = False, 127 | ) 128 | 129 | def draw(self, context): 130 | layout = self.layout 131 | layout.use_property_split = True 132 | layout.use_property_decorate = False 133 | 134 | # UI 135 | col = layout.column(align=True, heading="Show in Sidebar") 136 | row = col.row(align=True) 137 | sub = row.row(align=True) 138 | sub.prop(self, "show_in_sidebar", text="") 139 | sub = sub.row(align=True) 140 | sub.active = self.show_in_sidebar 141 | sub.prop(self, "sidebar_category", text="") 142 | 143 | # Defaults 144 | layout.separator() 145 | col = layout.column(align=True) 146 | row = col.row(align=True) 147 | row.prop(self, "solver", text="Solver", expand=True) 148 | col.prop(self, "wireframe") 149 | col.prop(self, "show_in_editmode") 150 | 151 | # Advanced 152 | layout.separator() 153 | col = layout.column(align=True, heading="Put Cutters in Collection") 154 | row = col.row(align=True) 155 | sub = row.row(align=True) 156 | sub.prop(self, "use_collection", text="") 157 | sub = sub.row(align=True) 158 | sub.active = self.show_in_sidebar 159 | sub.prop(self, "collection_name", text="") 160 | 161 | col.prop(self, "parent") 162 | col.prop(self, "apply_order") 163 | col.prop(self, "pin") 164 | 165 | # Features 166 | layout.separator() 167 | col = layout.column(align=True, heading="Features") 168 | col.prop(self, "fast_modifier_apply") 169 | col.prop(self, "double_click") 170 | 171 | # Experimentals 172 | layout.separator() 173 | col = layout.column(align=True) 174 | col.prop(self, "versioning", text="⚠ Versioning") 175 | col.prop(self, "experimental", text="⚠ Experimental") 176 | 177 | 178 | 179 | #### ------------------------------ REGISTRATION ------------------------------ #### 180 | 181 | classes = [ 182 | BoolToolPreferences, 183 | ] 184 | 185 | def register(): 186 | for cls in classes: 187 | bpy.utils.register_class(cls) 188 | 189 | def unregister(): 190 | for cls in reversed(classes): 191 | bpy.utils.unregister_class(cls) 192 | -------------------------------------------------------------------------------- /source/operators/canvas.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import itertools 3 | from .. import __package__ as base_package 4 | 5 | from ..functions.poll import ( 6 | basic_poll, 7 | is_canvas, 8 | is_instanced_data, 9 | destructive_op_confirmation, 10 | ) 11 | from ..functions.modifier import ( 12 | apply_modifiers, 13 | ) 14 | from ..functions.object import ( 15 | object_visibility_set, 16 | delete_empty_collection, 17 | delete_cutter, 18 | change_parent, 19 | ) 20 | from ..functions.list import ( 21 | list_canvases, 22 | list_canvas_slices, 23 | list_canvas_cutters, 24 | list_cutter_users, 25 | list_selected_canvases, 26 | list_unused_cutters, 27 | list_pre_boolean_modifiers, 28 | ) 29 | 30 | 31 | #### ------------------------------ OPERATORS ------------------------------ #### 32 | 33 | # Toggle All Cutters 34 | class OBJECT_OT_boolean_toggle_all(bpy.types.Operator): 35 | bl_idname = "object.boolean_toggle_all" 36 | bl_label = "Toggle Boolean Cutters" 37 | bl_description = "Toggle all boolean cutters affecting selected canvases" 38 | bl_options = {'UNDO'} 39 | 40 | @classmethod 41 | def poll(cls, context): 42 | return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object) 43 | 44 | def execute(self, context): 45 | canvases = list_selected_canvases(context) 46 | cutters, modifiers = list_canvas_cutters(canvases) 47 | slices = list_canvas_slices(canvases) 48 | 49 | # Toggle Modifiers 50 | for mod in modifiers: 51 | mod.show_viewport = not mod.show_viewport 52 | mod.show_render = not mod.show_render 53 | 54 | # Hide Slices 55 | for slice in slices: 56 | slice.hide_viewport = not slice.hide_viewport 57 | slice.hide_render = not slice.hide_render 58 | for mod in slice.modifiers: 59 | if mod.type == 'BOOLEAN' and mod.object in cutters: 60 | mod.show_viewport = not mod.show_viewport 61 | mod.show_render = not mod.show_render 62 | 63 | # Hide Unused Cutters 64 | other_canvases = list_canvases() 65 | for obj in other_canvases: 66 | if obj not in canvases + slices: 67 | if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'): 68 | cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]] 69 | 70 | for cutter in cutters: 71 | cutter.hide_viewport = not cutter.hide_viewport 72 | 73 | return {'FINISHED'} 74 | 75 | 76 | # Remove All Cutters 77 | class OBJECT_OT_boolean_remove_all(bpy.types.Operator): 78 | bl_idname = "object.boolean_remove_all" 79 | bl_label = "Remove Boolean Cutters" 80 | bl_description = "Remove all boolean cutters from selected canvases" 81 | bl_options = {'UNDO'} 82 | 83 | @classmethod 84 | def poll(cls, context): 85 | return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object) 86 | 87 | def execute(self, context): 88 | prefs = context.preferences.addons[base_package].preferences 89 | 90 | canvases = list_selected_canvases(context) 91 | cutters, __ = list_canvas_cutters(canvases) 92 | slices = list_canvas_slices(canvases) 93 | 94 | # Remove Slices 95 | for slice in slices: 96 | if slice in canvases: 97 | canvases.remove(slice) 98 | delete_cutter(slice) 99 | 100 | for canvas in canvases: 101 | # Remove Modifiers 102 | for mod in canvas.modifiers: 103 | if mod.type == 'BOOLEAN' and "boolean_" in mod.name: 104 | if mod.object in cutters: 105 | canvas.modifiers.remove(mod) 106 | 107 | # remove_boolean_properties 108 | if canvas.booleans.canvas == True: 109 | canvas.booleans.canvas = False 110 | 111 | 112 | # Restore Orphaned Cutters 113 | unused_cutters, leftovers = list_unused_cutters(cutters, canvases, slices, do_leftovers=True) 114 | 115 | for cutter in unused_cutters: 116 | if cutter.booleans.carver: 117 | delete_cutter(cutter) 118 | else: 119 | # restore_visibility 120 | cutter.hide_render = False 121 | cutter.display_type = 'TEXTURED' 122 | cutter.lineart.usage = 'INHERIT' 123 | object_visibility_set(cutter, value=True) 124 | cutter.booleans.cutter = "" 125 | 126 | # remove_parent_&_collection 127 | if prefs.parent and cutter.parent in canvases: 128 | change_parent(cutter, None) 129 | 130 | if prefs.use_collection: 131 | cutters_collection = bpy.data.collections.get(prefs.collection_name) 132 | if cutters_collection in cutter.users_collection: 133 | bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter) 134 | 135 | # purge_empty_collection 136 | if prefs.use_collection: 137 | delete_empty_collection() 138 | 139 | 140 | # Change Leftover Cutter Parent 141 | if prefs.parent: 142 | for cutter in leftovers: 143 | if cutter.parent in canvases: 144 | other_canvases = list_cutter_users([cutter]) 145 | change_parent(cutter, other_canvases[0]) 146 | 147 | return {'FINISHED'} 148 | 149 | 150 | # Apply All Cutters 151 | class OBJECT_OT_boolean_apply_all(bpy.types.Operator): 152 | bl_idname = "object.boolean_apply_all" 153 | bl_label = "Apply All Boolean Cutters" 154 | bl_description = "Apply all boolean cutters on selected canvases" 155 | bl_options = {'UNDO'} 156 | 157 | @classmethod 158 | def poll(cls, context): 159 | return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object) 160 | 161 | 162 | def invoke(self, context, event): 163 | self.canvases = list_selected_canvases(context) 164 | return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutters") 165 | 166 | 167 | def execute(self, context): 168 | prefs = context.preferences.addons[base_package].preferences 169 | 170 | cutters, __ = list_canvas_cutters(self.canvases) 171 | slices = list_canvas_slices(self.canvases) 172 | 173 | # Select all faces of the cutter so that newly created faces in canvas 174 | # are also selected after applying the modifier. 175 | for cutter in cutters: 176 | for face in cutter.data.polygons: 177 | face.select = True 178 | 179 | for canvas in itertools.chain(self.canvases, slices): 180 | context.view_layer.objects.active = canvas 181 | 182 | # Apply Modifiers 183 | if prefs.apply_order == 'ALL': 184 | modifiers = [mod for mod in canvas.modifiers] 185 | elif prefs.apply_order == 'BEFORE': 186 | modifiers = list_pre_boolean_modifiers(canvas) 187 | elif prefs.apply_order == 'BOOLEANS': 188 | modifiers = [mod for mod in canvas.modifiers if mod.type == 'BOOLEAN' and "boolean_" in mod.name] 189 | 190 | apply_modifiers(context, canvas, modifiers) 191 | 192 | # remove_boolean_properties 193 | canvas.booleans.canvas = False 194 | canvas.booleans.slice = False 195 | 196 | 197 | # Purge Orphaned Cutters 198 | unused_cutters, leftovers = list_unused_cutters(cutters, self.canvases, slices, do_leftovers=True) 199 | 200 | purged_cutters = [] 201 | for cutter in unused_cutters: 202 | if cutter not in purged_cutters: 203 | # Transfer Children 204 | for child in cutter.children: 205 | change_parent(child, cutter.parent) 206 | 207 | # Purge 208 | delete_cutter(cutter) 209 | purged_cutters.append(cutter) 210 | 211 | # purge_empty_collection 212 | if prefs.use_collection: 213 | delete_empty_collection() 214 | 215 | 216 | # Change Leftover Cutter Parent 217 | if prefs.parent: 218 | for cutter in leftovers: 219 | if cutter.parent in self.canvases: 220 | other_canvases = list_cutter_users([cutter]) 221 | change_parent(cutter, other_canvases[0]) 222 | 223 | return {'FINISHED'} 224 | 225 | 226 | 227 | #### ------------------------------ REGISTRATION ------------------------------ #### 228 | 229 | addon_keymaps = [] 230 | 231 | classes = [ 232 | OBJECT_OT_boolean_toggle_all, 233 | OBJECT_OT_boolean_remove_all, 234 | OBJECT_OT_boolean_apply_all, 235 | ] 236 | 237 | 238 | def register(): 239 | for cls in classes: 240 | bpy.utils.register_class(cls) 241 | 242 | # KEYMAP 243 | addon = bpy.context.window_manager.keyconfigs.addon 244 | km = addon.keymaps.new(name="Object Mode") 245 | 246 | kmi = km.keymap_items.new("object.boolean_apply_all", 'NUMPAD_ENTER', 'PRESS', shift=True, ctrl=True) 247 | kmi.active = True 248 | addon_keymaps.append((km, kmi)) 249 | 250 | 251 | def unregister(): 252 | for cls in reversed(classes): 253 | bpy.utils.unregister_class(cls) 254 | 255 | # KEYMAP 256 | for km, kmi in addon_keymaps: 257 | km.keymap_items.remove(kmi) 258 | addon_keymaps.clear() 259 | -------------------------------------------------------------------------------- /source/ui.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .functions.poll import is_canvas 3 | from .functions.list import list_canvas_cutters 4 | 5 | 6 | #### ------------------------------ /ui/ ------------------------------ #### 7 | 8 | def carve_menu(self, context): 9 | layout = self.layout 10 | layout.operator("object.carve_box", text="Box Carve").shape='BOX' 11 | layout.operator("object.carve_box", text="Circle Carve").shape='CIRCLE' 12 | layout.operator("object.carve_polyline", text="Polyline Carve") 13 | 14 | 15 | def boolean_operators_menu(self, context): 16 | layout = self.layout 17 | layout.operator_context = 'INVOKE_DEFAULT' 18 | col = layout.column(align=True) 19 | 20 | col.label(text="Auto Boolean") 21 | col.operator("object.boolean_auto_difference", text="Difference", icon='SELECT_SUBTRACT') 22 | col.operator("object.boolean_auto_union", text="Union", icon='SELECT_EXTEND') 23 | col.operator("object.boolean_auto_intersect", text="Intersect", icon='SELECT_INTERSECT') 24 | col.operator("object.boolean_auto_slice", text="Slice", icon='SELECT_DIFFERENCE') 25 | 26 | col.separator() 27 | col.label(text="Brush Boolean") 28 | col.operator("object.boolean_brush_difference", text="Difference", icon='SELECT_SUBTRACT') 29 | col.operator("object.boolean_brush_union", text="Union", icon='SELECT_EXTEND') 30 | col.operator("object.boolean_brush_intersect", text="Intersect", icon='SELECT_INTERSECT') 31 | col.operator("object.boolean_brush_slice", text="Slice", icon='SELECT_DIFFERENCE') 32 | 33 | 34 | def boolean_extras_menu(self, context): 35 | layout = self.layout 36 | layout.operator_context = 'INVOKE_DEFAULT' 37 | col = layout.column(align=True) 38 | 39 | if context.active_object: 40 | # Canvas operators 41 | active_object = context.active_object 42 | if active_object.booleans.canvas == True and any(mod.name.startswith("boolean_") for mod in active_object.modifiers): 43 | col.separator() 44 | col.operator("object.boolean_toggle_all", text="Toggle All Cuters") 45 | col.operator("object.boolean_apply_all", text="Apply All Cutters") 46 | col.operator("object.boolean_remove_all", text="Remove All Cutters") 47 | 48 | # Cutter operators 49 | if active_object.booleans.cutter: 50 | col.separator() 51 | col.operator("object.boolean_toggle_cutter", text="Toggle Cutter").method='ALL' 52 | col.operator("object.boolean_apply_cutter", text="Apply Cutter").method='ALL' 53 | col.operator("object.boolean_remove_cutter", text="Remove Cutter").method='ALL' 54 | 55 | 56 | 57 | #### ------------------------------ PANELS ------------------------------ #### 58 | 59 | # Boolean Operators Panel 60 | class VIEW3D_PT_boolean(bpy.types.Panel): 61 | bl_label = "Boolean" 62 | bl_space_type = "VIEW_3D" 63 | bl_region_type = "UI" 64 | bl_category = "Edit" 65 | bl_context = "objectmode" 66 | bl_options = {'DEFAULT_CLOSED'} 67 | 68 | @classmethod 69 | def poll(cls, context): 70 | prefs = context.preferences.addons[__package__].preferences 71 | return prefs.show_in_sidebar 72 | 73 | def draw(self, context): 74 | boolean_operators_menu(self, context) 75 | 76 | 77 | # Properties Panel 78 | class VIEW3D_PT_boolean_properties(bpy.types.Panel): 79 | bl_label = "Properties" 80 | bl_space_type = "VIEW_3D" 81 | bl_region_type = "UI" 82 | bl_category = "Edit" 83 | bl_context = "objectmode" 84 | bl_parent_id = "VIEW3D_PT_boolean" 85 | 86 | @classmethod 87 | def poll(cls, context): 88 | prefs = context.preferences.addons[__package__].preferences 89 | if prefs.show_in_sidebar: 90 | if context.active_object: 91 | if is_canvas(context.active_object) or context.active_object.booleans.cutter: 92 | return True 93 | else: 94 | return False 95 | else: 96 | return False 97 | else: 98 | return False 99 | 100 | def draw(self, context): 101 | boolean_extras_menu(self, context) 102 | 103 | 104 | # Cutters Panel 105 | class VIEW3D_PT_boolean_cutters(bpy.types.Panel): 106 | bl_label = "Cutters" 107 | bl_space_type = "VIEW_3D" 108 | bl_region_type = "UI" 109 | bl_category = "Edit" 110 | bl_context = "objectmode" 111 | bl_parent_id = "VIEW3D_PT_boolean" 112 | 113 | @classmethod 114 | def poll(cls, context): 115 | prefs = context.preferences.addons[__package__].preferences 116 | if prefs.show_in_sidebar: 117 | if context.active_object: 118 | if is_canvas(context.active_object): 119 | return True 120 | else: 121 | return False 122 | else: 123 | return False 124 | else: 125 | return False 126 | 127 | def draw(self, context): 128 | layout = self.layout 129 | canvas = context.active_object 130 | __, modifiers = list_canvas_cutters([canvas]) 131 | 132 | for mod in modifiers: 133 | col = layout.column(align=True) 134 | row = col.row(align=True) 135 | 136 | # icon 137 | if mod.operation == 'DIFFERENCE': 138 | icon = 'SELECT_SUBTRACT' 139 | elif mod.operation == 'UNION': 140 | icon = 'SELECT_EXTEND' 141 | elif mod.operation == 'INTERSECT': 142 | icon = 'SELECT_INTERSECT' 143 | 144 | row.prop(mod.object, "name", text="", icon=icon) 145 | 146 | # Toggle 147 | op_toggle = row.operator("object.boolean_toggle_cutter", text="", icon='HIDE_OFF' if mod.show_viewport else 'HIDE_ON') 148 | op_toggle.method = 'SPECIFIED' 149 | op_toggle.specified_cutter = mod.object.name 150 | op_toggle.specified_canvas = canvas.name 151 | 152 | # Apply 153 | op_apply = row.operator("object.boolean_apply_cutter", text="", icon='CHECKMARK') 154 | op_apply.method = 'SPECIFIED' 155 | op_apply.specified_cutter = mod.object.name 156 | op_apply.specified_canvas = canvas.name 157 | 158 | # Remove 159 | op_remove = row.operator("object.boolean_remove_cutter", text="", icon='X') 160 | op_remove.method = 'SPECIFIED' 161 | op_remove.specified_cutter = mod.object.name 162 | op_remove.specified_canvas = canvas.name 163 | 164 | 165 | 166 | #### ------------------------------ MENUS ------------------------------ #### 167 | 168 | # Carve Menu 169 | class VIEW3D_MT_carve(bpy.types.Menu): 170 | bl_label = "Carve" 171 | bl_idname = "VIEW3D_MT_carve" 172 | 173 | def draw(self, context): 174 | carve_menu(self, context) 175 | 176 | 177 | # 3D Viewport (Object Mode) -> Object 178 | class VIEW3D_MT_boolean(bpy.types.Menu): 179 | bl_label = "Boolean" 180 | bl_idname = "VIEW3D_MT_boolean" 181 | 182 | def draw(self, context): 183 | layout = self.layout 184 | layout.menu("VIEW3D_MT_carve") 185 | layout.separator() 186 | boolean_operators_menu(self, context) 187 | boolean_extras_menu(self, context) 188 | 189 | 190 | # Shift-Ctrl-B Menu 191 | class VIEW3D_MT_boolean_popup(bpy.types.Menu): 192 | bl_label = "Boolean" 193 | bl_idname = "VIEW3D_MT_boolean_popup" 194 | 195 | def draw(self, context): 196 | boolean_operators_menu(self, context) 197 | boolean_extras_menu(self, context) 198 | 199 | 200 | def object_mode_menu(self, context): 201 | layout = self.layout 202 | layout.separator() 203 | layout.menu("VIEW3D_MT_boolean") 204 | 205 | 206 | def edit_mode_menu(self, context): 207 | layout = self.layout 208 | layout.separator() 209 | layout.menu("VIEW3D_MT_carve") 210 | 211 | 212 | def boolean_select_menu(self, context): 213 | layout = self.layout 214 | active_obj = context.active_object 215 | if active_obj: 216 | if active_obj.booleans.canvas == True or active_obj.booleans.cutter: 217 | layout.separator() 218 | 219 | if active_obj.booleans.canvas == True: 220 | layout.operator("object.boolean_select_all", text="Boolean Cutters") 221 | if active_obj.booleans.cutter: 222 | layout.operator("object.select_cutter_canvas", text="Boolean Canvases") 223 | 224 | 225 | 226 | #### ------------------------------ REGISTRATION ------------------------------ #### 227 | 228 | addon_keymaps = [] 229 | 230 | classes = [ 231 | VIEW3D_MT_carve, 232 | VIEW3D_MT_boolean, 233 | VIEW3D_MT_boolean_popup, 234 | VIEW3D_PT_boolean, 235 | VIEW3D_PT_boolean_properties, 236 | VIEW3D_PT_boolean_cutters, 237 | ] 238 | 239 | def register(): 240 | for cls in classes: 241 | bpy.utils.register_class(cls) 242 | 243 | # MENU 244 | bpy.types.VIEW3D_MT_object.append(object_mode_menu) 245 | bpy.types.VIEW3D_MT_select_object.append(boolean_select_menu) 246 | bpy.types.VIEW3D_MT_edit_mesh.append(edit_mode_menu) 247 | 248 | # KEYMAP 249 | addon = bpy.context.window_manager.keyconfigs.addon 250 | km = addon.keymaps.new(name="Object Mode") 251 | 252 | kmi = km.keymap_items.new("wm.call_menu", 'B', 'PRESS', ctrl=True, shift=True) 253 | kmi.properties.name = "VIEW3D_MT_boolean_popup" 254 | kmi.active = True 255 | addon_keymaps.append((km, kmi)) 256 | 257 | 258 | def unregister(): 259 | for cls in reversed(classes): 260 | bpy.utils.unregister_class(cls) 261 | 262 | # MENU 263 | bpy.types.VIEW3D_MT_object.remove(object_mode_menu) 264 | bpy.types.VIEW3D_MT_select_object.remove(boolean_select_menu) 265 | bpy.types.VIEW3D_MT_edit_mesh.remove(edit_mode_menu) 266 | 267 | # KEYMAP 268 | for km, kmi in addon_keymaps: 269 | km.keymap_items.remove(kmi) 270 | addon_keymaps.clear() 271 | -------------------------------------------------------------------------------- /source/tools/common/types.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | import os 4 | from mathutils import Vector, Matrix 5 | 6 | from ...functions.mesh import ( 7 | ensure_attribute, 8 | shade_smooth_by_angle, 9 | ) 10 | from ...functions.modifier import ( 11 | add_modifier_asset, 12 | ) 13 | 14 | 15 | #### ------------------------------ CLASSES ------------------------------ #### 16 | 17 | class Selection: 18 | """Storage of viable selected and active object(s) throughout the modal.""" 19 | 20 | def __init__(self, selected, active): 21 | self.selected: list = selected 22 | self.active = active 23 | self.modifiers = {} 24 | 25 | 26 | class Mouse: 27 | """ 28 | Mouse positions throughout different phases of the modal operator. 29 | Each class variable is a 2D vector in screen space (x, y). 30 | """ 31 | 32 | def __init__(self): 33 | self.initial = Vector() 34 | self.current = Vector() 35 | self.extrude = Vector() 36 | self.cached = Vector() # Used for custom modifier keys. 37 | 38 | self.current_3d = Vector() 39 | self.cached_3d = Vector() 40 | 41 | @classmethod 42 | def from_event(self, event): 43 | self.initial = Vector((event.mouse_region_x, event.mouse_region_y)) 44 | self.current = Vector((event.mouse_region_x, event.mouse_region_y)) 45 | 46 | self.current_3d = None 47 | return self 48 | 49 | 50 | class Workplane: 51 | """Local 3D coordinate system used as the drawing plane for creating shapes.""" 52 | 53 | def __init__(self, matrix, location, normal): 54 | self.matrix: Matrix = matrix # full 4x4 transform matrix. 55 | self.location: Vector = location # origin point of the plane in world space. 56 | self.normal: Vector = normal # perpendicular direction of the plane. 57 | 58 | 59 | class Cutter: 60 | """Object created for cutting, as well as it's `bmesh`, and other properties.""" 61 | 62 | def __init__(self, obj, mesh, bm, faces, verts): 63 | self.obj = obj 64 | self.mesh = mesh 65 | self.bm = bm 66 | self.faces: list = faces 67 | self.verts: list = verts 68 | self.center = Vector() # Center of the geometry. 69 | 70 | 71 | # Effects 72 | class Effects: 73 | 74 | def __init__(self): 75 | self.array = None 76 | self.bevel = None 77 | self.smooth = None 78 | self.weld = None 79 | 80 | def from_invoke(self, cls, context): 81 | """Add modifiers to the cutter object during invoke, if they're enabled on tool level.""" 82 | 83 | # Smooth by Angle 84 | if cls.auto_smooth: 85 | self.add_auto_smooth_modifier(cls, context) 86 | 87 | # Array 88 | if cls.rows > 1 or cls.columns > 1: 89 | self.add_array_modifier(cls) 90 | else: 91 | self.array = None 92 | 93 | # Bevel 94 | if hasattr(cls, "use_bevel") and cls.use_bevel: 95 | self.add_bevel_modifier(cls, affect='VERTICES') 96 | else: 97 | self.bevel = None 98 | 99 | return self 100 | 101 | def update(self, cls, effect): 102 | """Update bevel modifier during modal.""" 103 | 104 | # Update array count. 105 | if effect == 'ARRAY_COUNT': 106 | if self.array is None: 107 | self.add_array_modifier(cls) 108 | 109 | else: 110 | if cls.columns > 1 or cls.rows > 1: 111 | self.array["Socket_2"] = cls.columns 112 | self.array["Socket_3"] = cls.rows 113 | 114 | # Remove modifier if it's no longer needed. 115 | if cls.columns == 1 and cls.rows == 1: 116 | cls.cutter.obj.modifiers.remove(self.array) 117 | self.array = None 118 | 119 | # Update array gap. 120 | if effect == 'ARRAY_GAP': 121 | if cls.columns > 1 or cls.row > 1: 122 | if self.array is not None: 123 | self.array["Socket_4"] = cls.gap 124 | 125 | # Force the modifier to update in viewport. 126 | self.array.show_viewport = False 127 | self.array.show_viewport = True 128 | 129 | # Update bevel width & segments 130 | if effect == 'BEVEL': 131 | self.bevel.segments = cls.bevel_segments 132 | self.bevel.width = cls.bevel_width 133 | 134 | 135 | # Array 136 | def add_array_modifier(self, cls): 137 | """Adds an array modifier(s) on the cutter object.""" 138 | 139 | cutter = cls.cutter.obj 140 | 141 | # Load geometry nodes modifier asset. 142 | if self.array is None: 143 | root = os.path.abspath(os.path.join(__file__, "..", "..", "..")) 144 | assets_path = os.path.join(root, "assets.blend") 145 | mod = add_modifier_asset(cutter, path=assets_path, asset="cutter_array") 146 | 147 | if not mod: 148 | cls.report({'WARNING'}, "Array modifier cannot be loaded for cutter") 149 | return 150 | 151 | # Columns 152 | if cls.columns > 1: 153 | mod["Socket_2"] = cls.columns 154 | 155 | # Rows 156 | if cls.rows > 1: 157 | mod["Socket_3"] = cls.rows 158 | 159 | # Gap 160 | mod["Socket_4"] = cls.gap 161 | 162 | self.array = mod 163 | 164 | 165 | # Bevel 166 | def add_bevel_modifier(self, cls, affect='EDGES'): 167 | """Adds a bevel modifier on the cutter object.""" 168 | 169 | cutter = cls.cutter.obj 170 | bm = cls.cutter.bm 171 | faces = cls.cutter.faces 172 | 173 | mod = cutter.modifiers.new("cutter_bevel", 'BEVEL') 174 | mod.limit_method = 'WEIGHT' 175 | mod.segments = cls.bevel_segments 176 | mod.width = cls.bevel_width 177 | mod.profile = cls.bevel_profile 178 | 179 | """NOTE: 180 | In order to allow beveling during the shape creation phase, 181 | when we only have one face, we need to bevel vertices instead of edges, 182 | and then change it to edges when cutter is manifold (and transfer weights). 183 | """ 184 | mod.affect = affect 185 | if affect == 'EDGES': 186 | attr = ensure_attribute(bm, "bevel_weight_edge", 'EDGE') 187 | 188 | # Mark all edges except ones belonging to original and extruded face. 189 | for edge in bm.edges: 190 | if edge in faces[0].edges: 191 | continue 192 | if edge in faces[-1].edges: 193 | continue 194 | edge[attr] = 1.0 195 | 196 | elif affect == 'VERTICES': 197 | attr = ensure_attribute(bm, "bevel_weight_vert", 'VERTEX') 198 | face = cls.cutter.faces[0] 199 | 200 | # Mark vertices of the original face. 201 | verts = [vert for vert in face.verts] 202 | for v in verts: 203 | v[attr] = 1.0 204 | 205 | # Add Weld modifier (necessary for merging overlapping vertices). 206 | # Otherwise live cut produces corrupted booleans because of non-manifold geometry. 207 | self.add_weld_modifier(cls) 208 | 209 | self.bevel = mod 210 | 211 | 212 | def transfer_bevel_weights(self, cls): 213 | """Transfer bevel weights from vertices to edges.""" 214 | 215 | if not cls.use_bevel: 216 | return 217 | 218 | bm = cls.cutter.bm 219 | faces = cls.cutter.faces 220 | 221 | # Ensure default edge weights attribute. 222 | edge_attr = ensure_attribute(bm, "bevel_weight_edge", 'EDGE') 223 | 224 | for edge in bm.edges: 225 | if edge in faces[0].edges: 226 | continue 227 | if edge in faces[-1].edges: 228 | continue 229 | edge[edge_attr] = 1.0 230 | 231 | self.bevel.affect = 'EDGES' 232 | 233 | 234 | # Smooth by Angle 235 | def add_auto_smooth_modifier(self, cls, context): 236 | """Adds a 'Smooth by Angle' modifier on cutter object, a.k.a. Auto Smooth.""" 237 | 238 | obj = cls.cutter.obj 239 | mesh = cls.cutter.mesh 240 | bm = cls.cutter.bm 241 | 242 | modifier_asset_path = "nodes\\geometry_nodes_essentials.blend\\NodeTree\\Smooth by Angle" 243 | modifier_asset_file = modifier_asset_path[:modifier_asset_path.find(".blend") + 6] 244 | modifier_asset_name = modifier_asset_path.rsplit("\\", 1)[1] 245 | 246 | # Try adding modifier with `bpy.ops` operator(s) first. 247 | context_override = { 248 | "object": obj, 249 | "active_object": obj, 250 | "selected_objects": [obj], 251 | "selected_editable_objects": [obj], 252 | } 253 | with context.temp_override(**context_override): 254 | try: 255 | # Try adding the modifier with `shade_auto_smooth` operator. 256 | bpy.ops.object.shade_auto_smooth() 257 | except: 258 | # Try adding the modifier with path to Essentials library. 259 | bpy.ops.object.modifier_add_node_group(asset_library_type="ESSENTIALS", 260 | asset_library_identifier="", 261 | relative_asset_identifier=modifier_asset_path) 262 | 263 | mod = obj.modifiers.active 264 | 265 | # Try loading the node group manually if `bpy.ops` operators fail. 266 | if mod is None: 267 | dir = os.path.join(os.path.dirname(bpy.app.binary_path), "5.0", "datafiles", "assets") 268 | assets_path = os.path.join(dir, modifier_asset_file) 269 | mod = add_modifier_asset(obj, path=assets_path, asset=modifier_asset_name) 270 | 271 | # Resort to destructive editing if everything fails. 272 | if mod is None: 273 | print("Smooth by Angle modifier couldn't be added.") 274 | print("Destructively marking sharp edges and smooth faces in the mesh") 275 | shade_smooth_by_angle(bm, mesh, angle=math.degrees(cls.sharp_angle)) 276 | else: 277 | # Set smoothing angle. 278 | for face in bm.faces: 279 | face.smooth = True 280 | bm.to_mesh(mesh) 281 | 282 | mod.use_pin_to_last = True 283 | mod["Input_1"] = cls.sharp_angle 284 | 285 | self.smooth = mod 286 | 287 | 288 | # Weld 289 | def add_weld_modifier(self, cls): 290 | if self.weld is None: 291 | self.weld = cls.cutter.obj.modifiers.new("cutter_weld", 'WELD') 292 | return self.weld 293 | -------------------------------------------------------------------------------- /source/tools/carver_box.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | from mathutils import Vector 4 | from .. import __file__ as base_file 5 | 6 | from .common.base import ( 7 | CarverBase, 8 | ) 9 | from .common.properties import ( 10 | CarverPropsArray, 11 | CarverPropsBevel, 12 | ) 13 | from .common.types import ( 14 | Selection, 15 | Mouse, 16 | Workplane, 17 | Cutter, 18 | Effects, 19 | ) 20 | from .common.ui import ( 21 | carver_ui_common, 22 | ) 23 | 24 | 25 | description = "Cut primitive shapes into mesh objects by box drawing" 26 | 27 | #### ------------------------------ TOOLS ------------------------------ #### 28 | 29 | class OBJECT_WT_carve_box(bpy.types.WorkSpaceTool): 30 | bl_idname = "object.carve_box" 31 | bl_label = "Box Carve" 32 | bl_description = description 33 | 34 | bl_space_type = 'VIEW_3D' 35 | bl_context_mode = 'OBJECT' 36 | 37 | bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_box") 38 | bl_keymap = ( 39 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}), 40 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}), 41 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": None}), 42 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": None}), 43 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": None}), 44 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": None}), 45 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": None}), 46 | ("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": None}), 47 | ) 48 | 49 | def draw_settings(context, layout, tool): 50 | props = tool.operator_properties("object.carve_box") 51 | carver_ui_common(context, layout, props) 52 | 53 | 54 | class MESH_WT_carve_box(OBJECT_WT_carve_box): 55 | bl_context_mode = 'EDIT_MESH' 56 | 57 | 58 | 59 | #### ------------------------------ OPERATORS ------------------------------ #### 60 | 61 | class OBJECT_OT_carve_box(CarverBase, 62 | CarverPropsArray, 63 | CarverPropsBevel): 64 | bl_idname = "object.carve_box" 65 | bl_label = "Box Carve" 66 | bl_description = description 67 | bl_options = {'REGISTER', 'UNDO', 'DEPENDS_ON_CURSOR'} 68 | bl_cursor_pending = 'PICK_AREA' 69 | 70 | # SHAPE-properties 71 | shape = 'BOX' 72 | 73 | aspect: bpy.props.EnumProperty( 74 | name = "Aspect", 75 | description = "The initial aspect", 76 | items = (('FREE', "Free", "Use an unconstrained aspect"), 77 | ('FIXED', "Fixed", "Use a fixed 1:1 aspect")), 78 | default = 'FREE', 79 | ) 80 | origin: bpy.props.EnumProperty( 81 | name = "Origin", 82 | description = "The initial position for placement", 83 | items = (('EDGE', "Edge", ""), 84 | ('CENTER', "Center", "")), 85 | default = 'EDGE', 86 | ) 87 | rotation: bpy.props.FloatProperty( 88 | name = "Rotation", 89 | subtype = "ANGLE", 90 | soft_min = -360, soft_max = 360, 91 | default = 0, 92 | ) 93 | 94 | 95 | @classmethod 96 | def poll(cls, context): 97 | return context.mode in ('OBJECT', 'EDIT_MESH') and context.area.type == 'VIEW_3D' 98 | 99 | 100 | def invoke(self, context, event): 101 | # Validate Selection 102 | self.objects = Selection(*self.validate_selection(context)) 103 | 104 | if len(self.objects.selected) == 0: 105 | self.report({'WARNING'}, "Select mesh objects that should be carved") 106 | bpy.ops.view3d.select_box('INVOKE_DEFAULT') 107 | return {'CANCELLED'} 108 | 109 | # Initialize Core Components 110 | self.mouse = Mouse().from_event(event) 111 | self.workplane = Workplane(*self.calculate_workplane(context)) 112 | self.cutter = Cutter(*self.create_cutter(context)) 113 | self.effects = Effects().from_invoke(self, context) 114 | 115 | # cached_variables 116 | """Important for storing context as it was when operator was invoked (untouched by the modal).""" 117 | self.phase = "DRAW" 118 | self.initial_origin = self.origin # Initial shape origin. 119 | self.initial_aspect = self.aspect # Initial shape aspect. 120 | self._stored_phase = "DRAW" 121 | 122 | # Add Draw Handler 123 | self._handler = bpy.types.SpaceView3D.draw_handler_add(self.draw_shaders, 124 | (context,), 125 | 'WINDOW', 'POST_VIEW') 126 | context.window.cursor_set("MUTE") 127 | context.window_manager.modal_handler_add(self) 128 | 129 | return {'RUNNING_MODAL'} 130 | 131 | 132 | def modal(self, context, event): 133 | # Status Bar Text 134 | self.status(context) 135 | 136 | # find_the_limit_of_the_3d_viewport_region 137 | self.redraw_region(context) 138 | 139 | # Modifier Keys 140 | self.event_aspect(context, event) 141 | self.event_origin(context, event) 142 | self.event_rotate(context, event) 143 | self.event_bevel(context, event) 144 | self.event_array(context, event) 145 | self.event_flip(context, event) 146 | self.event_move(context, event) 147 | 148 | if event.type in {'MIDDLEMOUSE'}: 149 | return {'PASS_THROUGH'} 150 | if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}: 151 | if self.phase != "BEVEL": 152 | return {'PASS_THROUGH'} 153 | 154 | 155 | # Mouse Move 156 | if event.type == 'MOUSEMOVE': 157 | self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y)) 158 | 159 | # Draw 160 | if self.phase == "DRAW": 161 | self.update_cutter_shape(context) 162 | 163 | # Extrude 164 | elif self.phase == "EXTRUDE": 165 | self.set_extrusion_depth(context) 166 | 167 | 168 | # Confirm 169 | elif event.type == 'LEFTMOUSE': 170 | # Confirm Shape 171 | if self.phase == "DRAW" and event.value == 'RELEASE': 172 | """ 173 | Protection against creating a very small rectangle (or even with 0 dimensions) 174 | by clicking and releasing very quickly, in a very small distance. 175 | """ 176 | delta_x = abs(event.mouse_region_x - self.mouse.initial[0]) 177 | delta_y = abs(event.mouse_region_y - self.mouse.initial[1]) 178 | min_distance = 5 179 | 180 | if delta_x < min_distance or delta_y < min_distance: 181 | self.finalize(context, clean_up=True, abort=True) 182 | return {'FINISHED'} 183 | 184 | self.extrude_cutter(context) 185 | self.Cut(context) 186 | 187 | # Not setting depth manually, performing a cut here. 188 | if self.depth != 'MANUAL': 189 | self.confirm(context) 190 | return {'FINISHED'} 191 | else: 192 | return {'RUNNING_MODAL'} 193 | 194 | # Confirm Depth 195 | if self.phase == "EXTRUDE" and event.value == 'PRESS': 196 | self.confirm(context) 197 | return {'FINISHED'} 198 | 199 | 200 | # Cancel 201 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 202 | self.finalize(context, clean_up=True, abort=True) 203 | return {'FINISHED'} 204 | 205 | return {'RUNNING_MODAL'} 206 | 207 | 208 | def status(cls, context): 209 | """Set the status bar text to modal modifier keys.""" 210 | 211 | # Draw 212 | def modal_keys_draw(self, context): 213 | layout = self.layout 214 | row = layout.row(align=True) 215 | 216 | row.label(text="", icon='MOUSE_MOVE') 217 | row.label(text="Draw") 218 | row.label(text="", icon='MOUSE_LMB') 219 | row.label(text="Confirm") 220 | row.label(text="", icon='MOUSE_MMB') 221 | row.label(text="Rotate View") 222 | row.label(text="", icon='MOUSE_RMB') 223 | row.label(text="Cancel") 224 | 225 | row.label(text="", icon='EVENT_SPACEKEY') 226 | row.label(text=" Move") 227 | row.label(text="", icon='EVENT_R') 228 | row.label(text="Rotate") 229 | row.label(text="", icon='KEY_SHIFT') 230 | row.label(text="Aspect") 231 | row.label(text="", icon='EVENT_ALT') 232 | row.label(text=" Origin") 233 | 234 | row.label(text="", icon='EVENT_LEFT_ARROW') 235 | row.label(text="", icon='EVENT_DOWN_ARROW') 236 | row.label(text="", icon='EVENT_RIGHT_ARROW') 237 | row.label(text="", icon='EVENT_UP_ARROW') 238 | row.label(text="Array") 239 | row.label(text="", icon='EVENT_B') 240 | row.label(text="Bevel") 241 | 242 | # Restore rest of the status bar. 243 | layout.separator_spacer() 244 | layout.template_reports_banner() 245 | layout.separator_spacer() 246 | layout.template_running_jobs() 247 | 248 | layout.separator_spacer() 249 | row = layout.row() 250 | row.alignment = "RIGHT" 251 | text = context.screen.statusbar_info() 252 | row.label(text=text + " ") 253 | 254 | # Extrude 255 | def modal_keys_extrude(self, context): 256 | layout = self.layout 257 | row = layout.row(align=True) 258 | 259 | row.label(text="", icon='MOUSE_MOVE') 260 | row.label(text="Set Depth") 261 | row.label(text="", icon='MOUSE_LMB') 262 | row.label(text="Confirm") 263 | row.label(text="", icon='MOUSE_MMB') 264 | row.label(text="Rotate View") 265 | row.label(text="", icon='MOUSE_RMB') 266 | row.label(text="Cancel") 267 | 268 | row.label(text="", icon='EVENT_SPACEKEY') 269 | row.label(text=" Move") 270 | row.label(text="", icon='EVENT_R') 271 | row.label(text="Rotate") 272 | row.label(text="", icon='EVENT_F') 273 | row.label(text="Flip Direction") 274 | 275 | row.label(text="", icon='EVENT_LEFT_ARROW') 276 | row.label(text="", icon='EVENT_DOWN_ARROW') 277 | row.label(text="", icon='EVENT_RIGHT_ARROW') 278 | row.label(text="", icon='EVENT_UP_ARROW') 279 | row.label(text="Array") 280 | row.label(text="", icon='EVENT_B') 281 | row.label(text="Bevel") 282 | 283 | # Restore rest of the status bar. 284 | layout.separator_spacer() 285 | layout.template_reports_banner() 286 | layout.separator_spacer() 287 | layout.template_running_jobs() 288 | 289 | layout.separator_spacer() 290 | row = layout.row() 291 | row.alignment = "RIGHT" 292 | text = context.screen.statusbar_info() 293 | row.label(text=text + " ") 294 | 295 | # Missing keys: 296 | # Wheelup and Wheeldown to control bevel segments when B is pressed. 297 | # A to adjust array gap when array effect is used. 298 | 299 | if cls.phase == 'DRAW': 300 | context.workspace.status_text_set(modal_keys_draw) 301 | elif cls.phase == 'EXTRUDE': 302 | context.workspace.status_text_set(modal_keys_extrude) 303 | 304 | 305 | 306 | #### ------------------------------ REGISTRATION ------------------------------ #### 307 | 308 | classes = [ 309 | OBJECT_OT_carve_box, 310 | ] 311 | 312 | def register(): 313 | for cls in classes: 314 | bpy.utils.register_class(cls) 315 | 316 | def unregister(): 317 | for cls in reversed(classes): 318 | bpy.utils.unregister_class(cls) 319 | -------------------------------------------------------------------------------- /source/operators/boolean.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from collections import defaultdict 3 | from .. import __package__ as base_package 4 | 5 | from ..functions.poll import ( 6 | basic_poll, 7 | is_linked, 8 | is_instanced_data, 9 | list_candidate_objects, 10 | destructive_op_confirmation, 11 | ) 12 | from ..functions.modifier import ( 13 | add_boolean_modifier, 14 | apply_modifiers, 15 | get_modifiers_to_apply, 16 | ) 17 | from ..functions.object import ( 18 | set_cutter_properties, 19 | change_parent, 20 | create_slice, 21 | delete_cutter, 22 | ) 23 | 24 | 25 | #### ------------------------------ PROPERTIES ------------------------------ #### 26 | 27 | class ModifierProperties(): 28 | material_mode: bpy.props.EnumProperty( 29 | name = "Materials", 30 | description = "Method for setting materials on the new faces", 31 | items = (('INDEX', "Index Based", ("Set the material on new faces based on the order of the material slot lists. If a material doesn't exist on the\n" 32 | "modifier object, the face will use the same material slot or the first if the object doesn't have enough slots.")), 33 | ('TRANSFER', "Transfer", ("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n" 34 | "For empty slots, fall back to using the same material index as the operand mesh."))), 35 | default = 'INDEX', 36 | ) 37 | use_self: bpy.props.BoolProperty( 38 | name = "Self Intersection", 39 | description = "Allow self-intersection in operands", 40 | default = False, 41 | ) 42 | use_hole_tolerant: bpy.props.BoolProperty( 43 | name = "Hole Tolerant", 44 | description = "Better results when there are holes (slower)", 45 | default = False, 46 | ) 47 | double_threshold: bpy.props.FloatProperty( 48 | name = "Overlap Threshold", 49 | description = "Threshold for checking overlapping geometry", 50 | subtype = 'DISTANCE', 51 | min = 0, max = 1, precision = 12, step = 0.0001, 52 | default = 0.000001, 53 | ) 54 | 55 | def draw(self, context): 56 | prefs = context.preferences.addons[base_package].preferences 57 | 58 | layout = self.layout 59 | layout.use_property_split = True 60 | 61 | if prefs.solver == 'EXACT': 62 | layout.prop(self, "material_mode") 63 | layout.prop(self, "use_self") 64 | layout.prop(self, "use_hole_tolerant") 65 | elif prefs.solver == 'FLOAT': 66 | layout.prop(self, "double_threshold") 67 | 68 | 69 | 70 | #### ------------------------------ /brush_boolean/ ------------------------------ #### 71 | 72 | class BrushBoolean(ModifierProperties): 73 | @classmethod 74 | def poll(cls, context): 75 | return basic_poll(cls, context) 76 | 77 | 78 | def invoke(self, context, event): 79 | # Abort if there are less than 2 selected objects. 80 | if len(context.selected_objects) < 2: 81 | self.report({'WARNING'}, "Boolean operator needs at least two selected objects") 82 | return {'CANCELLED'} 83 | 84 | # Abort if active object is linked. 85 | if is_linked(context, context.active_object): 86 | self.report({'WARNING'}, "Boolean operators cannot be performed on linked objects") 87 | return {'CANCELLED'} 88 | 89 | return self.execute(context) 90 | 91 | 92 | def execute(self, context): 93 | prefs = context.preferences.addons[base_package].preferences 94 | canvas = context.active_object 95 | cutters = list_candidate_objects(self, context, context.active_object) 96 | 97 | if len(cutters) == 0: 98 | return {'CANCELLED'} 99 | 100 | # Create slices. 101 | if self.mode == "SLICE": 102 | for cutter in cutters: 103 | """NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds.""" 104 | slice = create_slice(context, canvas, modifier=True) 105 | add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin) 106 | 107 | for cutter in cutters: 108 | mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode 109 | display = 'WIRE' if prefs.wireframe else 'BOUNDS' 110 | set_cutter_properties(context, cutter, self.mode, display=display, collection=prefs.use_collection) 111 | add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin) 112 | if prefs.parent: 113 | change_parent(cutter, canvas) 114 | 115 | canvas.booleans.canvas = True 116 | 117 | return {'FINISHED'} 118 | 119 | 120 | class OBJECT_OT_boolean_brush_union(bpy.types.Operator, BrushBoolean): 121 | bl_idname = "object.boolean_brush_union" 122 | bl_label = "Boolean Union (Brush)" 123 | bl_description = "Merge selected objects into active one" 124 | bl_options = {'REGISTER', 'UNDO'} 125 | 126 | mode = "UNION" 127 | 128 | 129 | class OBJECT_OT_boolean_brush_intersect(bpy.types.Operator, BrushBoolean): 130 | bl_idname = "object.boolean_brush_intersect" 131 | bl_label = "Boolean Intersection (Brush)" 132 | bl_description = "Only keep the parts of the active object that are interesecting selected objects" 133 | bl_options = {'REGISTER', 'UNDO'} 134 | 135 | mode = "INTERSECT" 136 | 137 | 138 | class OBJECT_OT_boolean_brush_difference(bpy.types.Operator, BrushBoolean): 139 | bl_idname = "object.boolean_brush_difference" 140 | bl_label = "Boolean Difference (Brush)" 141 | bl_description = "Subtract selected objects from active one" 142 | bl_options = {'REGISTER', 'UNDO'} 143 | 144 | mode = "DIFFERENCE" 145 | 146 | 147 | class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean): 148 | bl_idname = "object.boolean_brush_slice" 149 | bl_label = "Boolean Slice (Brush)" 150 | bl_description = "Slice active object along the selected ones. Will create slices as separate objects" 151 | bl_options = {'REGISTER', 'UNDO'} 152 | 153 | mode = "SLICE" 154 | 155 | 156 | 157 | #### ------------------------------ /auto_boolean/ ------------------------------ #### 158 | 159 | class AutoBoolean(ModifierProperties): 160 | @classmethod 161 | def poll(cls, context): 162 | return basic_poll(cls, context) 163 | 164 | 165 | def invoke(self, context, event): 166 | # Abort if there are less than 2 selected objects. 167 | if len(context.selected_objects) < 2: 168 | self.report({'WARNING'}, "Boolean operator needs at least two selected objects") 169 | return {'CANCELLED'} 170 | 171 | # Abort if active object is linked. 172 | if is_linked(context, context.active_object): 173 | self.report({'ERROR'}, "Modifiers cannot be applied to linked object") 174 | return {'CANCELLED'} 175 | 176 | return destructive_op_confirmation(self, context, event, [context.active_object], title="Auto Boolean") 177 | 178 | 179 | def execute(self, context): 180 | prefs = context.preferences.addons[base_package].preferences 181 | canvas = context.active_object 182 | cutters = list_candidate_objects(self, context, context.active_object) 183 | new_modifiers = defaultdict(list) 184 | 185 | if len(cutters) == 0: 186 | return {'CANCELLED'} 187 | 188 | # Create slices. 189 | if self.mode == "SLICE": 190 | for cutter in cutters: 191 | """NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds.""" 192 | slice = create_slice(context, canvas) 193 | modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin) 194 | new_modifiers[slice].append(modifier) 195 | slice.select_set(True) 196 | 197 | for cutter in cutters: 198 | # Add boolean modifier on canvas. 199 | mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode 200 | modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin) 201 | new_modifiers[canvas].append(modifier) 202 | 203 | # Transfer cutters children to canvas. 204 | for child in cutter.children: 205 | change_parent(child, canvas) 206 | 207 | # Select all faces of the cutter so that newly created faces in canvas 208 | # are also selected after applying the modifier. 209 | for face in cutter.data.polygons: 210 | face.select = True 211 | 212 | # Apply modifiers on canvas & slices. 213 | for obj, modifiers in new_modifiers.items(): 214 | modifiers = get_modifiers_to_apply(context, obj, modifiers) 215 | apply_modifiers(context, obj, modifiers) 216 | 217 | # Delete cutters. 218 | for cutter in cutters: 219 | delete_cutter(cutter) 220 | 221 | return {'FINISHED'} 222 | 223 | 224 | class OBJECT_OT_boolean_auto_union(bpy.types.Operator, AutoBoolean): 225 | bl_idname = "object.boolean_auto_union" 226 | bl_label = "Boolean Union (Auto)" 227 | bl_description = "Merge selected objects into active one" 228 | bl_options = {'REGISTER', 'UNDO'} 229 | 230 | mode = "UNION" 231 | 232 | 233 | class OBJECT_OT_boolean_auto_difference(bpy.types.Operator, AutoBoolean): 234 | bl_idname = "object.boolean_auto_difference" 235 | bl_label = "Boolean Difference (Auto)" 236 | bl_description = "Subtract selected objects from active one" 237 | bl_options = {'REGISTER', 'UNDO'} 238 | 239 | mode = "DIFFERENCE" 240 | 241 | 242 | class OBJECT_OT_boolean_auto_intersect(bpy.types.Operator, AutoBoolean): 243 | bl_idname = "object.boolean_auto_intersect" 244 | bl_label = "Boolean Intersect (Auto)" 245 | bl_description = "Only keep the parts of the active object that are interesecting selected objects" 246 | bl_options = {'REGISTER', 'UNDO'} 247 | 248 | mode = "INTERSECT" 249 | 250 | 251 | class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean): 252 | bl_idname = "object.boolean_auto_slice" 253 | bl_label = "Boolean Slice (Auto)" 254 | bl_description = "Slice active object along the selected ones. Will create slices as separate objects" 255 | bl_options = {'REGISTER', 'UNDO'} 256 | 257 | mode = "SLICE" 258 | 259 | 260 | 261 | #### ------------------------------ REGISTRATION ------------------------------ #### 262 | 263 | addon_keymaps = [] 264 | 265 | classes = [ 266 | OBJECT_OT_boolean_brush_union, 267 | OBJECT_OT_boolean_brush_difference, 268 | OBJECT_OT_boolean_brush_intersect, 269 | OBJECT_OT_boolean_brush_slice, 270 | 271 | OBJECT_OT_boolean_auto_union, 272 | OBJECT_OT_boolean_auto_difference, 273 | OBJECT_OT_boolean_auto_intersect, 274 | OBJECT_OT_boolean_auto_slice, 275 | ] 276 | 277 | 278 | def register(): 279 | for cls in classes: 280 | bpy.utils.register_class(cls) 281 | 282 | # KEYMAP 283 | addon = bpy.context.window_manager.keyconfigs.addon 284 | km = addon.keymaps.new(name="Object Mode") 285 | 286 | # Brush Operators 287 | kmi = km.keymap_items.new("object.boolean_brush_union", 'NUMPAD_PLUS', 'PRESS', ctrl=True) 288 | kmi.active = True 289 | addon_keymaps.append((km, kmi)) 290 | 291 | kmi = km.keymap_items.new("object.boolean_brush_difference", 'NUMPAD_MINUS', 'PRESS', ctrl=True) 292 | kmi.active = True 293 | addon_keymaps.append((km, kmi)) 294 | 295 | kmi = km.keymap_items.new("object.boolean_brush_intersect", 'NUMPAD_ASTERIX', 'PRESS', ctrl=True) 296 | kmi.active = True 297 | addon_keymaps.append((km, kmi)) 298 | 299 | kmi = km.keymap_items.new("object.boolean_brush_slice", 'NUMPAD_SLASH', 'PRESS', ctrl=True) 300 | kmi.active = True 301 | addon_keymaps.append((km, kmi)) 302 | 303 | # Auto Operators 304 | kmi = km.keymap_items.new("object.boolean_auto_union", 'NUMPAD_PLUS', 'PRESS', ctrl=True, shift=True) 305 | kmi.active = True 306 | addon_keymaps.append((km, kmi)) 307 | 308 | kmi = km.keymap_items.new("object.boolean_auto_difference", 'NUMPAD_MINUS', 'PRESS', ctrl=True, shift=True) 309 | kmi.active = True 310 | addon_keymaps.append((km, kmi)) 311 | 312 | kmi = km.keymap_items.new("object.boolean_auto_intersect", 'NUMPAD_ASTERIX', 'PRESS', ctrl=True, shift=True) 313 | kmi.active = True 314 | addon_keymaps.append((km, kmi)) 315 | 316 | kmi = km.keymap_items.new("object.boolean_auto_slice", 'NUMPAD_SLASH', 'PRESS', ctrl=True, shift=True) 317 | kmi.active = True 318 | addon_keymaps.append((km, kmi)) 319 | 320 | 321 | def unregister(): 322 | for cls in reversed(classes): 323 | bpy.utils.unregister_class(cls) 324 | 325 | # KEYMAP 326 | for km, kmi in addon_keymaps: 327 | km.keymap_items.remove(kmi) 328 | addon_keymaps.clear() 329 | -------------------------------------------------------------------------------- /source/operators/cutter.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from .. import __package__ as base_package 3 | 4 | from ..functions.poll import ( 5 | basic_poll, 6 | is_instanced_data, 7 | destructive_op_confirmation, 8 | ) 9 | from ..functions.modifier import ( 10 | apply_modifiers, 11 | ) 12 | from ..functions.object import ( 13 | object_visibility_set, 14 | delete_empty_collection, 15 | delete_cutter, 16 | change_parent, 17 | ) 18 | from ..functions.list import ( 19 | list_canvases, 20 | list_selected_cutters, 21 | list_canvas_cutters, 22 | list_canvas_slices, 23 | list_cutter_users, 24 | list_unused_cutters, 25 | ) 26 | 27 | 28 | #### ------------------------------ OPERATORS ------------------------------ #### 29 | 30 | # Toggle Boolean Cutter 31 | class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator): 32 | bl_idname = "object.boolean_toggle_cutter" 33 | bl_label = "Toggle Boolean Cutter" 34 | bl_description = "Toggle this boolean cutter. If cutter is the active object it will be toggled for every canvas that uses it" 35 | bl_options = {'UNDO'} 36 | 37 | method: bpy.props.EnumProperty( 38 | name = "Method", 39 | items = (('ALL', "All", "Remove cutter from all canvases that use it"), 40 | ('SPECIFIED', "Specified", "Remove cutter from specified canvas")), 41 | default = 'ALL', 42 | ) 43 | 44 | specified_cutter: bpy.props.StringProperty( 45 | ) 46 | specified_canvas: bpy.props.StringProperty( 47 | ) 48 | 49 | @classmethod 50 | def poll(cls, context): 51 | return basic_poll(cls, context, check_linked=True) 52 | 53 | def execute(self, context): 54 | if self.method == 'SPECIFIED': 55 | canvases = [context.scene.objects[self.specified_canvas]] 56 | cutters = [context.scene.objects[self.specified_cutter]] 57 | slices = list_canvas_slices(canvases) 58 | elif self.method == 'ALL': 59 | cutters = list_selected_cutters(context) 60 | canvases = list_cutter_users(cutters) 61 | 62 | if cutters: 63 | for canvas in canvases: 64 | # toggle_slices_visibility (for_all_canvases) 65 | if canvas.booleans.slice == True: 66 | if any(modifier.object in cutters for modifier in canvas.modifiers): 67 | canvas.hide_viewport = not canvas.hide_viewport 68 | canvas.hide_render = not canvas.hide_render 69 | 70 | # Toggle Modifiers 71 | for mod in canvas.modifiers: 72 | if mod.type == 'BOOLEAN' and mod.object in cutters: 73 | mod.show_viewport = not mod.show_viewport 74 | mod.show_render = not mod.show_render 75 | 76 | 77 | if self.method == 'SPECIFIED': 78 | # toggle_slices_visibility (for_specified_canvas) 79 | for slice in slices: 80 | for mod in slice.modifiers: 81 | if mod.type == 'BOOLEAN' and mod.object in cutters: 82 | slice.hide_viewport = not slice.hide_viewport 83 | slice.hide_render = not slice.hide_render 84 | mod.show_viewport = not mod.show_viewport 85 | mod.show_render = not mod.show_render 86 | 87 | # hide_cutter_if_not_used_by_any_visible_modifiers 88 | other_canvases = list_canvases() 89 | for obj in other_canvases: 90 | if obj not in canvases + slices: 91 | if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'): 92 | cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]] 93 | 94 | for cutter in cutters: 95 | cutter.hide_viewport = not cutter.hide_viewport 96 | 97 | else: 98 | self.report({'INFO'}, "Boolean cutters are not selected") 99 | 100 | return {'FINISHED'} 101 | 102 | 103 | # Remove Boolean Cutter 104 | class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator): 105 | bl_idname = "object.boolean_remove_cutter" 106 | bl_label = "Remove Boolean Cutter" 107 | bl_description = "Remove this boolean cutter. If cutter is the active object it will be removed from every canvas that uses it" 108 | bl_options = {'UNDO'} 109 | 110 | method: bpy.props.EnumProperty( 111 | name = "Method", 112 | items = (('ALL', "All", "Remove cutter from all canvases that use it"), 113 | ('SPECIFIED', "Specified", "Remove cutter from specified canvas")), 114 | default = 'ALL', 115 | ) 116 | 117 | specified_cutter: bpy.props.StringProperty( 118 | ) 119 | specified_canvas: bpy.props.StringProperty( 120 | ) 121 | 122 | @classmethod 123 | def poll(cls, context): 124 | return basic_poll(cls, context, check_linked=True) 125 | 126 | def execute(self, context): 127 | prefs = context.preferences.addons[base_package].preferences 128 | leftovers = [] 129 | 130 | if self.method == 'SPECIFIED': 131 | canvases = [context.scene.objects[self.specified_canvas]] 132 | cutters = [context.scene.objects[self.specified_cutter]] 133 | slices = list_canvas_slices(canvases) 134 | elif self.method == 'ALL': 135 | cutters = list_selected_cutters(context) 136 | canvases = list_cutter_users(cutters) 137 | 138 | if cutters: 139 | # Remove Modifiers 140 | for canvas in canvases: 141 | for mod in canvas.modifiers: 142 | if "boolean_" in mod.name: 143 | if mod.object in cutters: 144 | canvas.modifiers.remove(mod) 145 | 146 | # remove_canvas_property_if_needed 147 | other_cutters, __ = list_canvas_cutters([canvas]) 148 | if len(other_cutters) == 0: 149 | canvas.booleans.canvas = False 150 | 151 | # Remove Slices (for_all_method) 152 | if canvas.booleans.slice == True: 153 | delete_cutter(canvas) 154 | 155 | 156 | if self.method == 'SPECIFIED': 157 | # Remove Slices (for_specified_method) 158 | for slice in slices: 159 | for mod in slice.modifiers: 160 | if mod.type == 'BOOLEAN' and mod.object in cutters: 161 | delete_cutter(slice) 162 | 163 | cutters, leftovers = list_unused_cutters(cutters, canvases, do_leftovers=True) 164 | 165 | 166 | # Restore Orphaned Cutters 167 | for cutter in cutters: 168 | if self.method == 'SPECIFIED' and cutter.booleans.carver: 169 | delete_cutter(cutter) 170 | else: 171 | # restore_visibility 172 | cutter.hide_render = False 173 | cutter.display_type = 'TEXTURED' 174 | cutter.lineart.usage = 'INHERIT' 175 | object_visibility_set(cutter, value=True) 176 | cutter.booleans.cutter = "" 177 | 178 | # remove_parent_&_collection 179 | if prefs.parent and cutter.parent in canvases: 180 | change_parent(cutter, None) 181 | 182 | if prefs.use_collection: 183 | cutters_collection = bpy.data.collections.get(prefs.collection_name) 184 | if cutters_collection in cutter.users_collection: 185 | bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter) 186 | 187 | # purge_empty_collection 188 | if prefs.use_collection: 189 | delete_empty_collection() 190 | 191 | 192 | # Change Leftover Cutter Parent 193 | if prefs.parent and leftovers != None: 194 | for cutter in leftovers: 195 | if cutter.parent in canvases: 196 | other_canvases = list_cutter_users([cutter]) 197 | change_parent(cutter, other_canvases[0]) 198 | 199 | else: 200 | self.report({'INFO'}, "Boolean cutters are not selected") 201 | 202 | return {'FINISHED'} 203 | 204 | 205 | # Apply Boolean Cutter 206 | class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator): 207 | bl_idname = "object.boolean_apply_cutter" 208 | bl_label = "Apply Boolean Cutter" 209 | bl_description = "Apply this boolean cutter. If cutter is the active object it will be applied to every canvas that uses it" 210 | bl_options = {'UNDO'} 211 | 212 | method: bpy.props.EnumProperty( 213 | name = "Method", 214 | items = (('ALL', "All", "Remove cutter from all canvases that use it"), 215 | ('SPECIFIED', "Specified", "Remove cutter from specified canvas")), 216 | default = 'ALL', 217 | ) 218 | 219 | specified_cutter: bpy.props.StringProperty( 220 | ) 221 | specified_canvas: bpy.props.StringProperty( 222 | ) 223 | 224 | @classmethod 225 | def poll(cls, context): 226 | return basic_poll(cls, context, check_linked=True) 227 | 228 | 229 | def invoke(self, context, event): 230 | # Filter Objects 231 | if self.method == 'SPECIFIED': 232 | self.cutters = [context.scene.objects[self.specified_cutter]] 233 | self.canvases = [context.scene.objects[self.specified_canvas]] 234 | self.slices = list_canvas_slices(self.canvases) 235 | 236 | elif self.method == 'ALL': 237 | self.cutters = list_selected_cutters(context) 238 | self.canvases = list_cutter_users(self.cutters) 239 | 240 | return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutter") 241 | 242 | 243 | def execute(self, context): 244 | prefs = bpy.context.preferences.addons[base_package].preferences 245 | leftovers = [] 246 | 247 | if self.cutters: 248 | # Select all faces of the cutter so that newly created faces in canvas 249 | # are also selected after applying the modifier. 250 | for cutter in self.cutters: 251 | for face in cutter.data.polygons: 252 | face.select = True 253 | 254 | # Apply Modifiers 255 | for canvas in self.canvases: 256 | context.view_layer.objects.active = canvas 257 | 258 | boolean_mods = [] 259 | for mod in canvas.modifiers: 260 | if "boolean_" in mod.name: 261 | if mod.object in self.cutters: 262 | boolean_mods.append(mod) 263 | apply_modifiers(context, canvas, boolean_mods) 264 | 265 | # remove_canvas_property_if_needed 266 | other_cutters, __ = list_canvas_cutters([canvas]) 267 | if len(other_cutters) == 0: 268 | canvas.booleans.canvas = False 269 | canvas.booleans.slice = False 270 | 271 | 272 | if self.method == 'SPECIFIED': 273 | # Apply Modifier for Slices (for_specified_method) 274 | for slice in self.slices: 275 | boolean_mods = [] 276 | for mod in slice.modifiers: 277 | if mod.type == 'BOOLEAN' and mod.object in self.cutters: 278 | boolean_mods.append(mod) 279 | apply_modifiers(context, slice, boolean_mods) 280 | 281 | 282 | unused_cutters, leftovers = list_unused_cutters(self.cutters, self.canvases, do_leftovers=True) 283 | 284 | 285 | for cutter in unused_cutters: 286 | # Transfer Children 287 | for child in cutter.children: 288 | change_parent(child, cutter.parent) 289 | 290 | # Purge Orphaned Cutters 291 | delete_cutter(cutter) 292 | 293 | # purge_empty_collection 294 | if prefs.use_collection: 295 | delete_empty_collection() 296 | 297 | 298 | # Change Leftover Cutter Parent 299 | if prefs.parent and leftovers != None: 300 | for cutter in leftovers: 301 | if cutter.parent in self.canvases: 302 | other_canvases = list_cutter_users([cutter]) 303 | change_parent(cutter, other_canvases[0]) 304 | 305 | else: 306 | self.report({'INFO'}, "Boolean cutters are not selected") 307 | 308 | return {'FINISHED'} 309 | 310 | 311 | 312 | #### ------------------------------ REGISTRATION ------------------------------ #### 313 | 314 | addon_keymaps = [] 315 | 316 | classes = [ 317 | OBJECT_OT_boolean_toggle_cutter, 318 | OBJECT_OT_boolean_remove_cutter, 319 | OBJECT_OT_boolean_apply_cutter, 320 | ] 321 | 322 | 323 | def register(): 324 | for cls in classes: 325 | bpy.utils.register_class(cls) 326 | 327 | # KEYMAP 328 | addon = bpy.context.window_manager.keyconfigs.addon 329 | km = addon.keymaps.new(name="Object Mode") 330 | 331 | kmi = km.keymap_items.new("object.boolean_apply_cutter", 'NUMPAD_ENTER', 'PRESS', ctrl=True) 332 | kmi.properties.method = 'ALL' 333 | kmi.active = True 334 | addon_keymaps.append((km, kmi)) 335 | 336 | 337 | def unregister(): 338 | for cls in reversed(classes): 339 | bpy.utils.unregister_class(cls) 340 | 341 | # KEYMAP 342 | for km, kmi in addon_keymaps: 343 | km.keymap_items.remove(kmi) 344 | addon_keymaps.clear() 345 | -------------------------------------------------------------------------------- /source/tools/carver_polyline.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | import os 4 | from mathutils import Vector 5 | from bpy_extras import view3d_utils 6 | from .. import __file__ as base_file 7 | 8 | from .common.base import ( 9 | CarverBase, 10 | ) 11 | from .common.properties import ( 12 | CarverPropsArray, 13 | ) 14 | from .common.types import ( 15 | Selection, 16 | Mouse, 17 | Workplane, 18 | Cutter, 19 | Effects, 20 | ) 21 | from .common.ui import ( 22 | carver_ui_common, 23 | ) 24 | 25 | 26 | description = "Cut custom polygonal shapes into mesh objects" 27 | 28 | #### ------------------------------ TOOLS ------------------------------ #### 29 | 30 | class OBJECT_WT_carve_polyline(bpy.types.WorkSpaceTool): 31 | bl_idname = "object.carve_polyline" 32 | bl_label = "Polyline Carve" 33 | bl_description = description 34 | 35 | bl_space_type = 'VIEW_3D' 36 | bl_context_mode = 'OBJECT' 37 | 38 | bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_polyline") 39 | bl_keymap = ( 40 | ("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None), 41 | ("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, None), 42 | # select 43 | ("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None), 44 | ("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("mode", 'ADD')]}), 45 | ("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("mode", 'SUB')]}), 46 | ) 47 | 48 | def draw_settings(context, layout, tool): 49 | props = tool.operator_properties("object.carve_polyline") 50 | carver_ui_common(context, layout, props) 51 | 52 | 53 | class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline): 54 | bl_context_mode = 'EDIT_MESH' 55 | 56 | 57 | 58 | #### ------------------------------ OPERATORS ------------------------------ #### 59 | 60 | class OBJECT_OT_carve_polyline(CarverBase, 61 | CarverPropsArray): 62 | bl_idname = "object.carve_polyline" 63 | bl_label = "Polyline Carve" 64 | bl_description = description 65 | bl_options = {'REGISTER', 'UNDO', 'DEPENDS_ON_CURSOR'} 66 | bl_cursor_pending = 'PICK_AREA' 67 | 68 | # SHAPE-properties 69 | shape = 'POLYLINE' 70 | origin = None 71 | aspect = None 72 | rotation = 0 73 | 74 | 75 | @classmethod 76 | def poll(cls, context): 77 | return context.mode in ('OBJECT', 'EDIT_MESH') and context.area.type == 'VIEW_3D' 78 | 79 | 80 | def invoke(self, context, event): 81 | # Validate Selection 82 | self.objects = Selection(*self.validate_selection(context)) 83 | 84 | if len(self.objects.selected) == 0: 85 | bpy.ops.view3d.select('INVOKE_DEFAULT') 86 | return {'CANCELLED'} 87 | 88 | # Initialize Core Components 89 | self.mouse = Mouse().from_event(event) 90 | self.workplane = Workplane(*self.calculate_workplane(context)) 91 | self.cutter = Cutter(*self.create_cutter(context)) 92 | self.effects = Effects().from_invoke(self, context) 93 | 94 | # cached_variables 95 | """Important for storing context as it was when operator was invoked (untouched by the modal).""" 96 | self.phase = "DRAW" 97 | self._distance_from_first = 0 98 | self._stored_phase = "DRAW" 99 | 100 | # Add Draw Handler 101 | self._handler = bpy.types.SpaceView3D.draw_handler_add(self.draw_shaders, 102 | (context,), 103 | 'WINDOW', 'POST_VIEW') 104 | context.window.cursor_set("MUTE") 105 | context.window_manager.modal_handler_add(self) 106 | 107 | return {'RUNNING_MODAL'} 108 | 109 | 110 | def modal(self, context, event): 111 | # Status Bar Text 112 | self.status(context) 113 | 114 | # find_the_limit_of_the_3d_viewport_region 115 | self.redraw_region(context) 116 | 117 | # Modifier Keys 118 | self.event_array(context, event) 119 | self.event_move(context, event) 120 | 121 | if event.type in {'MIDDLEMOUSE'}: 122 | return {'PASS_THROUGH'} 123 | if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}: 124 | if self.phase != "BEVEL": 125 | return {'PASS_THROUGH'} 126 | 127 | 128 | # Mouse Move 129 | if event.type == 'MOUSEMOVE': 130 | self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y)) 131 | 132 | # Draw 133 | if self.phase == "DRAW": 134 | # Calculate the distance from the initial mouse position. 135 | if self.mouse.current_3d: 136 | first_vert_world = self.cutter.obj.matrix_world @ self.cutter.verts[0].co 137 | first_vert_screen = view3d_utils.location_3d_to_region_2d(context.region, 138 | context.region_data, 139 | first_vert_world) 140 | distance_screen = (Vector(self.mouse.current) - first_vert_screen).length 141 | self._distance_from_first = max(100 - distance_screen, 0) 142 | 143 | self.update_cutter_shape(context) 144 | 145 | # Extrude 146 | elif self.phase == "EXTRUDE": 147 | self.set_extrusion_depth(context) 148 | 149 | 150 | # Add Points & Confirm 151 | elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': 152 | if self.phase == "DRAW": 153 | # Confirm Shape (if clicked on the first vert) 154 | if self._distance_from_first > 75: 155 | verts = self.cutter.verts 156 | if len(verts) > 3: 157 | self._remove_polyline_point(context, jump_mouse=False) 158 | self.extrude_cutter(context) 159 | self.Cut(context) 160 | 161 | # Not setting depth manually, performing a cut here. 162 | if self.depth != 'MANUAL': 163 | self.confirm(context) 164 | return {'FINISHED'} 165 | else: 166 | return {'RUNNING_MODAL'} 167 | 168 | # Add Point 169 | else: 170 | self._insert_polyline_point() 171 | 172 | # Confirm Depth 173 | if self.phase == "EXTRUDE": 174 | self.confirm(context) 175 | return {'FINISHED'} 176 | 177 | 178 | # Confirm 179 | elif event.type == 'RET': 180 | verts = self.cutter.verts 181 | if len(verts) > 2: 182 | # Confirm Shape 183 | if self.phase == "DRAW" and event.value == 'RELEASE': 184 | self.extrude_cutter(context) 185 | self.Cut(context) 186 | 187 | # Not setting depth manually, performing a cut here. 188 | if self.depth != 'MANUAL': 189 | self.confirm(context) 190 | return {'FINISHED'} 191 | else: 192 | return {'RUNNING_MODAL'} 193 | 194 | # Confirm Depth 195 | if self.phase == "EXTRUDE" and event.value == 'PRESS': 196 | self.confirm(context) 197 | return {'FINISHED'} 198 | else: 199 | self.report({'WARNING'}, "At least three points are required to make a polygonal shape") 200 | 201 | 202 | # Remove Last Point 203 | if event.type == 'BACK_SPACE' and event.value == 'PRESS': 204 | self._remove_polyline_point(context) 205 | 206 | 207 | # Cancel 208 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 209 | self.finalize(context, clean_up=True, abort=True) 210 | return {'FINISHED'} 211 | 212 | return {'RUNNING_MODAL'} 213 | 214 | 215 | def status(cls, context): 216 | """Set the status bar text to modal modifier keys.""" 217 | 218 | # Draw 219 | def modal_keys_draw(self, context): 220 | layout = self.layout 221 | row = layout.row(align=True) 222 | 223 | row.label(text="", icon='MOUSE_LMB') 224 | row.label(text="Insert Point") 225 | row.label(text="", icon='MOUSE_MMB') 226 | row.label(text="Rotate View") 227 | row.label(text="", icon='MOUSE_RMB') 228 | row.label(text="Cancel") 229 | row.label(text="", icon='KEY_RETURN') 230 | row.label(text="Confirm") 231 | 232 | row.label(text="", icon='EVENT_SPACEKEY') 233 | row.label(text=" Move") 234 | row.label(text="", icon='EVENT_BACKSPACE') 235 | row.label(text=" Remove Last Point") 236 | 237 | row.label(text="", icon='EVENT_LEFT_ARROW') 238 | row.label(text="", icon='EVENT_DOWN_ARROW') 239 | row.label(text="", icon='EVENT_RIGHT_ARROW') 240 | row.label(text="", icon='EVENT_UP_ARROW') 241 | row.label(text="Array") 242 | 243 | # Restore rest of the status bar. 244 | layout.separator_spacer() 245 | layout.template_reports_banner() 246 | layout.separator_spacer() 247 | layout.template_running_jobs() 248 | 249 | layout.separator_spacer() 250 | row = layout.row() 251 | row.alignment = "RIGHT" 252 | text = context.screen.statusbar_info() 253 | row.label(text=text + " ") 254 | 255 | # Extrude 256 | def modal_keys_extrude(self, context): 257 | layout = self.layout 258 | row = layout.row(align=True) 259 | 260 | row.label(text="", icon='MOUSE_MOVE') 261 | row.label(text="Set Depth") 262 | row.label(text="", icon='MOUSE_LMB') 263 | row.label(text="", icon='KEY_RETURN') 264 | row.label(text="Confirm") 265 | row.label(text="", icon='MOUSE_MMB') 266 | row.label(text="Rotate View") 267 | row.label(text="", icon='MOUSE_RMB') 268 | row.label(text="Cancel") 269 | 270 | row.label(text="", icon='EVENT_SPACEKEY') 271 | row.label(text=" Move") 272 | row.label(text="", icon='EVENT_R') 273 | row.label(text="Rotate") 274 | row.label(text="", icon='EVENT_F') 275 | row.label(text="Flip Direction") 276 | 277 | row.label(text="", icon='EVENT_LEFT_ARROW') 278 | row.label(text="", icon='EVENT_DOWN_ARROW') 279 | row.label(text="", icon='EVENT_RIGHT_ARROW') 280 | row.label(text="", icon='EVENT_UP_ARROW') 281 | row.label(text="Array") 282 | 283 | # Restore rest of the status bar. 284 | layout.separator_spacer() 285 | layout.template_reports_banner() 286 | layout.separator_spacer() 287 | layout.template_running_jobs() 288 | 289 | layout.separator_spacer() 290 | row = layout.row() 291 | row.alignment = "RIGHT" 292 | text = context.screen.statusbar_info() 293 | row.label(text=text + " ") 294 | 295 | # Missing keys: 296 | # A to adjust array gap when array effect is used. 297 | 298 | if cls.phase == 'DRAW': 299 | context.workspace.status_text_set(modal_keys_draw) 300 | elif cls.phase == 'EXTRUDE': 301 | context.workspace.status_text_set(modal_keys_extrude) 302 | 303 | 304 | # Polyline-specific features. 305 | def _insert_polyline_point(self): 306 | """Inserts a new vertex in the cutter geometry and connects it to the previous last one.""" 307 | 308 | bm = self.cutter.bm 309 | verts = self.cutter.verts 310 | x, y = self.mouse.current_3d.x, self.mouse.current_3d.y 311 | 312 | # Lock the position of the last vert to cursor position at the moment of press. 313 | last_vert = verts[-1] 314 | last_vert.co = Vector((x, y, 0)) 315 | 316 | # Find and remove edge between last vert and the first vert. 317 | if verts.index(last_vert) != 1: 318 | first_vert = verts[0] 319 | edge_to_remove = None 320 | for edge in last_vert.link_edges: 321 | if first_vert in edge.verts: 322 | edge_to_remove = edge 323 | break 324 | if edge_to_remove: 325 | self.cutter.bm.edges.remove(edge_to_remove) 326 | 327 | # Insert new point in bmesh and connect to last one. 328 | new_vert = bm.verts.new(Vector((x, y, 0))) 329 | bm.edges.new([last_vert, new_vert]) 330 | verts.append(new_vert) 331 | 332 | # Create a new face. 333 | if len(verts) >= 3: 334 | face = self.cutter.bm.faces.new(verts) 335 | self.cutter.faces = [face] 336 | 337 | # Update bmesh. 338 | bm.to_mesh(self.cutter.mesh) 339 | 340 | 341 | def _remove_polyline_point(self, context, jump_mouse=True): 342 | """Removes the last vertex in cutter geometry and moves cursor to the one before that.""" 343 | 344 | if self.phase != "DRAW": 345 | return 346 | 347 | obj = self.cutter.obj 348 | bm = self.cutter.bm 349 | verts = self.cutter.verts 350 | faces = self.cutter.faces 351 | 352 | if len(verts) <= 2: 353 | return 354 | 355 | # Remove last vertex. 356 | last_vert = verts[-1] 357 | bm.verts.remove(last_vert) 358 | verts.pop() 359 | 360 | # Reconstruct the face. 361 | face = faces[0] 362 | if face is not None: 363 | if len(verts) >= 3: 364 | new_face = bm.faces.new(verts) 365 | faces[0] = new_face 366 | else: 367 | faces[0] = None 368 | 369 | # Create an edge between new last vertex and the first vertex. 370 | new_last = verts[-1] 371 | first_vert = verts[0] 372 | edge_exists = any(first_vert in edge.verts for edge in new_last.link_edges) 373 | if not edge_exists: 374 | bm.edges.new([new_last, first_vert]) 375 | 376 | # Update bmesh. 377 | bm.to_mesh(self.cutter.mesh) 378 | 379 | # Jump mouse to the new last vert. 380 | if jump_mouse: 381 | vert_world = obj.matrix_world @ new_last.co 382 | screen_pos = view3d_utils.location_3d_to_region_2d(context.region, 383 | context.region_data, 384 | vert_world) 385 | if screen_pos: 386 | context.window.cursor_warp(int(screen_pos.x), int(screen_pos.y)) 387 | 388 | 389 | 390 | #### ------------------------------ REGISTRATION ------------------------------ #### 391 | 392 | classes = [ 393 | OBJECT_OT_carve_polyline, 394 | ] 395 | 396 | def register(): 397 | for cls in classes: 398 | bpy.utils.register_class(cls) 399 | 400 | def unregister(): 401 | for cls in reversed(classes): 402 | bpy.utils.unregister_class(cls) 403 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------