2025-12-01
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import bpy, gpu, mathutils, math
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
import bpy
|
||||
import gpu
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
from .math import (
|
||||
draw_circle,
|
||||
draw_polygon,
|
||||
draw_array,
|
||||
)
|
||||
|
||||
|
||||
magic_number = 1.41
|
||||
color = (0.48, 0.04, 0.04, 1.0)
|
||||
secondary_color = (0.28, 0.04, 0.04, 1.0)
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
@@ -48,172 +58,74 @@ def draw_shader(color, alpha, type, coords, size=1, indices=None):
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def carver_overlay(self, context):
|
||||
"""Shape (rectangle, circle) overlay for carver tool"""
|
||||
def carver_shape_box(self, context, shape):
|
||||
"""Shape overlay for box carver tool"""
|
||||
|
||||
color = (0.48, 0.04, 0.04, 1.0)
|
||||
secondary_color = (0.28, 0.04, 0.04, 1.0)
|
||||
subdivision = self.subdivision if shape == 'CIRCLE' else 4
|
||||
rotation = 0 if shape == 'CIRCLE' else 45
|
||||
|
||||
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()}}
|
||||
# Create Shape
|
||||
coords, indices, bounds = draw_circle(self, subdivision, rotation)
|
||||
self.verts = coords
|
||||
|
||||
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)
|
||||
# Draw Shaders
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
if not self.rotate and not self.bevel:
|
||||
draw_shader(color, 0.6, 'OUTLINE', bounds, size=2)
|
||||
|
||||
# Array
|
||||
if self.rows > 1 or self.columns > 1:
|
||||
carver_shape_array(self, coords, indices, 'SOLID')
|
||||
|
||||
|
||||
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:
|
||||
if self.snap:
|
||||
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"""
|
||||
def carver_shape_polyline(self, context):
|
||||
"""Shape overlay for polyline carver tool"""
|
||||
|
||||
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)
|
||||
# Create Shape
|
||||
coords, indices, first_point, array_coords = draw_polygon(self)
|
||||
self.verts = list(dict.fromkeys(self.mouse_path))
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx <= len(self.mouse_path) else 1
|
||||
indices.append((0, i1, i2))
|
||||
# Draw Shaders
|
||||
draw_shader(color, 1.0, 'POINTS', coords, size=5)
|
||||
draw_shader(color, 1.0, 'LINE_LOOP' if self.closed else 'LINES', coords, size=2)
|
||||
|
||||
# circle_around_first_point
|
||||
radius = self.distance_from_first
|
||||
segments = 4
|
||||
if self.closed and len(self.mouse_path) > 2:
|
||||
# polygon_fill
|
||||
draw_shader(color, 0.4, 'SOLID', coords, size=2, indices=indices[:-2])
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
# 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
|
||||
if len(self.mouse_path) > 2 and (self.rows > 1 or self.columns > 1):
|
||||
carver_shape_array(self, array_coords, indices, 'LINE_LOOP' if self.closed == False else 'SOLID')
|
||||
|
||||
|
||||
# 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)
|
||||
if self.snap:
|
||||
mini_grid(self, context)
|
||||
|
||||
return coords, indices, click_point, rows, columns
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
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 carver_shape_array(self, verts, indices, shader):
|
||||
"""Draws given shape for each row and column of the array"""
|
||||
|
||||
def create_2d_circle(self, step, rotation):
|
||||
"""Create the vertices of a 2d circle at (0, 0)"""
|
||||
rows, columns = draw_array(self, verts)
|
||||
self.duplicates = {**{f"row_{k}": v for k, v in rows.items()}, **{f"column_{k}": v for k, v in columns.items()}}
|
||||
|
||||
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
|
||||
if self.rows > 1:
|
||||
for i, duplicate in rows.items():
|
||||
draw_shader(secondary_color, 0.4, shader, duplicate, size=2, indices=indices[:-2])
|
||||
if self.columns > 1:
|
||||
for i, duplicate in columns.items():
|
||||
draw_shader(secondary_color, 0.4, shader, duplicate, size=2, indices=indices[:-2])
|
||||
|
||||
|
||||
def mini_grid(self, context):
|
||||
@@ -222,8 +134,8 @@ def mini_grid(self, context):
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
|
||||
for i, a in enumerate(context.screen.areas):
|
||||
if a.type == 'VIEW_3D':
|
||||
for i, area in enumerate(context.screen.areas):
|
||||
if area.type == 'VIEW_3D':
|
||||
space = context.screen.areas[i].spaces.active
|
||||
screen_height = context.screen.areas[i].height
|
||||
screen_width = context.screen.areas[i].width
|
||||
@@ -262,139 +174,3 @@ def mini_grid(self, context):
|
||||
(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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bpy
|
||||
from .object import convert_to_mesh
|
||||
|
||||
|
||||
#### ------------------------------ /all/ ------------------------------ ####
|
||||
@@ -18,35 +17,6 @@ def list_canvases():
|
||||
|
||||
#### ------------------------------ /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"""
|
||||
|
||||
@@ -175,17 +145,17 @@ def list_unused_cutters(cutters, *canvases, do_leftovers=False):
|
||||
return cutters, leftovers
|
||||
|
||||
|
||||
def list_pre_boolean_modifiers(obj):
|
||||
"""Returns list of boolean modifiers + all modifiers that come before last boolean modifier"""
|
||||
def list_pre_boolean_modifiers(obj) -> list:
|
||||
"""Returns a list of boolean modifiers & modifiers that come before last boolean modifier"""
|
||||
|
||||
# find_the_index_of_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_found_list_all_modifiers_before
|
||||
# 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:
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import bpy
|
||||
import math
|
||||
import mathutils
|
||||
|
||||
|
||||
magic_number = 1.41
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def draw_circle(self, subdivision, rotation):
|
||||
"""Returns the coordinates & indices of a 2d circle in screen-space"""
|
||||
|
||||
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
|
||||
|
||||
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_offset_x, self.position_offset_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)
|
||||
|
||||
|
||||
# BOUNDING_BOX
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(tris_verts)
|
||||
bounds = [
|
||||
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
|
||||
]
|
||||
|
||||
return tris_verts, indices, bounds
|
||||
|
||||
|
||||
def draw_polygon(self):
|
||||
"""Returns polygonal 2d shape in screen-space where 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_offset_x, self.position_offset_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)
|
||||
|
||||
|
||||
# ARRAY (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_coords = unique_verts if self.closed else unique_verts[:-1]
|
||||
|
||||
return coords, indices, click_point, array_coords
|
||||
|
||||
|
||||
def draw_array(self, verts):
|
||||
"""Duplicates given list of vertices in rows and columns (on screen-space x and y axis)"""
|
||||
"""Returns two dicts of lists of vertices for rows and columns separately"""
|
||||
|
||||
# get_bounding_box_of_the_shape
|
||||
"""NOTE: Calculated separately because verts needed for array differs from verts needed for shape for polyline"""
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(verts)
|
||||
|
||||
rows = {}
|
||||
if self.rows > 1:
|
||||
# Offset
|
||||
offset = mathutils.Vector((((max_x - min_x) + (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, -((max_y - min_y) + (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):
|
||||
# get_bounding_box_of_the_shape
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(verts)
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
# clamp_radius_to_reduce_clipping
|
||||
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
|
||||
|
||||
|
||||
def get_bounding_box(verts):
|
||||
"""Calculates the bounding box coordinates from a list of vertices"""
|
||||
|
||||
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)
|
||||
|
||||
return min_x, min_y, max_x, max_y
|
||||
@@ -1,4 +1,7 @@
|
||||
import bpy, bmesh, mathutils, math
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
import math
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
@@ -19,8 +22,8 @@ def create_cutter_shape(self, context):
|
||||
if self.depth == 'CURSOR':
|
||||
plane_point = context.scene.cursor.location
|
||||
elif self.depth == 'VIEW':
|
||||
plane_point = mathutils.Vector((0.0, 0.0, 0.0))
|
||||
|
||||
__, plane_point = combined_bounding_box(self.selected_objects)
|
||||
plane_point = mathutils.Vector(plane_point)
|
||||
|
||||
# Create Mesh & Object
|
||||
faces = {}
|
||||
@@ -61,7 +64,7 @@ def extrude(self, mesh):
|
||||
faces = [f for f in bm.faces]
|
||||
|
||||
# move_the_mesh_towards_view
|
||||
box_bounding = combined_bounding_box(self.selected_objects)
|
||||
box_bounding, __ = combined_bounding_box(self.selected_objects)
|
||||
for face in faces:
|
||||
for vert in face.verts:
|
||||
vert.co += -self.view_depth * box_bounding
|
||||
@@ -85,7 +88,7 @@ def extrude(self, mesh):
|
||||
|
||||
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')))
|
||||
|
||||
@@ -103,7 +106,10 @@ def combined_bounding_box(objects):
|
||||
|
||||
# Calculate the diagonal of the combined bounding box
|
||||
bounding_box_diag = (max_corner - min_corner).length
|
||||
return bounding_box_diag
|
||||
# Calculate the center of bounding box
|
||||
bounding_box_center = (max_corner + min_corner) * 0.5
|
||||
|
||||
return bounding_box_diag, bounding_box_center
|
||||
|
||||
|
||||
def create_face(context, direction, depth, bm, name, faces, verts, polyline=False):
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
)
|
||||
from .poll import (
|
||||
is_instanced_data,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ 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"
|
||||
|
||||
if bpy.app.version < (5, 0, 0) and solver == 'FLOAT':
|
||||
solver = 'FAST'
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
modifier = obj.modifiers.new("boolean_" + cutter.name, 'BOOLEAN')
|
||||
modifier.operation = mode
|
||||
modifier.object = cutter
|
||||
modifier.solver = solver
|
||||
|
||||
# Set solver options (inherited from operator properties).
|
||||
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)
|
||||
obj.modifiers.move(index, 0)
|
||||
|
||||
return modifier
|
||||
|
||||
|
||||
def apply_modifiers(context, obj, modifiers: list):
|
||||
"""
|
||||
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`.
|
||||
|
||||
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
|
||||
|
||||
# Make object data unique if it's instanced.
|
||||
if is_instanced_data(obj):
|
||||
context.active_object.data = context.active_object.data.copy()
|
||||
|
||||
try:
|
||||
# Don't use this method if it's not enabled by user in add-on preferences.
|
||||
if not prefs.fast_modifier_apply:
|
||||
raise Exception("")
|
||||
|
||||
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)
|
||||
|
||||
# Create `bmesh` from temporary mesh and update edit mesh.
|
||||
if context.mode == 'EDIT_MESH':
|
||||
bm = bmesh.from_edit_mesh(obj.data)
|
||||
bm.clear()
|
||||
bm.from_mesh(temp_data)
|
||||
bmesh.update_edit_mesh(obj.data)
|
||||
else:
|
||||
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)
|
||||
for mod in modifiers:
|
||||
obj.modifiers.remove(mod)
|
||||
|
||||
# Remove shape keys if there are any.
|
||||
# (after above operations none of the shape keys have any effect).
|
||||
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")
|
||||
|
||||
context_override = {"object": obj, "mode": 'OBJECT'}
|
||||
with context.temp_override(**context_override):
|
||||
# Apply shape keys if there are any.
|
||||
if obj.data.shape_keys:
|
||||
bpy.ops.object.shape_key_remove(all=True, apply_mix=True)
|
||||
|
||||
# If all modifiers need to be applied convert to Mesh.
|
||||
if modifiers == obj.modifiers.values():
|
||||
print("Applying all modifiers by converting to Mesh")
|
||||
convert_to_mesh(context, obj)
|
||||
return
|
||||
|
||||
for mod in modifiers:
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def hide_modifiers(obj, excluding: list):
|
||||
"""Hides all modifiers of a given object in viewport except those in excluding list"""
|
||||
|
||||
visible_modifiers = []
|
||||
for mod in obj.modifiers:
|
||||
if mod in excluding:
|
||||
continue
|
||||
if mod.show_viewport == True:
|
||||
visible_modifiers.append(mod)
|
||||
mod.show_viewport = False
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for mod in visible_modifiers:
|
||||
mod.show_viewport = True
|
||||
@@ -1,86 +1,10 @@
|
||||
import bpy, bmesh, mathutils
|
||||
import bpy
|
||||
import 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"""
|
||||
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
import bpy
|
||||
from .list import list_canvas_cutters
|
||||
|
||||
from .list import (
|
||||
list_canvas_cutters,
|
||||
list_cutter_users,
|
||||
)
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
)
|
||||
|
||||
|
||||
#### ------------------------------ 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
|
||||
def basic_poll(cls, context, check_linked=False):
|
||||
"""Basic poll for boolean operators."""
|
||||
|
||||
return True
|
||||
if context.mode != 'OBJECT':
|
||||
return False
|
||||
if context.active_object is None:
|
||||
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
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_linked(context, obj=None):
|
||||
if not obj:
|
||||
obj = context.active_object
|
||||
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:
|
||||
@@ -31,19 +47,22 @@ def is_linked(context, obj=None):
|
||||
|
||||
|
||||
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:
|
||||
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"""
|
||||
"""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
|
||||
@@ -59,18 +78,103 @@ def is_instanced_data(obj):
|
||||
return False
|
||||
|
||||
|
||||
def active_modifier_poll(context):
|
||||
"""Checks whether the active modifier for active object is a boolean"""
|
||||
def active_modifier_poll(obj):
|
||||
"""Checks whether the active modifier for active object is a boolean."""
|
||||
|
||||
if context.object:
|
||||
if len(context.object.modifiers) == 0:
|
||||
return False
|
||||
# Check if active modifier exists.
|
||||
if len(obj.modifiers) == 0:
|
||||
return False
|
||||
if obj.modifiers.active is None:
|
||||
return False
|
||||
|
||||
modifier = context.object.modifiers.active
|
||||
if modifier and modifier.type == "BOOLEAN":
|
||||
if modifier.object == None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
# 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"):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
has_instanced_data = any(obj for obj in canvases if is_instanced_data(obj))
|
||||
has_shape_keys = any(obj for obj in canvases if obj.data.shape_keys)
|
||||
|
||||
if has_instanced_data or has_shape_keys:
|
||||
# Instanced data message.
|
||||
if has_instanced_data and not has_shape_keys:
|
||||
message = ("Object(s) you're trying to cut have instanced object data.\n"
|
||||
"In order to apply modifiers, they need to be made single-user.\n"
|
||||
"Do you proceed?")
|
||||
|
||||
# Shape keys message.
|
||||
if has_shape_keys and not has_instanced_data:
|
||||
message = ("Object(s) you're trying to cut have shape keys.\n"
|
||||
"In order to apply modifiers shape keys need to be applied as well.\n"
|
||||
"Do you proceed?")
|
||||
|
||||
# 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"
|
||||
"Do you proceed?")
|
||||
|
||||
popup = context.window_manager.invoke_confirm(self, event, title=title,
|
||||
confirm_text="Yes", icon='WARNING',
|
||||
message=message)
|
||||
|
||||
return popup
|
||||
|
||||
# Execute without confirmation window.
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import bpy, mathutils
|
||||
import bpy
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
from .draw import get_bounding_box_coords
|
||||
|
||||
from .math import get_bounding_box
|
||||
from .poll import is_linked, is_instanced_data
|
||||
|
||||
|
||||
@@ -59,7 +61,7 @@ def is_inside_selection(context, obj, rect_min, rect_max):
|
||||
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)
|
||||
@@ -69,33 +71,30 @@ def is_inside_selection(context, obj, rect_min, rect_max):
|
||||
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"""
|
||||
def selection_fallback(self, context, objects, shape='BOX', include_cutters=False):
|
||||
"""Returns mesh objects that fall inside given 2d rectangle (bounding box of the shape) coordinates"""
|
||||
"""Needed to know exactly which objects should be carved, to avoid adding and applying unnecessary modifiers"""
|
||||
"""NOTE: bounding box isn't always returning correct results, but checking full shape 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:
|
||||
if 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)))
|
||||
|
||||
elif shape == 'BOX':
|
||||
if self.origin == 'EDGE':
|
||||
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)
|
||||
elif self.origin == 'CENTER':
|
||||
# get_bounding_box_of_the_shape
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(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])))
|
||||
rect_min = mathutils.Vector((min(min_x, max_x), min(min_y, max_y)))
|
||||
rect_max = mathutils.Vector((max(min_x, max_x), max(min_y, max_y)))
|
||||
|
||||
# ARRAY
|
||||
if self.rows > 1:
|
||||
@@ -103,6 +102,7 @@ def selection_fallback(self, context, objects, include_cutters=False):
|
||||
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':
|
||||
@@ -120,11 +120,8 @@ def selection_fallback(self, context, objects, include_cutters=False):
|
||||
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")
|
||||
self.report({'ERROR'}, f"Modifiers cannot be applied to {obj.name} because it has instanced object data")
|
||||
continue
|
||||
|
||||
intersecting_objects.append(obj)
|
||||
|
||||
Reference in New Issue
Block a user