update addons (for 5.2)
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user