update addons (for 5.2)
This commit is contained in:
@@ -1,42 +1,36 @@
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
for mod in [icons,
|
||||
operators,
|
||||
for mod in [operators,
|
||||
tools,
|
||||
ui,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
]:
|
||||
importlib.reload(mod)
|
||||
print("Add-on Reloaded: Bool Tool")
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
icons,
|
||||
operators,
|
||||
tools,
|
||||
ui,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = [
|
||||
icons,
|
||||
modules = (
|
||||
operators,
|
||||
tools,
|
||||
ui,
|
||||
manual,
|
||||
preferences,
|
||||
properties,
|
||||
ui,
|
||||
versioning,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
|
||||
@@ -2,18 +2,17 @@ schema_version = "1.0.0"
|
||||
|
||||
id = "bool_tool"
|
||||
name = "Bool Tool"
|
||||
version = "2.0.0"
|
||||
tagline = "Quick boolean operators and tools for hard surface modeling"
|
||||
version = "2.1.0"
|
||||
tagline = "Quick Boolean operators and tools for hard surface modeling"
|
||||
tags = ["Modeling", "Object"]
|
||||
type = "add-on"
|
||||
|
||||
maintainer = "Nika Kutsniashvili <nickberckley@gmail.com>"
|
||||
website = "https://github.com/nickberckley/bool_tool"
|
||||
tags = ["Modeling", "Object"]
|
||||
maintainer = "Nika Kutsniashvili"
|
||||
website = "https://nickberckley.github.io/bool_tool/"
|
||||
support = "https://github.com/nickberckley/bool_tool/issues/new?template=bug_report.md"
|
||||
|
||||
blender_version_min = "4.5.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||
license = [
|
||||
"SPDX:GPL-3.0-or-later",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
|
||||
# Paths
|
||||
ICONS_PATH = os.path.join(os.path.dirname(__file__), "ui", "icons")
|
||||
|
||||
# Object types that can have evaluated mesh, and can be converted to Mesh.
|
||||
CONVERTABLE_TYPES = (
|
||||
'CURVE',
|
||||
'CURVES',
|
||||
'FONT',
|
||||
# 'POINTCLOUD', # Doesn't work in Blender 5.2
|
||||
# 'GREASEPENCIL' # Doesn't work in Blender 5.2
|
||||
'EMPTY',
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
import bpy
|
||||
|
||||
from .modifier import (
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from .object import (
|
||||
is_linked,
|
||||
change_parent,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def is_canvas(obj):
|
||||
"""Checks whether the object is a Boolean canvas (i.e. has Boolean cutters)."""
|
||||
|
||||
if obj.booleans.canvas == False:
|
||||
return False
|
||||
else:
|
||||
# Even if object is marked as a canvas, check if it actually has any cutters.
|
||||
cutters, __ = list_canvas_cutters([obj])
|
||||
if len(cutters) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /list/ ------------------------------ ####
|
||||
|
||||
def list_all_canvases(scene):
|
||||
"""Returns the list of all Boolean canvases in the scene."""
|
||||
|
||||
canvases = []
|
||||
|
||||
for obj in scene.objects:
|
||||
if is_canvas(obj):
|
||||
canvases.append(obj)
|
||||
|
||||
return canvases
|
||||
|
||||
|
||||
def list_selected_canvases(context):
|
||||
"""Returns the list of canvases in the selection."""
|
||||
|
||||
canvases = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj == active_object:
|
||||
continue
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
if is_canvas(obj):
|
||||
canvases.append(obj)
|
||||
|
||||
if active_object:
|
||||
if is_canvas(active_object):
|
||||
canvases.append(active_object)
|
||||
|
||||
return canvases
|
||||
|
||||
|
||||
def list_canvas_cutters(canvases: list) -> tuple[list, dict]:
|
||||
"""List cutters (and their associated modifiers) that are used by specified canvases."""
|
||||
|
||||
cutters = []
|
||||
modifiers = {}
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
|
||||
if mod.object not in cutters:
|
||||
cutters.append(mod.object)
|
||||
modifiers.setdefault(canvas, []).append(mod)
|
||||
|
||||
return cutters, modifiers
|
||||
|
||||
|
||||
def list_canvas_slices(context, canvases: list):
|
||||
"""Returns the list of slices of specified canvases."""
|
||||
|
||||
slices = []
|
||||
for obj in context.scene.objects:
|
||||
if obj.booleans.slice:
|
||||
if obj.booleans.slice_of in canvases:
|
||||
slices.append(obj)
|
||||
|
||||
return slices
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /filter/ ------------------------------ ####
|
||||
|
||||
def filter_canvases(self, context, canvases: list) -> list:
|
||||
"""Filter out objects from the given list if they can't be cut."""
|
||||
|
||||
usable_canvases = []
|
||||
for canvas in canvases:
|
||||
# Exclude non-Mesh types.
|
||||
if canvas.type != 'MESH':
|
||||
self.report({'WARNING'}, f"{canvas.name} is not a Mesh type. Only Meshes can be cut")
|
||||
continue
|
||||
# Exclude linked objects.
|
||||
if is_linked(context, canvas):
|
||||
self.report({'WARNING'}, f"{canvas.name} is linked and can not be used as a cutter")
|
||||
continue
|
||||
|
||||
usable_canvases.append(canvas)
|
||||
|
||||
return usable_canvases
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /filter/ ------------------------------ ####
|
||||
|
||||
def create_slice(context, canvas, modifier=False):
|
||||
"""Creates copy of canvas to be used as slice."""
|
||||
|
||||
slice = canvas.copy()
|
||||
slice.data = canvas.data.copy()
|
||||
slice.name = slice.data.name = canvas.name + "_slice"
|
||||
|
||||
# Parent to canvas.
|
||||
change_parent(context, slice, canvas, inverse=True)
|
||||
|
||||
# Set Boolean properties.
|
||||
if modifier == True:
|
||||
slice.booleans.canvas = True
|
||||
slice.booleans.slice = True
|
||||
slice.booleans.slice_of = canvas
|
||||
|
||||
# Add to canvas collections.
|
||||
for coll in canvas.users_collection:
|
||||
coll.objects.link(slice)
|
||||
|
||||
# Add slices to local view.
|
||||
if context.space_data.local_view:
|
||||
slice.local_view_set(context.space_data, True)
|
||||
|
||||
return slice
|
||||
@@ -0,0 +1,188 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..constants import (
|
||||
CONVERTABLE_TYPES,
|
||||
)
|
||||
from .modifier import (
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from .object import (
|
||||
is_linked,
|
||||
has_evaluated_mesh,
|
||||
object_visibility_set,
|
||||
convert_to_mesh,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from .scene import (
|
||||
ensure_collection,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /list/ ------------------------------ ####
|
||||
|
||||
def list_selected_cutters(context):
|
||||
"""Returns the list of cutters in the selection."""
|
||||
|
||||
cutters = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj == active_object:
|
||||
continue
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
if obj.booleans.cutter:
|
||||
cutters.append(obj)
|
||||
|
||||
if active_object:
|
||||
if active_object.booleans.cutter:
|
||||
cutters.append(active_object)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def list_cutter_users(cutters: list, exclude: list=None) -> dict:
|
||||
"""
|
||||
List canvases that use specified cutters.
|
||||
Canvases that should be excluded from the search can be specified with the `exclude` arg.
|
||||
Returns a dict of canvases (keys) and set of their Boolean modifiers that use cutters (values).
|
||||
"""
|
||||
|
||||
cutter_users = {}
|
||||
|
||||
for key, values in bpy.data.user_map(subset=cutters).items():
|
||||
for value in values:
|
||||
if value.id_type != 'OBJECT':
|
||||
continue
|
||||
if exclude and value in exclude:
|
||||
continue
|
||||
if len(value.modifiers) == 0:
|
||||
continue
|
||||
|
||||
for mod in value.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object not in cutters:
|
||||
continue
|
||||
|
||||
cutter_users.setdefault(value, set()).add(mod)
|
||||
|
||||
return cutter_users
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /filter/ ------------------------------ ####
|
||||
|
||||
def filter_cutters(self, context, cutters: list, canvases: list) -> list:
|
||||
"""
|
||||
Filter out objects from the given list if they can't be used as a cutter.
|
||||
If non-mesh type object has evaluated mesh, and can be converted to mesh it will be.
|
||||
"""
|
||||
|
||||
usable_cutters = []
|
||||
for cutter in cutters:
|
||||
# Exclude object if it is in both lists.
|
||||
if cutter in canvases:
|
||||
continue
|
||||
# Exclude linked objects.
|
||||
if is_linked(context, cutter):
|
||||
self.report({'WARNING'}, f"{cutter.name} is linked and can not be used as a cutter")
|
||||
continue
|
||||
|
||||
if cutter.type == 'MESH':
|
||||
# Exclude if object is already a cutter for canvas.
|
||||
users = list_cutter_users([cutter]).keys()
|
||||
if any(canvas in users for canvas in canvases):
|
||||
continue
|
||||
# Exclude if canvas is cutting the object (avoid dependancy loop).
|
||||
users = list_cutter_users(canvases).keys()
|
||||
if cutter in users:
|
||||
self.report({'WARNING'}, f"{cutter.name} can not cut its own cutter (dependancy loop)")
|
||||
continue
|
||||
|
||||
usable_cutters.append(cutter)
|
||||
|
||||
elif cutter.type in CONVERTABLE_TYPES:
|
||||
if not has_evaluated_mesh(context, cutter):
|
||||
continue
|
||||
|
||||
convert_to_mesh(context, cutter)
|
||||
usable_cutters.append(cutter)
|
||||
|
||||
return usable_cutters
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operate/ ------------------------------ ####
|
||||
|
||||
def make_cutter(context, cutter, mode: str, display='BOUNDS', collection=True):
|
||||
"""Ensures the cutter has the correct properties."""
|
||||
|
||||
# Hide Cutters
|
||||
cutter.hide_render = True
|
||||
cutter.display_type = display
|
||||
cutter.lineart.usage = 'EXCLUDE'
|
||||
object_visibility_set(cutter, value=False)
|
||||
|
||||
# Cutters Collection
|
||||
if collection:
|
||||
cutters_collection = ensure_collection(context)
|
||||
if cutters_collection not in cutter.users_collection:
|
||||
cutters_collection.objects.link(cutter)
|
||||
|
||||
# Set Boolean Property
|
||||
cutter.booleans.cutter = mode.capitalize()
|
||||
|
||||
|
||||
def restore_cutter(context, cutter, unparent=True, unlink_collection=True):
|
||||
"""Remove Boolean properties from a cutter object to restore it to a normal state."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Restore Unused Cutters
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
|
||||
# Remove Parent & Collection
|
||||
if unparent:
|
||||
change_parent(context, cutter, None)
|
||||
|
||||
if unlink_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
cutters_collection.objects.unlink(cutter)
|
||||
|
||||
|
||||
def handle_unused_cutters(context, cutters: list, canvases: list, delete=True):
|
||||
"""Deletes or restores cutters with no remaining users (besides given list of canvases)."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
for cutter in cutters:
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases).keys()
|
||||
|
||||
if len(other_canvases) == 0:
|
||||
if delete:
|
||||
# Delete unused cutters & transfer their children to canvas.
|
||||
for child in cutter.children:
|
||||
change_parent(context, child, cutter.parent, inverse=True)
|
||||
delete_object(cutter)
|
||||
|
||||
else:
|
||||
# Restore unused cutters.
|
||||
restore_cutter(context, cutter,
|
||||
unparent=prefs.parent and cutter.parent in canvases,
|
||||
unlink_collection=prefs.use_collection)
|
||||
|
||||
else:
|
||||
# Cutter has other users, parent it to one of them.
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
new_parent = next(c for c in other_canvases if not c.booleans.slice)
|
||||
change_parent(context, cutter, new_parent, inverse=True)
|
||||
@@ -10,7 +10,7 @@ from gpu_extras.batch import batch_for_shader
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def draw_shader(type, color, alpha, coords, size=1, indices=None):
|
||||
"""Creates a batch for a draw type"""
|
||||
"""Sets up and draws a batch for a given GPU shader type."""
|
||||
|
||||
gpu.state.blend_set('ALPHA')
|
||||
|
||||
@@ -48,9 +48,10 @@ def draw_shader(type, color, alpha, coords, size=1, indices=None):
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def draw_bmesh_faces(faces, world_matrix):
|
||||
def draw_bmesh_faces(faces, world_matrix) -> tuple[list, list]:
|
||||
"""
|
||||
Get world-space vertex pairs and indices from `bmesh` face. To be used in GPU batch.
|
||||
Gets world-space vertex pairs and indices from `bmesh` faces.
|
||||
Returns the list of 3D vertices and the list of triangle indices to be used in GPU batch.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
@@ -65,13 +66,13 @@ def draw_bmesh_faces(faces, world_matrix):
|
||||
for face in faces:
|
||||
face_indices = []
|
||||
|
||||
# Collect unique vertices only (avoid storing verts that are shared by faces multiple times).
|
||||
# (Iterating over face corners because unlike `face.verts` they're ordered).
|
||||
# Collect unique vertices only (avoid storing verts that are shared by multiple faces).
|
||||
# (NOTE: Iterating over face corners because unlike `face.verts` they're ordered).
|
||||
for loop in face.loops:
|
||||
vert = loop.vert
|
||||
co = world_matrix @ Vector(vert.co)
|
||||
|
||||
if vert not in vert_index_map:
|
||||
co = world_matrix @ Vector(vert.co)
|
||||
vertices.append(co)
|
||||
vert_index_map[vert] = vert_count
|
||||
face_indices.append(vert_count)
|
||||
@@ -94,46 +95,28 @@ def draw_bmesh_faces(faces, world_matrix):
|
||||
return vertices, indices
|
||||
|
||||
|
||||
def draw_bmesh_edges(edges, world_matrix):
|
||||
"""Convert bmesh edges into world-space vertex pairs to be used in GPU batch."""
|
||||
|
||||
if not edges:
|
||||
return None
|
||||
|
||||
vertices = []
|
||||
for edge in edges:
|
||||
v1 = world_matrix @ edge.verts[0].co
|
||||
v2 = world_matrix @ edge.verts[1].co
|
||||
vertices.append(v1)
|
||||
vertices.append(v2)
|
||||
|
||||
return vertices
|
||||
|
||||
|
||||
def draw_circle_around_point(context, obj, vert, radius, segments):
|
||||
def draw_circle_billboard(context, origin: Vector, radius: float, segments: int) -> list:
|
||||
"""
|
||||
Draws the screen-aligned circle around given vertex of the object.
|
||||
Returns the list of vertices for GPU batch.
|
||||
Draws a view-facing circle in the world-space around the given origin Vector.
|
||||
Returns the list of 3D vertices.
|
||||
"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
vert_world = obj.matrix_world @ vert.co
|
||||
origin_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, origin)
|
||||
radius = min(radius, 25)
|
||||
|
||||
vertices = []
|
||||
for i in range(segments + 1):
|
||||
angle = i * (2 * math.pi / segments)
|
||||
|
||||
# Calculate offset and vertex position in screen-space.
|
||||
# Add offset in screen-space.
|
||||
offset_x = radius * math.cos(angle)
|
||||
offset_y = radius * math.sin(angle)
|
||||
vert_screen = view3d_utils.location_3d_to_region_2d(region, rv3d, vert_world)
|
||||
vert_2d = Vector((origin_2d.x + offset_x, origin_2d.y + offset_y))
|
||||
|
||||
if vert_screen:
|
||||
# Add offset in screen-space and convert back to world-space.
|
||||
circle_screen = Vector((vert_screen.x + offset_x, vert_screen.y + offset_y))
|
||||
circle_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, circle_screen, vert_world)
|
||||
vertices.append(circle_3d)
|
||||
# Convert back to world-space.
|
||||
vert_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, vert_2d, origin)
|
||||
vertices.append(vert_3d)
|
||||
|
||||
return vertices
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import bpy
|
||||
|
||||
|
||||
#### ------------------------------ /all/ ------------------------------ ####
|
||||
|
||||
def list_canvases():
|
||||
"""List all canvases in the scene"""
|
||||
|
||||
canvas = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.booleans.canvas:
|
||||
canvas.append(obj)
|
||||
|
||||
return canvas
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /selected/ ------------------------------ ####
|
||||
|
||||
def list_selected_cutters(context):
|
||||
"""List selected cutters"""
|
||||
|
||||
cutters = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj != active_object and obj.type == 'MESH':
|
||||
if obj.booleans.cutter:
|
||||
cutters.append(obj)
|
||||
|
||||
if active_object:
|
||||
if active_object.booleans.cutter:
|
||||
cutters.append(active_object)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def list_selected_canvases(context):
|
||||
"""List selected canvases"""
|
||||
|
||||
canvases = []
|
||||
active_object = context.active_object
|
||||
selected_objects = context.selected_objects
|
||||
|
||||
if selected_objects:
|
||||
for obj in selected_objects:
|
||||
if obj != active_object and obj.type == 'MESH':
|
||||
if obj.booleans.canvas:
|
||||
canvases.append(obj)
|
||||
|
||||
if active_object:
|
||||
if active_object.booleans.canvas:
|
||||
canvases.append(active_object)
|
||||
|
||||
return canvases
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /users/ ------------------------------ ####
|
||||
|
||||
def list_canvas_cutters(canvases):
|
||||
"""List cutters that are used by specified canvases"""
|
||||
|
||||
cutters = []
|
||||
modifiers = []
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "boolean_" in mod.name:
|
||||
if mod.object:
|
||||
cutters.append(mod.object)
|
||||
modifiers.append(mod)
|
||||
|
||||
return cutters, modifiers
|
||||
|
||||
|
||||
def list_canvas_slices(canvases):
|
||||
"""Returns list of slices for specified canvases"""
|
||||
|
||||
slices = []
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.booleans.slice:
|
||||
if obj.booleans.slice_of in canvases:
|
||||
slices.append(obj)
|
||||
|
||||
return slices
|
||||
|
||||
|
||||
def list_cutter_users(cutters):
|
||||
"""List canvases that use specified cutters"""
|
||||
|
||||
cutter_users = []
|
||||
|
||||
for cutter in cutters:
|
||||
object = bpy.data.objects.get(cutter.name)
|
||||
|
||||
for key, values in bpy.data.user_map(subset=[object]).items():
|
||||
for value in values:
|
||||
# filter_only_object_type_users
|
||||
if value.id_type == 'OBJECT':
|
||||
for mod in value.modifiers:
|
||||
if mod.type == 'BOOLEAN':
|
||||
if mod.object and mod.object == cutter:
|
||||
cutter_users.append(value)
|
||||
|
||||
return cutter_users
|
||||
|
||||
|
||||
def list_cutter_modifiers(canvases, cutters):
|
||||
"""List modifiers on specified canvases that use specified cutters"""
|
||||
|
||||
if not canvases:
|
||||
canvases = list_canvases()
|
||||
|
||||
modifiers = []
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN':
|
||||
if mod.object in cutters:
|
||||
modifiers.append(mod)
|
||||
|
||||
return modifiers
|
||||
|
||||
|
||||
def list_unused_cutters(cutters, *canvases, do_leftovers=False):
|
||||
"""Takes in list of cutters and returns only those that have no other user besides specified canvas"""
|
||||
"""When `include_visible` is True it will return cutters that aren't used by any visible modifiers"""
|
||||
|
||||
other_canvases = list_canvases()
|
||||
original_cutters = cutters[:]
|
||||
|
||||
for obj in other_canvases:
|
||||
if obj in canvases:
|
||||
return
|
||||
|
||||
if any(mod.object in cutters for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
|
||||
leftovers = []
|
||||
# return_cutters_that_do_have_other_users_(so_that_parents_can_be_reassigned)
|
||||
if do_leftovers:
|
||||
leftovers = [cutter for cutter in original_cutters if cutter not in cutters]
|
||||
|
||||
return cutters, leftovers
|
||||
|
||||
|
||||
def list_pre_boolean_modifiers(obj) -> list:
|
||||
"""Returns a list of boolean modifiers & modifiers that come before last boolean modifier"""
|
||||
|
||||
# Find the index of a last boolean modifier
|
||||
last_boolean_index = -1
|
||||
for i in reversed(range(len(obj.modifiers))):
|
||||
if obj.modifiers[i].type == 'BOOLEAN':
|
||||
last_boolean_index = i
|
||||
break
|
||||
|
||||
# If boolean modifier is found, list all modifiers that come before it.
|
||||
if last_boolean_index != -1:
|
||||
return [mod for mod in obj.modifiers[:last_boolean_index + 1]]
|
||||
else:
|
||||
return []
|
||||
@@ -1,77 +0,0 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def distance_from_point_to_segment(point, start, end) -> float:
|
||||
"""
|
||||
Calculates the shortest distance between a point and a segment.
|
||||
All three inputs should be `mathutils.Vector` objects.
|
||||
This is an alternative to `mathutils.geometry.intersect_point_line`.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
segment = end - start
|
||||
start_to_point = point - start
|
||||
|
||||
# projection_along_segment
|
||||
c1 = start_to_point.dot(segment)
|
||||
if c1 <= 0:
|
||||
return (point - start).length
|
||||
|
||||
# segment_length_squared
|
||||
c2 = segment.dot(segment)
|
||||
if c2 <= c1:
|
||||
return (point - end).length
|
||||
|
||||
t = c1 / c2
|
||||
closest_point = start + t * segment
|
||||
distance = (point - closest_point).length
|
||||
|
||||
return distance
|
||||
|
||||
|
||||
def region_2d_to_line_3d(region, rv3d, point_2d: Vector, line_origin: Vector, line_direction: Vector) -> tuple[Vector, Vector]:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D ray and finds closest
|
||||
points between that ray and a given 3D line.
|
||||
"""
|
||||
|
||||
if line_origin is None or line_direction is None:
|
||||
return None, None
|
||||
|
||||
# Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction).
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
# Find closest points to each other on each line (second line being a ray).
|
||||
closest_points = mathutils.geometry.intersect_line_line(ray_origin,
|
||||
ray_origin + ray_direction,
|
||||
line_origin,
|
||||
line_origin + line_direction)
|
||||
|
||||
return closest_points
|
||||
|
||||
|
||||
def region_2d_to_plane_3d(region, rv3d, point_2d: Vector, plane: tuple[Vector]) -> Vector:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D point on a plane in world-space.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
location, normal = plane
|
||||
|
||||
# Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction).
|
||||
p3_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
p3_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
# Intersect the point with the plane.
|
||||
p3_on_plane = mathutils.geometry.intersect_line_plane(p3_origin, # First point of line.
|
||||
p3_origin + p3_direction, # Second point of line.
|
||||
location, # `plane_co` (a point on the plane).
|
||||
normal) # `plane_no` (the direction the plane is facing).
|
||||
|
||||
return p3_on_plane
|
||||
@@ -1,17 +1,56 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
import math
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
from .object import hide_objects
|
||||
from .types import Ray
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def extrude_face(bm, face):
|
||||
"""Extrudes cutter face (created by carve operation) along view vector to create a non-manifold mesh"""
|
||||
def is_instanced_mesh(data):
|
||||
"""
|
||||
Checks if `obj.data` has more than one users, i.e. is instanced.
|
||||
Function only considers object types as users, and excludes other pointers.
|
||||
"""
|
||||
|
||||
data = bpy.data.meshes.get(data.name)
|
||||
users = 0
|
||||
|
||||
for key, values in bpy.data.user_map(subset=[data]).items():
|
||||
for value in values:
|
||||
if value.id_type == 'OBJECT':
|
||||
users += 1
|
||||
|
||||
if users > 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def are_intersecting(obj_a, obj_b):
|
||||
"""Checks if bounding boxes of two given objects intersect."""
|
||||
|
||||
def world_bounds(obj):
|
||||
corners = [obj.matrix_world @ Vector(c) for c in obj.bound_box]
|
||||
xs = [c.x for c in corners]
|
||||
ys = [c.y for c in corners]
|
||||
zs = [c.z for c in corners]
|
||||
return (min(xs), max(xs)), (min(ys), max(ys)), (min(zs), max(zs))
|
||||
|
||||
(ax0, ax1), (ay0, ay1), (az0, az1) = world_bounds(obj_a)
|
||||
(bx0, bx1), (by0, by1), (bz0, bz1) = world_bounds(obj_b)
|
||||
|
||||
return (
|
||||
ax1 >= bx0 and ax0 <= bx1 and
|
||||
ay1 >= by0 and ay0 <= by1 and
|
||||
az1 >= bz0 and az0 <= bz1
|
||||
)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operate/ ------------------------------ ####
|
||||
|
||||
def extrude_face(bm, face) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMEdge], list[bmesh.types.BMFace]]:
|
||||
"""Extrudes the `bmesh` face and returns tuple of lists of extruded vertices, edges and faces."""
|
||||
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
@@ -26,8 +65,8 @@ def extrude_face(bm, face):
|
||||
return extruded_verts, extruded_edges, extruded_faces
|
||||
|
||||
|
||||
def shade_smooth_by_angle(bm, mesh, angle=30):
|
||||
"""Replication of "Auto Smooth" functionality: Marks faces as smooth, sharp edges (by angle) as sharp"""
|
||||
def shade_smooth_by_angle(bm, mesh, angle: float=30):
|
||||
"""Replication of "Auto Smooth": Marks faces as smooth & edges above the angle as sharp."""
|
||||
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
@@ -51,26 +90,6 @@ def shade_smooth_by_angle(bm, mesh, angle=30):
|
||||
bm.to_mesh(mesh)
|
||||
|
||||
|
||||
def are_intersecting(obj_a, obj_b):
|
||||
"""Checks if bounding boxes of two given objects intersect."""
|
||||
|
||||
def world_bounds(obj):
|
||||
corners = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box]
|
||||
xs = [c.x for c in corners]
|
||||
ys = [c.y for c in corners]
|
||||
zs = [c.z for c in corners]
|
||||
return (min(xs), max(xs)), (min(ys), max(ys)), (min(zs), max(zs))
|
||||
|
||||
(ax0, ax1), (ay0, ay1), (az0, az1) = world_bounds(obj_a)
|
||||
(bx0, bx1), (by0, by1), (bz0, bz1) = world_bounds(obj_b)
|
||||
|
||||
return (
|
||||
ax1 >= bx0 and ax0 <= bx1 and
|
||||
ay1 >= by0 and ay0 <= by1 and
|
||||
az1 >= bz0 and az0 <= bz1
|
||||
)
|
||||
|
||||
|
||||
def ensure_attribute(bm, name, domain):
|
||||
"""Ensure that the attribute with the given name and domain exists on mesh."""
|
||||
|
||||
@@ -85,21 +104,3 @@ def ensure_attribute(bm, name, domain):
|
||||
attr = bm.verts.layers.float.new(name)
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
def raycast(context, position, objects):
|
||||
"""Cast a ray in the scene to get the surface on any of the given objects."""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
|
||||
origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, position)
|
||||
direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, position)
|
||||
|
||||
# Cast Ray
|
||||
with hide_objects(context, exceptions=objects):
|
||||
hit, location, normal, index, object, matrix = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
ray = Ray(hit, location, normal, index, object, matrix)
|
||||
|
||||
return ray
|
||||
|
||||
@@ -3,42 +3,102 @@ import bmesh
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.list import (
|
||||
list_pre_boolean_modifiers,
|
||||
from .mesh import (
|
||||
is_instanced_mesh,
|
||||
)
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
)
|
||||
from .poll import (
|
||||
is_instanced_data,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /list/ ------------------------------ ####
|
||||
|
||||
def enumerate_boolean_modifiers(obj) -> int:
|
||||
"""Returns the number of Boolean modifiers on the object."""
|
||||
|
||||
num = 0
|
||||
for mod in obj.modifiers:
|
||||
if is_boolean_modifier(mod):
|
||||
num += 1
|
||||
|
||||
return num
|
||||
|
||||
|
||||
def get_modifiers_to_apply(context, obj, custom_list=None) -> list:
|
||||
"""Returns the list of modifiers that need to be applied based on add-on preferences."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Apply all modifiers.
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = list(obj.modifiers)
|
||||
|
||||
# Apply only Boolean modifiers.
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
if custom_list is None:
|
||||
modifiers = [mod for mod in obj.modifiers if is_boolean_modifier(mod)]
|
||||
else:
|
||||
modifiers = custom_list
|
||||
|
||||
# Apply all modifiers that come before last Boolean modifier.
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
# Find the index of a last Boolean modifier.
|
||||
last_boolean_index = -1
|
||||
for i in reversed(range(len(obj.modifiers))):
|
||||
if obj.modifiers[i].type == 'BOOLEAN':
|
||||
last_boolean_index = i
|
||||
break
|
||||
|
||||
# If a Boolean modifier is found, list all modifiers that come before it.
|
||||
if last_boolean_index != -1:
|
||||
modifiers = [mod for mod in obj.modifiers[:last_boolean_index + 1]]
|
||||
else:
|
||||
modifiers = []
|
||||
|
||||
return modifiers
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def is_boolean_modifier(mod, check_cutter=True) -> bool:
|
||||
"""Checks if a modifier is a Boolean modifier (and optionally if it has a valid cutter)."""
|
||||
|
||||
if mod is None:
|
||||
return False
|
||||
if mod.type != 'BOOLEAN':
|
||||
return False
|
||||
if check_cutter and mod.object is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, redo=True):
|
||||
"Adds boolean modifier with specified cutter and properties to a single object"
|
||||
"""Adds the Boolean modifier with specified cutter and properties to a given object."""
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and solver == 'FLOAT':
|
||||
solver = 'FAST'
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
name = "boolean_" + cutter.name.replace("boolean_", "")
|
||||
|
||||
modifier = obj.modifiers.new("boolean_" + cutter.name.replace("boolean_", ""), 'BOOLEAN')
|
||||
modifier = obj.modifiers.new(name, 'BOOLEAN')
|
||||
modifier.operation = mode
|
||||
modifier.object = cutter
|
||||
modifier.solver = solver
|
||||
modifier.show_in_editmode = prefs.show_in_editmode
|
||||
|
||||
# Set solver options (inherited from operator properties).
|
||||
# Set solver options (inherited from operator properties, i.e. `self`).
|
||||
if redo:
|
||||
modifier.material_mode = self.material_mode
|
||||
modifier.use_self = self.use_self
|
||||
modifier.use_hole_tolerant = self.use_hole_tolerant
|
||||
modifier.double_threshold = self.double_threshold
|
||||
|
||||
if prefs.show_in_editmode:
|
||||
modifier.show_in_editmode = True
|
||||
|
||||
# Move modifier to the index 0 (make it first in the stack).
|
||||
if pin:
|
||||
index = obj.modifiers.find(modifier.name)
|
||||
@@ -50,19 +110,20 @@ def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, re
|
||||
def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
"""
|
||||
Apply modifiers on object.
|
||||
Instead of using `bpy.ops.object.modifier_apply`, this function uses
|
||||
`bpy.data.meshes.new_from_object` built-in function to create a temporary
|
||||
mesh from the evaluated object (basically with visible modifiers applied).
|
||||
Temporary mesh is then transferred to objects mesh with `bmesh`.
|
||||
Instead of using `bpy.ops.object.modifier_apply`, by default this function uses
|
||||
`to_mesh` built-in function to create a temporary mesh from the evaluated object
|
||||
(basically with visible modifiers applied). Temporary mesh is then transferred
|
||||
to objects mesh using `bmesh`.
|
||||
|
||||
This method is up to 2x faster, although it's considered experimental
|
||||
and may fail in some cases, so a fallback to `bpy.ops.object.modifier_apply` is kept.
|
||||
"""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
_stored_active_obj = context.active_object
|
||||
|
||||
# Make object data unique if it's instanced.
|
||||
if is_instanced_data(obj):
|
||||
if is_instanced_mesh(obj.data):
|
||||
context.active_object.data = context.active_object.data.copy()
|
||||
|
||||
try:
|
||||
@@ -71,10 +132,13 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
if not force_clean:
|
||||
raise Exception()
|
||||
|
||||
context.view_layer.objects.active = obj
|
||||
with hide_modifiers(obj, excluding=modifiers):
|
||||
# Create a temporary mesh from evaluated object.
|
||||
evaluated_obj = obj.evaluated_get(context.evaluated_depsgraph_get())
|
||||
temp_data = bpy.data.meshes.new_from_object(evaluated_obj)
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
evaluated_obj = obj.evaluated_get(depsgraph)
|
||||
temp_data = evaluated_obj.to_mesh(preserve_all_data_layers=True,
|
||||
depsgraph=depsgraph)
|
||||
|
||||
# Create `bmesh` from temporary mesh and update edit mesh.
|
||||
if context.mode == 'EDIT_MESH':
|
||||
@@ -86,11 +150,11 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(temp_data)
|
||||
bm.to_mesh(obj.data)
|
||||
|
||||
bm.free()
|
||||
evaluated_obj.to_mesh_clear()
|
||||
|
||||
# Remove modifiers and purge temporary mesh.
|
||||
bpy.data.meshes.remove(temp_data)
|
||||
# Remove modifiers.
|
||||
for mod in modifiers:
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
@@ -99,7 +163,6 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
if obj.data.shape_keys:
|
||||
obj.shape_key_clear()
|
||||
|
||||
# Use `bpy.ops` operator to apply modifiers if above fails.
|
||||
except Exception as e:
|
||||
# print("Error applying modifiers with `bmesh` method:", e, "falling back to `bpy.ops` method")
|
||||
|
||||
@@ -118,10 +181,12 @@ def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
for mod in modifiers:
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
|
||||
context.view_layer.objects.active = _stored_active_obj
|
||||
|
||||
|
||||
@contextmanager
|
||||
def hide_modifiers(obj, excluding: list):
|
||||
"""Hides all modifiers of a given object in viewport except those in excluding list"""
|
||||
"""Hides all modifiers of a given object in the viewport except those in `excluding` list."""
|
||||
|
||||
visible_modifiers = []
|
||||
for mod in obj.modifiers:
|
||||
@@ -139,10 +204,10 @@ def hide_modifiers(obj, excluding: list):
|
||||
|
||||
|
||||
def add_modifier_asset(obj, path: str, asset: str):
|
||||
"""Loads the node group asset and adds a Geometry Nodes modifier using it."""
|
||||
"""Loads in the node group asset and adds a Geometry Nodes modifier using it."""
|
||||
|
||||
try:
|
||||
# Load the node group.
|
||||
# Load in the node group.
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
with bpy.data.libraries.load(path, link=True, pack=True) as (data_from, data_to):
|
||||
if asset in data_from.node_groups:
|
||||
@@ -155,7 +220,7 @@ def add_modifier_asset(obj, path: str, asset: str):
|
||||
|
||||
node_group = bpy.data.node_groups[asset]
|
||||
|
||||
# Add modifier on the object.
|
||||
# Add modifier to the object.
|
||||
mod = obj.modifiers.new(asset, type='NODES')
|
||||
mod.node_group = node_group
|
||||
mod.show_group_selector = False
|
||||
@@ -168,16 +233,19 @@ def add_modifier_asset(obj, path: str, asset: str):
|
||||
return None
|
||||
|
||||
|
||||
def get_modifiers_to_apply(context, obj, new_modifiers) -> list:
|
||||
"""Returns the list of modifiers that need to be applied based on add-on preferences."""
|
||||
def update_modifier_input(modifier, socket: str, value):
|
||||
"""Change the value of the geometry nodes modifier input socket."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = [mod for mod in obj.modifiers]
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
modifiers = new_modifiers
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(obj)
|
||||
|
||||
return modifiers
|
||||
try:
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
socket = getattr(modifier.properties.inputs, socket)
|
||||
socket.value = value
|
||||
else:
|
||||
modifier[f"{socket}"] = value
|
||||
except:
|
||||
"""
|
||||
NOTE: There are plethora of reasons why this can fail, node trees are finicky.
|
||||
Accounting for all possible cases is borderline impossible, so this check is necessary
|
||||
to at least fail silently and not throw Python error to users.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,152 +1,129 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
#### ------------------------------ /poll/ ------------------------------ ####
|
||||
|
||||
def set_cutter_properties(context, cutter, mode, display='BOUNDS', collection=True):
|
||||
"""Ensures cutter is properly set: has right properties, is hidden, in a collection & parented"""
|
||||
def is_linked(context, obj) -> bool:
|
||||
"""Checks whether the object is linked from an external .blend file (including library-overrides)."""
|
||||
|
||||
# Hide Cutters
|
||||
cutter.hide_render = True
|
||||
cutter.display_type = display
|
||||
cutter.lineart.usage = 'EXCLUDE'
|
||||
object_visibility_set(cutter, value=False)
|
||||
if obj not in context.editable_objects:
|
||||
if obj.library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if obj.override_library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Cutters Collection
|
||||
if collection:
|
||||
cutters_collection = ensure_collection(context)
|
||||
if cutters_collection not in cutter.users_collection:
|
||||
cutters_collection.objects.link(cutter)
|
||||
|
||||
# add_boolean_property
|
||||
cutter.booleans.cutter = mode.capitalize()
|
||||
def has_evaluated_mesh(context, obj):
|
||||
"""Checks if an evaluated object has mesh (created by Geometry Nodes modifiers)."""
|
||||
|
||||
# Exclude cases that return Python errors.
|
||||
if not obj:
|
||||
return False
|
||||
if bpy.app.version < (5, 2, 0) and obj.type == 'EMPTY':
|
||||
return False
|
||||
if obj.instance_type != 'NONE':
|
||||
return False
|
||||
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
obj_eval = depsgraph.id_eval_get(obj)
|
||||
|
||||
geometry = None
|
||||
try:
|
||||
geometry = obj_eval.evaluated_geometry()
|
||||
except:
|
||||
pass
|
||||
|
||||
if not geometry or not geometry.mesh:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operate/ ------------------------------ ####
|
||||
|
||||
def object_visibility_set(obj, value=False):
|
||||
"Sets object visibility properties to either True or False"
|
||||
"""Sets object visibility properties to either True or False."""
|
||||
|
||||
obj.visible_camera = value
|
||||
obj.visible_shadow = value
|
||||
obj.visible_diffuse = value
|
||||
obj.visible_glossy = value
|
||||
obj.visible_shadow = value
|
||||
obj.visible_transmission = value
|
||||
obj.visible_volume_scatter = value
|
||||
if bpy.app.version >= (5, 2, 0):
|
||||
obj.visible_raycast = value
|
||||
|
||||
obj.hide_probe_volume = not value
|
||||
obj.hide_probe_sphere = not value
|
||||
obj.hide_probe_plane = not value
|
||||
|
||||
|
||||
def convert_to_mesh(context, obj):
|
||||
"Converts active object into mesh (applying all modifiers and shape keys in process)"
|
||||
"""Converts active object into mesh (applying all modifiers and shape keys in the process)."""
|
||||
|
||||
# store_selection
|
||||
original_mode = obj.mode
|
||||
|
||||
if original_mode != 'OBJECT':
|
||||
edit_objects = context.objects_in_mode
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Store selection.
|
||||
stored_active = context.active_object
|
||||
stored_selection = context.selected_objects
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for ob in context.scene.objects:
|
||||
ob.select_set(False)
|
||||
|
||||
# Convert
|
||||
# Make `obj` active and only one selected.
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
# Convert.
|
||||
bpy.ops.object.convert(target='MESH')
|
||||
|
||||
# restore_selection
|
||||
for obj in stored_selection:
|
||||
obj.select_set(True)
|
||||
if original_mode != 'OBJECT':
|
||||
for ob in edit_objects:
|
||||
ob.select_set(True)
|
||||
bpy.ops.object.mode_set(mode=original_mode)
|
||||
|
||||
# Restore selection.
|
||||
for ob in stored_selection:
|
||||
ob.select_set(True)
|
||||
context.view_layer.objects.active = stored_active
|
||||
|
||||
|
||||
def ensure_collection(context):
|
||||
"""Checks the existance of boolean cutters collection and creates it if it doesn't exist"""
|
||||
def change_parent(context, obj, parent, inverse=False):
|
||||
"""Changes or removes parent from an object while keeping the transformation."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
context.evaluated_depsgraph_get().update()
|
||||
|
||||
collection_name = prefs.collection_name
|
||||
cutters_collection = bpy.data.collections.get(collection_name)
|
||||
|
||||
if cutters_collection is None:
|
||||
cutters_collection = bpy.data.collections.new(collection_name)
|
||||
context.scene.collection.children.link(cutters_collection)
|
||||
cutters_collection.hide_render = True
|
||||
cutters_collection.color_tag = 'COLOR_01'
|
||||
# cutters_collection.hide_viewport = True
|
||||
# context.view_layer.layer_collection.children[collection_name].exclude = True
|
||||
|
||||
return cutters_collection
|
||||
|
||||
|
||||
def delete_empty_collection():
|
||||
"""Removes boolean cutters collection if it has no more objects in it"""
|
||||
|
||||
prefs = bpy.context.preferences.addons[base_package].preferences
|
||||
|
||||
collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if collection and not collection.objects:
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
|
||||
def delete_cutter(cutter):
|
||||
"""Deletes cutter object and purges it's mesh data"""
|
||||
|
||||
orphaned_mesh = cutter.data
|
||||
bpy.data.objects.remove(cutter)
|
||||
if orphaned_mesh.users == 0:
|
||||
bpy.data.meshes.remove(orphaned_mesh)
|
||||
|
||||
|
||||
def change_parent(obj, parent, force=False, inverse=False):
|
||||
"""Changes or removes parent from cutter object while keeping the transformation"""
|
||||
|
||||
if obj.parent is not None:
|
||||
if not force:
|
||||
return
|
||||
|
||||
matrix_copy = obj.matrix_world.copy()
|
||||
obj.parent = parent
|
||||
if inverse:
|
||||
if inverse and parent is not None:
|
||||
obj.matrix_parent_inverse = parent.matrix_world.inverted()
|
||||
obj.matrix_world = matrix_copy
|
||||
|
||||
|
||||
def create_slice(context, canvas, modifier=False):
|
||||
"""Creates copy of canvas to be used as slice"""
|
||||
|
||||
slice = canvas.copy()
|
||||
slice.data = canvas.data.copy()
|
||||
slice.name = slice.data.name = canvas.name + "_slice"
|
||||
change_parent(slice, canvas)
|
||||
|
||||
# Set Boolean Properties
|
||||
if modifier == True:
|
||||
slice.booleans.canvas = True
|
||||
slice.booleans.slice = True
|
||||
slice.booleans.slice_of = canvas
|
||||
|
||||
# Add to Canvas Collections
|
||||
for coll in canvas.users_collection:
|
||||
coll.objects.link(slice)
|
||||
|
||||
# add_slices_to_local_view
|
||||
if context.space_data.local_view:
|
||||
slice.local_view_set(context.space_data, True)
|
||||
|
||||
return slice
|
||||
|
||||
|
||||
def set_object_origin(obj, bm, point='CENTER', custom=None):
|
||||
"""Sets object origin to given position by shifting vertices"""
|
||||
def set_object_origin(obj, bm, point='CENTER', custom: Vector=None):
|
||||
"""Sets the origin of a mesh type object to given position by shifting vertices."""
|
||||
|
||||
# Center of the bounding box.
|
||||
if point == 'CENTER_OBJ':
|
||||
position_local = 0.125 * sum((mathutils.Vector(b) for b in obj.bound_box), mathutils.Vector())
|
||||
position_local = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
|
||||
position_world = obj.matrix_world @ position_local
|
||||
|
||||
# Center of the geometry.
|
||||
elif point == 'CENTER_MESH':
|
||||
if len(bm.verts) > 0:
|
||||
position_local = sum((v.co for v in bm.verts), mathutils.Vector()) / len(bm.verts)
|
||||
position_local = sum((v.co for v in bm.verts), Vector()) / len(bm.verts)
|
||||
else:
|
||||
position_local = mathutils.Vector((0, 0, 0))
|
||||
position_local = Vector((0, 0, 0))
|
||||
position_world = obj.matrix_world @ position_local
|
||||
|
||||
# Custom origin point (should be local Vector).
|
||||
@@ -154,8 +131,8 @@ def set_object_origin(obj, bm, point='CENTER', custom=None):
|
||||
position_local = custom
|
||||
position_world = obj.matrix_world @ custom
|
||||
|
||||
mat = mathutils.Matrix.Translation(position_local)
|
||||
bmesh.ops.transform(bm, matrix=mat.inverted(), verts=bm.verts)
|
||||
matrix = Matrix.Translation(position_local)
|
||||
bmesh.ops.transform(bm, matrix=matrix.inverted(), verts=bm.verts)
|
||||
bm.to_mesh(obj.data)
|
||||
|
||||
obj.location = position_world
|
||||
@@ -179,3 +156,13 @@ def hide_objects(context, exceptions: list):
|
||||
finally:
|
||||
for obj in hidden_objects:
|
||||
obj.hide_set(False)
|
||||
|
||||
|
||||
def delete_object(cutter, purge_data=True):
|
||||
"""Deletes the object and optionally purges its data if it has no more users."""
|
||||
|
||||
orphaned_data = cutter.data
|
||||
bpy.data.objects.remove(cutter)
|
||||
|
||||
if purge_data and orphaned_data.users == 0:
|
||||
bpy.data.meshes.remove(orphaned_data)
|
||||
|
||||
@@ -1,153 +1,44 @@
|
||||
import bpy
|
||||
|
||||
from .list import (
|
||||
list_canvas_cutters,
|
||||
list_cutter_users,
|
||||
from ..constants import (
|
||||
CONVERTABLE_TYPES,
|
||||
)
|
||||
from .mesh import (
|
||||
is_instanced_mesh,
|
||||
)
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
has_evaluated_mesh,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def basic_poll(cls, context, check_linked=False):
|
||||
"""Basic poll for boolean operators."""
|
||||
def basic_poll(cls, context, check_active=True):
|
||||
"""Basic poll for Boolean operators."""
|
||||
|
||||
if context.mode != 'OBJECT':
|
||||
return False
|
||||
if context.active_object is None:
|
||||
cls.poll_message_set("Boolean operators can only be performed in Object Mode")
|
||||
return False
|
||||
|
||||
obj = context.active_object
|
||||
if obj.type != 'MESH':
|
||||
cls.poll_message_set("Boolean operators can only be used for mesh objects")
|
||||
return False
|
||||
|
||||
if check_linked and is_linked(context, obj) == True:
|
||||
cls.poll_message_set("Boolean operators can not be executed on linked objects")
|
||||
return False
|
||||
if check_active:
|
||||
if context.active_object is None:
|
||||
cls.poll_message_set("No active object")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_linked(context, obj):
|
||||
"""Checks whether the object is linked from an external .blend file (including library-overrides)."""
|
||||
|
||||
if obj not in context.editable_objects:
|
||||
if obj.library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if obj.override_library:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_canvas(obj):
|
||||
"""Checks whether the object is a boolean canvas (i.e. has boolean cutters)."""
|
||||
|
||||
if obj.booleans.canvas == False:
|
||||
return False
|
||||
else:
|
||||
# Even if object is marked as canvas, check if it actually has any cutters
|
||||
cutters, __ = list_canvas_cutters([obj])
|
||||
if len(cutters) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_instanced_data(obj):
|
||||
"""Checks if `obj.data` has more than one users, i.e. is instanced."""
|
||||
"""Function only considers object types as users, and excludes pointers."""
|
||||
|
||||
data = bpy.data.meshes.get(obj.data.name)
|
||||
users = 0
|
||||
|
||||
for key, values in bpy.data.user_map(subset=[data]).items():
|
||||
for value in values:
|
||||
if value.id_type == 'OBJECT':
|
||||
users += 1
|
||||
|
||||
if users > 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def active_modifier_poll(obj):
|
||||
"""Checks whether the active modifier for active object is a boolean."""
|
||||
|
||||
# Check if active modifier exists.
|
||||
if len(obj.modifiers) == 0:
|
||||
return False
|
||||
if obj.modifiers.active is None:
|
||||
return False
|
||||
|
||||
# Check if active modifier is a boolean with a valid object.
|
||||
modifier = obj.modifiers.active
|
||||
if modifier.type != "BOOLEAN":
|
||||
return False
|
||||
if modifier.object is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def has_evaluated_mesh(context, obj):
|
||||
"""Checks if an object (non-mesh type) has an evaluated mesh created by Geometry Nodes modifiers."""
|
||||
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
obj_eval = depsgraph.id_eval_get(obj)
|
||||
geometry = obj_eval.evaluated_geometry()
|
||||
|
||||
if geometry.mesh:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def list_candidate_objects(self, context, canvas):
|
||||
"""Filter out objects from the selection that can't be used as a cutter."""
|
||||
|
||||
cutters = []
|
||||
for obj in context.selected_objects:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
if is_linked(context, obj):
|
||||
self.report({'WARNING'}, f"{obj.name} is linked and can not be used as a cutter")
|
||||
continue
|
||||
|
||||
if obj.type == 'MESH':
|
||||
# Exclude if object is already a cutter for canvas.
|
||||
if canvas in list_cutter_users([obj]):
|
||||
continue
|
||||
# Exclude if canvas is cutting the object (avoid dependancy loop).
|
||||
if obj in list_cutter_users([canvas]):
|
||||
self.report({'WARNING'}, f"{obj.name} can not cut its own cutter (dependancy loop)")
|
||||
continue
|
||||
|
||||
cutters.append(obj)
|
||||
|
||||
elif obj.type in ('CURVE', 'FONT'):
|
||||
if has_evaluated_mesh(context, obj):
|
||||
convert_to_mesh(context, obj)
|
||||
cutters.append(obj)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
def destructive_op_confirmation(self, context, event, canvases: list, title="Boolean Operation"):
|
||||
def destructive_op_confirmation(cls, context, event, canvases: list, title="Boolean Operation"):
|
||||
"""
|
||||
Creates & returns the confirmation pop-up window for destructive boolean operators.\n
|
||||
Confirmation window is triggered by canvas objects that have instanced object data or shape keys.\n
|
||||
If none of the canvas objects have them the operator is executed without any confirmation.
|
||||
Creates & returns the confirmation pop-up window for destructive Boolean operators.\n
|
||||
Confirmation window is triggered by canvases that have instanced data or shape keys.\n
|
||||
If none of the canvases have them the operator is executed without any confirmation.
|
||||
"""
|
||||
|
||||
has_instanced_data = any(obj for obj in canvases if is_instanced_data(obj))
|
||||
if len(canvases) == 0:
|
||||
return cls.execute(context)
|
||||
|
||||
has_instanced_data = any(obj for obj in canvases if is_instanced_mesh(obj.data))
|
||||
has_shape_keys = any(obj for obj in canvases if obj.data.shape_keys)
|
||||
|
||||
if has_instanced_data or has_shape_keys:
|
||||
@@ -166,15 +57,74 @@ def destructive_op_confirmation(self, context, event, canvases: list, title="Boo
|
||||
# Combined message.
|
||||
if has_instanced_data and has_shape_keys:
|
||||
message = ("Object(s) you're trying to cut have shape keys and instanced object data.\n"
|
||||
"In order to apply modifiers shape keys need to be applied, and object data made single user.\n"
|
||||
"In order to apply modifiers shape keys need to be applied & object data made single user.\n"
|
||||
"Do you proceed?")
|
||||
|
||||
popup = context.window_manager.invoke_confirm(self, event, title=title,
|
||||
popup = context.window_manager.invoke_confirm(cls, event, title=title,
|
||||
confirm_text="Yes", icon='WARNING',
|
||||
message=message)
|
||||
|
||||
cls._unflippable = True
|
||||
return popup
|
||||
|
||||
# Execute without confirmation window.
|
||||
else:
|
||||
return self.execute(context)
|
||||
return cls.execute(context)
|
||||
|
||||
|
||||
def convert_to_mesh_confirmation(cls, context, event, cutters: list, title="Boolean Operation"):
|
||||
"""
|
||||
Creates & returns the confirmation pop-up window when the object is
|
||||
about to be converted to mesh to be used as a cutter.
|
||||
|
||||
NOTE (1): Only triggers during brush Boolean operators,
|
||||
because object gets destroyed in the destructive one anyway.
|
||||
|
||||
NOTE (2): This is only required because of the limitation of legacy Boolean modifier.
|
||||
Geometry nodes implementation works with any object type. When the add-on is
|
||||
updated to work with custom modifiers this will not be necesary anymore.
|
||||
"""
|
||||
|
||||
if len(cutters) == 0:
|
||||
return cls.execute(context)
|
||||
|
||||
is_convertable = any(
|
||||
obj.type in CONVERTABLE_TYPES and has_evaluated_mesh(context, obj)
|
||||
for obj in cutters
|
||||
)
|
||||
|
||||
if is_convertable:
|
||||
message = ("Some of the selected objects are not of the Mesh type, but output mesh.\n"
|
||||
"In order to use them as cutters, they need to be converted to mesh.\n"
|
||||
"This is a destructive operator. Do you proceed?")
|
||||
|
||||
popup = context.window_manager.invoke_confirm(cls, event, title=title,
|
||||
confirm_text="Yes", icon='WARNING',
|
||||
message=message)
|
||||
|
||||
cls._unflippable = True
|
||||
return popup
|
||||
|
||||
# Execute without confirmation window.
|
||||
else:
|
||||
return cls.execute(context)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /operator_helpers/ ------------------------------ ####
|
||||
|
||||
def _guess_toggle_state(modifiers):
|
||||
"""Guess whether cutters should be hidden or revealed."""
|
||||
|
||||
enabled = 0
|
||||
disabled = 0
|
||||
for mod in modifiers:
|
||||
if mod.show_viewport:
|
||||
enabled += 1
|
||||
else:
|
||||
disabled += 1
|
||||
|
||||
if enabled > disabled:
|
||||
return "On"
|
||||
else:
|
||||
return "Off"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from .object import (
|
||||
hide_objects,
|
||||
)
|
||||
from .types import (
|
||||
Ray,
|
||||
)
|
||||
from .view import (
|
||||
region_2d_to_ray_3d,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def ensure_collection(context) -> bpy.types.Collection:
|
||||
"""Returns the Boolean cutters collection and creates it if it doesn't exist."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
collection_name = prefs.collection_name
|
||||
|
||||
coll = bpy.data.collections.get(collection_name)
|
||||
|
||||
# Create the collection if it doesn't exist.
|
||||
if coll is None:
|
||||
coll = bpy.data.collections.new(collection_name)
|
||||
coll.hide_render = True
|
||||
coll.color_tag = 'COLOR_01'
|
||||
context.scene.collection.children.link(coll)
|
||||
|
||||
return coll
|
||||
|
||||
|
||||
def delete_empty_collection(context):
|
||||
"""Removes Boolean cutters collection if it has no more objects in it."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
if not prefs.use_collection:
|
||||
return
|
||||
|
||||
collection_name = prefs.collection_name
|
||||
collection = bpy.data.collections.get(collection_name)
|
||||
|
||||
if not collection:
|
||||
return
|
||||
if collection.objects:
|
||||
return
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
|
||||
def raycast(context, position, objects):
|
||||
"""Cast a ray in the scene to get the surface on any of the given objects."""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
|
||||
origin, direction = region_2d_to_ray_3d(region, rv3d, position)
|
||||
|
||||
# Cast Ray
|
||||
with hide_objects(context, exceptions=objects):
|
||||
hit, location, normal, index, object, matrix = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
ray = Ray(hit, location, normal, index, object, matrix)
|
||||
|
||||
return ray
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
@@ -16,8 +15,8 @@ class Ray:
|
||||
obj,
|
||||
matrix: Matrix):
|
||||
self.hit = hit
|
||||
self.location = location if location is not None else mathutils.Vector()
|
||||
self.normal = normal if normal is not None else mathutils.Vector()
|
||||
self.location = location
|
||||
self.normal = normal
|
||||
self.index = index
|
||||
self.obj = obj
|
||||
self.matrix = matrix if matrix is not None else mathutils.Matrix()
|
||||
self.matrix = matrix
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def redraw_regions(context):
|
||||
"""Redraw regions to find the limits of the 3D viewport."""
|
||||
|
||||
for area in context.window.screen.areas:
|
||||
if area.type != 'VIEW_3D':
|
||||
continue
|
||||
for region in area.regions:
|
||||
if region.type in {'WINDOW', 'UI'}:
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
def distance_from_point_to_segment(point: Vector, line_p1: Vector, line_p2: Vector) -> float:
|
||||
"""
|
||||
Calculates the shortest distance between the point and the finite segment.
|
||||
This is an alternative to `mathutils.geometry.intersect_point_line` (w/ clamping).
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
segment = line_p2 - line_p1
|
||||
start_to_point = point - line_p1
|
||||
|
||||
# Projection along segment.
|
||||
c1 = start_to_point.dot(segment)
|
||||
if c1 <= 0:
|
||||
return (point - line_p1).length
|
||||
|
||||
# Segment length squared.
|
||||
c2 = segment.dot(segment)
|
||||
if c2 <= c1:
|
||||
return (point - line_p2).length
|
||||
|
||||
t = c1 / c2
|
||||
closest_point = line_p1 + t * segment
|
||||
distance = (point - closest_point).length
|
||||
|
||||
return distance
|
||||
|
||||
|
||||
def region_2d_to_ray_3d(region, rv3d, point_2d: Vector) -> tuple[Vector, Vector]:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D ray in the world-space.
|
||||
Returns a tuple of `ray_origin` and `ray_direction` Vectors.
|
||||
"""
|
||||
|
||||
origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
return origin, direction
|
||||
|
||||
|
||||
def region_2d_to_plane_3d(region, rv3d, point_2d: Vector, plane: tuple[Vector]) -> Vector:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D point on a plane in world-space.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
location, normal = plane
|
||||
p3_origin, p3_direction = region_2d_to_ray_3d(region, rv3d, point_2d)
|
||||
|
||||
# Intersect the point with the plane.
|
||||
p3_on_plane = mathutils.geometry.intersect_line_plane(
|
||||
p3_origin, # First point of line.
|
||||
p3_origin + p3_direction, # Second point of line.
|
||||
location, # `plane_co` (a point on the plane).
|
||||
normal) # `plane_no` (the direction the plane is facing).
|
||||
|
||||
return p3_on_plane
|
||||
@@ -1,24 +0,0 @@
|
||||
import bpy
|
||||
import os
|
||||
import bpy.utils.previews
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
svg_icons = {}
|
||||
icons = bpy.utils.previews.new()
|
||||
dir = os.path.join(os.path.dirname(__file__))
|
||||
|
||||
icons.load("MEASURE", os.path.join(dir, "measure.svg"), 'IMAGE')
|
||||
icons.load("CPU", os.path.join(dir, "cpu.svg"), 'IMAGE')
|
||||
svg_icons["main"] = icons
|
||||
|
||||
|
||||
def register():
|
||||
...
|
||||
|
||||
def unregister():
|
||||
# ICONS
|
||||
for pcoll in svg_icons.values():
|
||||
bpy.utils.previews.remove(pcoll)
|
||||
svg_icons.clear()
|
||||
@@ -4,31 +4,34 @@ import bpy
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def bool_tool_manual_map():
|
||||
url_manual_prefix = "https://github.com/nickberckley/bool_tool/wiki/"
|
||||
url_manual_prefix = "https://nickberckley.github.io/bool_tool/"
|
||||
|
||||
# Carver
|
||||
url_manual_mapping = (("bpy.ops.object.carve", "Carver"),
|
||||
# Brush Boolean
|
||||
("bpy.ops.object.boolean_brush_union", "Boolean-Operators"),
|
||||
("bpy.ops.object.boolean_brush_intersect", "Boolean-Operators"),
|
||||
("bpy.ops.object.boolean_brush_difference", "Boolean-Operators"),
|
||||
("bpy.ops.object.boolean_brush_slice", "Boolean-Operators"),
|
||||
url_manual_mapping = (# Brush Boolean
|
||||
("bpy.ops.object.boolean_brush_union", "booleans/brush_booleans.html"),
|
||||
("bpy.ops.object.boolean_brush_intersect", "booleans/brush_booleans.html"),
|
||||
("bpy.ops.object.boolean_brush_difference", "booleans/brush_booleans.html"),
|
||||
("bpy.ops.object.boolean_brush_slice", "booleans/brush_booleans.html"),
|
||||
# Auto Boolean
|
||||
("bpy.ops.object.boolean_auto_union", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_intersect", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_difference", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_slice", "Boolean-Operators#auto-boolean-operators"),
|
||||
("bpy.ops.object.boolean_auto_union", "booleans/auto_booleans.html"),
|
||||
("bpy.ops.object.boolean_auto_intersect", "booleans/auto_booleans.html"),
|
||||
("bpy.ops.object.boolean_auto_difference", "booleans/auto_booleans.html"),
|
||||
("bpy.ops.object.boolean_auto_slice", "booleans/auto_booleans.html"),
|
||||
# Carver
|
||||
("bpy.ops.object.carve_box", "carver/index.html"),
|
||||
("bpy.ops.object.carve_circle", "carver/index.html"),
|
||||
("bpy.ops.object.carve_polyline", "carver/index.html"),
|
||||
# Cutter Utilities
|
||||
("bpy.ops.object.boolean_toggle_cutter", "Utility-Operators#toggle-cutter"),
|
||||
("bpy.ops.object.boolean_remove_cutter", "Utility-Operators#remove-cutter"),
|
||||
("bpy.ops.object.boolean_apply_cutter", "Utility-Operators#apply-cutter"),
|
||||
("bpy.ops.object.boolean_toggle_cutter", "utilities/toggle.html"),
|
||||
("bpy.ops.object.boolean_remove_cutter", "utilities/remove.html"),
|
||||
("bpy.ops.object.boolean_apply_cutter", "utilities/apply.html"),
|
||||
# Canvas Utilities
|
||||
("bpy.ops.object.boolean_toggle_all", "Utility-Operators#toggle-all-cutters"),
|
||||
("bpy.ops.object.boolean_remove_all", "Utility-Operators#remove-all-cutters"),
|
||||
("bpy.ops.object.boolean_apply_all", "Utility-Operators#apply-all-cutters"),
|
||||
("bpy.ops.object.boolean_toggle_all", "utilities/toggle.html"),
|
||||
("bpy.ops.object.boolean_remove_all", "utilities/remove.html"),
|
||||
("bpy.ops.object.boolean_apply_all", "utilities/apply.html"),
|
||||
# Select
|
||||
("bpy.ops.object.select_cutter_canvas", "Utility-Operators#select-operators"),
|
||||
("bpy.ops.object.boolean_select_all", "Utility-Operators#select-operators"),
|
||||
("bpy.ops.object.select_cutter_canvas", "utilities/select.html"),
|
||||
("bpy.ops.object.boolean_select_all", "utilities/select.html"),
|
||||
)
|
||||
|
||||
return url_manual_prefix, url_manual_mapping
|
||||
|
||||
@@ -18,12 +18,12 @@ else:
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = [
|
||||
modules = (
|
||||
boolean,
|
||||
canvas,
|
||||
cutter,
|
||||
select,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
|
||||
@@ -2,12 +2,13 @@ import bpy
|
||||
from collections import defaultdict
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_linked,
|
||||
is_instanced_data,
|
||||
list_candidate_objects,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
filter_canvases,
|
||||
create_slice,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
filter_cutters,
|
||||
make_cutter,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
add_boolean_modifier,
|
||||
@@ -15,23 +16,36 @@ from ..functions.modifier import (
|
||||
get_modifiers_to_apply,
|
||||
)
|
||||
from ..functions.object import (
|
||||
set_cutter_properties,
|
||||
change_parent,
|
||||
create_slice,
|
||||
delete_cutter,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
convert_to_mesh_confirmation,
|
||||
destructive_op_confirmation,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PROPERTIES ------------------------------ ####
|
||||
|
||||
class ModifierProperties():
|
||||
class BooleanBase():
|
||||
# Add-on properties.
|
||||
flip: bpy.props.BoolProperty(
|
||||
name = "Flip Canvas & Cutters",
|
||||
options = {'SKIP_SAVE'},
|
||||
default = False,
|
||||
)
|
||||
|
||||
# Boolean modifier properties.
|
||||
material_mode: bpy.props.EnumProperty(
|
||||
name = "Materials",
|
||||
description = "Method for setting materials on the new faces",
|
||||
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"
|
||||
"modifier object, the face will use the same material slot or the first if the object doesn't have enough slots.")),
|
||||
('TRANSFER', "Transfer", ("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
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"
|
||||
"modifier object, the face will use the same material slot or the first if the object doesn't have enough slots.")),
|
||||
('TRANSFER', "Transfer",
|
||||
("Transfer materials from non-empty slots to the result mesh, adding new materials as necessary.\n"
|
||||
"For empty slots, fall back to using the same material index as the operand mesh."))),
|
||||
default = 'INDEX',
|
||||
)
|
||||
use_self: bpy.props.BoolProperty(
|
||||
@@ -52,12 +66,24 @@ class ModifierProperties():
|
||||
default = 0.000001,
|
||||
)
|
||||
|
||||
# Built-in Methods.
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
|
||||
col = layout.column()
|
||||
col.prop(self, "flip")
|
||||
if self._unflippable:
|
||||
col.enabled = False
|
||||
|
||||
layout.separator()
|
||||
if prefs.solver == 'EXACT':
|
||||
layout.prop(self, "material_mode")
|
||||
layout.prop(self, "use_self")
|
||||
@@ -66,53 +92,86 @@ class ModifierProperties():
|
||||
layout.prop(self, "double_threshold")
|
||||
|
||||
|
||||
# Custom Methods.
|
||||
def _filter_objects(self, context) -> tuple[list, list]:
|
||||
"""Returns lists of cutters & canvases."""
|
||||
|
||||
canvases = [context.active_object]
|
||||
cutters = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
|
||||
# Flip.
|
||||
if self.flip:
|
||||
canvases, cutters = cutters, canvases
|
||||
|
||||
# Filter Canvases.
|
||||
canvases = filter_canvases(self, context, canvases)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return None, None
|
||||
|
||||
# Filter Cutters.
|
||||
cutters = filter_cutters(self, context, cutters, canvases)
|
||||
if len(cutters) == 0:
|
||||
self.report({'WARNING'}, "No valid cutters selected")
|
||||
return None, None
|
||||
|
||||
return canvases, cutters
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /brush_boolean/ ------------------------------ ####
|
||||
|
||||
class BrushBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
class BrushBoolean(BooleanBase):
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Abort if there are less than 2 selected objects.
|
||||
# Abort if there are less than 2 objects selected.
|
||||
if len(context.selected_objects) < 2:
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'WARNING'}, "Boolean operators cannot be performed on linked objects")
|
||||
return {'CANCELLED'}
|
||||
if not self.flip:
|
||||
cutters = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
else:
|
||||
cutters = [context.active_object]
|
||||
|
||||
return self.execute(context)
|
||||
self._unflippable = False
|
||||
return convert_to_mesh_confirmation(self, context, event, cutters, "Brush Boolean")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
|
||||
if len(cutters) == 0:
|
||||
# Create lists of cutters & canvases.
|
||||
canvases, cutters = self._filter_objects(context)
|
||||
if canvases is None or cutters is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
slice = create_slice(context, canvas, modifier=True)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
"""
|
||||
NOTE: Slices need to be created in a separate loop to avoid
|
||||
inheriting Boolean modifiers that the operator adds.
|
||||
"""
|
||||
for canvas in canvases:
|
||||
slice = create_slice(context, canvas, modifier=True)
|
||||
add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
|
||||
for cutter in cutters:
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
display = 'WIRE' if prefs.wireframe else 'BOUNDS'
|
||||
set_cutter_properties(context, cutter, self.mode, display=display, collection=prefs.use_collection)
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
if prefs.parent:
|
||||
change_parent(cutter, canvas)
|
||||
make_cutter(context, cutter, self.mode,
|
||||
display=prefs.display,
|
||||
collection=prefs.use_collection)
|
||||
|
||||
canvas.booleans.canvas = True
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
for canvas in canvases:
|
||||
add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
|
||||
if prefs.parent:
|
||||
change_parent(context, cutter, canvases[0], inverse=True)
|
||||
|
||||
# Set Boolean property to canvases.
|
||||
for canvas in canvases:
|
||||
canvas.booleans.canvas = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -120,29 +179,47 @@ class BrushBoolean(ModifierProperties):
|
||||
class OBJECT_OT_boolean_brush_union(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_union"
|
||||
bl_label = "Boolean Union (Brush)"
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Merge selected objects into the active one"
|
||||
else:
|
||||
return "Merge the active object into selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_intersect(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_intersect"
|
||||
bl_label = "Boolean Intersection (Brush)"
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Only keep parts of the active object that are interesecting selected objects"
|
||||
else:
|
||||
return "Only keep parts of selected objects that are interesecting the active one"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_difference(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_difference"
|
||||
bl_label = "Boolean Difference (Brush)"
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Subtract selected objects from the active one"
|
||||
else:
|
||||
return "Subtract the active object from selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
bl_idname = "object.boolean_brush_slice"
|
||||
@@ -152,15 +229,18 @@ class OBJECT_OT_boolean_brush_slice(bpy.types.Operator, BrushBoolean):
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Slice the active object by selected ones. Will create slices as separate objects"
|
||||
else:
|
||||
return "Slice selected objects by the active one. Will create slices as separate objects"
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /auto_boolean/ ------------------------------ ####
|
||||
|
||||
class AutoBoolean(ModifierProperties):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context)
|
||||
|
||||
class AutoBoolean(BooleanBase):
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Abort if there are less than 2 selected objects.
|
||||
@@ -168,55 +248,66 @@ class AutoBoolean(ModifierProperties):
|
||||
self.report({'WARNING'}, "Boolean operator needs at least two selected objects")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Abort if active object is linked.
|
||||
if is_linked(context, context.active_object):
|
||||
self.report({'ERROR'}, "Modifiers cannot be applied to linked object")
|
||||
return {'CANCELLED'}
|
||||
if not self.flip:
|
||||
canvases = [context.active_object]
|
||||
else:
|
||||
canvases = [obj for obj in context.selected_objects if obj != context.active_object]
|
||||
|
||||
return destructive_op_confirmation(self, context, event, [context.active_object], title="Auto Boolean")
|
||||
for canvas in canvases:
|
||||
if canvas.type != 'MESH':
|
||||
canvases.remove(canvas)
|
||||
|
||||
self._unflippable = False
|
||||
return destructive_op_confirmation(self, context, event, canvases, "Auto Boolean")
|
||||
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
canvas = context.active_object
|
||||
cutters = list_candidate_objects(self, context, context.active_object)
|
||||
new_modifiers = defaultdict(list)
|
||||
|
||||
if len(cutters) == 0:
|
||||
# Create lists of cutters & canvases.
|
||||
canvases, cutters = self._filter_objects(context)
|
||||
if canvases is None or cutters is None:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Create slices.
|
||||
if self.mode == "SLICE":
|
||||
for cutter in cutters:
|
||||
"""NOTE: Slices need to be created in a separate loop to avoid inheriting boolean modifiers that the operator adds."""
|
||||
slice = create_slice(context, canvas)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT", prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[slice].append(modifier)
|
||||
slice.select_set(True)
|
||||
"""
|
||||
NOTE: Slices need to be created in a separate loop to avoid
|
||||
inheriting Boolean modifiers that the operator adds.
|
||||
"""
|
||||
for canvas in canvases:
|
||||
slice = create_slice(context, canvas)
|
||||
modifier = add_boolean_modifier(self, context, slice, cutter, "INTERSECT",
|
||||
prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[slice].append(modifier)
|
||||
slice.select_set(True)
|
||||
|
||||
for cutter in cutters:
|
||||
# Add boolean modifier on canvas.
|
||||
# Add Boolean modifier on canvases.
|
||||
mode = "DIFFERENCE" if self.mode == "SLICE" else self.mode
|
||||
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[canvas].append(modifier)
|
||||
for canvas in canvases:
|
||||
modifier = add_boolean_modifier(self, context, canvas, cutter, mode, prefs.solver, pin=prefs.pin)
|
||||
new_modifiers[canvas].append(modifier)
|
||||
|
||||
# Transfer cutters children to canvas.
|
||||
# Transfer cutters children to a canvas.
|
||||
for child in cutter.children:
|
||||
change_parent(child, canvas)
|
||||
change_parent(context, child, canvases[0])
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply modifiers on canvas & slices.
|
||||
# Apply modifiers on canvases & slices.
|
||||
for obj, modifiers in new_modifiers.items():
|
||||
modifiers = get_modifiers_to_apply(context, obj, modifiers)
|
||||
apply_modifiers(context, obj, modifiers)
|
||||
|
||||
# Delete cutters.
|
||||
for cutter in cutters:
|
||||
delete_cutter(cutter)
|
||||
delete_object(cutter)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -224,29 +315,47 @@ class AutoBoolean(ModifierProperties):
|
||||
class OBJECT_OT_boolean_auto_union(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_union"
|
||||
bl_label = "Boolean Union (Auto)"
|
||||
bl_description = "Merge selected objects into active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "UNION"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Merge selected objects into the active one"
|
||||
else:
|
||||
return "Merge the active object into selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_difference(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_difference"
|
||||
bl_label = "Boolean Difference (Auto)"
|
||||
bl_description = "Subtract selected objects from active one"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "DIFFERENCE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Subtract selected objects from the active one"
|
||||
else:
|
||||
return "Subtract the active object from selected ones"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_intersect(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_intersect"
|
||||
bl_label = "Boolean Intersect (Auto)"
|
||||
bl_description = "Only keep the parts of the active object that are interesecting selected objects"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
mode = "INTERSECT"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Only keep parts of the active object that are interesecting selected objects"
|
||||
else:
|
||||
return "Only keep parts of selected objects that are interesecting the active one"
|
||||
|
||||
|
||||
class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
|
||||
bl_idname = "object.boolean_auto_slice"
|
||||
@@ -256,13 +365,20 @@ class OBJECT_OT_boolean_auto_slice(bpy.types.Operator, AutoBoolean):
|
||||
|
||||
mode = "SLICE"
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if not properties.flip:
|
||||
return "Slice the active object by selected ones. Will create slices as separate objects"
|
||||
else:
|
||||
return "Slice selected objects by the active one. Will create slices as separate objects"
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_brush_union,
|
||||
OBJECT_OT_boolean_brush_difference,
|
||||
OBJECT_OT_boolean_brush_intersect,
|
||||
@@ -272,7 +388,7 @@ classes = [
|
||||
OBJECT_OT_boolean_auto_difference,
|
||||
OBJECT_OT_boolean_auto_intersect,
|
||||
OBJECT_OT_boolean_auto_slice,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -2,29 +2,30 @@ import bpy
|
||||
import itertools
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_canvas,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
list_selected_canvases,
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_cutter_users,
|
||||
handle_unused_cutters,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
get_modifiers_to_apply,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from ..functions.object import (
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_canvases,
|
||||
list_canvas_slices,
|
||||
list_canvas_cutters,
|
||||
list_cutter_users,
|
||||
list_selected_canvases,
|
||||
list_unused_cutters,
|
||||
list_pre_boolean_modifiers,
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
destructive_op_confirmation,
|
||||
_guess_toggle_state,
|
||||
)
|
||||
from ..functions.scene import (
|
||||
delete_empty_collection,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,41 +35,50 @@ from ..functions.list import (
|
||||
class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_toggle_all"
|
||||
bl_label = "Toggle Boolean Cutters"
|
||||
bl_description = "Toggle all boolean cutters affecting selected canvases"
|
||||
bl_description = "Toggle all Boolean cutters affecting selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
cutters, modifiers = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(canvases)
|
||||
modifiers = list(itertools.chain.from_iterable(modifiers.values()))
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
state = _guess_toggle_state(modifiers)
|
||||
state = True if state == "On" else False
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in modifiers:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Slices
|
||||
for slice in slices:
|
||||
slice.hide_viewport = not slice.hide_viewport
|
||||
slice.hide_render = not slice.hide_render
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Unused Cutters
|
||||
other_canvases = list_canvases()
|
||||
for obj in other_canvases:
|
||||
if obj not in canvases + slices:
|
||||
if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
|
||||
for cutter in cutters:
|
||||
cutter.hide_viewport = not cutter.hide_viewport
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases + slices).keys()
|
||||
if len(other_canvases) == 0:
|
||||
cutter.hide_viewport = state
|
||||
cutter.hide_set(state)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -77,72 +87,49 @@ class OBJECT_OT_boolean_toggle_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_remove_all"
|
||||
bl_label = "Remove Boolean Cutters"
|
||||
bl_description = "Remove all boolean cutters from selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
bl_description = "Remove all Boolean cutters from selected canvases"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
delete_cutters: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutters",
|
||||
description = "Completely remove cutters if they're not used by any other remaining canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(canvases)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
cutters, modifiers = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
# Remove Slices
|
||||
for slice in slices:
|
||||
if slice in canvases:
|
||||
canvases.remove(slice)
|
||||
delete_cutter(slice)
|
||||
delete_object(slice)
|
||||
|
||||
for canvas in canvases:
|
||||
# Remove Modifiers
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "boolean_" in mod.name:
|
||||
if mod.object in cutters:
|
||||
canvas.modifiers.remove(mod)
|
||||
# Remove Modifiers
|
||||
for canvas, mods in modifiers.items():
|
||||
for mod in mods:
|
||||
canvas.modifiers.remove(mod)
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# remove_boolean_properties
|
||||
if canvas.booleans.canvas == True:
|
||||
canvas.booleans.canvas = False
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, list(cutters), canvases + slices,
|
||||
delete=self.delete_cutters)
|
||||
|
||||
|
||||
# Restore Orphaned Cutters
|
||||
unused_cutters, leftovers = list_unused_cutters(cutters, canvases, slices, do_leftovers=True)
|
||||
|
||||
for cutter in unused_cutters:
|
||||
if cutter.booleans.carver:
|
||||
delete_cutter(cutter)
|
||||
else:
|
||||
# restore_visibility
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
|
||||
# remove_parent_&_collection
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
change_parent(cutter, None)
|
||||
|
||||
if prefs.use_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -151,74 +138,56 @@ class OBJECT_OT_boolean_remove_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_apply_all"
|
||||
bl_label = "Apply All Boolean Cutters"
|
||||
bl_description = "Apply all boolean cutters on selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
bl_description = "Apply all Boolean cutters to selected canvases"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
delete_cutters: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutters",
|
||||
description = "Completely remove cutters if they're not used by any other remaining canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True) and is_canvas(context.active_object)
|
||||
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.canvases = list_selected_canvases(context)
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutters")
|
||||
# Filter canvases.
|
||||
canvases = list_selected_canvases(context)
|
||||
if len(canvases) == 0:
|
||||
self.report({'WARNING'}, "No valid canvases selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
return destructive_op_confirmation(self, context, event, canvases, title="Apply Boolean Cutters")
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
cutters, __ = list_canvas_cutters(self.canvases)
|
||||
slices = list_canvas_slices(self.canvases)
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in cutters:
|
||||
for cutter in list(cutters):
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
for canvas in itertools.chain(self.canvases, slices):
|
||||
context.view_layer.objects.active = canvas
|
||||
|
||||
for canvas in itertools.chain(canvases, slices):
|
||||
# Apply Modifiers
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = [mod for mod in canvas.modifiers]
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(canvas)
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
modifiers = [mod for mod in canvas.modifiers if mod.type == 'BOOLEAN' and "boolean_" in mod.name]
|
||||
|
||||
modifiers = get_modifiers_to_apply(context, canvas)
|
||||
apply_modifiers(context, canvas, modifiers)
|
||||
|
||||
# remove_boolean_properties
|
||||
# Remove Boolean Properties
|
||||
canvas.booleans.canvas = False
|
||||
canvas.booleans.slice = False
|
||||
canvas.booleans.slice_of = None
|
||||
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, list(cutters), canvases, delete=self.delete_cutters)
|
||||
|
||||
# Purge Orphaned Cutters
|
||||
unused_cutters, leftovers = list_unused_cutters(cutters, self.canvases, slices, do_leftovers=True)
|
||||
|
||||
purged_cutters = []
|
||||
for cutter in unused_cutters:
|
||||
if cutter not in purged_cutters:
|
||||
# Transfer Children
|
||||
for child in cutter.children:
|
||||
change_parent(child, cutter.parent)
|
||||
|
||||
# Purge
|
||||
delete_cutter(cutter)
|
||||
purged_cutters.append(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in self.canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -228,11 +197,11 @@ class OBJECT_OT_boolean_apply_all(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_toggle_all,
|
||||
OBJECT_OT_boolean_remove_all,
|
||||
OBJECT_OT_boolean_apply_all,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import bpy
|
||||
import itertools
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
is_instanced_data,
|
||||
destructive_op_confirmation,
|
||||
from ..functions.canvas import (
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_selected_cutters,
|
||||
list_cutter_users,
|
||||
restore_cutter,
|
||||
handle_unused_cutters,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
apply_modifiers,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
from ..functions.object import (
|
||||
object_visibility_set,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
change_parent,
|
||||
delete_object,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_canvases,
|
||||
list_selected_cutters,
|
||||
list_canvas_cutters,
|
||||
list_canvas_slices,
|
||||
list_cutter_users,
|
||||
list_unused_cutters,
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
destructive_op_confirmation,
|
||||
_guess_toggle_state,
|
||||
)
|
||||
from ..functions.scene import (
|
||||
delete_empty_collection,
|
||||
)
|
||||
from ..ui.common import (
|
||||
preserve_list_index,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,13 +39,19 @@ from ..functions.list import (
|
||||
class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_toggle_cutter"
|
||||
bl_label = "Toggle Boolean Cutter"
|
||||
bl_description = "Toggle this boolean cutter. If cutter is the active object it will be toggled for every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Toggle selected cutter and its effect")
|
||||
else:
|
||||
return ("Toggle all selected cutters and their effects")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
|
||||
items = (('ALL', "All", "Toggle all selected cutters"),
|
||||
('SPECIFIED', "Specified", "Toggle selected cutter")),
|
||||
default = 'ALL',
|
||||
)
|
||||
|
||||
@@ -45,57 +59,71 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
)
|
||||
specified_modifier: bpy.props.StringProperty(
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
slices = list_canvas_slices(canvases)
|
||||
cutters: list = [context.scene.objects[self.specified_cutter]]
|
||||
canvases: list = [context.scene.objects[self.specified_canvas]]
|
||||
modifiers: list = [canvases[0].modifiers.get(self.specified_modifier)]
|
||||
slices: list = list_canvas_slices(context, canvases)
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
cutters: list = list_selected_cutters(context)
|
||||
canvases: dict = list_cutter_users(cutters)
|
||||
modifiers: list = list(itertools.chain.from_iterable(canvases.values()))
|
||||
|
||||
if cutters:
|
||||
for canvas in canvases:
|
||||
# toggle_slices_visibility (for_all_canvases)
|
||||
if canvas.booleans.slice == True:
|
||||
if any(modifier.object in cutters for modifier in canvas.modifiers):
|
||||
canvas.hide_viewport = not canvas.hide_viewport
|
||||
canvas.hide_render = not canvas.hide_render
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in canvas.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# toggle_slices_visibility (for_specified_canvas)
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
slice.hide_viewport = not slice.hide_viewport
|
||||
slice.hide_render = not slice.hide_render
|
||||
mod.show_viewport = not mod.show_viewport
|
||||
mod.show_render = not mod.show_render
|
||||
|
||||
# hide_cutter_if_not_used_by_any_visible_modifiers
|
||||
other_canvases = list_canvases()
|
||||
for obj in other_canvases:
|
||||
if obj not in canvases + slices:
|
||||
if any(mod.object in cutters and mod.show_viewport for mod in obj.modifiers if mod.type == 'BOOLEAN'):
|
||||
cutters[:] = [cutter for cutter in cutters if cutter not in [mod.object for mod in obj.modifiers]]
|
||||
|
||||
for cutter in cutters:
|
||||
cutter.hide_viewport = not cutter.hide_viewport
|
||||
|
||||
else:
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
state = _guess_toggle_state(modifiers)
|
||||
state = True if state == "On" else False
|
||||
|
||||
# Toggle Modifiers
|
||||
for mod in modifiers:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
# Hide Slices
|
||||
if self.method == 'ALL':
|
||||
for canvas in canvases.keys():
|
||||
if not canvas.booleans.slice:
|
||||
continue
|
||||
|
||||
slice = canvas
|
||||
if any(modifier.object in cutters for modifier in slice.modifiers):
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
|
||||
elif self.method == 'SPECIFIED':
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
mod.show_viewport = not state
|
||||
mod.show_render = not state
|
||||
|
||||
slice.hide_viewport = state
|
||||
slice.hide_render = state
|
||||
slice.hide_set(state)
|
||||
|
||||
# Hide Unused Cutters
|
||||
for cutter in cutters:
|
||||
other_canvases = list_cutter_users([cutter], exclude=canvases + slices).keys()
|
||||
if len(other_canvases) == 0:
|
||||
cutter.hide_viewport = state
|
||||
cutter.hide_set(state)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -104,9 +132,16 @@ class OBJECT_OT_boolean_toggle_cutter(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_remove_cutter"
|
||||
bl_label = "Remove Boolean Cutter"
|
||||
bl_description = "Remove this boolean cutter. If cutter is the active object it will be removed from every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Remove this cutter and the Boolean modifier that uses it.\n"
|
||||
"If the cutter is not used by any other canvas it will be deleted from the scene")
|
||||
else:
|
||||
return ("Remove selected Boolean cutters from all canvases that uses them; restore their visibility")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
@@ -118,86 +153,70 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
)
|
||||
specified_modifier: bpy.props.StringProperty(
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def execute(self, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
leftovers = []
|
||||
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
slices = list_canvas_slices(canvases)
|
||||
cutters: list = [context.scene.objects[self.specified_cutter]]
|
||||
canvases: dict = {
|
||||
context.scene.objects[self.specified_canvas]:
|
||||
[context.scene.objects[self.specified_canvas].modifiers.get(self.specified_modifier)]
|
||||
}
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
cutters: list = list_selected_cutters(context)
|
||||
canvases: dict = list_cutter_users(cutters)
|
||||
|
||||
if cutters:
|
||||
# Remove Modifiers
|
||||
for canvas in canvases:
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if mod.object in cutters:
|
||||
canvas.modifiers.remove(mod)
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# Remove Slices (for_all_method)
|
||||
if canvas.booleans.slice == True:
|
||||
delete_cutter(canvas)
|
||||
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# Remove Slices (for_specified_method)
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in cutters:
|
||||
delete_cutter(slice)
|
||||
|
||||
cutters, leftovers = list_unused_cutters(cutters, canvases, do_leftovers=True)
|
||||
|
||||
|
||||
# Restore Orphaned Cutters
|
||||
for cutter in cutters:
|
||||
if self.method == 'SPECIFIED' and cutter.booleans.carver:
|
||||
delete_cutter(cutter)
|
||||
else:
|
||||
# restore_visibility
|
||||
cutter.hide_render = False
|
||||
cutter.display_type = 'TEXTURED'
|
||||
cutter.lineart.usage = 'INHERIT'
|
||||
object_visibility_set(cutter, value=True)
|
||||
cutter.booleans.cutter = ""
|
||||
|
||||
# remove_parent_&_collection
|
||||
if prefs.parent and cutter.parent in canvases:
|
||||
change_parent(cutter, None)
|
||||
|
||||
if prefs.use_collection:
|
||||
cutters_collection = bpy.data.collections.get(prefs.collection_name)
|
||||
if cutters_collection in cutter.users_collection:
|
||||
bpy.data.collections.get(prefs.collection_name).objects.unlink(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent and leftovers != None:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
|
||||
else:
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Remove Slices
|
||||
slices = list_canvas_slices(context, canvases.keys())
|
||||
for slice in slices:
|
||||
for mod in slice.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
if slice in canvases:
|
||||
del canvases[slice]
|
||||
delete_object(slice)
|
||||
continue
|
||||
|
||||
# Remove Modifiers
|
||||
for canvas, modifiers in canvases.items():
|
||||
for mod in modifiers:
|
||||
with preserve_list_index(canvas.booleans, "modifiers_list_index"):
|
||||
canvas.modifiers.remove(mod)
|
||||
|
||||
# Unset canvas property if it's no longer needed.
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
|
||||
# Handle Unused Cutters
|
||||
if self.method == 'SPECIFIED':
|
||||
handle_unused_cutters(context, cutters, list(canvases.keys()), delete=True)
|
||||
|
||||
elif self.method == 'ALL':
|
||||
for cutter in cutters:
|
||||
# Restore unused cutters, even if they don't have other users.
|
||||
restore_cutter(context, cutter,
|
||||
unparent=prefs.parent and cutter.parent in canvases,
|
||||
unlink_collection=prefs.use_collection)
|
||||
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -206,104 +225,100 @@ class OBJECT_OT_boolean_remove_cutter(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_apply_cutter"
|
||||
bl_label = "Apply Boolean Cutter"
|
||||
bl_description = "Apply this boolean cutter. If cutter is the active object it will be applied to every canvas that uses it"
|
||||
bl_options = {'UNDO'}
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
if properties.specified_cutter:
|
||||
return ("Apply the modifier using this Boolean cutter")
|
||||
else:
|
||||
return ("Apply this Boolean cutter to every canvas object that uses it")
|
||||
|
||||
method: bpy.props.EnumProperty(
|
||||
name = "Method",
|
||||
items = (('ALL', "All", "Remove cutter from all canvases that use it"),
|
||||
('SPECIFIED', "Specified", "Remove cutter from specified canvas")),
|
||||
items = (('ALL', "All", "Apply all selected cutter to all canvases that use them"),
|
||||
('SPECIFIED', "Specified", "Apply selected cutter to a specified canvas")),
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
default = 'ALL',
|
||||
)
|
||||
|
||||
specified_cutter: bpy.props.StringProperty(
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
specified_canvas: bpy.props.StringProperty(
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
)
|
||||
|
||||
delete: bpy.props.BoolProperty(
|
||||
name = "Delete Unused Cutter",
|
||||
description = "Completely remove the cutter object if it's not used by any other canvas",
|
||||
default = True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context, check_linked=True)
|
||||
|
||||
return basic_poll(cls, context, check_active=False)
|
||||
|
||||
def invoke(self, context, event):
|
||||
# Filter Objects
|
||||
if self.method == 'SPECIFIED':
|
||||
self.cutters = [context.scene.objects[self.specified_cutter]]
|
||||
self.canvases = [context.scene.objects[self.specified_canvas]]
|
||||
self.slices = list_canvas_slices(self.canvases)
|
||||
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
elif self.method == 'ALL':
|
||||
self.cutters = list_selected_cutters(context)
|
||||
self.canvases = list_cutter_users(self.cutters)
|
||||
|
||||
return destructive_op_confirmation(self, context, event, self.canvases, title="Apply Boolean Cutter")
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
|
||||
return destructive_op_confirmation(self, context, event, canvases, title="Apply Boolean Cutter")
|
||||
|
||||
def execute(self, context):
|
||||
prefs = bpy.context.preferences.addons[base_package].preferences
|
||||
leftovers = []
|
||||
|
||||
if self.cutters:
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in self.cutters:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
# Create lists of cutters & canvases.
|
||||
if self.method == 'SPECIFIED':
|
||||
cutters = [context.scene.objects[self.specified_cutter]]
|
||||
canvases = [context.scene.objects[self.specified_canvas]]
|
||||
slices = list_canvas_slices(context, canvases)
|
||||
elif self.method == 'ALL':
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
slices = []
|
||||
|
||||
# Apply Modifiers
|
||||
for canvas in self.canvases:
|
||||
context.view_layer.objects.active = canvas
|
||||
if len(cutters) == 0:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
if "boolean_" in mod.name:
|
||||
if mod.object in self.cutters:
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, canvas, boolean_mods)
|
||||
if not canvases:
|
||||
return {'CANCELLED'}
|
||||
|
||||
# remove_canvas_property_if_needed
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
# are also selected after applying the modifier.
|
||||
for cutter in cutters:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
# Apply Modifiers
|
||||
for canvas in itertools.chain(canvases, slices):
|
||||
boolean_mods = []
|
||||
for mod in canvas.modifiers:
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
if mod.object in cutters:
|
||||
boolean_mods.append(mod)
|
||||
|
||||
if boolean_mods:
|
||||
with preserve_list_index(canvas.booleans, "modifiers_list_index"):
|
||||
apply_modifiers(context, canvas, boolean_mods)
|
||||
|
||||
# Unset canvas property if it's no longer needed.
|
||||
other_cutters, __ = list_canvas_cutters([canvas])
|
||||
if len(other_cutters) == 0:
|
||||
canvas.booleans.canvas = False
|
||||
canvas.booleans.slice = False
|
||||
canvas.booleans.slice_of = None
|
||||
|
||||
# Handle Unused Cutters
|
||||
handle_unused_cutters(context, cutters, canvases, delete=self.delete)
|
||||
|
||||
if self.method == 'SPECIFIED':
|
||||
# Apply Modifier for Slices (for_specified_method)
|
||||
for slice in self.slices:
|
||||
boolean_mods = []
|
||||
for mod in slice.modifiers:
|
||||
if mod.type == 'BOOLEAN' and mod.object in self.cutters:
|
||||
boolean_mods.append(mod)
|
||||
apply_modifiers(context, slice, boolean_mods)
|
||||
|
||||
|
||||
unused_cutters, leftovers = list_unused_cutters(self.cutters, self.canvases, do_leftovers=True)
|
||||
|
||||
|
||||
for cutter in unused_cutters:
|
||||
# Transfer Children
|
||||
for child in cutter.children:
|
||||
change_parent(child, cutter.parent)
|
||||
|
||||
# Purge Orphaned Cutters
|
||||
delete_cutter(cutter)
|
||||
|
||||
# purge_empty_collection
|
||||
if prefs.use_collection:
|
||||
delete_empty_collection()
|
||||
|
||||
|
||||
# Change Leftover Cutter Parent
|
||||
if prefs.parent and leftovers != None:
|
||||
for cutter in leftovers:
|
||||
if cutter.parent in self.canvases:
|
||||
other_canvases = list_cutter_users([cutter])
|
||||
change_parent(cutter, other_canvases[0])
|
||||
|
||||
else:
|
||||
self.report({'INFO'}, "Boolean cutters are not selected")
|
||||
# Purge Empty Collection
|
||||
delete_empty_collection(context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -313,11 +328,11 @@ class OBJECT_OT_boolean_apply_cutter(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_boolean_toggle_cutter,
|
||||
OBJECT_OT_boolean_remove_cutter,
|
||||
OBJECT_OT_boolean_apply_cutter,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
active_modifier_poll,
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
from ..functions.list import (
|
||||
list_selected_cutters,
|
||||
list_selected_canvases,
|
||||
list_canvas_cutters,
|
||||
)
|
||||
from ..functions.cutter import (
|
||||
list_selected_cutters,
|
||||
list_cutter_users,
|
||||
)
|
||||
from ..functions.poll import (
|
||||
basic_poll,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
@@ -20,21 +20,24 @@ from ..functions.list import (
|
||||
class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
|
||||
bl_idname = "object.select_cutter_canvas"
|
||||
bl_label = "Select Boolean Canvas"
|
||||
bl_description = "Select all the objects that use selected objects as boolean cutters"
|
||||
bl_description = "Select all canvases that use selected objects as Boolean cutters"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context) and context.active_object.booleans.cutter
|
||||
return basic_poll(cls, context, check_active=True) and context.active_object.booleans.cutter
|
||||
|
||||
def execute(self, context):
|
||||
cutters = list_selected_cutters(context)
|
||||
canvases = list_cutter_users(cutters)
|
||||
canvases = list_cutter_users(cutters).keys()
|
||||
|
||||
# Select Canvases
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
# Select canvases.
|
||||
for canvas in canvases:
|
||||
canvas.select_set(True)
|
||||
if not canvas.booleans.slice:
|
||||
canvas.select_set(True)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -43,19 +46,21 @@ class OBJECT_OT_select_cutter_canvas(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_select_all(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_select_all"
|
||||
bl_label = "Select Boolean Cutters"
|
||||
bl_description = "Select all boolean cutters affecting active object"
|
||||
bl_description = "Select all Boolean cutters affecting selected canvases"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return basic_poll(cls, context) and is_canvas(context.active_object)
|
||||
return basic_poll(cls, context, check_active=True) and is_canvas(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
canvases = list_selected_canvases(context)
|
||||
cutters, __ = list_canvas_cutters(canvases)
|
||||
|
||||
# select_cutters
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
# Select cutters.
|
||||
for cutter in cutters:
|
||||
cutter.select_set(True)
|
||||
|
||||
@@ -66,24 +71,24 @@ class OBJECT_OT_boolean_select_all(bpy.types.Operator):
|
||||
class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
|
||||
bl_idname = "object.boolean_select_cutter"
|
||||
bl_label = "Select Boolean Cutter"
|
||||
bl_description = "Select object that is used as boolean cutter by this modifier"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
return (basic_poll(cls, context) and active_modifier_poll(context.active_object) and
|
||||
context.area.type == 'PROPERTIES' and context.space_data.context == 'MODIFIER' and
|
||||
prefs.double_click)
|
||||
cutter: bpy.props.StringProperty()
|
||||
extend: bpy.props.BoolProperty()
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.extend = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
modifier = context.object.modifiers.active
|
||||
if modifier and modifier.type == "BOOLEAN":
|
||||
cutter = modifier.object
|
||||
cutter = bpy.data.objects.get(self.cutter)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
if not self.extend:
|
||||
for obj in context.scene.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
if cutter:
|
||||
cutter.select_set(True)
|
||||
context.view_layer.objects.active = cutter
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -93,31 +98,16 @@ class OBJECT_OT_boolean_select_cutter(bpy.types.Operator):
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_select_cutter_canvas,
|
||||
OBJECT_OT_boolean_select_all,
|
||||
OBJECT_OT_boolean_select_cutter,
|
||||
]
|
||||
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# KEYMAP
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Property Editor", space_type='PROPERTIES')
|
||||
|
||||
kmi = km.keymap_items.new("object.boolean_select_cutter", type='LEFTMOUSE', value='DOUBLE_CLICK')
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# KEYMAP
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
@@ -8,9 +8,9 @@ def update_sidebar_category(self, context):
|
||||
"""Change sidebar category of add-ons panel."""
|
||||
|
||||
panel_classes = [
|
||||
ui.VIEW3D_PT_boolean,
|
||||
ui.VIEW3D_PT_boolean_properties,
|
||||
ui.VIEW3D_PT_boolean_cutters,
|
||||
ui.panels.VIEW3D_PT_boolean,
|
||||
ui.panels.VIEW3D_PT_boolean_helpers,
|
||||
ui.panels.VIEW3D_PT_boolean_cutters,
|
||||
]
|
||||
|
||||
for cls in panel_classes:
|
||||
@@ -29,14 +29,22 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
# UI
|
||||
category: bpy.props.EnumProperty(
|
||||
name = "Category",
|
||||
items = (('ADDON', "Add-on", "General add-on features"),
|
||||
('SHARED', "Shared", "Features shared by all of add-ons operators and tools"),
|
||||
('OPERATORS', "Boolean Operators", "Features for brush and auto Boolean operators")),
|
||||
default = 'OPERATORS'
|
||||
)
|
||||
|
||||
show_in_sidebar: bpy.props.BoolProperty(
|
||||
name = "Show Addon Panel in Sidebar",
|
||||
name = "Show Add-on Panel in Sidebar",
|
||||
description = "Add a sidebar panel in 3D Viewport with add-ons operators and properties",
|
||||
default = True,
|
||||
)
|
||||
sidebar_category: bpy.props.StringProperty(
|
||||
name = "Category Name",
|
||||
description = "Sidebar category name. Using the name of the existing category will add panel there",
|
||||
description = "Sidebar category name. Using the name of the existing category will add the panel there",
|
||||
default = "Edit",
|
||||
update = update_sidebar_category,
|
||||
)
|
||||
@@ -44,87 +52,67 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
# Defaults
|
||||
solver: bpy.props.EnumProperty(
|
||||
name = "Boolean Solver",
|
||||
description = "Which solver to use for automatic and brush booleans",
|
||||
description = "Which solver to use for automatic and brush Boolean operators",
|
||||
items = [('FLOAT', "Float", ""),
|
||||
('EXACT', "Exact", ""),
|
||||
('MANIFOLD', "Manifold", "")],
|
||||
default = 'FLOAT',
|
||||
)
|
||||
wireframe: bpy.props.BoolProperty(
|
||||
name = "Display Cutters as Wireframe",
|
||||
description = ("When enabled cutters will be displayed as wireframes, instead of bounding boxes.\n"
|
||||
"It's better for visualizating the shape, but might be harder to see and have performance cost"),
|
||||
default = False,
|
||||
display: bpy.props.EnumProperty(
|
||||
name = "Cutter Display",
|
||||
items = (('WIRE', "Wire", "Display the cutter object as a wireframe"),
|
||||
('BOUNDS', "Bounds", "Display only the bounds of the cutter object")),
|
||||
default = 'BOUNDS'
|
||||
)
|
||||
show_in_editmode: bpy.props.BoolProperty(
|
||||
name = "Enable 'Show in Edit Mode' by Default",
|
||||
description = "Every new boolean modifier created with brush boolean wil have 'Show in Edit Mode' enabled by default",
|
||||
description = "Added Boolean modifiers will have 'Show in Edit Mode' enabled by default",
|
||||
default = True,
|
||||
)
|
||||
|
||||
# Advanced
|
||||
use_collection: bpy.props.BoolProperty(
|
||||
name = "Put Cutters in Collection",
|
||||
description = ("Brush boolean operators will put all cutters in same collection, and create one if it doesn't exist.\n"
|
||||
"Useful for scene management, and quickly selecting and removing all clutter when needed"),
|
||||
description = ("Put all cutters in the same collection, and create one if it doesn't exist"),
|
||||
default = True,
|
||||
)
|
||||
collection_name: bpy.props.StringProperty(
|
||||
name = "Collection Name",
|
||||
description = "Name of the collection where cutters will be added",
|
||||
default = "boolean_cutters",
|
||||
)
|
||||
parent: bpy.props.BoolProperty(
|
||||
name = "Parent Cutters to Object",
|
||||
description = ("Cutters will be parented to first canvas they're applied to. Works best when one cutter is used one canvas.\n"
|
||||
"NOTE: This doesn't affect Carver tool, which has its own property for this"),
|
||||
description = ("Cutters will be parented to first canvas they're applied to"),
|
||||
default = True,
|
||||
)
|
||||
apply_order: bpy.props.EnumProperty(
|
||||
name = "When Applying Cutters...",
|
||||
description = ("What happens when boolean cutters are applied on object.\n"
|
||||
"Either when performing auto-boolean, using 'Apply All Cutters' operator.\n"
|
||||
"NOTE: This doesn't apply to Carver tool on 'Destructive' mode; or when applying individual cutters"),
|
||||
items = (('ALL', "Apply All Modifiers", "All modifiers on object will be applied (this includes shape keys as well)"),
|
||||
('BEFORE', "Apply Booleans & Everything Before", "Alongside boolean modifiers all modifiers will be applied that come before the last boolean"),
|
||||
('BOOLEANS', "Only Apply Booleans", "Only apply boolean modifiers. This method will fail if object has shape keys")),
|
||||
name = "Apply Modifiers",
|
||||
description = ("Which modifiers to apply when using add-ons destructive operators.\n"
|
||||
"NOTE: This option is not used when applying individual cutters"),
|
||||
items = (('ALL', "All",
|
||||
"All modifiers on object (and shape keys) will be applied during destructive operators"),
|
||||
('BEFORE', "Booleans & Everything Before",
|
||||
"Apply all modifiers that come before the last Boolean modifier in the stack"),
|
||||
('BOOLEANS', "Booleans",
|
||||
"Only apply Boolean modifiers")),
|
||||
default = 'ALL',
|
||||
)
|
||||
pin: bpy.props.BoolProperty(
|
||||
name = "Pin Boolean Modifiers",
|
||||
description = ("When enabled boolean modifiers will be placed above every other modifier on the object (if there are any).\n"
|
||||
"Order of modifiers can drastically affect the result (especially when performing auto boolean).\n"
|
||||
"NOTE: This doesn't affect Carver tool, which has its own property for this"),
|
||||
description = ("Always make new Boolean modifiers first in the modifier stack.\n"
|
||||
"NOTE: Order of modifiers can drastically affect the final result"),
|
||||
default = False,
|
||||
)
|
||||
|
||||
# Features
|
||||
fast_modifier_apply: bpy.props.BoolProperty(
|
||||
name = "Faster Destructive Booleans",
|
||||
description = ("Experimental method of applying modifiers that results in 30-50% faster destructive booleans.\n"
|
||||
"Performance improvements also affect the add-ons operators that apply cutters.\n"
|
||||
description = ("Experimental method of applying modifiers that results in 30-50% faster destructive Booleans.\n"
|
||||
"However, changing modifier properties in the redo panel (like material transfer)\n"
|
||||
"is not available for this method yet."),
|
||||
default = False,
|
||||
)
|
||||
double_click: bpy.props.BoolProperty(
|
||||
name = "Double-click Select",
|
||||
description = ("Select boolean cutters by dbl-clicking on the boolean modifier.\n"
|
||||
"This feature works in entire modifier properties area, not just on boolean modifier header,\n"
|
||||
"therefore can result in lot of misclicks and unintended selections."),
|
||||
default = False,
|
||||
)
|
||||
|
||||
# Debug
|
||||
versioning: bpy.props.BoolProperty(
|
||||
name = "Versioning",
|
||||
description = ("Because of the drastic changes in add-on data, it's necessary to do versioning when loading old files\n"
|
||||
"where Bool Tool cutters(brushes) are not applied. If you don't have files like that, you can ignore this")
|
||||
)
|
||||
experimental: bpy.props.BoolProperty(
|
||||
name = "Experimental",
|
||||
description = "Enable experimental features",
|
||||
default = False,
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
@@ -132,55 +120,57 @@ class BoolToolPreferences(bpy.types.AddonPreferences):
|
||||
layout.use_property_decorate = False
|
||||
|
||||
# UI
|
||||
col = layout.column(align=True, heading="Show in Sidebar")
|
||||
row = col.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
sub.prop(self, "show_in_sidebar", text="")
|
||||
sub = sub.row(align=True)
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "sidebar_category", text="")
|
||||
|
||||
# Defaults
|
||||
row = layout.row()
|
||||
row.prop(self, "category", expand=True)
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "solver", text="Solver", expand=True)
|
||||
col.prop(self, "wireframe")
|
||||
col.prop(self, "show_in_editmode")
|
||||
|
||||
# Advanced
|
||||
layout.separator()
|
||||
col = layout.column(align=True, heading="Put Cutters in Collection")
|
||||
row = col.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
sub.prop(self, "use_collection", text="")
|
||||
sub = sub.row(align=True)
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "collection_name", text="")
|
||||
# Add-on Properties
|
||||
if self.category == 'ADDON':
|
||||
col = layout.column(align=True, heading="Show in Sidebar")
|
||||
row = col.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
sub.prop(self, "show_in_sidebar", text="")
|
||||
sub = sub.row(align=True)
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "sidebar_category", text="")
|
||||
|
||||
col.prop(self, "parent")
|
||||
col.prop(self, "apply_order")
|
||||
col.prop(self, "pin")
|
||||
col.separator()
|
||||
col = layout.column(align=True, heading="Features")
|
||||
col.prop(self, "fast_modifier_apply")
|
||||
|
||||
# Features
|
||||
layout.separator()
|
||||
col = layout.column(align=True, heading="Features")
|
||||
col.prop(self, "fast_modifier_apply")
|
||||
col.prop(self, "double_click")
|
||||
# Shared Properties
|
||||
if self.category == 'SHARED':
|
||||
col = layout.column()
|
||||
col.prop(self, "show_in_editmode")
|
||||
col.prop(self, "apply_order")
|
||||
|
||||
# Experimentals
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(self, "versioning", text="⚠ Versioning")
|
||||
col.prop(self, "experimental", text="⚠ Experimental")
|
||||
# Boolean Operator Properties
|
||||
if self.category == 'OPERATORS':
|
||||
col = layout.column(align=True, heading="Put Cutters in Collection")
|
||||
row = col.row(align=True)
|
||||
sub = row.row(align=True)
|
||||
sub.prop(self, "use_collection", text="")
|
||||
sub = sub.row(align=True)
|
||||
sub.active = self.show_in_sidebar
|
||||
sub.prop(self, "collection_name", text="", placeholder="boolean_cutters")
|
||||
|
||||
col = layout.column()
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "solver", text="Solver", expand=True)
|
||||
row = col.row(align=True)
|
||||
row.prop(self, "display", expand=True)
|
||||
|
||||
col = layout.column()
|
||||
col.prop(self, "parent")
|
||||
col.prop(self, "pin")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
BoolToolPreferences,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -32,13 +32,15 @@ class OBJECT_PG_booleans(bpy.types.PropertyGroup):
|
||||
default = False,
|
||||
)
|
||||
|
||||
modifiers_list_index: bpy.props.IntProperty()
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_PG_booleans,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -21,12 +21,12 @@ else:
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
"""NOTE: Order of modules is important because of dependancies. Don't change without a reason."""
|
||||
modules = [
|
||||
modules = (
|
||||
carver_circle,
|
||||
carver_box,
|
||||
carver_polyline,
|
||||
ui,
|
||||
]
|
||||
)
|
||||
|
||||
main_tools = [
|
||||
carver_box.OBJECT_WT_carve_box,
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import bpy
|
||||
import os
|
||||
from mathutils import Vector
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from ..constants import (
|
||||
ICONS_PATH,
|
||||
)
|
||||
from ..functions.view import (
|
||||
redraw_regions,
|
||||
)
|
||||
|
||||
from .common.base import (
|
||||
CarverBase,
|
||||
)
|
||||
from .common.properties import (
|
||||
CarverPropsArray,
|
||||
CarverPropsBevel,
|
||||
)
|
||||
from .common.types import (
|
||||
@@ -34,7 +39,7 @@ class OBJECT_WT_carve_box(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_box")
|
||||
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_box")
|
||||
bl_keymap = (
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
|
||||
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
|
||||
@@ -58,9 +63,7 @@ class MESH_WT_carve_box(OBJECT_WT_carve_box):
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
|
||||
class OBJECT_OT_carve_box(CarverBase,
|
||||
CarverPropsArray,
|
||||
CarverPropsBevel):
|
||||
class OBJECT_OT_carve_box(CarverBase, CarverPropsBevel):
|
||||
bl_idname = "object.carve_box"
|
||||
bl_label = "Box Carve"
|
||||
bl_description = description
|
||||
@@ -70,6 +73,7 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
# SHAPE-properties
|
||||
shape = 'BOX'
|
||||
|
||||
# NOTE: There are registered on operator level because they need to be overriden or hidden per-tool.
|
||||
aspect: bpy.props.EnumProperty(
|
||||
name = "Aspect",
|
||||
description = "The initial aspect",
|
||||
@@ -115,8 +119,8 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
# cached_variables
|
||||
"""Important for storing context as it was when operator was invoked (untouched by the modal)."""
|
||||
self.phase = "DRAW"
|
||||
self.initial_origin = self.origin # Initial shape origin.
|
||||
self.initial_aspect = self.aspect # Initial shape aspect.
|
||||
self._initial_origin = self.origin # Initial shape origin.
|
||||
self._initial_aspect = self.aspect # Initial shape aspect.
|
||||
self._stored_phase = "DRAW"
|
||||
|
||||
# Add Draw Handler
|
||||
@@ -130,12 +134,11 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
redraw_regions(context)
|
||||
|
||||
# Status Bar Text
|
||||
self.status(context)
|
||||
|
||||
# find_the_limit_of_the_3d_viewport_region
|
||||
self.redraw_region(context)
|
||||
|
||||
# Modifier Keys
|
||||
self.event_aspect(context, event)
|
||||
self.event_origin(context, event)
|
||||
@@ -151,7 +154,6 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
if self.phase != "BEVEL":
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
|
||||
# Mouse Move
|
||||
if event.type == 'MOUSEMOVE':
|
||||
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
@@ -164,7 +166,6 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
|
||||
# Confirm
|
||||
elif event.type == 'LEFTMOUSE':
|
||||
# Confirm Shape
|
||||
@@ -178,7 +179,7 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
min_distance = 5
|
||||
|
||||
if delta_x < min_distance or delta_y < min_distance:
|
||||
self.finalize(context, clean_up=True, abort=True)
|
||||
self.finalize(context, abort=True)
|
||||
return {'FINISHED'}
|
||||
|
||||
self.extrude_cutter(context)
|
||||
@@ -188,18 +189,15 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
if self.depth != 'MANUAL':
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
# Confirm Depth
|
||||
if self.phase == "EXTRUDE" and event.value == 'PRESS':
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Cancel
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.finalize(context, clean_up=True, abort=True)
|
||||
self.finalize(context, abort=True)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
@@ -305,9 +303,9 @@ class OBJECT_OT_carve_box(CarverBase,
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_carve_box,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import bpy
|
||||
import os
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from ..constants import (
|
||||
ICONS_PATH,
|
||||
)
|
||||
|
||||
from .common.ui import (
|
||||
carver_ui_common,
|
||||
@@ -20,7 +23,7 @@ class OBJECT_WT_carve_circle(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_circle")
|
||||
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_circle")
|
||||
bl_keymap = (
|
||||
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
|
||||
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
|
||||
@@ -51,6 +54,7 @@ class OBJECT_OT_carve_circle(OBJECT_OT_carve_box):
|
||||
# SHAPE-properties
|
||||
shape = 'CIRCLE'
|
||||
|
||||
# NOTE: There are registered on operator level because they need to be overriden or hidden per-tool.
|
||||
subdivision: bpy.props.IntProperty(
|
||||
name = "Circle Subdivisions",
|
||||
description = "Number of vertices that will make up the circular shape that will be extruded into a cylinder",
|
||||
@@ -76,9 +80,9 @@ class OBJECT_OT_carve_circle(OBJECT_OT_carve_box):
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_carve_circle,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import bpy
|
||||
import math
|
||||
import os
|
||||
from mathutils import Vector
|
||||
from bpy_extras import view3d_utils
|
||||
from .. import __file__ as base_file
|
||||
|
||||
from ..constants import (
|
||||
ICONS_PATH,
|
||||
)
|
||||
from ..functions.view import (
|
||||
redraw_regions,
|
||||
)
|
||||
|
||||
from .common.base import (
|
||||
CarverBase,
|
||||
)
|
||||
from .common.properties import (
|
||||
CarverPropsArray,
|
||||
)
|
||||
from .common.types import (
|
||||
Selection,
|
||||
Mouse,
|
||||
@@ -35,7 +37,7 @@ class OBJECT_WT_carve_polyline(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'OBJECT'
|
||||
|
||||
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_polyline")
|
||||
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_polyline")
|
||||
bl_keymap = (
|
||||
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None),
|
||||
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, None),
|
||||
@@ -57,8 +59,7 @@ class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline):
|
||||
|
||||
#### ------------------------------ OPERATORS ------------------------------ ####
|
||||
|
||||
class OBJECT_OT_carve_polyline(CarverBase,
|
||||
CarverPropsArray):
|
||||
class OBJECT_OT_carve_polyline(CarverBase):
|
||||
bl_idname = "object.carve_polyline"
|
||||
bl_label = "Polyline Carve"
|
||||
bl_description = description
|
||||
@@ -108,12 +109,11 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
redraw_regions(context)
|
||||
|
||||
# Status Bar Text
|
||||
self.status(context)
|
||||
|
||||
# find_the_limit_of_the_3d_viewport_region
|
||||
self.redraw_region(context)
|
||||
|
||||
# Modifier Keys
|
||||
self.event_array(context, event)
|
||||
self.event_move(context, event)
|
||||
@@ -124,7 +124,6 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
if self.phase != "BEVEL":
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
|
||||
# Mouse Move
|
||||
if event.type == 'MOUSEMOVE':
|
||||
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
|
||||
@@ -146,12 +145,15 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
|
||||
# Add Points & Confirm
|
||||
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
|
||||
if self.phase == "DRAW":
|
||||
# Confirm Shape (if clicked on the first vert)
|
||||
if self._distance_from_first > 75:
|
||||
# Add Point
|
||||
if self._distance_from_first < 75:
|
||||
self._insert_polyline_point()
|
||||
|
||||
# Confirm Shape (if clicked near the first vert).
|
||||
else:
|
||||
verts = self.cutter.verts
|
||||
if len(verts) > 3:
|
||||
self._remove_polyline_point(context, jump_mouse=False)
|
||||
@@ -165,17 +167,12 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
else:
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
# Add Point
|
||||
else:
|
||||
self._insert_polyline_point()
|
||||
|
||||
# Confirm Depth
|
||||
if self.phase == "EXTRUDE":
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
# Confirm
|
||||
# Enter (Confirm)
|
||||
elif event.type == 'RET':
|
||||
verts = self.cutter.verts
|
||||
if len(verts) > 2:
|
||||
@@ -195,18 +192,14 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
if self.phase == "EXTRUDE" and event.value == 'PRESS':
|
||||
self.confirm(context)
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
self.report({'WARNING'}, "At least three points are required to make a polygonal shape")
|
||||
|
||||
|
||||
# Remove Last Point
|
||||
if event.type == 'BACK_SPACE' and event.value == 'PRESS':
|
||||
self._remove_polyline_point(context)
|
||||
|
||||
|
||||
# Cancel
|
||||
elif event.type in {'RIGHTMOUSE', 'ESC'}:
|
||||
self.finalize(context, clean_up=True, abort=True)
|
||||
self.finalize(context, abort=True)
|
||||
return {'FINISHED'}
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
@@ -301,7 +294,7 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
context.workspace.status_text_set(modal_keys_extrude)
|
||||
|
||||
|
||||
# Polyline-specific features.
|
||||
# Polyline-specific methods.
|
||||
def _insert_polyline_point(self):
|
||||
"""Inserts a new vertex in the cutter geometry and connects it to the previous last one."""
|
||||
|
||||
@@ -383,15 +376,16 @@ class OBJECT_OT_carve_polyline(CarverBase,
|
||||
context.region_data,
|
||||
vert_world)
|
||||
if screen_pos:
|
||||
context.window.cursor_warp(int(screen_pos.x), int(screen_pos.y))
|
||||
x, y = int(screen_pos.x), int(screen_pos.y)
|
||||
context.window.cursor_warp(x, y)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
OBJECT_OT_carve_polyline,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import math
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .types import (
|
||||
Effects,
|
||||
from ...functions.cutter import (
|
||||
make_cutter,
|
||||
)
|
||||
from .properties import (
|
||||
CarverPropsOperator,
|
||||
CarverPropsShape,
|
||||
CarverPropsModifier,
|
||||
CarverPropsCutter,
|
||||
)
|
||||
|
||||
from ...functions.draw import (
|
||||
draw_shader,
|
||||
draw_bmesh_faces,
|
||||
draw_circle_around_point,
|
||||
)
|
||||
from ...functions.math import (
|
||||
distance_from_point_to_segment,
|
||||
region_2d_to_plane_3d,
|
||||
region_2d_to_line_3d,
|
||||
draw_circle_billboard,
|
||||
)
|
||||
from ...functions.mesh import (
|
||||
is_instanced_mesh,
|
||||
extrude_face,
|
||||
are_intersecting,
|
||||
raycast,
|
||||
)
|
||||
from ...functions.modifier import (
|
||||
add_boolean_modifier,
|
||||
@@ -35,300 +24,44 @@ from ...functions.modifier import (
|
||||
get_modifiers_to_apply,
|
||||
)
|
||||
from ...functions.object import (
|
||||
set_cutter_properties,
|
||||
delete_empty_collection,
|
||||
delete_cutter,
|
||||
set_object_origin,
|
||||
)
|
||||
from ...functions.poll import (
|
||||
is_linked,
|
||||
is_instanced_data,
|
||||
set_object_origin,
|
||||
delete_object,
|
||||
)
|
||||
from ...functions.scene import (
|
||||
delete_empty_collection,
|
||||
raycast,
|
||||
)
|
||||
from ...functions.view import (
|
||||
distance_from_point_to_segment,
|
||||
region_2d_to_ray_3d,
|
||||
region_2d_to_plane_3d,
|
||||
)
|
||||
|
||||
from .events import (
|
||||
CarverEvents,
|
||||
)
|
||||
from .properties import (
|
||||
CarverPropsOperator,
|
||||
CarverPropsShape,
|
||||
CarverPropsModifier,
|
||||
CarverPropsCutter,
|
||||
CarverPropsArray,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /base/ ------------------------------ ####
|
||||
|
||||
class CarverEvents():
|
||||
|
||||
def _custom_modifier_event(self, context, event, modifier,
|
||||
cursor='NONE', store_values=False, restore_mouse=True,
|
||||
postprocess=None):
|
||||
"""Creates custom modifier event when key is held and hides cursor until it's released"""
|
||||
|
||||
# Initialize Modifier Phase
|
||||
if event.value == 'PRESS':
|
||||
if self.phase in ("DRAW", "EXTRUDE"):
|
||||
self._stored_phase = self.phase
|
||||
self.phase = modifier
|
||||
self.mouse.cached = self.mouse.current
|
||||
context.window.cursor_set(cursor)
|
||||
|
||||
if store_values:
|
||||
# Store center of the geometry as a Vector.
|
||||
verts = [v.co for v in self.cutter.bm.verts]
|
||||
center = sum(verts, Vector()) / len(verts)
|
||||
self.cutter.center = self.cutter.obj.matrix_world @ center
|
||||
|
||||
# End Modifier Phase
|
||||
elif event.value == 'RELEASE':
|
||||
if self.phase == modifier:
|
||||
context.window.cursor_set('MUTE')
|
||||
if restore_mouse:
|
||||
context.window.cursor_warp(int(self.mouse.cached[0]), int(self.mouse.cached[1]) + 100)
|
||||
self.mouse.current = self.mouse.cached
|
||||
self.phase = self._stored_phase
|
||||
|
||||
if postprocess is not None:
|
||||
postprocess(self)
|
||||
|
||||
return self._stored_phase
|
||||
|
||||
|
||||
# Individual Events
|
||||
def event_aspect(self, context, event):
|
||||
"""Modifier keys for changing aspect of the shape"""
|
||||
|
||||
if event.shift and self.phase == "DRAW":
|
||||
if self.initial_aspect == 'FREE':
|
||||
self.aspect = 'FIXED'
|
||||
elif self.initial_aspect == 'FIXED':
|
||||
self.aspect = 'FREE'
|
||||
else:
|
||||
self.aspect = self.initial_aspect
|
||||
|
||||
|
||||
def event_origin(self, context, event):
|
||||
"""Modifier keys for changing the origin of the shape"""
|
||||
|
||||
if event.alt and self.phase == "DRAW":
|
||||
if self.initial_origin == 'EDGE':
|
||||
self.origin = 'CENTER'
|
||||
elif self.initial_origin == 'CENTER':
|
||||
self.origin = 'EDGE'
|
||||
else:
|
||||
self.origin = self.initial_origin
|
||||
|
||||
|
||||
def event_rotate(self, context, event):
|
||||
"""Modifier keys for rotating the shape"""
|
||||
|
||||
def _remove_rotate_phase_properties(self):
|
||||
del self._stored_mouse_pos_3d
|
||||
del self._stored_rotation
|
||||
del self._stored_cutter_center
|
||||
del self._stored_cutter_euler
|
||||
|
||||
# Restore origin at edge (first vertex).
|
||||
if self.origin == 'EDGE':
|
||||
point = self.cutter.bm.verts[0].co
|
||||
set_object_origin(self.cutter.obj, self.cutter.bm, point='CUSTOM', custom=point)
|
||||
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'R':
|
||||
stored_phase = self._custom_modifier_event(context, event, "ROTATE",
|
||||
cursor='MOVE_X', store_values=True, restore_mouse=False,
|
||||
postprocess=_remove_rotate_phase_properties)
|
||||
|
||||
if self.phase == "ROTATE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
# Store values.
|
||||
obj = self.cutter.obj
|
||||
if not hasattr(self, '_stored_mouse_pos_3d'):
|
||||
self._stored_mouse_pos_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_rotation = self.rotation
|
||||
self._stored_cutter_center = self.cutter.center
|
||||
self._stored_cutter_euler = obj.rotation_euler.copy()
|
||||
|
||||
# Calculate angle and direction.
|
||||
to_start = (self._stored_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
to_current = (current_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
|
||||
angle = to_start.angle(to_current)
|
||||
cross = to_start.cross(to_current)
|
||||
if cross.dot(self.workplane.normal) < 0:
|
||||
angle = -angle
|
||||
|
||||
if abs(angle) > 0.0001:
|
||||
self.rotation = self._stored_rotation + angle
|
||||
|
||||
# Offset the object location when drawing from edge to move rotation pivot to center.
|
||||
if self.origin == 'EDGE':
|
||||
set_object_origin(obj, self.cutter.bm, point='CENTER_OBJ')
|
||||
|
||||
# Calculate rotation amount.
|
||||
rotation_total = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
|
||||
rotation_stored = Matrix.Rotation(self._stored_rotation, 4, self.workplane.normal)
|
||||
rotation_matrix = rotation_total @ rotation_stored.inverted()
|
||||
new_rot = rotation_matrix @ self._stored_cutter_euler.to_matrix().to_4x4()
|
||||
|
||||
# Rotate.
|
||||
obj.rotation_euler = new_rot.to_euler()
|
||||
|
||||
|
||||
def event_bevel(self, context, event):
|
||||
"""Modifier keys for beveling the shape"""
|
||||
|
||||
def _remove_empty_bevel_modifier(self):
|
||||
bevel = self.effects.bevel
|
||||
if bevel.width == 0:
|
||||
self.cutter.obj.modifiers.remove(bevel)
|
||||
self.effects.bevel = None
|
||||
|
||||
if self.effects.weld is not None:
|
||||
self.cutter.obj.modifiers.remove(self.effects.weld)
|
||||
self.effects.weld = None
|
||||
|
||||
|
||||
if self.shape != 'BOX':
|
||||
return
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'B':
|
||||
stored_phase = self._custom_modifier_event(context, event, "BEVEL",
|
||||
cursor='PICK_AREA', store_values=True,
|
||||
postprocess=_remove_empty_bevel_modifier)
|
||||
|
||||
if self.phase == "BEVEL":
|
||||
self.use_bevel = True
|
||||
|
||||
# Initialize bevel effect if it doesn't exist.
|
||||
if self.effects.bevel is None:
|
||||
self.bevel_width = 0
|
||||
affect = 'VERTICES' if stored_phase == "DRAW" else 'EDGES'
|
||||
self.effects.add_bevel_modifier(self, affect=affect)
|
||||
|
||||
# Force the geometry to update.
|
||||
if stored_phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif stored_phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Calculate bevel width.
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.cached, self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.current, self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.bevel_width = d * 0.2
|
||||
|
||||
# Adjust bevel segments.
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
self.bevel_segments += 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
self.bevel_segments -= 1
|
||||
|
||||
# Update modifier.
|
||||
self.effects.update(self, 'BEVEL')
|
||||
|
||||
|
||||
def event_array(self, context, event):
|
||||
"""Modifier keys for creating the array of the shape"""
|
||||
|
||||
# Add duplicates.
|
||||
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
|
||||
self.columns -= 1
|
||||
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
|
||||
self.columns += 1
|
||||
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
|
||||
self.rows -= 1
|
||||
if event.type == 'UP_ARROW' and event.value == 'PRESS':
|
||||
self.rows += 1
|
||||
|
||||
if event.type in ['LEFT_ARROW',
|
||||
'RIGHT_ARROW',
|
||||
'DOWN_ARROW',
|
||||
'UP_ARROW',] and event.value == 'PRESS':
|
||||
self.effects.update(self, 'ARRAY_COUNT')
|
||||
|
||||
# Force the geometry to update.
|
||||
if self.phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Adjust gap.
|
||||
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
|
||||
stored_phase = self._custom_modifier_event(context, event, "ARRAY", cursor='MUTE',
|
||||
store_values=True, restore_mouse=True)
|
||||
|
||||
if self.phase == "ARRAY":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.cached, self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.current, self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.gap = 1 + (d * 0.2)
|
||||
|
||||
self.effects.update(self, 'ARRAY_GAP')
|
||||
|
||||
|
||||
def event_flip(self, context, event):
|
||||
"""Modifier keys for flipping the direction of extrusion."""
|
||||
|
||||
if event.type == 'F' and event.value == 'PRESS':
|
||||
if self.phase == 'EXTRUDE':
|
||||
self.flip_direction = not self.flip_direction
|
||||
|
||||
|
||||
def event_move(self, context, event):
|
||||
"""Modifier keys for moving the cutter shape."""
|
||||
|
||||
def _remove_move_phase_properties(self):
|
||||
del self._stored_cutter_location
|
||||
self.mouse.cached_3d = None
|
||||
|
||||
|
||||
if event.type == 'SPACE':
|
||||
stored_phase = self._custom_modifier_event(context, event, "MOVE",
|
||||
cursor='SCROLL_XY', restore_mouse=False,
|
||||
postprocess=_remove_move_phase_properties)
|
||||
|
||||
if self.phase == "MOVE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
if not hasattr(self, '_stored_cutter_location'):
|
||||
self.mouse.cached_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_cutter_location = self.cutter.obj.location.copy()
|
||||
|
||||
offset = current_mouse_pos_3d - self.mouse.cached_3d
|
||||
self.cutter.obj.location = self._stored_cutter_location + offset
|
||||
|
||||
#### ------------------------------ CLASSES ------------------------------ ####
|
||||
|
||||
class CarverBase(bpy.types.Operator,
|
||||
CarverEvents,
|
||||
CarverPropsOperator,
|
||||
CarverPropsShape,
|
||||
CarverPropsModifier,
|
||||
CarverPropsCutter):
|
||||
CarverPropsCutter,
|
||||
CarverPropsArray):
|
||||
"""Base class for Carver operators."""
|
||||
|
||||
def redraw_region(self, context):
|
||||
"""Redraw the region to find the limits of the 3D viewport."""
|
||||
|
||||
region_types = {'WINDOW', 'UI'}
|
||||
for area in context.window.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for region in area.regions:
|
||||
if not region_types or region.type in region_types:
|
||||
region.tag_redraw()
|
||||
|
||||
|
||||
# Core Methods
|
||||
def validate_selection(self, context):
|
||||
"""Filter out selection to get the list of viable canvases."""
|
||||
|
||||
@@ -349,10 +82,14 @@ class CarverBase(bpy.types.Operator,
|
||||
continue
|
||||
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
if is_instanced_data(obj):
|
||||
if is_instanced_mesh(obj.data):
|
||||
self.report({'WARNING'}, f"Modifiers cannot be applied to {obj.name} because it has instanced object data")
|
||||
continue
|
||||
|
||||
if obj.data.shape_keys:
|
||||
self.report({'WARNING'}, f"Modifiers cannot be applied to {obj.name} because it has shape keys")
|
||||
continue
|
||||
|
||||
selected.append(obj)
|
||||
|
||||
# Ensure the active object.
|
||||
@@ -367,11 +104,10 @@ class CarverBase(bpy.types.Operator,
|
||||
return selected, active
|
||||
|
||||
|
||||
# Core Methods
|
||||
def calculate_workplane(self, context):
|
||||
"""
|
||||
Calculates matrix, location (origin point), and normal (direction)
|
||||
of the workplane based on the active alignment method.
|
||||
of the workplane based on the chosen alignment method.
|
||||
"""
|
||||
|
||||
if self.alignment == 'SURFACE':
|
||||
@@ -393,7 +129,7 @@ class CarverBase(bpy.types.Operator,
|
||||
|
||||
|
||||
def create_cutter(self, context):
|
||||
"""Creates a cutter object with correct properties & initializes `bmesh` geometry."""
|
||||
"""Creates a cutter object with correct properties & initializes `bmesh`."""
|
||||
|
||||
# Create the Mesh & bmesh
|
||||
mesh = bpy.data.meshes.new(name="boolean_cutter")
|
||||
@@ -402,15 +138,15 @@ class CarverBase(bpy.types.Operator,
|
||||
# Create the Object
|
||||
obj = bpy.data.objects.new("boolean_cutter", mesh)
|
||||
obj.matrix_world = self.workplane.matrix
|
||||
set_cutter_properties(context, obj, "Difference", collection=True,
|
||||
display='WIRE' if self.shape == 'POLYLINE' else self.display)
|
||||
make_cutter(context, obj, "Difference", collection=True,
|
||||
display='WIRE' if self.shape == 'POLYLINE' else self.display)
|
||||
obj.booleans.carver = True
|
||||
|
||||
# Initial Rotation
|
||||
if self.rotation != 0:
|
||||
rotation_matrix = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
|
||||
temp_matrix_world = rotation_matrix @ obj.matrix_world
|
||||
obj.rotation_euler = temp_matrix_world.to_euler()
|
||||
rotation_matrix = rotation_matrix @ obj.matrix_world
|
||||
obj.rotation_euler = rotation_matrix.to_euler()
|
||||
|
||||
# Create Verts
|
||||
if self.shape == 'BOX':
|
||||
@@ -471,18 +207,21 @@ class CarverBase(bpy.types.Operator,
|
||||
# Draw Line
|
||||
if self.phase in ("BEVEL", "ROTATE", "ARRAY"):
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(context.region, context.region_data,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
vertices = [self.cutter.center, current_mouse_pos_3d]
|
||||
if vertices is not None:
|
||||
draw_shader('LINES', (0.00, 0.00, 0.00), 1.0, vertices)
|
||||
|
||||
# Draw Circle around First Vertex
|
||||
# Draw circle around first vertex.
|
||||
if self.shape == 'POLYLINE' and self.phase == 'DRAW':
|
||||
verts = self.cutter.verts
|
||||
if len(verts) > 3:
|
||||
vertices = draw_circle_around_point(context, obj, verts[0], self._distance_from_first, 4)
|
||||
vertices = draw_circle_billboard(context,
|
||||
obj.matrix_world @ verts[0].co,
|
||||
radius=self._distance_from_first,
|
||||
segments=4)
|
||||
if len(vertices) > 0:
|
||||
draw_shader('LINE_LOOP', (0, 0, 0), 1.0, vertices)
|
||||
|
||||
@@ -497,7 +236,7 @@ class CarverBase(bpy.types.Operator,
|
||||
bm = self.cutter.bm
|
||||
face = self.cutter.faces[0]
|
||||
|
||||
# Get the mouse positon on the workplane.
|
||||
# Get the mouse positon x, y on the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
@@ -582,7 +321,6 @@ class CarverBase(bpy.types.Operator,
|
||||
if self.alignment == 'CURSOR' and self.depth == 'CURSOR':
|
||||
self.depth = 'AUTO'
|
||||
|
||||
|
||||
# Push the extruded face towards the furthest point of the collective bounding box.
|
||||
if self.depth == 'AUTO':
|
||||
corners = []
|
||||
@@ -599,10 +337,9 @@ class CarverBase(bpy.types.Operator,
|
||||
offset = self.offset if self.alignment == 'SURFACE' else 0.1
|
||||
|
||||
for v in face.verts:
|
||||
if self.depth == 'AUTO':
|
||||
v.co += normal * (furthest_corner - offset)
|
||||
v.co += normal * (furthest_corner - offset)
|
||||
|
||||
# Push the extruded face towards the plane of 3D cursor.
|
||||
# Push the extruded face towards the plane of the 3D cursor.
|
||||
elif self.depth == 'CURSOR':
|
||||
local_cursor = self.cutter.obj.matrix_world.inverted() @ context.scene.cursor.location
|
||||
for v in extruded_verts:
|
||||
@@ -647,20 +384,24 @@ class CarverBase(bpy.types.Operator,
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
normal = self.cutter.obj.matrix_world.to_3x3().inverted() @ self.workplane.normal
|
||||
|
||||
# Find the point on 3D line (along the normal) closest to the cursor.
|
||||
# and calculate the distance between that and the extrude origin.
|
||||
closest_points = region_2d_to_line_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
self._extrude_origin, self.workplane.normal)
|
||||
# Convert the mouse position into a 3D ray and find closest points between...
|
||||
# that ray and the line coming from the extrude origin along the workplane normal.
|
||||
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.current)
|
||||
closest_points = mathutils.geometry.intersect_line_line(ray_origin,
|
||||
ray_origin + ray_direction,
|
||||
self._extrude_origin,
|
||||
self._extrude_origin + self.workplane.normal)
|
||||
|
||||
if closest_points is None:
|
||||
return
|
||||
|
||||
# Calculate the distance between the extrude origin and mouse position.
|
||||
offset_vector = closest_points[1] - self._extrude_origin
|
||||
distance = offset_vector.dot(self.workplane.normal)
|
||||
|
||||
# Offset vertices of the extruded face from their their original coordinates.
|
||||
normal = self.cutter.obj.matrix_world.to_3x3().inverted() @ self.workplane.normal
|
||||
if distance is not None:
|
||||
for v, vert_co in zip(self._extrude_faces[-1].verts, self._extrude_verts_co):
|
||||
offset = normal * distance
|
||||
@@ -695,68 +436,71 @@ class CarverBase(bpy.types.Operator,
|
||||
ray_obj_matrix = ray.obj.matrix_world
|
||||
mesh = ray.obj.evaluated_get(context.view_layer.depsgraph).to_mesh()
|
||||
|
||||
if mesh is not None:
|
||||
# create_temporary_bmesh
|
||||
temp_bm = bmesh.new()
|
||||
temp_bm.from_mesh(mesh)
|
||||
temp_bm.faces.ensure_lookup_table()
|
||||
if mesh is None:
|
||||
self.alignment = 'VIEW'
|
||||
return None, None, None
|
||||
|
||||
face = temp_bm.faces[ray.index]
|
||||
# Create temporary `bmesh`.
|
||||
temp_bm = bmesh.new()
|
||||
temp_bm.from_mesh(mesh)
|
||||
temp_bm.faces.ensure_lookup_table()
|
||||
|
||||
if self.orientation == 'FACE':
|
||||
# Get the tangent, normal, and bitangent from the face normal.
|
||||
tangent = face.calc_tangent_edge()
|
||||
normal = face.normal
|
||||
bitangent = normal.cross(tangent)
|
||||
face = temp_bm.faces[ray.index]
|
||||
|
||||
elif self.orientation in ('CLOSEST_EDGE', 'LONGEST_EDGE'):
|
||||
# Get the tangent, normal, and bitangent from the longest or the closest edge of the face.
|
||||
if self.orientation == 'FACE':
|
||||
# Get the tangent, normal, and bitangent from the face normal.
|
||||
tangent = face.calc_tangent_edge()
|
||||
normal = face.normal
|
||||
bitangent = normal.cross(tangent)
|
||||
|
||||
if self.orientation == 'LONGEST_EDGE':
|
||||
lengths = [
|
||||
(ray_obj_matrix @ edge.verts[0].co - ray_obj_matrix @ edge.verts[1].co).length
|
||||
for edge in face.edges
|
||||
]
|
||||
longest_edge = sorted(zip(lengths, face.edges), key=lambda x: x[0], reverse=True)[0][1]
|
||||
edge = longest_edge
|
||||
elif self.orientation in ('CLOSEST_EDGE', 'LONGEST_EDGE'):
|
||||
# Get the tangent, normal, and bitangent from the longest or the closest edge of the face.
|
||||
|
||||
elif self.orientation == 'CLOSEST_EDGE':
|
||||
distances = [
|
||||
distance_from_point_to_segment(
|
||||
ray.location,
|
||||
ray_obj_matrix @ edge.verts[0].co,
|
||||
ray_obj_matrix @ edge.verts[1].co,
|
||||
)
|
||||
for edge in face.edges
|
||||
]
|
||||
closest_edge = sorted(zip(distances, face.edges), key=lambda x: x[0])[0][1]
|
||||
edge = closest_edge
|
||||
if self.orientation == 'LONGEST_EDGE':
|
||||
lengths = [
|
||||
(ray_obj_matrix @ edge.verts[0].co - ray_obj_matrix @ edge.verts[1].co).length
|
||||
for edge in face.edges
|
||||
]
|
||||
longest_edge = sorted(zip(lengths, face.edges), key=lambda x: x[0], reverse=True)[0][1]
|
||||
edge = longest_edge
|
||||
|
||||
# Get the loop (face corner) for the edge that is also in the face.
|
||||
face_corner = next(loop for loop in edge.link_loops if loop.face == face)
|
||||
elif self.orientation == 'CLOSEST_EDGE':
|
||||
distances = [
|
||||
distance_from_point_to_segment(
|
||||
ray.location,
|
||||
ray_obj_matrix @ edge.verts[0].co,
|
||||
ray_obj_matrix @ edge.verts[1].co,
|
||||
)
|
||||
for edge in face.edges
|
||||
]
|
||||
closest_edge = sorted(zip(distances, face.edges), key=lambda x: x[0])[0][1]
|
||||
edge = closest_edge
|
||||
|
||||
start = face_corner.vert
|
||||
end = face_corner.link_loop_next.vert
|
||||
direction = (end.co - start.co)
|
||||
# Get the loop (face corner) for the edge that is also in the face.
|
||||
face_corner = next(loop for loop in edge.link_loops if loop.face == face)
|
||||
|
||||
tangent = edge.calc_tangent(face_corner)
|
||||
normal = direction.cross(tangent)
|
||||
bitangent = normal.cross(tangent)
|
||||
start = face_corner.vert
|
||||
end = face_corner.link_loop_next.vert
|
||||
direction = (end.co - start.co)
|
||||
|
||||
# Construct Matrix
|
||||
matrix = Matrix.Identity(4)
|
||||
matrix[0].xyz = (ray_obj_matrix.to_3x3() @ tangent).normalized()
|
||||
matrix[1].xyz = (ray_obj_matrix.to_3x3() @ bitangent).normalized()
|
||||
matrix[2].xyz = (ray_obj_matrix.to_3x3() @ normal).normalized()
|
||||
matrix[3].xyz = ray.location + (ray.normal * self.offset)
|
||||
tangent = edge.calc_tangent(face_corner)
|
||||
normal = direction.cross(tangent)
|
||||
bitangent = normal.cross(tangent)
|
||||
|
||||
# destroy_temporary_bmesh
|
||||
temp_bm.free()
|
||||
del mesh
|
||||
# Construct Matrix
|
||||
matrix = Matrix.Identity(4)
|
||||
matrix[0].xyz = (ray_obj_matrix.to_3x3() @ tangent).normalized()
|
||||
matrix[1].xyz = (ray_obj_matrix.to_3x3() @ bitangent).normalized()
|
||||
matrix[2].xyz = (ray_obj_matrix.to_3x3() @ normal).normalized()
|
||||
matrix[3].xyz = ray.location + (ray.normal * self.offset)
|
||||
|
||||
matrix = matrix.transposed()
|
||||
location = ray.location + (ray.normal * self.offset)
|
||||
normal = ray.normal
|
||||
# destroy_temporary_bmesh
|
||||
temp_bm.free()
|
||||
del mesh
|
||||
|
||||
matrix = matrix.transposed()
|
||||
location = ray.location + (ray.normal * self.offset)
|
||||
normal = ray.normal
|
||||
|
||||
return matrix, location, normal
|
||||
|
||||
@@ -776,8 +520,7 @@ class CarverBase(bpy.types.Operator,
|
||||
context.scene.cursor.location)
|
||||
else:
|
||||
# Put the location at the closest point of the bounding box of all selected objects.
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse.initial)
|
||||
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse.initial)
|
||||
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.initial)
|
||||
|
||||
corners = []
|
||||
for obj in self.objects.selected:
|
||||
@@ -803,8 +546,7 @@ class CarverBase(bpy.types.Operator,
|
||||
matrix = Matrix.Identity(4)
|
||||
normal = matrix.col[2].xyz
|
||||
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse.initial)
|
||||
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse.initial)
|
||||
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.initial)
|
||||
t = (matrix.translation.z - ray_origin.z) / ray_direction.z
|
||||
location = ray_origin + ray_direction * t
|
||||
matrix.translation = location
|
||||
@@ -851,7 +593,6 @@ class CarverBase(bpy.types.Operator,
|
||||
|
||||
cutter = self.cutter.obj
|
||||
|
||||
# Add Modifier(s)
|
||||
for obj in self.objects.selected:
|
||||
mod = add_boolean_modifier(self, context, obj,
|
||||
cutter, "DIFFERENCE",
|
||||
@@ -877,7 +618,7 @@ class CarverBase(bpy.types.Operator,
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
if not intersecting_canvases:
|
||||
self.finalize(context, clean_up=True)
|
||||
self.finalize(context)
|
||||
return
|
||||
|
||||
# Select all faces of the cutter so that newly created faces in canvas
|
||||
@@ -920,7 +661,7 @@ class CarverBase(bpy.types.Operator,
|
||||
for obj in intersecting_canvases:
|
||||
obj.booleans.canvas = True
|
||||
|
||||
self.finalize(context)
|
||||
self.finalize(context, clean_up=False)
|
||||
return
|
||||
|
||||
elif self.mode == 'DESTRUCTIVE':
|
||||
@@ -930,11 +671,11 @@ class CarverBase(bpy.types.Operator,
|
||||
modifiers = get_modifiers_to_apply(context, obj, [modifiers])
|
||||
apply_modifiers(context, obj, modifiers, force_clean=True)
|
||||
|
||||
self.finalize(context, clean_up=True)
|
||||
self.finalize(context)
|
||||
return
|
||||
|
||||
|
||||
def finalize(self, context, clean_up=False, abort=False):
|
||||
def finalize(self, context, clean_up=True, abort=False):
|
||||
"""
|
||||
Finalize and clean-up after the operation ends.
|
||||
Regardless of whether it was confirmed or cancelled.
|
||||
@@ -943,15 +684,19 @@ class CarverBase(bpy.types.Operator,
|
||||
# Operation was aborted, or successfully finished in the Destructive mode.
|
||||
# Delete everything created by the operator (i.e. cutter).
|
||||
if clean_up:
|
||||
delete_cutter(self.cutter.obj)
|
||||
delete_object(self.cutter.obj)
|
||||
self.cutter.bm.free()
|
||||
delete_empty_collection()
|
||||
delete_empty_collection(context)
|
||||
|
||||
# Remove modifiers added by the operator.
|
||||
if abort:
|
||||
for obj, mod in self.objects.modifiers.items():
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
# Clean-up temporary changes made by operator.
|
||||
if self.effects.array:
|
||||
self.effects.array.use_pin_to_last = False
|
||||
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handler, 'WINDOW')
|
||||
context.workspace.status_text_set(None)
|
||||
context.window.cursor_set('DEFAULT' if context.mode == 'OBJECT' else 'CROSSHAIR')
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import bpy
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from ...functions.object import (
|
||||
set_object_origin,
|
||||
)
|
||||
from ...functions.view import (
|
||||
region_2d_to_plane_3d,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ CLASSES ------------------------------ ####
|
||||
|
||||
class CarverEvents():
|
||||
|
||||
# Private Methods.
|
||||
def _custom_modifier_event(self, context, event, modifier,
|
||||
cursor='NONE', store_values=False, restore_mouse=True,
|
||||
postprocess=None):
|
||||
"""Creates a custom modifier event when the key is held down."""
|
||||
|
||||
# Initialize Modifier Phase
|
||||
if event.value == 'PRESS':
|
||||
if self.phase in ("DRAW", "EXTRUDE"):
|
||||
self._stored_phase = self.phase
|
||||
self.phase = modifier
|
||||
self.mouse.cached = self.mouse.current
|
||||
context.window.cursor_set(cursor)
|
||||
|
||||
if store_values:
|
||||
# Store center of the geometry as a Vector.
|
||||
verts = [v.co for v in self.cutter.bm.verts]
|
||||
center = sum(verts, Vector()) / len(verts)
|
||||
self.cutter.center = self.cutter.obj.matrix_world @ center
|
||||
|
||||
# End Modifier Phase
|
||||
elif event.value == 'RELEASE':
|
||||
if self.phase == modifier:
|
||||
context.window.cursor_set('MUTE')
|
||||
if restore_mouse:
|
||||
context.window.cursor_warp(int(self.mouse.cached[0]), int(self.mouse.cached[1]) + 100)
|
||||
self.mouse.current = self.mouse.cached
|
||||
self.phase = self._stored_phase
|
||||
|
||||
if postprocess is not None:
|
||||
postprocess(self)
|
||||
|
||||
return self._stored_phase
|
||||
|
||||
|
||||
# Public Methods.
|
||||
def event_aspect(self, context, event):
|
||||
"""Modifier key for changing the aspect of the shape."""
|
||||
|
||||
if event.shift and self.phase == "DRAW":
|
||||
if self._initial_aspect == 'FREE':
|
||||
self.aspect = 'FIXED'
|
||||
elif self._initial_aspect == 'FIXED':
|
||||
self.aspect = 'FREE'
|
||||
else:
|
||||
self.aspect = self._initial_aspect
|
||||
|
||||
|
||||
def event_origin(self, context, event):
|
||||
"""Modifier key for changing the origin of the shape."""
|
||||
|
||||
if event.alt and self.phase == "DRAW":
|
||||
if self._initial_origin == 'EDGE':
|
||||
self.origin = 'CENTER'
|
||||
elif self._initial_origin == 'CENTER':
|
||||
self.origin = 'EDGE'
|
||||
else:
|
||||
self.origin = self._initial_origin
|
||||
|
||||
|
||||
def event_rotate(self, context, event):
|
||||
"""Modifier key for rotating the shape."""
|
||||
|
||||
def _remove_rotate_phase_properties(self):
|
||||
del self._stored_mouse_pos_3d
|
||||
del self._stored_rotation
|
||||
del self._stored_cutter_center
|
||||
del self._stored_cutter_euler
|
||||
|
||||
# Restore origin at edge (first vertex).
|
||||
if self.origin == 'EDGE':
|
||||
self.cutter.bm.verts.ensure_lookup_table()
|
||||
point = self.cutter.bm.verts[0].co
|
||||
set_object_origin(self.cutter.obj, self.cutter.bm, point='CUSTOM', custom=point)
|
||||
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'R':
|
||||
_rm = False if self._stored_phase == "DRAW" else True
|
||||
stored_phase = self._custom_modifier_event(context, event, "ROTATE",
|
||||
cursor='MOVE_X', store_values=True, restore_mouse=_rm,
|
||||
postprocess=_remove_rotate_phase_properties)
|
||||
|
||||
if self.phase == "ROTATE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
# Store values.
|
||||
obj = self.cutter.obj
|
||||
if not hasattr(self, '_stored_mouse_pos_3d'):
|
||||
self._stored_mouse_pos_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_rotation = self.rotation
|
||||
self._stored_cutter_center = self.cutter.center
|
||||
self._stored_cutter_euler = obj.rotation_euler.copy()
|
||||
|
||||
# Calculate angle and direction.
|
||||
to_start = (self._stored_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
to_current = (current_mouse_pos_3d - self._stored_cutter_center).normalized()
|
||||
|
||||
angle = to_start.angle(to_current)
|
||||
cross = to_start.cross(to_current)
|
||||
if cross.dot(self.workplane.normal) < 0:
|
||||
angle = -angle
|
||||
|
||||
if abs(angle) > 0.0001:
|
||||
self.rotation = self._stored_rotation + angle
|
||||
|
||||
# Offset the object location when drawing from edge to move rotation pivot to center.
|
||||
if self.origin == 'EDGE':
|
||||
set_object_origin(obj, self.cutter.bm, point='CENTER_OBJ')
|
||||
|
||||
# Calculate rotation amount.
|
||||
rotation_total = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
|
||||
rotation_stored = Matrix.Rotation(self._stored_rotation, 4, self.workplane.normal)
|
||||
rotation_matrix = rotation_total @ rotation_stored.inverted()
|
||||
new_rot = rotation_matrix @ self._stored_cutter_euler.to_matrix().to_4x4()
|
||||
|
||||
# Rotate.
|
||||
obj.rotation_euler = new_rot.to_euler()
|
||||
|
||||
|
||||
def event_bevel(self, context, event):
|
||||
"""Modifier key for beveling the shape."""
|
||||
|
||||
def _remove_empty_bevel_modifier(self):
|
||||
bevel = self.effects.bevel
|
||||
if bevel.width == 0:
|
||||
self.cutter.obj.modifiers.remove(bevel)
|
||||
self.effects.bevel = None
|
||||
|
||||
if self.effects.weld is not None:
|
||||
self.cutter.obj.modifiers.remove(self.effects.weld)
|
||||
self.effects.weld = None
|
||||
|
||||
|
||||
if self.shape != 'BOX':
|
||||
return
|
||||
|
||||
# Set correct phase.
|
||||
if event.type == 'B':
|
||||
stored_phase = self._custom_modifier_event(context, event, "BEVEL",
|
||||
cursor='PICK_AREA', store_values=True,
|
||||
postprocess=_remove_empty_bevel_modifier)
|
||||
|
||||
if self.phase == "BEVEL":
|
||||
self.use_bevel = True
|
||||
|
||||
# Initialize bevel effect if it doesn't exist.
|
||||
if self.effects.bevel is None:
|
||||
self.bevel_width = 0
|
||||
affect = 'VERTICES' if stored_phase == "DRAW" else 'EDGES'
|
||||
self.effects.add_bevel_modifier(self, affect=affect)
|
||||
|
||||
# Force the geometry to update.
|
||||
if stored_phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif stored_phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Calculate bevel width.
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.cached,
|
||||
self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.bevel_width = d * 0.2
|
||||
|
||||
# Adjust bevel segments.
|
||||
if event.type == 'WHEELUPMOUSE':
|
||||
self.bevel_segments += 1
|
||||
elif event.type == 'WHEELDOWNMOUSE':
|
||||
self.bevel_segments -= 1
|
||||
|
||||
# Update modifier.
|
||||
self.effects.update(self, "BEVEL")
|
||||
|
||||
|
||||
def event_array(self, context, event):
|
||||
"""Modifier key for arraying the shape."""
|
||||
|
||||
# Add duplicates.
|
||||
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
|
||||
self.columns -= 1
|
||||
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
|
||||
self.columns += 1
|
||||
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
|
||||
self.rows -= 1
|
||||
if event.type == 'UP_ARROW' and event.value == 'PRESS':
|
||||
self.rows += 1
|
||||
|
||||
if event.type in ['LEFT_ARROW',
|
||||
'RIGHT_ARROW',
|
||||
'DOWN_ARROW',
|
||||
'UP_ARROW',] and event.value == 'PRESS':
|
||||
self.effects.update(self, "ARRAY_COUNT")
|
||||
|
||||
# Force the geometry to update.
|
||||
if self.phase == "DRAW":
|
||||
self.update_cutter_shape(context)
|
||||
elif self.phase == "EXTRUDE":
|
||||
self.set_extrusion_depth(context)
|
||||
|
||||
# Adjust gap.
|
||||
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
|
||||
stored_phase = self._custom_modifier_event(context, event, "ARRAY",
|
||||
cursor='MUTE', store_values=True)
|
||||
|
||||
if self.phase == "ARRAY":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.cached,
|
||||
self.cutter.center)
|
||||
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
self.cutter.center)
|
||||
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
|
||||
self.gap = 1 + (d * 0.2)
|
||||
|
||||
self.effects.update(self, "ARRAY_GAP")
|
||||
|
||||
|
||||
def event_flip(self, context, event):
|
||||
"""Modifier key for flipping the direction of extrusion."""
|
||||
|
||||
if event.type == 'F' and event.value == 'PRESS':
|
||||
if self.phase == 'EXTRUDE':
|
||||
self.flip_direction = not self.flip_direction
|
||||
|
||||
|
||||
def event_move(self, context, event):
|
||||
"""Modifier key for moving the shape."""
|
||||
|
||||
def _remove_move_phase_properties(self):
|
||||
del self._stored_cutter_location
|
||||
self.mouse.cached_3d = None
|
||||
|
||||
|
||||
if event.type == 'SPACE':
|
||||
stored_phase = self._custom_modifier_event(context, event, "MOVE",
|
||||
cursor='SCROLL_XY', restore_mouse=False,
|
||||
postprocess=_remove_move_phase_properties)
|
||||
|
||||
if self.phase == "MOVE":
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
# Project current mouse position onto the workplane.
|
||||
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
|
||||
self.mouse.current,
|
||||
(self.workplane.location, self.workplane.normal))
|
||||
if current_mouse_pos_3d is not None:
|
||||
if not hasattr(self, '_stored_cutter_location'):
|
||||
self.mouse.cached_3d = current_mouse_pos_3d.copy()
|
||||
self._stored_cutter_location = self.cutter.obj.location.copy()
|
||||
|
||||
offset = current_mouse_pos_3d - self.mouse.cached_3d
|
||||
self.cutter.obj.location = self._stored_cutter_location + offset
|
||||
@@ -1,40 +1,59 @@
|
||||
import bpy
|
||||
import math
|
||||
|
||||
|
||||
# Import Custom Icons
|
||||
from ... import icons
|
||||
svg_icons = icons.svg_icons["main"]
|
||||
icon_measure = svg_icons["MEASURE"].icon_id
|
||||
icon_cpu = svg_icons["CPU"].icon_id
|
||||
from ...ui.icons import (
|
||||
get_custom_icon,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PROPERTIES ------------------------------ ####
|
||||
|
||||
class CarverPropsOperator():
|
||||
|
||||
# NOTE: Defining enum items inside a function is necessary to allow correct import of custom icons.
|
||||
# Without this, classes (and their properties) are registered before `register` function of modules
|
||||
# are called, resulting in empty preview collections.
|
||||
def get_depth_items(self, context):
|
||||
icon_measure = get_custom_icon("MEASURE")
|
||||
icon_cpu = get_custom_icon("CPU")
|
||||
|
||||
items = [
|
||||
('MANUAL', "Manual", "Depth can be manually set after creating a cutter shape", icon_measure, 0),
|
||||
('AUTO', "Auto", "Depth is set automatically to cover selected objects entirely", icon_cpu, 1),
|
||||
('CURSOR', "3D Cursor", "Depth is set to 3D cursors location", 'PIVOT_CURSOR', 2),
|
||||
]
|
||||
return items
|
||||
|
||||
# OPERATOR-properties
|
||||
mode: bpy.props.EnumProperty(
|
||||
name = "Mode",
|
||||
items = (('DESTRUCTIVE', "Destructive",
|
||||
"Boolean cutters are immediatelly applied and removed after the cut", 'MESH_DATA', 0),
|
||||
"Boolean cutters are immediatelly applied and removed after the cut",
|
||||
'MESH_DATA', 0),
|
||||
('MODIFIER', "Modifier",
|
||||
"Cuts are stored as boolean modifiers and cutters are placed inside the collection", 'MODIFIER_DATA', 1)),
|
||||
"Cuts are stored as Boolean modifiers and cutters are placed inside the collection", 'MODIFIER_DATA', 1)),
|
||||
default = 'MODIFIER',
|
||||
)
|
||||
alignment: bpy.props.EnumProperty(
|
||||
name = "Alignment",
|
||||
items = (('SURFACE', "Surface", "Align cutters to the surface normal of the mesh under the mouse", 'SNAP_NORMAL', 0),
|
||||
('VIEW', "View", "Align cutters to the current view", 'VIEW_CAMERA_UNSELECTED', 1),
|
||||
('CURSOR', "3D Cursor", "Align cutters to the 3D cursor orientation", 'ORIENTATION_CURSOR', 2),
|
||||
('GRID', "Grid", "Align cutters to the world grid", 'GRID', 3)),
|
||||
items = (('SURFACE', "Surface",
|
||||
"Align cutters to the surface normal of the mesh under the mouse",
|
||||
'SNAP_NORMAL', 0),
|
||||
('VIEW', "View",
|
||||
"Align cutters to the current view",
|
||||
'VIEW_CAMERA_UNSELECTED', 1),
|
||||
('CURSOR', "3D Cursor",
|
||||
"Align cutters to the 3D cursor orientation",
|
||||
'ORIENTATION_CURSOR', 2),
|
||||
('GRID', "Grid",
|
||||
"Align cutters to the world grid",
|
||||
'GRID', 3)),
|
||||
default = 'SURFACE',
|
||||
)
|
||||
depth: bpy.props.EnumProperty(
|
||||
name = "Depth",
|
||||
items = (('MANUAL', "Manual", "Depth can be manually set after creating a cutter shape", icon_measure, 0),
|
||||
('AUTO', "Auto", "Depth is set automatically to cover selected objects entirely", icon_cpu, 1),
|
||||
('CURSOR', "3D Cursor", "Depth is set to 3D cursors location", 'PIVOT_CURSOR', 2)),
|
||||
default = 'MANUAL',
|
||||
items = get_depth_items,
|
||||
default = 0,
|
||||
)
|
||||
|
||||
|
||||
@@ -72,7 +91,7 @@ class CarverPropsShape():
|
||||
flip_direction: bpy.props.BoolProperty(
|
||||
name = "Flip Direction",
|
||||
description = "Change which way the geometry is extruded",
|
||||
options = {'SKIP_SAVE', 'HIDDEN', 'SKIP_PRESET', },
|
||||
options = {'SKIP_SAVE', 'HIDDEN'},
|
||||
default = False,
|
||||
)
|
||||
|
||||
@@ -88,7 +107,7 @@ class CarverPropsModifier():
|
||||
)
|
||||
pin: bpy.props.BoolProperty(
|
||||
name = "Pin Boolean Modifier",
|
||||
description = ("Boolean modifier will be placed first in modifier stack, above other modifier (if there are any).\n"
|
||||
description = ("Always make new Boolean modifiers first in the modifier stack.\n"
|
||||
"NOTE: Order of modifiers can drastically affect the result (especially in destructive mode)"),
|
||||
default = True,
|
||||
)
|
||||
@@ -98,13 +117,13 @@ class CarverPropsCutter():
|
||||
# CUTTER-properties
|
||||
hide: bpy.props.BoolProperty(
|
||||
name = "Hide Cutter",
|
||||
description = ("Hide cutter objects in the viewport after they're created."),
|
||||
description = ("Hide cutter objects in the viewport after they are created."),
|
||||
default = True,
|
||||
)
|
||||
parent: bpy.props.BoolProperty(
|
||||
name = "Parent to Canvas",
|
||||
description = ("Cutters will be parented to active object being cut, even if cutting multiple objects.\n"
|
||||
"If there is no active object in selection cutters parent might be chosen seemingly randomly"),
|
||||
description = ("Parent new cutters to active object (even if they are cutting multiple objects).\n"
|
||||
"If there is no active object in the selection, parent might be chosen randomly"),
|
||||
default = True,
|
||||
)
|
||||
display: bpy.props.EnumProperty(
|
||||
@@ -115,24 +134,29 @@ class CarverPropsCutter():
|
||||
)
|
||||
cutter_origin: bpy.props.EnumProperty(
|
||||
name = "Cutter Origin Point",
|
||||
items = (('CENTER_OBJ', "Bounding Box", "Put the object origin at the center of the cutters bounding box"),
|
||||
('CENTER_MESH', "Geometry", "Put the object origin at the center of the cutters geometry (not including effects)"),
|
||||
('FACE_CENTER', "First Face", "Put the object origin at the center of cutters first face (i.e. shape)"),
|
||||
('MOUSE_INITIAL', "Mouse Click", "Put the object origin at the point where mouse was first clicked"),
|
||||
('CANVAS', "Same as Canvas", "Put the object origin of the cutter to the origin point of the cutter")),
|
||||
items = (('CENTER_OBJ', "Bounding Box",
|
||||
"Put the object origin at the center of the cutters bounding box"),
|
||||
('CENTER_MESH', "Geometry",
|
||||
"Put the object origin at the center of the cutters geometry (not including effects)"),
|
||||
('FACE_CENTER', "First Face",
|
||||
"Put the object origin at the center of cutters first face (i.e. shape)"),
|
||||
('MOUSE_INITIAL', "Mouse Click",
|
||||
"Put the object origin at the point where mouse was first clicked"),
|
||||
('CANVAS', "Same as Canvas",
|
||||
"Put the object origin of the cutter to the origin point of the cutter")),
|
||||
default = 'CENTER_MESH',
|
||||
)
|
||||
|
||||
auto_smooth: bpy.props.BoolProperty(
|
||||
name = "Shade Auto Smooth",
|
||||
description = ("Cutter object will be shaded smooth with sharp edges (above specified degrees) marked as sharp\n"
|
||||
"NOTE: This is a one time operator. 'Smooth by Angle' modifier will not be added on cutter"),
|
||||
description = ("Add 'Smooth by Angle' modifier to cutter object"),
|
||||
default = True,
|
||||
)
|
||||
sharp_angle: bpy.props.FloatProperty(
|
||||
name = "Angle",
|
||||
description = "Maximum face angle for sharp edges",
|
||||
subtype = "ANGLE",
|
||||
precision = 3,
|
||||
min = 0, max = math.pi,
|
||||
default = 0.523599,
|
||||
)
|
||||
@@ -142,13 +166,13 @@ class CarverPropsArray():
|
||||
# ARRAY-properties
|
||||
rows: bpy.props.IntProperty(
|
||||
name = "Rows",
|
||||
description = "Number of times shape is duplicated horizontally",
|
||||
description = "Number of times the shape is duplicated horizontally",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
columns: bpy.props.IntProperty(
|
||||
name = "Columns",
|
||||
description = "Number of times shape is duplicated vertically",
|
||||
description = "Number of times the shape is duplicated vertically",
|
||||
min = 1, soft_max = 16,
|
||||
default = 1,
|
||||
)
|
||||
|
||||
@@ -9,13 +9,14 @@ from ...functions.mesh import (
|
||||
)
|
||||
from ...functions.modifier import (
|
||||
add_modifier_asset,
|
||||
update_modifier_input,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ CLASSES ------------------------------ ####
|
||||
|
||||
class Selection:
|
||||
"""Storage of viable selected and active object(s) throughout the modal."""
|
||||
"""Storage of viable cannvas objects and their Boolean modifiers."""
|
||||
|
||||
def __init__(self, selected, active):
|
||||
self.selected: list = selected
|
||||
@@ -24,10 +25,7 @@ class Selection:
|
||||
|
||||
|
||||
class Mouse:
|
||||
"""
|
||||
Mouse positions throughout different phases of the modal operator.
|
||||
Each class variable is a 2D vector in screen space (x, y).
|
||||
"""
|
||||
"""Mouse positions throughout different phases of the modal operator."""
|
||||
|
||||
def __init__(self):
|
||||
self.initial = Vector()
|
||||
@@ -99,17 +97,17 @@ class Effects:
|
||||
return self
|
||||
|
||||
def update(self, cls, effect):
|
||||
"""Update bevel modifier during modal."""
|
||||
"""Update cutter modifiers during modal."""
|
||||
|
||||
# Update array count.
|
||||
if effect == 'ARRAY_COUNT':
|
||||
if effect == "ARRAY_COUNT":
|
||||
if self.array is None:
|
||||
self.add_array_modifier(cls)
|
||||
|
||||
else:
|
||||
if cls.columns > 1 or cls.rows > 1:
|
||||
self.array["Socket_2"] = cls.columns
|
||||
self.array["Socket_3"] = cls.rows
|
||||
update_modifier_input(self.array, "Socket_2", cls.columns)
|
||||
update_modifier_input(self.array, "Socket_3", cls.rows)
|
||||
|
||||
# Remove modifier if it's no longer needed.
|
||||
if cls.columns == 1 and cls.rows == 1:
|
||||
@@ -117,12 +115,12 @@ class Effects:
|
||||
self.array = None
|
||||
|
||||
# Update array gap.
|
||||
if effect == 'ARRAY_GAP':
|
||||
if cls.columns > 1 or cls.row > 1:
|
||||
if effect == "ARRAY_GAP":
|
||||
if cls.columns > 1 or cls.rows > 1:
|
||||
if self.array is not None:
|
||||
self.array["Socket_4"] = cls.gap
|
||||
update_modifier_input(self.array, "Socket_4", cls.gap)
|
||||
|
||||
# Force the modifier to update in viewport.
|
||||
# NOTE: Force the modifier to update in viewport when key is pressed.
|
||||
self.array.show_viewport = False
|
||||
self.array.show_viewport = True
|
||||
|
||||
@@ -150,15 +148,16 @@ class Effects:
|
||||
|
||||
# Columns
|
||||
if cls.columns > 1:
|
||||
mod["Socket_2"] = cls.columns
|
||||
update_modifier_input(mod, "Socket_2", cls.columns)
|
||||
|
||||
# Rows
|
||||
if cls.rows > 1:
|
||||
mod["Socket_3"] = cls.rows
|
||||
update_modifier_input(mod, "Socket_3", cls.rows)
|
||||
|
||||
# Gap
|
||||
mod["Socket_4"] = cls.gap
|
||||
update_modifier_input(mod, "Socket_4", cls.gap)
|
||||
|
||||
mod.use_pin_to_last = True
|
||||
self.array = mod
|
||||
|
||||
|
||||
@@ -233,7 +232,7 @@ class Effects:
|
||||
|
||||
# Smooth by Angle
|
||||
def add_auto_smooth_modifier(self, cls, context):
|
||||
"""Adds a 'Smooth by Angle' modifier on cutter object, a.k.a. Auto Smooth."""
|
||||
"""Adds a 'Smooth by Angle' modifier on the cutter object (a.k.a. Auto Smooth)."""
|
||||
|
||||
obj = cls.cutter.obj
|
||||
mesh = cls.cutter.mesh
|
||||
@@ -262,7 +261,7 @@ class Effects:
|
||||
|
||||
mod = obj.modifiers.active
|
||||
|
||||
# Try loading the node group manually if `bpy.ops` operators fail.
|
||||
# Try loading the node group manually if `bpy.ops` operators failed.
|
||||
if mod is None:
|
||||
dir = os.path.join(os.path.dirname(bpy.app.binary_path), "5.0", "datafiles", "assets")
|
||||
assets_path = os.path.join(dir, modifier_asset_file)
|
||||
@@ -274,13 +273,13 @@ class Effects:
|
||||
print("Destructively marking sharp edges and smooth faces in the mesh")
|
||||
shade_smooth_by_angle(bm, mesh, angle=math.degrees(cls.sharp_angle))
|
||||
else:
|
||||
# Set smoothing angle.
|
||||
# Set smoothing angle (necessary for all methods except `shade_auto_smooth()`).
|
||||
for face in bm.faces:
|
||||
face.smooth = True
|
||||
bm.to_mesh(mesh)
|
||||
|
||||
mod.use_pin_to_last = True
|
||||
mod["Input_1"] = cls.sharp_angle
|
||||
update_modifier_input(mod, "Input_1", cls.sharp_angle)
|
||||
|
||||
self.smooth = mod
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
from ... import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ /toolbar/ ------------------------------ ####
|
||||
@@ -14,7 +13,7 @@ def carver_ui_common(context, layout, props):
|
||||
layout.prop(props, "solver", expand=True)
|
||||
|
||||
else:
|
||||
# Use labels for Properties editor/sidebar.
|
||||
# NOTE: Use labels for Properties editor/sidebar.
|
||||
layout.prop(props, "mode", text="Mode")
|
||||
layout.prop(props, "alignment", text="Alignment")
|
||||
layout.prop(props, "depth", text="Depth")
|
||||
@@ -43,20 +42,17 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode)
|
||||
props = tool.operator_properties(tool.idname)
|
||||
|
||||
# Box & Circle
|
||||
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
|
||||
if tool.idname == "object.carve_box":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
else:
|
||||
props = tool.operator_properties("object.carve_circle")
|
||||
|
||||
if tool.idname == "object.carve_circle":
|
||||
layout.prop(props, "subdivision", text="Vertices")
|
||||
layout.prop(props, "rotation")
|
||||
layout.prop(props, "aspect", expand=True)
|
||||
layout.prop(props, "origin", expand=True)
|
||||
layout.prop(props, "aspect", expand=True)
|
||||
|
||||
if props.alignment == 'SURFACE':
|
||||
layout.prop(props, "orientation")
|
||||
@@ -67,7 +63,6 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
|
||||
|
||||
# Polyline
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
if props.alignment == 'SURFACE':
|
||||
layout.prop(props, "offset", text="Offset")
|
||||
layout.prop(props, "align_to_all")
|
||||
@@ -85,13 +80,9 @@ class TOPBAR_PT_carver_effects(bpy.types.Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
if tool.idname == "object.carve_box":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
elif tool.idname == "object.carve_circle":
|
||||
props = tool.operator_properties("object.carve_circle")
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode)
|
||||
props = tool.operator_properties(tool.idname)
|
||||
|
||||
# Bevel
|
||||
if tool.idname == 'object.carve_box':
|
||||
@@ -116,6 +107,7 @@ class TOPBAR_PT_carver_effects(bpy.types.Panel):
|
||||
col.prop(props, "rows")
|
||||
col.prop(props, "gap")
|
||||
|
||||
|
||||
class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
bl_label = "Carver Cutter"
|
||||
bl_idname = "TOPBAR_PT_carver_cutter"
|
||||
@@ -128,25 +120,25 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False
|
||||
|
||||
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
|
||||
if tool.idname == "object.carve_box":
|
||||
props = tool.operator_properties("object.carve_box")
|
||||
elif tool.idname == "object.carve_circle":
|
||||
props = tool.operator_properties("object.carve_circle")
|
||||
elif tool.idname == "object.carve_polyline":
|
||||
props = tool.operator_properties("object.carve_polyline")
|
||||
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
|
||||
tool = context.workspace.tools.from_space_view3d_mode(mode)
|
||||
props = tool.operator_properties(tool.idname)
|
||||
|
||||
# modifier_&_cutter
|
||||
# Modifier properties.
|
||||
col = layout.column()
|
||||
col.prop(props, "pin", text="Pin Modifier")
|
||||
col.separator()
|
||||
|
||||
# Cutter object properties.
|
||||
col = layout.column()
|
||||
row = col.row()
|
||||
row.prop(props, "display", text="Display", expand=True)
|
||||
col.prop(props, "pin", text="Pin Modifier")
|
||||
if props.mode == 'MODIFIER':
|
||||
col.prop(props, "parent")
|
||||
col.prop(props, "hide")
|
||||
col.prop(props, "cutter_origin", text="Origin")
|
||||
|
||||
# auto_smooth
|
||||
# Auto Smooth.
|
||||
layout.separator()
|
||||
col = layout.column(align=True)
|
||||
col.prop(props, "auto_smooth", text="Auto Smooth")
|
||||
@@ -159,11 +151,11 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = [
|
||||
classes = (
|
||||
TOPBAR_PT_carver_shape,
|
||||
TOPBAR_PT_carver_effects,
|
||||
TOPBAR_PT_carver_cutter,
|
||||
]
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import bpy
|
||||
from .functions.poll import is_canvas
|
||||
from .functions.list import list_canvas_cutters
|
||||
|
||||
|
||||
#### ------------------------------ /ui/ ------------------------------ ####
|
||||
|
||||
def carve_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("object.carve_box", text="Box Carve").shape='BOX'
|
||||
layout.operator("object.carve_box", text="Circle Carve").shape='CIRCLE'
|
||||
layout.operator("object.carve_polyline", text="Polyline Carve")
|
||||
|
||||
|
||||
def boolean_operators_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
col.label(text="Auto Boolean")
|
||||
col.operator("object.boolean_auto_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_auto_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_auto_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_auto_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
col.separator()
|
||||
col.label(text="Brush Boolean")
|
||||
col.operator("object.boolean_brush_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_brush_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_brush_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_brush_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
|
||||
def boolean_extras_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
if context.active_object:
|
||||
# Canvas operators
|
||||
active_object = context.active_object
|
||||
if active_object.booleans.canvas == True and any(mod.name.startswith("boolean_") for mod in active_object.modifiers):
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_all", text="Toggle All Cuters")
|
||||
col.operator("object.boolean_apply_all", text="Apply All Cutters")
|
||||
col.operator("object.boolean_remove_all", text="Remove All Cutters")
|
||||
|
||||
# Cutter operators
|
||||
if active_object.booleans.cutter:
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_cutter", text="Toggle Cutter").method='ALL'
|
||||
col.operator("object.boolean_apply_cutter", text="Apply Cutter").method='ALL'
|
||||
col.operator("object.boolean_remove_cutter", text="Remove Cutter").method='ALL'
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ PANELS ------------------------------ ####
|
||||
|
||||
# Boolean Operators Panel
|
||||
class VIEW3D_PT_boolean(bpy.types.Panel):
|
||||
bl_label = "Boolean"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
return prefs.show_in_sidebar
|
||||
|
||||
def draw(self, context):
|
||||
boolean_operators_menu(self, context)
|
||||
|
||||
|
||||
# Properties Panel
|
||||
class VIEW3D_PT_boolean_properties(bpy.types.Panel):
|
||||
bl_label = "Properties"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
if prefs.show_in_sidebar:
|
||||
if context.active_object:
|
||||
if is_canvas(context.active_object) or context.active_object.booleans.cutter:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
# Cutters Panel
|
||||
class VIEW3D_PT_boolean_cutters(bpy.types.Panel):
|
||||
bl_label = "Cutters"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[__package__].preferences
|
||||
if prefs.show_in_sidebar:
|
||||
if context.active_object:
|
||||
if is_canvas(context.active_object):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
canvas = context.active_object
|
||||
__, modifiers = list_canvas_cutters([canvas])
|
||||
|
||||
for mod in modifiers:
|
||||
col = layout.column(align=True)
|
||||
row = col.row(align=True)
|
||||
|
||||
# icon
|
||||
if mod.operation == 'DIFFERENCE':
|
||||
icon = 'SELECT_SUBTRACT'
|
||||
elif mod.operation == 'UNION':
|
||||
icon = 'SELECT_EXTEND'
|
||||
elif mod.operation == 'INTERSECT':
|
||||
icon = 'SELECT_INTERSECT'
|
||||
|
||||
row.prop(mod.object, "name", text="", icon=icon)
|
||||
|
||||
# Toggle
|
||||
op_toggle = row.operator("object.boolean_toggle_cutter", text="", icon='HIDE_OFF' if mod.show_viewport else 'HIDE_ON')
|
||||
op_toggle.method = 'SPECIFIED'
|
||||
op_toggle.specified_cutter = mod.object.name
|
||||
op_toggle.specified_canvas = canvas.name
|
||||
|
||||
# Apply
|
||||
op_apply = row.operator("object.boolean_apply_cutter", text="", icon='CHECKMARK')
|
||||
op_apply.method = 'SPECIFIED'
|
||||
op_apply.specified_cutter = mod.object.name
|
||||
op_apply.specified_canvas = canvas.name
|
||||
|
||||
# Remove
|
||||
op_remove = row.operator("object.boolean_remove_cutter", text="", icon='X')
|
||||
op_remove.method = 'SPECIFIED'
|
||||
op_remove.specified_cutter = mod.object.name
|
||||
op_remove.specified_canvas = canvas.name
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ MENUS ------------------------------ ####
|
||||
|
||||
# Carve Menu
|
||||
class VIEW3D_MT_carve(bpy.types.Menu):
|
||||
bl_label = "Carve"
|
||||
bl_idname = "VIEW3D_MT_carve"
|
||||
|
||||
def draw(self, context):
|
||||
carve_menu(self, context)
|
||||
|
||||
|
||||
# 3D Viewport (Object Mode) -> Object
|
||||
class VIEW3D_MT_boolean(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
layout.separator()
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
# Shift-Ctrl-B Menu
|
||||
class VIEW3D_MT_boolean_popup(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean_popup"
|
||||
|
||||
def draw(self, context):
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
def object_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_boolean")
|
||||
|
||||
|
||||
def edit_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
|
||||
|
||||
def boolean_select_menu(self, context):
|
||||
layout = self.layout
|
||||
active_obj = context.active_object
|
||||
if active_obj:
|
||||
if active_obj.booleans.canvas == True or active_obj.booleans.cutter:
|
||||
layout.separator()
|
||||
|
||||
if active_obj.booleans.canvas == True:
|
||||
layout.operator("object.boolean_select_all", text="Boolean Cutters")
|
||||
if active_obj.booleans.cutter:
|
||||
layout.operator("object.select_cutter_canvas", text="Boolean Canvases")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = [
|
||||
VIEW3D_MT_carve,
|
||||
VIEW3D_MT_boolean,
|
||||
VIEW3D_MT_boolean_popup,
|
||||
VIEW3D_PT_boolean,
|
||||
VIEW3D_PT_boolean_properties,
|
||||
VIEW3D_PT_boolean_cutters,
|
||||
]
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.append(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.append(boolean_select_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.append(edit_mode_menu)
|
||||
|
||||
# KEYMAP
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Object Mode")
|
||||
|
||||
kmi = km.keymap_items.new("wm.call_menu", 'B', 'PRESS', ctrl=True, shift=True)
|
||||
kmi.properties.name = "VIEW3D_MT_boolean_popup"
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.remove(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.remove(boolean_select_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.remove(edit_mode_menu)
|
||||
|
||||
# KEYMAP
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
@@ -0,0 +1,34 @@
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
for mod in [icons,
|
||||
lists,
|
||||
menus,
|
||||
panels,
|
||||
]:
|
||||
importlib.reload(mod)
|
||||
else:
|
||||
import bpy
|
||||
from . import (
|
||||
icons,
|
||||
lists,
|
||||
menus,
|
||||
panels,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
modules = (
|
||||
icons,
|
||||
lists,
|
||||
menus,
|
||||
panels,
|
||||
)
|
||||
|
||||
def register():
|
||||
for module in modules:
|
||||
module.register()
|
||||
|
||||
def unregister():
|
||||
for module in reversed(modules):
|
||||
module.unregister()
|
||||
@@ -0,0 +1,96 @@
|
||||
import bpy
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
from ..functions.modifier import (
|
||||
enumerate_boolean_modifiers,
|
||||
is_boolean_modifier,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def get_modifier_from_list_index(obj, index: int):
|
||||
"""
|
||||
Returns the modifier of an object based on Cutters list index.
|
||||
Filters out non-Boolean modifiers to leave a list that matches Cutters one in length.
|
||||
"""
|
||||
|
||||
# Create a list of only Boolean modifiers.
|
||||
boolean_modifiers = []
|
||||
for mod in obj.modifiers:
|
||||
if is_boolean_modifier(mod):
|
||||
boolean_modifiers.append(mod)
|
||||
|
||||
if 0 <= index < len(boolean_modifiers):
|
||||
return boolean_modifiers[index]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@contextmanager
|
||||
def preserve_list_index(prop_owner, index_prop: str):
|
||||
"""Preserves index of active item in UI list."""
|
||||
|
||||
index = getattr(prop_owner, index_prop, 0)
|
||||
stored_index = index
|
||||
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
new_index = stored_index - 1
|
||||
|
||||
if new_index < 0:
|
||||
new_index = 0
|
||||
|
||||
setattr(prop_owner, index_prop, new_index)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /ui/ ------------------------------ ####
|
||||
|
||||
def boolean_operators_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
col.label(text="Auto Boolean")
|
||||
col.operator("object.boolean_auto_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_auto_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_auto_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_auto_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
col.separator()
|
||||
col.label(text="Brush Boolean")
|
||||
col.operator("object.boolean_brush_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
col.operator("object.boolean_brush_union", text="Union", icon='SELECT_EXTEND')
|
||||
col.operator("object.boolean_brush_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
col.operator("object.boolean_brush_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
|
||||
|
||||
def boolean_extras_menu(self, context, cutter_only=False):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
obj = context.active_object
|
||||
|
||||
if not obj:
|
||||
return
|
||||
|
||||
# Canvas operators
|
||||
if is_canvas(obj) and not cutter_only:
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_all", text="Toggle All Cuters")
|
||||
col.operator("object.boolean_apply_all", text="Apply All Cutters")
|
||||
col.operator("object.boolean_remove_all", text="Remove All Cutters")
|
||||
|
||||
# Cutter operators
|
||||
if obj.booleans.cutter:
|
||||
if not cutter_only:
|
||||
col.separator()
|
||||
col.operator("object.boolean_toggle_cutter", text="Toggle Cutter").method='ALL'
|
||||
col.operator("object.boolean_apply_cutter", text="Apply Cutter").method='ALL'
|
||||
col.operator("object.boolean_remove_cutter", text="Remove Cutter").method='ALL'
|
||||
@@ -0,0 +1,46 @@
|
||||
import bpy
|
||||
import os
|
||||
import bpy.utils.previews
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def get_custom_icon(icon_name: str):
|
||||
"""Returns the ID of a custom icon registered by add-on."""
|
||||
|
||||
svg_icons = preview_collections["main"]
|
||||
if not svg_icons:
|
||||
return 0
|
||||
|
||||
icon = svg_icons[icon_name].icon_id
|
||||
if not icon:
|
||||
return 0
|
||||
|
||||
return icon
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
preview_collections = {}
|
||||
svg_icons_dir = os.path.join(os.path.dirname(__file__), "svg")
|
||||
|
||||
icons = {
|
||||
"MEASURE": "measure.svg",
|
||||
"CPU": "cpu.svg",
|
||||
}
|
||||
|
||||
|
||||
def register():
|
||||
svg_icons = bpy.utils.previews.new()
|
||||
|
||||
for key, file in icons.items():
|
||||
svg_icons.load(key, os.path.join(svg_icons_dir, file), 'IMAGE')
|
||||
|
||||
preview_collections["main"] = svg_icons
|
||||
|
||||
|
||||
def unregister():
|
||||
for pcoll in preview_collections.values():
|
||||
bpy.utils.previews.remove(pcoll)
|
||||
preview_collections.clear()
|
||||
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,92 @@
|
||||
import bpy
|
||||
|
||||
from ..functions.modifier import (
|
||||
is_boolean_modifier,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /cutters_list/ ------------------------------ ####
|
||||
|
||||
class VIEW3D_UL_boolean_cutters(bpy.types.UIList):
|
||||
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
canvas = context.active_object
|
||||
mod = item
|
||||
|
||||
# Pick Icon
|
||||
if mod.operation == 'DIFFERENCE':
|
||||
icon = 'SELECT_SUBTRACT'
|
||||
elif mod.operation == 'UNION':
|
||||
icon = 'SELECT_EXTEND'
|
||||
elif mod.operation == 'INTERSECT':
|
||||
icon = 'SELECT_INTERSECT'
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(mod.object, "name", text="", icon=icon, emboss=False)
|
||||
|
||||
# Select Cutter
|
||||
op_select = row.operator("object.boolean_select_cutter", text="", icon='RESTRICT_SELECT_OFF', emboss=False)
|
||||
op_select.cutter = mod.object.name
|
||||
|
||||
# Toggle Cutter
|
||||
icon = 'HIDE_OFF' if mod.show_viewport else 'HIDE_ON'
|
||||
op_toggle = row.operator("object.boolean_toggle_cutter", text="", icon=icon, emboss=False)
|
||||
op_toggle.method = 'SPECIFIED'
|
||||
op_toggle.specified_cutter = mod.object.name
|
||||
op_toggle.specified_canvas = canvas.name
|
||||
op_toggle.specified_modifier = mod.name
|
||||
|
||||
|
||||
def filter_items(self, context, data, propname):
|
||||
flags = []
|
||||
indices = []
|
||||
|
||||
modifiers = getattr(data, propname)
|
||||
for mod in modifiers:
|
||||
if is_boolean_modifier(mod):
|
||||
flags.append(self.bitflag_filter_item)
|
||||
else:
|
||||
flags.append(0)
|
||||
|
||||
# Search Filter
|
||||
if self.filter_name:
|
||||
filter_name = self.filter_name.lower()
|
||||
for i, mod in enumerate(modifiers):
|
||||
if flags[i] != self.bitflag_filter_item:
|
||||
continue
|
||||
if filter_name not in mod.object.name.lower():
|
||||
flags[i] = 0
|
||||
|
||||
# Invert
|
||||
if self.use_filter_invert:
|
||||
for i, mod in enumerate(modifiers):
|
||||
if not is_boolean_modifier(mod):
|
||||
continue
|
||||
flags[i] ^= self.bitflag_filter_item
|
||||
|
||||
# Sort by Name
|
||||
indices = list(range(len(modifiers)))
|
||||
if self.use_filter_sort_alpha:
|
||||
sorted_indices = sorted(range(len(modifiers)),
|
||||
key=lambda i: modifiers[i].object.name if modifiers[i].object else "")
|
||||
indices = [0] * len(modifiers)
|
||||
for rank, original_i in enumerate(sorted_indices):
|
||||
indices[original_i] = rank
|
||||
|
||||
return flags, indices
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = (
|
||||
VIEW3D_UL_boolean_cutters,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -0,0 +1,129 @@
|
||||
import bpy
|
||||
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
boolean_operators_menu,
|
||||
boolean_extras_menu,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ /boolean_menu/ ------------------------------ ####
|
||||
|
||||
# 3D Viewport (Object Mode) -> Object
|
||||
class VIEW3D_MT_boolean(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
layout.separator()
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
def object_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_boolean")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /popup_menu/ ------------------------------ ####
|
||||
|
||||
# Shift-Ctrl-B Menu
|
||||
class VIEW3D_MT_boolean_popup(bpy.types.Menu):
|
||||
bl_label = "Boolean"
|
||||
bl_idname = "VIEW3D_MT_boolean_popup"
|
||||
|
||||
def draw(self, context):
|
||||
boolean_operators_menu(self, context)
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /carve_menu/ ------------------------------ ####
|
||||
|
||||
class VIEW3D_MT_carve(bpy.types.Menu):
|
||||
bl_label = "Carve"
|
||||
bl_idname = "VIEW3D_MT_carve"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("object.carve_box", text="Box Carve")
|
||||
layout.operator("object.carve_circle", text="Circle Carve")
|
||||
layout.operator("object.carve_polyline", text="Polyline Carve")
|
||||
|
||||
|
||||
# Separate "Carve" menu in Edit Mode (where we don't have "Boolean" menu).
|
||||
def edit_mode_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu("VIEW3D_MT_carve")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ /select_menu/ ------------------------------ ####
|
||||
|
||||
def select_menu(self, context):
|
||||
layout = self.layout
|
||||
obj = context.active_object
|
||||
|
||||
if not obj:
|
||||
return
|
||||
|
||||
if is_canvas(obj) or obj.booleans.cutter:
|
||||
layout.separator()
|
||||
|
||||
if is_canvas(obj):
|
||||
layout.operator("object.boolean_select_all", text="Boolean Cutters")
|
||||
if obj.booleans.cutter:
|
||||
layout.operator("object.select_cutter_canvas", text="Boolean Canvases")
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
classes = (
|
||||
VIEW3D_MT_boolean,
|
||||
VIEW3D_MT_boolean_popup,
|
||||
VIEW3D_MT_carve,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.append(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.append(edit_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.append(select_menu)
|
||||
|
||||
# KEYMAP
|
||||
addon = bpy.context.window_manager.keyconfigs.addon
|
||||
km = addon.keymaps.new(name="Object Mode")
|
||||
|
||||
kmi = km.keymap_items.new("wm.call_menu", 'B', 'PRESS', ctrl=True, shift=True)
|
||||
kmi.properties.name = "VIEW3D_MT_boolean_popup"
|
||||
kmi.active = True
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# MENU
|
||||
bpy.types.VIEW3D_MT_object.remove(object_mode_menu)
|
||||
bpy.types.VIEW3D_MT_edit_mesh.remove(edit_mode_menu)
|
||||
bpy.types.VIEW3D_MT_select_object.remove(select_menu)
|
||||
|
||||
# KEYMAP
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
@@ -0,0 +1,182 @@
|
||||
import bpy
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.canvas import (
|
||||
is_canvas,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
get_modifier_from_list_index,
|
||||
boolean_extras_menu,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ PANELS ------------------------------ ####
|
||||
|
||||
# Boolean Operators Panel
|
||||
class VIEW3D_PT_boolean(bpy.types.Panel):
|
||||
bl_label = "Boolean"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
return prefs.show_in_sidebar
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator_context = 'INVOKE_DEFAULT'
|
||||
col = layout.column(align=True)
|
||||
|
||||
col.label(text="Auto Boolean")
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
row.operator("object.boolean_auto_difference", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_union", text="Union", icon='SELECT_EXTEND')
|
||||
row.operator("object.boolean_auto_union", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
row.operator("object.boolean_auto_intersect", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_auto_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
row.operator("object.boolean_auto_slice", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
|
||||
col.separator()
|
||||
col.label(text="Brush Boolean")
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_difference", text="Difference", icon='SELECT_SUBTRACT')
|
||||
row.operator("object.boolean_brush_difference", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_union", text="Union", icon='SELECT_EXTEND')
|
||||
row.operator("object.boolean_brush_union", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_intersect", text="Intersect", icon='SELECT_INTERSECT')
|
||||
row.operator("object.boolean_brush_intersect", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
row = col.row(align=False)
|
||||
row.operator("object.boolean_brush_slice", text="Slice", icon='SELECT_DIFFERENCE')
|
||||
row.operator("object.boolean_brush_slice", text="", icon='UV_SYNC_SELECT').flip=True
|
||||
|
||||
|
||||
# Cutters Panel
|
||||
class VIEW3D_PT_boolean_cutters(bpy.types.Panel):
|
||||
bl_label = "Cutters"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
if prefs.show_in_sidebar:
|
||||
if context.active_object:
|
||||
if is_canvas(context.active_object):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
canvas = context.active_object
|
||||
|
||||
# Cutters List
|
||||
row = layout.row()
|
||||
col = row.column()
|
||||
col.template_list(
|
||||
"VIEW3D_UL_boolean_cutters",
|
||||
"",
|
||||
canvas, "modifiers",
|
||||
canvas.booleans, "modifiers_list_index",
|
||||
rows=5,
|
||||
)
|
||||
|
||||
# Filter & Operators
|
||||
col = row.column(align=True)
|
||||
cutters_list_index = canvas.booleans.modifiers_list_index
|
||||
mod = get_modifier_from_list_index(canvas, cutters_list_index)
|
||||
sub = col.column(align=True)
|
||||
|
||||
# Apply Cutter
|
||||
op_apply = sub.operator("object.boolean_apply_cutter", text="", icon='CHECKMARK')
|
||||
op_apply.method = 'SPECIFIED'
|
||||
op_apply.specified_cutter = mod.object.name if mod else ""
|
||||
op_apply.specified_canvas = canvas.name
|
||||
|
||||
# Remove Cutter
|
||||
op_remove = sub.operator("object.boolean_remove_cutter", text="", icon='X')
|
||||
op_remove.method = 'SPECIFIED'
|
||||
op_remove.specified_cutter = mod.object.name if mod else ""
|
||||
op_remove.specified_canvas = canvas.name
|
||||
op_remove.specified_modifier = mod.name if mod else ""
|
||||
|
||||
if cutters_list_index < 0 or not mod:
|
||||
sub.enabled = False
|
||||
|
||||
col.separator()
|
||||
col.menu("VIEW3D_MT_boolean_specials", icon='DOWNARROW_HLT', text="")
|
||||
|
||||
|
||||
# Helpers Panel
|
||||
class VIEW3D_PT_boolean_helpers(bpy.types.Panel):
|
||||
bl_label = "Helpers"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Edit"
|
||||
bl_context = "objectmode"
|
||||
bl_parent_id = "VIEW3D_PT_boolean"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
if not prefs.show_in_sidebar:
|
||||
return False
|
||||
if not context.active_object:
|
||||
return False
|
||||
if context.active_object.booleans.cutter:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def draw(self, context):
|
||||
boolean_extras_menu(self, context, cutter_only=True)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ MENUS ------------------------------ ####
|
||||
|
||||
# Specials
|
||||
class VIEW3D_MT_boolean_specials(bpy.types.Menu):
|
||||
bl_label = "Boolean Operators"
|
||||
bl_idname = "VIEW3D_MT_boolean_specials"
|
||||
|
||||
def draw(self, context):
|
||||
boolean_extras_menu(self, context)
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
classes = (
|
||||
VIEW3D_PT_boolean,
|
||||
VIEW3D_PT_boolean_cutters,
|
||||
VIEW3D_PT_boolean_helpers,
|
||||
VIEW3D_MT_boolean_specials,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,43 +0,0 @@
|
||||
import bpy
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def populate_boolean_properties(scene):
|
||||
prefs = bpy.context.preferences.addons[__package__].preferences
|
||||
if prefs.versioning:
|
||||
for obj in bpy.data.objects:
|
||||
if not obj.get("BoolToolRoot"):
|
||||
continue
|
||||
|
||||
# Convert Canvas
|
||||
if obj.get("BoolToolRoot"):
|
||||
obj.booleans.canvas = True
|
||||
del obj["BoolToolRoot"]
|
||||
if obj.get("BoolTool_FTransform"):
|
||||
del obj["BoolTool_FTransform"]
|
||||
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'BOOLEAN' and "BTool_" in mod.name:
|
||||
mod.name = "boolean_" + mod.object.name
|
||||
cutter = mod.object
|
||||
|
||||
# Convert Canvases
|
||||
if cutter.get("BoolToolBrush"):
|
||||
cutter.booleans.cutter = cutter.get("BoolToolBrush")
|
||||
del cutter["BoolToolBrush"]
|
||||
if cutter.get("BoolTool_FTransform"):
|
||||
del cutter["BoolTool_FTransform"]
|
||||
|
||||
|
||||
|
||||
#### ------------------------------ REGISTRATION ------------------------------ ####
|
||||
|
||||
def register():
|
||||
# HANDLERS
|
||||
bpy.app.handlers.load_post.append(populate_boolean_properties)
|
||||
|
||||
def unregister():
|
||||
# HANDLERS
|
||||
bpy.app.handlers.load_post.remove(populate_boolean_properties)
|
||||
Reference in New Issue
Block a user