├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------