2025-07-01
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
import bpy, gpu, mathutils, math
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
magic_number = 1.41
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def draw_shader(color, alpha, type, coords, size=1, indices=None):
|
||||
"""Creates a batch for a draw type"""
|
||||
|
||||
gpu.state.blend_set('ALPHA')
|
||||
|
||||
if type == 'POINTS':
|
||||
gpu.state.program_point_size_set(False)
|
||||
gpu.state.point_size_set(size)
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
shader.uniform_float("color", (color[0], color[1], color[2], alpha))
|
||||
batch = batch_for_shader(shader, 'POINTS', {"pos": coords}, indices=indices)
|
||||
|
||||
elif type in 'LINES':
|
||||
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
|
||||
shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
|
||||
shader.uniform_float("lineWidth", size)
|
||||
shader.uniform_float("color", (color[0], color[1], color[2], alpha))
|
||||
batch = batch_for_shader(shader, 'LINES', {"pos": coords}, indices=indices)
|
||||
|
||||
elif type in 'LINE_LOOP':
|
||||
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
|
||||
shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
|
||||
shader.uniform_float("lineWidth", size)
|
||||
shader.uniform_float("color", (color[0], color[1], color[2], alpha))
|
||||
batch = batch_for_shader(shader, 'LINE_LOOP', {"pos": coords})
|
||||
|
||||
if type == 'SOLID':
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
shader.uniform_float("color", (color[0], color[1], color[2], alpha))
|
||||
batch = batch_for_shader(shader, 'TRIS', {"pos": coords}, indices=indices)
|
||||
|
||||
if type == 'OUTLINE':
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
shader.uniform_float("color", (color[0], color[1], color[2], alpha))
|
||||
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": coords})
|
||||
gpu.state.line_width_set(size)
|
||||
|
||||
batch.draw(shader)
|
||||
gpu.state.point_size_set(1.0)
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def carver_overlay(self, context):
|
||||
"""Shape (rectangle, circle) overlay for carver tool"""
|
||||
|
||||
color = (0.48, 0.04, 0.04, 1.0)
|
||||
secondary_color = (0.28, 0.04, 0.04, 1.0)
|
||||
|
||||
if self.shape == 'CIRCLE':
|
||||
coords, indices, rows, columns = draw_circle(self, self.subdivision, 0)
|
||||
# coords = coords[1:] # remove_extra_vertex
|
||||
self.verts = coords
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
if not self.rotate:
|
||||
bounds, __, __ = get_bounding_box_coords(self, coords)
|
||||
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
|
||||
|
||||
|
||||
elif self.shape == 'BOX':
|
||||
coords, indices, rows, columns = draw_circle(self, 4, 45)
|
||||
self.verts = coords
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
if (self.rotate == False) and (self.bevel == False):
|
||||
bounds, __, __ = get_bounding_box_coords(self, coords)
|
||||
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
|
||||
|
||||
|
||||
elif self.shape == 'POLYLINE':
|
||||
coords, indices, first_point, rows, columns = draw_polygon(self)
|
||||
self.verts = list(dict.fromkeys(self.mouse_path))
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
|
||||
draw_shader(color, 1.0, 'LINE_LOOP' if self.closed else 'LINES', coords, size=2)
|
||||
draw_shader(color, 1.0, 'POINTS', coords, size=5)
|
||||
if self.closed and len(self.mouse_path) > 2:
|
||||
# polygon_fill
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
|
||||
if (self.closed and len(coords) > 3) or (self.closed == False and len(coords) > 4):
|
||||
# circle_around_first_point
|
||||
draw_shader(color, 0.8, 'OUTLINE', first_point, size=3)
|
||||
|
||||
|
||||
# Snapping Grid
|
||||
if self.snap and self.move == False:
|
||||
mini_grid(self, context)
|
||||
|
||||
# ARRAY
|
||||
array_shader = 'LINE_LOOP' if self.shape == 'POLYLINE' and self.closed == False else 'SOLID'
|
||||
if self.rows > 1:
|
||||
for i, duplicate in rows.items():
|
||||
draw_shader(secondary_color, 0.4, array_shader, duplicate, size=2, indices=indices[:-2])
|
||||
if self.columns > 1:
|
||||
for i, duplicate in columns.items():
|
||||
draw_shader(secondary_color, 0.4, array_shader, duplicate, size=2, indices=indices[:-2])
|
||||
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def draw_polygon(self):
|
||||
"""Returns polygonal 2d shape in which each cursor click is taken as a new vertice"""
|
||||
|
||||
indices = []
|
||||
coords = []
|
||||
for idx, vals in enumerate(self.mouse_path):
|
||||
vert = mathutils.Vector([vals[0], vals[1], 0.0])
|
||||
vert += mathutils.Vector([self.position_x, self.position_y, 0.0])
|
||||
coords.append(vert)
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx <= len(self.mouse_path) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
# circle_around_first_point
|
||||
radius = self.distance_from_first
|
||||
segments = 4
|
||||
|
||||
click_point = [coords[0]]
|
||||
for i in range(segments + 1):
|
||||
angle = i * (2 * math.pi / segments)
|
||||
x = coords[0][0] + radius * math.cos(angle)
|
||||
y = coords[0][1] + radius * math.sin(angle)
|
||||
z = coords[0][2]
|
||||
vector = mathutils.Vector((x, y, z))
|
||||
click_point.append(vector)
|
||||
|
||||
# remove_duplicate_verts
|
||||
# NOTE: This is needed to remove extra vertices for duplicates which are not removed because `dict.fromkeys()`...
|
||||
# NOTE: can't be called on `coords` list, because it contains unfrozen Vectors.
|
||||
unique_verts = []
|
||||
for vert in coords:
|
||||
if vert not in unique_verts:
|
||||
unique_verts.append(vert)
|
||||
|
||||
|
||||
# ARRAY
|
||||
rows = columns = {}
|
||||
if len(self.mouse_path) > 2:
|
||||
array_coords = unique_verts if self.closed else unique_verts[:-1]
|
||||
get_bounding_box_coords(self, array_coords)
|
||||
rows, columns = array(self, array_coords)
|
||||
|
||||
return coords, indices, click_point, rows, columns
|
||||
|
||||
|
||||
def draw_circle(self, subdivision, rotation):
|
||||
"""Returns the coordinates & indices of a circle using a triangle fan"""
|
||||
"""NOTE: Origin point code is duplicated on purpose (to experiment with different math easily)"""
|
||||
|
||||
def create_2d_circle(self, step, rotation):
|
||||
"""Create the vertices of a 2d circle at (0, 0)"""
|
||||
|
||||
modifier = 2 if self.shape == 'CIRCLE' else magic_number
|
||||
if self.origin == 'CENTER':
|
||||
modifier /= 2
|
||||
|
||||
verts = []
|
||||
for i in range(step):
|
||||
angle = (360 / step) * i + rotation
|
||||
verts.append(math.cos(math.radians(angle)) * ((self.mouse_path[1][0] - self.mouse_path[0][0]) / modifier))
|
||||
verts.append(math.sin(math.radians(angle)) * ((self.mouse_path[1][1] - self.mouse_path[0][1]) / modifier))
|
||||
verts.append(0.0)
|
||||
|
||||
verts.append(math.cos(math.radians(0.0 + rotation)) * ((self.mouse_path[1][0] - self.mouse_path[0][0]) / modifier))
|
||||
verts.append(math.sin(math.radians(0.0 + rotation)) * ((self.mouse_path[1][1] - self.mouse_path[0][1]) / modifier))
|
||||
verts.append(0.0)
|
||||
|
||||
return verts
|
||||
|
||||
tris_verts = []
|
||||
indices = []
|
||||
verts = create_2d_circle(self, int(subdivision), rotation)
|
||||
|
||||
rotation_matrix = mathutils.Matrix.Rotation(self.rotation, 4, 'Z')
|
||||
fixed_point = mathutils.Vector((self.mouse_path[0][0], self.mouse_path[0][1], 0.0))
|
||||
current_mouse_position = mathutils.Vector((self.mouse_path[1][0], self.mouse_path[1][1], 0.0))
|
||||
shape_center = fixed_point + (current_mouse_position - fixed_point) / 2
|
||||
|
||||
min_x = min(verts[0::3]) if self.mouse_path[1][0] > self.mouse_path[0][0] else -min(verts[0::3])
|
||||
min_y = min(verts[1::3]) if self.mouse_path[1][1] > self.mouse_path[0][1] else -min(verts[1::3])
|
||||
|
||||
for idx in range((len(verts) // 3) - 1):
|
||||
x = verts[idx * 3]
|
||||
y = verts[idx * 3 + 1]
|
||||
z = verts[idx * 3 + 2]
|
||||
vert = mathutils.Vector((x, y, z))
|
||||
vert = rotation_matrix @ vert
|
||||
vert = vert + fixed_point if self.origin == 'CENTER' else shape_center - vert
|
||||
vert += mathutils.Vector((self.position_x, self.position_y, 0.0))
|
||||
tris_verts.append(vert)
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx + 2 <= ((360 / int(subdivision)) * (idx + 1) + rotation) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
|
||||
# BEVEL
|
||||
if self.use_bevel and self.bevel_radius > 0.01:
|
||||
tris_verts, indices = bevel_verts(self, tris_verts, (self.bevel_radius * 50), self.bevel_segments)
|
||||
|
||||
# ARRAY
|
||||
rows, columns = array(self, tris_verts)
|
||||
|
||||
return tris_verts, indices, rows, columns
|
||||
|
||||
|
||||
def mini_grid(self, context):
|
||||
"""Draws snap mini-grid around the cursor based on the overlay grid"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
for i, a in enumerate(context.screen.areas):
|
||||
if a.type == 'VIEW_3D':
|
||||
space = context.screen.areas[i].spaces.active
|
||||
screen_height = context.screen.areas[i].height
|
||||
screen_width = context.screen.areas[i].width
|
||||
|
||||
# draw_the_snap_grid_(only_in_the_orthographic_view)
|
||||
if not space.region_3d.is_perspective:
|
||||
grid_scale = space.overlay.grid_scale
|
||||
grid_subdivisions = space.overlay.grid_subdivisions
|
||||
increment = (grid_scale / grid_subdivisions)
|
||||
|
||||
# get_the_3d_location_of_the_mouse_forced_to_a_snap_value_in_the_operator
|
||||
mouse_coord = self.mouse_path[len(self.mouse_path) - 1]
|
||||
snap_loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_coord, (0, 0, 0))
|
||||
|
||||
# add_the_increment_to_get_the_closest_location_on_the_grid
|
||||
snap_loc[0] += increment
|
||||
snap_loc[1] += increment
|
||||
|
||||
# get_the_2d_location_of_the_snap_location
|
||||
snap_loc = view3d_utils.location_3d_to_region_2d(region, rv3d, snap_loc)
|
||||
|
||||
# get_the_increment_value
|
||||
snap_value = snap_loc[0] - mouse_coord[0]
|
||||
|
||||
# draw_lines_on_x_and_z_axis_from_the_cursor_through_the_screen
|
||||
grid_coords = [(0, mouse_coord[1]), (screen_width, mouse_coord[1]),
|
||||
(mouse_coord[0], 0), (mouse_coord[0], screen_height)]
|
||||
|
||||
grid_coords += [(mouse_coord[0] + snap_value, mouse_coord[1] + 25 + snap_value),
|
||||
(mouse_coord[0] + snap_value, mouse_coord[1] - 25 - snap_value),
|
||||
(mouse_coord[0] + 25 + snap_value, mouse_coord[1] + snap_value),
|
||||
(mouse_coord[0] - 25 - snap_value, mouse_coord[1] + snap_value),
|
||||
(mouse_coord[0] - snap_value, mouse_coord[1] + 25 + snap_value),
|
||||
(mouse_coord[0] - snap_value, mouse_coord[1] - 25 - snap_value),
|
||||
(mouse_coord[0] + 25 + snap_value, mouse_coord[1] - snap_value),
|
||||
(mouse_coord[0] - 25 - snap_value, mouse_coord[1] - snap_value),]
|
||||
|
||||
draw_shader((1.0, 1.0, 1.0), 0.66, 'LINES', grid_coords, size=1.5)
|
||||
|
||||
|
||||
def get_bounding_box_coords(self, verts):
|
||||
"""Calculates the bounding box coordinates from a list of vertices in a counter-clockwise order"""
|
||||
|
||||
if verts:
|
||||
min_x = min(v[0] for v in verts)
|
||||
max_x = max(v[0] for v in verts)
|
||||
min_y = min(v[1] for v in verts)
|
||||
max_y = max(v[1] for v in verts)
|
||||
self.center_origin = [(min_x, min_y), (max_x, max_y)]
|
||||
|
||||
bounding_box_coords = [
|
||||
mathutils.Vector((min_x, min_y, 0)), # bottom-left
|
||||
mathutils.Vector((max_x, min_y, 0)), # bottom-right
|
||||
mathutils.Vector((max_x, max_y, 0)), # top-right
|
||||
mathutils.Vector((min_x, max_y, 0)), # top-left
|
||||
mathutils.Vector((min_x, min_y, 0)) # closing_the_loop_manually
|
||||
]
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
return bounding_box_coords, width, height
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
def array(self, verts):
|
||||
"""Duplicates given list of vertices in rows and columns (on x and y axis)"""
|
||||
"""Returns two dicts of lists of vertices for rows and columns separately"""
|
||||
|
||||
# ensure_bounding_box_(needed_when_array_is_set_before_original_is_drawn)
|
||||
if len(self.center_origin) == 0:
|
||||
get_bounding_box_coords(self, verts)
|
||||
|
||||
rows = {}
|
||||
if self.rows > 1:
|
||||
# Offset
|
||||
offset = mathutils.Vector((((self.center_origin[1][0] - self.center_origin[0][0]) + (self.rows_gap)), 0.0, 0.0))
|
||||
if self.rows_direction == 'LEFT':
|
||||
offset.x = -offset.x
|
||||
|
||||
for i in range(self.rows - 1):
|
||||
accumulated_offset = offset * (i + 1)
|
||||
rows[i] = [vert.copy() + accumulated_offset for vert in verts]
|
||||
|
||||
columns = {}
|
||||
if self.columns > 1:
|
||||
# Offset
|
||||
offset = mathutils.Vector((0.0, -((self.center_origin[1][1] - self.center_origin[0][1]) + (self.columns_gap)), 0.0))
|
||||
if self.columns_direction == 'UP':
|
||||
offset.y = -offset.y
|
||||
|
||||
for i in range(self.columns - 1):
|
||||
accumulated_offset = offset * (i + 1)
|
||||
columns[i] = [vert.copy() + accumulated_offset for vert in verts]
|
||||
for row_idx, row in rows.items():
|
||||
columns[(i, row_idx)] = [vert.copy() + accumulated_offset for vert in row]
|
||||
|
||||
return rows, columns
|
||||
|
||||
|
||||
def bevel_verts(self, verts, radius, segments):
|
||||
"""Takes in list of verts(Vectors) and bevels them, Returns a new list with new vertices"""
|
||||
|
||||
def get_rounded_corner(self, angular_point, p1, p2, radius, segments):
|
||||
# clamp_radius_to_reduce_clipping
|
||||
__, width, height = get_bounding_box_coords(self, verts)
|
||||
max_radius = min(width / 2.5, height / 2.5)
|
||||
clamped_radius = min(radius, max_radius)
|
||||
|
||||
if radius > clamped_radius:
|
||||
radius = clamped_radius
|
||||
|
||||
|
||||
# calculate_vectors (NOTE: Why it only works when reversed like this is unknown to me)
|
||||
if self.bevel_profile == 'CONVEX':
|
||||
vector1 = -(p1 - angular_point)
|
||||
vector2 = -(p2 - angular_point)
|
||||
elif self.bevel_profile == 'CONCAVE':
|
||||
vector1 = p2 - angular_point
|
||||
vector2 = p1 - angular_point
|
||||
|
||||
# compute_lengths_of_vectors
|
||||
length1 = vector1.length
|
||||
length2 = vector2.length
|
||||
if length1 == 0 or length2 == 0:
|
||||
return [angular_point] * segments
|
||||
|
||||
vector1.normalize()
|
||||
vector2.normalize()
|
||||
|
||||
# calculate_the_angle_between_the_vectors
|
||||
dot_product = vector1.dot(vector2)
|
||||
angle = math.acos(max(-1.0, min(1.0, dot_product)))
|
||||
|
||||
arc_length = radius * angle
|
||||
segment_length = arc_length / (segments - 1)
|
||||
bisector = (vector1 + vector2).normalized()
|
||||
|
||||
# generate_points_along_the_arc
|
||||
rounded_corners = []
|
||||
for i in range(segments):
|
||||
fraction = i / (segments - 1)
|
||||
theta = angle * fraction
|
||||
interpolated_vector = (vector1 * math.sin(theta) + vector2 * math.cos(theta)).normalized() * radius
|
||||
if self.bevel_profile == 'CONVEX':
|
||||
point_on_arc = angular_point + interpolated_vector - bisector * (clamped_radius * magic_number)
|
||||
elif self.bevel_profile == 'CONCAVE':
|
||||
point_on_arc = angular_point + interpolated_vector - bisector / (clamped_radius)
|
||||
rounded_corners.append(point_on_arc)
|
||||
|
||||
return rounded_corners
|
||||
|
||||
rounded_verts = []
|
||||
indices = []
|
||||
num_verts = len(verts)
|
||||
|
||||
for idx in range(num_verts):
|
||||
angular_point = verts[idx]
|
||||
prev_idx = (idx - 1) % num_verts
|
||||
next_idx = (idx + 1) % num_verts
|
||||
|
||||
p1 = verts[prev_idx]
|
||||
p2 = verts[next_idx]
|
||||
|
||||
corner_points = get_rounded_corner(self, angular_point, p1, p2, radius, segments)
|
||||
rounded_verts.extend(corner_points)
|
||||
|
||||
for idx, vert in enumerate(reversed(rounded_verts)):
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx + 2 <= len(rounded_verts) else 1
|
||||
indices.append((0, i1, i2))
|
||||
|
||||
return rounded_verts, indices
|
||||
@@ -0,0 +1,192 @@
|
||||
import bpy
|
||||
from .object import convert_to_mesh
|
||||
|
||||
|
||||
#### ------------------------------ /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_candidate_objects(self, context, canvas):
|
||||
"""Filter out objects from selected ones that can't be used as a cutter"""
|
||||
|
||||
cutters = []
|
||||
for obj in context.selected_objects:
|
||||
if obj != context.active_object and obj.type in ('MESH', 'CURVE', 'FONT'):
|
||||
if obj.library or obj.override_library:
|
||||
self.report({'ERROR'}, f"{obj.name} is linked and can not be used as a cutter")
|
||||
|
||||
else:
|
||||
if obj.type in ('CURVE', 'FONT'):
|
||||
if obj.data.bevel_depth != 0 or obj.data.extrude != 0:
|
||||
convert_to_mesh(context, obj)
|
||||
cutters.append(obj)
|
||||
|
||||
else:
|
||||
# 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)
|
||||
|
||||
return cutters
|
||||
|
||||
|
||||
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):
|
||||
"""Returns list of boolean modifiers + all modifiers that come before last boolean modifier"""
|
||||
|
||||
# find_the_index_of_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_found_list_all_modifiers_before
|
||||
if last_boolean_index != -1:
|
||||
return [mod for mod in obj.modifiers[:last_boolean_index + 1]]
|
||||
else:
|
||||
return []
|
||||
@@ -0,0 +1,161 @@
|
||||
import bpy, bmesh, mathutils, math
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def create_cutter_shape(self, context):
|
||||
"""Creates flat mesh from the vertices provided in `self.verts` (which is created by `carver_overlay`)"""
|
||||
|
||||
# ALIGNMENT: View
|
||||
coords = self.mouse_path[0][0], self.mouse_path[0][1]
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
depth_location = view3d_utils.region_2d_to_vector_3d(region, rv3d, coords)
|
||||
self.view_depth = depth_location
|
||||
plane_direction = depth_location.normalized()
|
||||
|
||||
# depth
|
||||
if self.depth == 'CURSOR':
|
||||
plane_point = context.scene.cursor.location
|
||||
elif self.depth == 'VIEW':
|
||||
plane_point = mathutils.Vector((0.0, 0.0, 0.0))
|
||||
|
||||
|
||||
# Create Mesh & Object
|
||||
faces = {}
|
||||
mesh = bpy.data.meshes.new(name='cutter')
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
obj = bpy.data.objects.new('cutter', mesh)
|
||||
obj.booleans.carver = True
|
||||
self.cutter = obj
|
||||
context.collection.objects.link(obj)
|
||||
|
||||
# Create Faces from `self.verts`
|
||||
create_face(context, plane_direction, plane_point,
|
||||
bm, "original", faces, self.verts)
|
||||
|
||||
# ARRAY
|
||||
if len(self.duplicates) > 0:
|
||||
for i, duplicate in self.duplicates.items():
|
||||
create_face(context, plane_direction, plane_point,
|
||||
bm, str(i), faces, duplicate)
|
||||
|
||||
bm.verts.index_update()
|
||||
for i, face in faces.items():
|
||||
bm.faces.new(face)
|
||||
|
||||
# remove_doubles
|
||||
bmesh.ops.remove_doubles(bm, verts=[v for v in bm.verts], dist=0.0001)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
|
||||
|
||||
def extrude(self, mesh):
|
||||
"""Extrudes cutter face (created by carve operation) along view vector to create a non-manifold mesh"""
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
faces = [f for f in bm.faces]
|
||||
|
||||
# move_the_mesh_towards_view
|
||||
box_bounding = combined_bounding_box(self.selected_objects)
|
||||
for face in faces:
|
||||
for vert in face.verts:
|
||||
vert.co += -self.view_depth * box_bounding
|
||||
|
||||
# extrude_the_face
|
||||
ret = bmesh.ops.extrude_face_region(bm, geom=faces)
|
||||
verts_extruded = [v for v in ret['geom'] if isinstance(v, bmesh.types.BMVert)]
|
||||
for v in verts_extruded:
|
||||
if self.depth == 'CURSOR':
|
||||
v.co += self.view_depth * box_bounding
|
||||
elif self.depth == 'VIEW':
|
||||
v.co += self.view_depth * box_bounding * 2
|
||||
|
||||
# correct_normals
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
|
||||
|
||||
def combined_bounding_box(objects):
|
||||
"""Calculate the combined bounding box of multiple objects."""
|
||||
|
||||
min_corner = mathutils.Vector((float('inf'), float('inf'), float('inf')))
|
||||
max_corner = mathutils.Vector((-float('inf'), -float('inf'), -float('inf')))
|
||||
|
||||
for obj in objects:
|
||||
# Transform the bounding box corners to world space
|
||||
bbox_corners = [obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box]
|
||||
|
||||
for corner in bbox_corners:
|
||||
min_corner.x = min(min_corner.x, corner.x)
|
||||
min_corner.y = min(min_corner.y, corner.y)
|
||||
min_corner.z = min(min_corner.z, corner.z)
|
||||
max_corner.x = max(max_corner.x, corner.x)
|
||||
max_corner.y = max(max_corner.y, corner.y)
|
||||
max_corner.z = max(max_corner.z, corner.z)
|
||||
|
||||
# Calculate the diagonal of the combined bounding box
|
||||
bounding_box_diag = (max_corner - min_corner).length
|
||||
return bounding_box_diag
|
||||
|
||||
|
||||
def create_face(context, direction, depth, bm, name, faces, verts, polyline=False):
|
||||
"""Creates bmesh face with given list of vertices and appends it to given 'faces' dict"""
|
||||
|
||||
def intersect_line_plane(context, vert, direction, depth):
|
||||
"""Finds the intersection of a line going through each vertex and the infinite plane"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
vec = view3d_utils.region_2d_to_vector_3d(region, rv3d, vert)
|
||||
p0 = view3d_utils.region_2d_to_location_3d(region, rv3d, vert, vec)
|
||||
p1 = p0 + direction
|
||||
loc = mathutils.geometry.intersect_line_plane(p0, p1, depth, direction)
|
||||
|
||||
return loc
|
||||
|
||||
face_verts = []
|
||||
for i, vert in enumerate(verts):
|
||||
loc = intersect_line_plane(context, vert, direction, depth)
|
||||
vertex = bm.verts.new(loc)
|
||||
face_verts.append(vertex)
|
||||
|
||||
faces[name] = face_verts
|
||||
|
||||
|
||||
def shade_smooth_by_angle(obj, angle=30):
|
||||
"""Replication of "Auto Smooth" functionality: Marks faces as smooth, sharp edges (by angle) as sharp"""
|
||||
|
||||
mesh = obj.data
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
|
||||
# shade_smooth
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
|
||||
# select_sharp_edges
|
||||
for edge in bm.edges:
|
||||
if len(edge.link_faces) == 2:
|
||||
face1, face2 = edge.link_faces
|
||||
edge_angle = math.degrees(face1.normal.angle(face2.normal))
|
||||
if edge_angle >= angle:
|
||||
edge.select = True
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.update()
|
||||
|
||||
# mark_sharp_edges
|
||||
for edge in mesh.edges:
|
||||
if edge.select:
|
||||
edge.use_edge_sharp = True
|
||||
mesh.update()
|
||||
@@ -0,0 +1,225 @@
|
||||
import bpy, bmesh, mathutils
|
||||
from .. import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def add_boolean_modifier(self, context, canvas, cutter, mode, solver, apply=False, pin=False, redo=True, single_user=False):
|
||||
"Adds boolean modifier with specified cutter and properties to a single object"
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
modifier = canvas.modifiers.new("boolean_" + cutter.name, 'BOOLEAN')
|
||||
modifier.operation = mode
|
||||
modifier.object = cutter
|
||||
modifier.solver = solver
|
||||
|
||||
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
|
||||
|
||||
if pin:
|
||||
index = canvas.modifiers.find(modifier.name)
|
||||
canvas.modifiers.move(index, 0)
|
||||
|
||||
if apply:
|
||||
for face in cutter.data.polygons:
|
||||
face.select = True
|
||||
|
||||
if context.mode == 'EDIT_MESH':
|
||||
"""Applying boolean modifier in mesh edit mode:"""
|
||||
"""1. Hiding other visible modifiers and creating new (temporary) mesh from evaluated object"""
|
||||
"""2. Transfering temporary mesh to `bmesh` to update active mesh in edit mode"""
|
||||
"""3. Removing boolean modifier and purging temporary mesh"""
|
||||
"""4. Restoring visibility of other modifiers from (1)"""
|
||||
|
||||
visible_modifiers = []
|
||||
for mod in canvas.modifiers:
|
||||
if mod == modifier:
|
||||
continue
|
||||
if mod.show_viewport == True:
|
||||
visible_modifiers.append(mod)
|
||||
mod.show_viewport = False
|
||||
|
||||
evaluated_obj = canvas.evaluated_get(context.evaluated_depsgraph_get())
|
||||
temp_data = bpy.data.meshes.new_from_object(evaluated_obj)
|
||||
|
||||
bm = bmesh.from_edit_mesh(canvas.data)
|
||||
bm.clear()
|
||||
bm.from_mesh(temp_data)
|
||||
bmesh.update_edit_mesh(canvas.data)
|
||||
evaluated_obj.to_mesh_clear()
|
||||
|
||||
canvas.modifiers.remove(modifier)
|
||||
bpy.data.meshes.remove(temp_data)
|
||||
|
||||
for mod in visible_modifiers:
|
||||
mod.show_viewport = True
|
||||
|
||||
else:
|
||||
context_override = {'object': canvas, 'mode': 'OBJECT'}
|
||||
with context.temp_override(**context_override):
|
||||
apply_modifier(context, canvas, modifier, single_user=single_user)
|
||||
|
||||
|
||||
def apply_modifier(context, obj, modifier, single_user=False):
|
||||
"""Applies given modifier to object."""
|
||||
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
try:
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
except:
|
||||
if single_user:
|
||||
# Make Single User
|
||||
context.active_object.data = context.active_object.data.copy()
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
|
||||
|
||||
def set_cutter_properties(context, canvas, cutter, mode, parent=True, hide=False, collection=True):
|
||||
"""Ensures cutter is properly set: has right properties, is hidden, in a collection & parented"""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Hide Cutters
|
||||
cutter.hide_render = True
|
||||
cutter.display_type = 'WIRE' if prefs.wireframe else 'BOUNDS'
|
||||
cutter.lineart.usage = 'EXCLUDE'
|
||||
object_visibility_set(cutter, value=False)
|
||||
if hide:
|
||||
cutter.hide_set(True)
|
||||
|
||||
# parent_to_active_canvas
|
||||
if parent and cutter.parent == None:
|
||||
cutter.parent = canvas
|
||||
cutter.matrix_parent_inverse = canvas.matrix_world.inverted()
|
||||
|
||||
# Cutters Collection
|
||||
if collection:
|
||||
cutters_collection = ensure_collection(context)
|
||||
if cutters_collection not in cutter.users_collection:
|
||||
cutters_collection.objects.link(cutter)
|
||||
if cutter.booleans.carver and parent == False:
|
||||
context.collection.objects.unlink(cutter)
|
||||
|
||||
# add_boolean_property
|
||||
cutter.booleans.cutter = mode.capitalize()
|
||||
|
||||
|
||||
def object_visibility_set(obj, value=False):
|
||||
"Sets object visibility properties to either True or False"
|
||||
|
||||
obj.visible_camera = value
|
||||
obj.visible_diffuse = value
|
||||
obj.visible_glossy = value
|
||||
obj.visible_shadow = value
|
||||
obj.visible_transmission = value
|
||||
obj.visible_volume_scatter = value
|
||||
|
||||
|
||||
def convert_to_mesh(context, obj):
|
||||
"Converts active object into mesh (applying all modifiers and shape keys in process)"
|
||||
|
||||
# store_selection
|
||||
stored_active = context.active_object
|
||||
stored_selection = context.selected_objects
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
# Convert
|
||||
obj.select_set(True)
|
||||
context.view_layer.objects.active = obj
|
||||
bpy.ops.object.convert(target='MESH')
|
||||
|
||||
# restore_selection
|
||||
for obj in stored_selection:
|
||||
obj.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"""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
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(object, parent):
|
||||
"""Changes or removes parent from cutter object while keeping the transformation"""
|
||||
|
||||
matrix_copy = object.matrix_world.copy()
|
||||
object.parent = parent
|
||||
object.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, position=False):
|
||||
"""Sets object origin to given position by shifting vertices"""
|
||||
|
||||
# default_to_center_of_bounding_box_if_no_position_provided
|
||||
if position == False:
|
||||
position = 0.125 * sum((mathutils.Vector(b) for b in obj.bound_box), mathutils.Vector())
|
||||
|
||||
mat = mathutils.Matrix.Translation(position - obj.location)
|
||||
obj.location = position
|
||||
obj.data.transform(mat.inverted())
|
||||
obj.data.update()
|
||||
@@ -0,0 +1,76 @@
|
||||
import bpy
|
||||
from .list import list_canvas_cutters
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def basic_poll(context, check_linked=False):
|
||||
if context.mode == 'OBJECT':
|
||||
if context.active_object is not None:
|
||||
if context.active_object.type == 'MESH':
|
||||
if check_linked and is_linked(context) == True:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_linked(context, obj=None):
|
||||
if not obj:
|
||||
obj = context.active_object
|
||||
|
||||
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):
|
||||
if obj.booleans.canvas == False:
|
||||
return False
|
||||
else:
|
||||
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(context):
|
||||
"""Checks whether the active modifier for active object is a boolean"""
|
||||
|
||||
if context.object:
|
||||
if len(context.object.modifiers) == 0:
|
||||
return False
|
||||
|
||||
modifier = context.object.modifiers.active
|
||||
if modifier and modifier.type == "BOOLEAN":
|
||||
if modifier.object == None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -0,0 +1,132 @@
|
||||
import bpy, mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
from .draw import get_bounding_box_coords
|
||||
from .poll import is_linked, is_instanced_data
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def cursor_snap(self, context, event, mouse_pos):
|
||||
"""Find the closest position on the overlay grid and snap the mouse on it"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
for i, a in enumerate(context.screen.areas):
|
||||
if a.type == 'VIEW_3D':
|
||||
space = context.screen.areas[i].spaces.active
|
||||
|
||||
# get_the_grid_overlay
|
||||
grid_scale = space.overlay.grid_scale
|
||||
grid_subdivisions = space.overlay.grid_subdivisions
|
||||
|
||||
# use_grid_scale_and_subdivision_to_get_the_increment
|
||||
increment = (grid_scale / grid_subdivisions)
|
||||
half_increment = increment / 2
|
||||
|
||||
# convert_2d_location_of_the_mouse_in_3d
|
||||
for index, loc in enumerate(reversed(mouse_pos)):
|
||||
mouse_loc_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, loc, (0, 0, 0))
|
||||
|
||||
# get_the_remainder_from_the_mouse_location_and_the_ratio (test_if_the_remainder_>_to_the_half_of_the_increment)
|
||||
for i in range(3):
|
||||
modulo = mouse_loc_3d[i] % increment
|
||||
if modulo < half_increment:
|
||||
modulo = -modulo
|
||||
else:
|
||||
modulo = increment - modulo
|
||||
|
||||
# add_the_remainder_to_get_the_closest_location_on_the_grid
|
||||
mouse_loc_3d[i] = mouse_loc_3d[i] + modulo
|
||||
|
||||
snap_loc_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, mouse_loc_3d)
|
||||
|
||||
# replace_the_last_mouse_location_by_the_snapped_location
|
||||
if len(self.mouse_path) > 0:
|
||||
self.mouse_path[len(self.mouse_path) - (index + 1) ] = tuple(snap_loc_2d)
|
||||
|
||||
|
||||
def is_inside_selection(context, obj, rect_min, rect_max):
|
||||
"""Checks if the bounding box of an object intersects with the selection bounding box"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.space_data.region_3d
|
||||
|
||||
bound_corners = [obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box]
|
||||
bound_corners_2d = [view3d_utils.location_3d_to_region_2d(region, rv3d, corner) for corner in bound_corners]
|
||||
|
||||
# check_if_2d_point_is_inside_rectangle_(defined_by_min_and_max_points)
|
||||
for corner_2d in bound_corners_2d:
|
||||
if corner_2d and (rect_min.x <= corner_2d.x <= rect_max.x and rect_min.y <= corner_2d.y <= rect_max.y):
|
||||
return True
|
||||
|
||||
# check_if_any_part_of_the_bounding_box_intersects_the_selection_rectangle
|
||||
min_x = min(corner_2d.x for corner_2d in bound_corners_2d if corner_2d)
|
||||
max_x = max(corner_2d.x for corner_2d in bound_corners_2d if corner_2d)
|
||||
min_y = min(corner_2d.y for corner_2d in bound_corners_2d if corner_2d)
|
||||
max_y = max(corner_2d.y for corner_2d in bound_corners_2d if corner_2d)
|
||||
|
||||
return not (max_x < rect_min.x or min_x > rect_max.x or max_y < rect_min.y or min_y > rect_max.y)
|
||||
|
||||
|
||||
def selection_fallback(self, context, objects, include_cutters=False):
|
||||
"""Selects mesh objects that fall inside given 2d rectangle coordinates"""
|
||||
"""Used to get exactly which objects should be cut and avoid adding and applying unnecessary modifiers"""
|
||||
"""NOTE: bounding box isn't always returning correct results for objects, but full surface check would be too expensive"""
|
||||
|
||||
# convert_2d_rectangle_coordinates_to_world_coordinates
|
||||
if self.origin == 'EDGE':
|
||||
if self.shape == 'POLYLINE':
|
||||
x_values = [point[0] for point in self.mouse_path]
|
||||
y_values = [point[1] for point in self.mouse_path]
|
||||
rect_min = mathutils.Vector((min(x_values), min(y_values)))
|
||||
rect_max = mathutils.Vector((max(x_values), max(y_values)))
|
||||
else:
|
||||
rect_min = mathutils.Vector((min(self.mouse_path[0][0], self.mouse_path[1][0]),
|
||||
min(self.mouse_path[0][1], self.mouse_path[1][1])))
|
||||
rect_max = mathutils.Vector((max(self.mouse_path[0][0], self.mouse_path[1][0]),
|
||||
max(self.mouse_path[0][1], self.mouse_path[1][1])))
|
||||
|
||||
elif self.origin == 'CENTER':
|
||||
# ensure_bounding_box_(needed_when_array_is_set_before_original_is_drawn)
|
||||
if len(self.center_origin) == 0:
|
||||
get_bounding_box_coords(self, self.verts)
|
||||
|
||||
rect_min = mathutils.Vector((min(self.center_origin[0][0], self.center_origin[1][0]),
|
||||
min(self.center_origin[0][1], self.center_origin[1][1])))
|
||||
rect_max = mathutils.Vector((max(self.center_origin[0][0], self.center_origin[1][0]),
|
||||
max(self.center_origin[0][1], self.center_origin[1][1])))
|
||||
|
||||
# ARRAY
|
||||
if self.rows > 1:
|
||||
rect_max.x = rect_min.x + (rect_max.x - rect_min.x) * self.rows + (self.rows_gap * (self.rows - 1))
|
||||
if self.columns > 1:
|
||||
rect_min.y = rect_max.y - (rect_max.y - rect_min.y) * self.columns - (self.columns_gap * (self.columns - 1))
|
||||
|
||||
intersecting_objects = []
|
||||
for obj in objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
if obj == self.cutter:
|
||||
continue
|
||||
if tuple(round(v, 4) for v in obj.dimensions) == (0.0, 0.0, 0.0):
|
||||
continue
|
||||
if include_cutters == False and obj.booleans.cutter != "":
|
||||
continue
|
||||
|
||||
if is_inside_selection(context, obj, rect_min, rect_max):
|
||||
if is_linked(context, obj):
|
||||
self.report({'ERROR'}, f"{obj.name} is linked and can not be carved")
|
||||
continue
|
||||
|
||||
if self.mode == 'DESTRUCTIVE':
|
||||
if obj.data.shape_keys:
|
||||
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has shape keys")
|
||||
continue
|
||||
if is_instanced_data(obj):
|
||||
self.report({'ERROR'}, f"Modifiers can't be applied to {obj.name} because it has instanced object data")
|
||||
continue
|
||||
|
||||
intersecting_objects.append(obj)
|
||||
|
||||
return intersecting_objects
|
||||
Reference in New Issue
Block a user