2026-01-01
This commit is contained in:
@@ -1,22 +1,15 @@
|
||||
import bpy
|
||||
import gpu
|
||||
import math
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector
|
||||
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 ------------------------------ ####
|
||||
|
||||
def draw_shader(color, alpha, type, coords, size=1, indices=None):
|
||||
def draw_shader(type, color, alpha, coords, size=1, indices=None):
|
||||
"""Creates a batch for a draw type"""
|
||||
|
||||
gpu.state.blend_set('ALPHA')
|
||||
@@ -29,6 +22,7 @@ def draw_shader(color, alpha, type, coords, size=1, indices=None):
|
||||
batch = batch_for_shader(shader, 'POINTS', {"pos": coords}, indices=indices)
|
||||
|
||||
elif type in 'LINES':
|
||||
gpu.state.line_width_set(size)
|
||||
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
|
||||
shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
|
||||
shader.uniform_float("lineWidth", size)
|
||||
@@ -43,134 +37,103 @@ def draw_shader(color, alpha, type, coords, size=1, indices=None):
|
||||
batch = batch_for_shader(shader, 'LINE_LOOP', {"pos": coords})
|
||||
|
||||
if type == 'SOLID':
|
||||
gpu.state.depth_test_set('NONE')
|
||||
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.line_width_set(1.0)
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def carver_shape_box(self, context, shape):
|
||||
"""Shape overlay for box carver tool"""
|
||||
def draw_bmesh_faces(faces, world_matrix):
|
||||
"""
|
||||
Get world-space vertex pairs and indices from `bmesh` face. To be used in GPU batch.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
subdivision = self.subdivision if shape == 'CIRCLE' else 4
|
||||
rotation = 0 if shape == 'CIRCLE' else 45
|
||||
if not faces:
|
||||
return None, None
|
||||
|
||||
# Create Shape
|
||||
coords, indices, bounds = draw_circle(self, subdivision, rotation)
|
||||
self.verts = coords
|
||||
vertices = []
|
||||
indices = []
|
||||
|
||||
# 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)
|
||||
vert_index_map = {}
|
||||
vert_count = 0
|
||||
for face in faces:
|
||||
face_indices = []
|
||||
|
||||
# Array
|
||||
if self.rows > 1 or self.columns > 1:
|
||||
carver_shape_array(self, coords, indices, 'SOLID')
|
||||
# Collect unique vertices only (avoid storing verts that are shared by faces multiple times).
|
||||
# (Iterating over face corners because unlike `face.verts` they're ordered).
|
||||
for loop in face.loops:
|
||||
vert = loop.vert
|
||||
co = world_matrix @ Vector(vert.co)
|
||||
|
||||
if vert not in vert_index_map:
|
||||
vertices.append(co)
|
||||
vert_index_map[vert] = vert_count
|
||||
face_indices.append(vert_count)
|
||||
vert_count += 1
|
||||
else:
|
||||
face_indices.append(vert_index_map[vert])
|
||||
|
||||
# Triangulate face and map local indices to global vertex indices.
|
||||
if len(face_indices) >= 3:
|
||||
try:
|
||||
face_verts_co = [vertices[idx] for idx in face_indices]
|
||||
tris = mathutils.geometry.tessellate_polygon([face_verts_co])
|
||||
for tri in tris:
|
||||
indices.append((face_indices[tri[0]], face_indices[tri[1]], face_indices[tri[2]]))
|
||||
except:
|
||||
# Fallback to simple fan triangulation if tessellation fails.
|
||||
for i in range(1, len(face_indices) - 1):
|
||||
indices.append((face_indices[0], face_indices[i], face_indices[i + 1]))
|
||||
|
||||
return vertices, indices
|
||||
|
||||
|
||||
if self.snap:
|
||||
mini_grid(self, context)
|
||||
def draw_bmesh_edges(edges, world_matrix):
|
||||
"""Convert bmesh edges into world-space vertex pairs to be used in GPU batch."""
|
||||
|
||||
gpu.state.blend_set('NONE')
|
||||
if not edges:
|
||||
return None
|
||||
|
||||
vertices = []
|
||||
for edge in edges:
|
||||
v1 = world_matrix @ edge.verts[0].co
|
||||
v2 = world_matrix @ edge.verts[1].co
|
||||
vertices.append(v1)
|
||||
vertices.append(v2)
|
||||
|
||||
return vertices
|
||||
|
||||
|
||||
def carver_shape_polyline(self, context):
|
||||
"""Shape overlay for polyline carver tool"""
|
||||
|
||||
# Create Shape
|
||||
coords, indices, first_point, array_coords = draw_polygon(self)
|
||||
self.verts = list(dict.fromkeys(self.mouse_path))
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
# 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')
|
||||
|
||||
|
||||
if self.snap:
|
||||
mini_grid(self, context)
|
||||
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
|
||||
def carver_shape_array(self, verts, indices, shader):
|
||||
"""Draws given shape for each row and column of the array"""
|
||||
|
||||
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()}}
|
||||
|
||||
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):
|
||||
"""Draws snap mini-grid around the cursor based on the overlay grid"""
|
||||
def draw_circle_around_point(context, obj, vert, radius, segments):
|
||||
"""
|
||||
Draws the screen-aligned circle around given vertex of the object.
|
||||
Returns the list of vertices for GPU batch.
|
||||
"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
vert_world = obj.matrix_world @ vert.co
|
||||
radius = min(radius, 25)
|
||||
|
||||
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
|
||||
vertices = []
|
||||
for i in range(segments + 1):
|
||||
angle = i * (2 * math.pi / segments)
|
||||
|
||||
# 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)
|
||||
# Calculate offset and vertex position in screen-space.
|
||||
offset_x = radius * math.cos(angle)
|
||||
offset_y = radius * math.sin(angle)
|
||||
vert_screen = view3d_utils.location_3d_to_region_2d(region, rv3d, vert_world)
|
||||
|
||||
# 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))
|
||||
if vert_screen:
|
||||
# Add offset in screen-space and convert back to world-space.
|
||||
circle_screen = Vector((vert_screen.x + offset_x, vert_screen.y + offset_y))
|
||||
circle_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, circle_screen, vert_world)
|
||||
vertices.append(circle_3d)
|
||||
|
||||
# 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)
|
||||
return vertices
|
||||
|
||||
@@ -1,237 +1,77 @@
|
||||
import bpy
|
||||
import math
|
||||
import mathutils
|
||||
from mathutils import Vector
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
|
||||
magic_number = 1.41
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def draw_circle(self, subdivision, rotation):
|
||||
"""Returns the coordinates & indices of a 2d circle in screen-space"""
|
||||
def distance_from_point_to_segment(point, start, end) -> float:
|
||||
"""
|
||||
Calculates the shortest distance between a point and a segment.
|
||||
All three inputs should be `mathutils.Vector` objects.
|
||||
This is an alternative to `mathutils.geometry.intersect_point_line`.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
def create_2d_circle(self, step, rotation):
|
||||
"""Create the vertices of a 2d circle at (0, 0)"""
|
||||
segment = end - start
|
||||
start_to_point = point - start
|
||||
|
||||
modifier = 2 if self.shape == 'CIRCLE' else magic_number
|
||||
if self.origin == 'CENTER':
|
||||
modifier /= 2
|
||||
# projection_along_segment
|
||||
c1 = start_to_point.dot(segment)
|
||||
if c1 <= 0:
|
||||
return (point - start).length
|
||||
|
||||
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)
|
||||
# segment_length_squared
|
||||
c2 = segment.dot(segment)
|
||||
if c2 <= c1:
|
||||
return (point - end).length
|
||||
|
||||
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)
|
||||
t = c1 / c2
|
||||
closest_point = start + t * segment
|
||||
distance = (point - closest_point).length
|
||||
|
||||
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)
|
||||
return distance
|
||||
|
||||
|
||||
# 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
|
||||
]
|
||||
def region_2d_to_line_3d(region, rv3d, point_2d: Vector, line_origin: Vector, line_direction: Vector) -> tuple[Vector, Vector]:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D ray and finds closest
|
||||
points between that ray and a given 3D line.
|
||||
"""
|
||||
|
||||
return tris_verts, indices, bounds
|
||||
if line_origin is None or line_direction is None:
|
||||
return None, None
|
||||
|
||||
# Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction).
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
# Find closest points to each other on each line (second line being a ray).
|
||||
closest_points = mathutils.geometry.intersect_line_line(ray_origin,
|
||||
ray_origin + ray_direction,
|
||||
line_origin,
|
||||
line_origin + line_direction)
|
||||
|
||||
return closest_points
|
||||
|
||||
|
||||
def draw_polygon(self):
|
||||
"""Returns polygonal 2d shape in screen-space where each cursor click is taken as a new vertice"""
|
||||
def region_2d_to_plane_3d(region, rv3d, point_2d: Vector, plane: tuple[Vector]) -> Vector:
|
||||
"""
|
||||
Converts a 2D screen-space point into a 3D point on a plane in world-space.
|
||||
Adapted from "Blockout" extension by niewinny (https://github.com/niewinny/blockout).
|
||||
"""
|
||||
|
||||
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)
|
||||
location, normal = plane
|
||||
|
||||
i1 = idx + 1
|
||||
i2 = idx + 2 if idx <= len(self.mouse_path) else 1
|
||||
indices.append((0, i1, i2))
|
||||
# Convert the screen-space 2D point Vector into a world-space 3D ray (origin + direction).
|
||||
p3_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, point_2d)
|
||||
p3_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, point_2d)
|
||||
|
||||
# circle_around_first_point
|
||||
radius = self.distance_from_first
|
||||
segments = 4
|
||||
# Intersect the point with the plane.
|
||||
p3_on_plane = mathutils.geometry.intersect_line_plane(p3_origin, # First point of line.
|
||||
p3_origin + p3_direction, # Second point of line.
|
||||
location, # `plane_co` (a point on the plane).
|
||||
normal) # `plane_no` (the direction the plane is facing).
|
||||
|
||||
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
|
||||
return p3_on_plane
|
||||
|
||||
@@ -4,164 +4,102 @@ import mathutils
|
||||
import math
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
from .object import hide_objects
|
||||
from .types import Ray
|
||||
|
||||
|
||||
#### ------------------------------ 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 = combined_bounding_box(self.selected_objects)
|
||||
plane_point = mathutils.Vector(plane_point)
|
||||
|
||||
# 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):
|
||||
def extrude_face(bm, face):
|
||||
"""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]
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
# 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
|
||||
result = bmesh.ops.extrude_face_region(bm, geom=[bm.faces[face.index]])
|
||||
|
||||
# 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
|
||||
# Offset extruded vertices.
|
||||
extruded_verts = [v for v in result['geom'] if isinstance(v, bmesh.types.BMVert)]
|
||||
extruded_edges = [e for e in result['geom'] if isinstance(e, bmesh.types.BMEdge)]
|
||||
extruded_faces = [f for f in result['geom'] if isinstance(f, bmesh.types.BMFace)]
|
||||
|
||||
# correct_normals
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
mesh.update()
|
||||
bm.free()
|
||||
return extruded_verts, extruded_edges, extruded_faces
|
||||
|
||||
|
||||
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
|
||||
# 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):
|
||||
"""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):
|
||||
def shade_smooth_by_angle(bm, mesh, 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
|
||||
if len(edge.link_faces) != 2:
|
||||
continue
|
||||
|
||||
face1, face2 = edge.link_faces
|
||||
if face1.normal.length <= 0 or face2.normal.length <= 0:\
|
||||
continue
|
||||
|
||||
edge_angle = math.degrees(face1.normal.angle(face2.normal))
|
||||
if edge_angle < 0:
|
||||
continue
|
||||
if edge_angle < angle:
|
||||
continue
|
||||
|
||||
edge.smooth = False
|
||||
|
||||
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()
|
||||
|
||||
def are_intersecting(obj_a, obj_b):
|
||||
"""Checks if bounding boxes of two given objects intersect."""
|
||||
|
||||
def world_bounds(obj):
|
||||
corners = [obj.matrix_world @ mathutils.Vector(c) for c in obj.bound_box]
|
||||
xs = [c.x for c in corners]
|
||||
ys = [c.y for c in corners]
|
||||
zs = [c.z for c in corners]
|
||||
return (min(xs), max(xs)), (min(ys), max(ys)), (min(zs), max(zs))
|
||||
|
||||
(ax0, ax1), (ay0, ay1), (az0, az1) = world_bounds(obj_a)
|
||||
(bx0, bx1), (by0, by1), (bz0, bz1) = world_bounds(obj_b)
|
||||
|
||||
return (
|
||||
ax1 >= bx0 and ax0 <= bx1 and
|
||||
ay1 >= by0 and ay0 <= by1 and
|
||||
az1 >= bz0 and az0 <= bz1
|
||||
)
|
||||
|
||||
|
||||
def ensure_attribute(bm, name, domain):
|
||||
"""Ensure that the attribute with the given name and domain exists on mesh."""
|
||||
|
||||
if domain == 'EDGE':
|
||||
attr = bm.edges.layers.float.get(name)
|
||||
if not attr:
|
||||
attr = bm.edges.layers.float.new(name)
|
||||
|
||||
elif domain == 'VERTEX':
|
||||
attr = bm.verts.layers.float.get(name)
|
||||
if not attr:
|
||||
attr = bm.verts.layers.float.new(name)
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
def raycast(context, position, objects):
|
||||
"""Cast a ray in the scene to get the surface on any of the given objects."""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
depsgraph = context.view_layer.depsgraph
|
||||
|
||||
origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, position)
|
||||
direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, position)
|
||||
|
||||
# Cast Ray
|
||||
with hide_objects(context, exceptions=objects):
|
||||
hit, location, normal, index, object, matrix = context.scene.ray_cast(depsgraph, origin, direction)
|
||||
ray = Ray(hit, location, normal, index, object, matrix)
|
||||
|
||||
return ray
|
||||
|
||||
@@ -3,6 +3,9 @@ import bmesh
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
from ..functions.list import (
|
||||
list_pre_boolean_modifiers,
|
||||
)
|
||||
from .object import (
|
||||
convert_to_mesh,
|
||||
)
|
||||
@@ -21,7 +24,7 @@ def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, re
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
modifier = obj.modifiers.new("boolean_" + cutter.name, 'BOOLEAN')
|
||||
modifier = obj.modifiers.new("boolean_" + cutter.name.replace("boolean_", ""), 'BOOLEAN')
|
||||
modifier.operation = mode
|
||||
modifier.object = cutter
|
||||
modifier.solver = solver
|
||||
@@ -44,7 +47,7 @@ def add_boolean_modifier(self, context, obj, cutter, mode, solver, pin=False, re
|
||||
return modifier
|
||||
|
||||
|
||||
def apply_modifiers(context, obj, modifiers: list):
|
||||
def apply_modifiers(context, obj, modifiers: list, force_clean=False):
|
||||
"""
|
||||
Apply modifiers on object.
|
||||
Instead of using `bpy.ops.object.modifier_apply`, this function uses
|
||||
@@ -63,9 +66,10 @@ def apply_modifiers(context, obj, modifiers: list):
|
||||
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.
|
||||
# Don't use this method if it's not enabled by user in preferences, unless caller forces it.
|
||||
if not prefs.fast_modifier_apply:
|
||||
raise Exception("")
|
||||
if not force_clean:
|
||||
raise Exception()
|
||||
|
||||
with hide_modifiers(obj, excluding=modifiers):
|
||||
# Create a temporary mesh from evaluated object.
|
||||
@@ -99,7 +103,7 @@ def apply_modifiers(context, obj, modifiers: list):
|
||||
except Exception as e:
|
||||
# print("Error applying modifiers with `bmesh` method:", e, "falling back to `bpy.ops` method")
|
||||
|
||||
context_override = {"object": obj, "mode": 'OBJECT'}
|
||||
context_override = {"active_object": obj, "mode": 'OBJECT'}
|
||||
with context.temp_override(**context_override):
|
||||
# Apply shape keys if there are any.
|
||||
if obj.data.shape_keys:
|
||||
@@ -132,3 +136,48 @@ def hide_modifiers(obj, excluding: list):
|
||||
finally:
|
||||
for mod in visible_modifiers:
|
||||
mod.show_viewport = True
|
||||
|
||||
|
||||
def add_modifier_asset(obj, path: str, asset: str):
|
||||
"""Loads the node group asset and adds a Geometry Nodes modifier using it."""
|
||||
|
||||
try:
|
||||
# Load the node group.
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
with bpy.data.libraries.load(path, link=True, pack=True) as (data_from, data_to):
|
||||
if asset in data_from.node_groups:
|
||||
data_to.node_groups = [asset]
|
||||
|
||||
else:
|
||||
with bpy.data.libraries.load(path) as (data_from, data_to):
|
||||
if asset in data_from.node_groups:
|
||||
data_to.node_groups = [asset]
|
||||
|
||||
node_group = bpy.data.node_groups[asset]
|
||||
|
||||
# Add modifier on the object.
|
||||
mod = obj.modifiers.new(asset, type='NODES')
|
||||
mod.node_group = node_group
|
||||
mod.show_group_selector = False
|
||||
mod.show_manage_panel = False
|
||||
|
||||
return mod
|
||||
|
||||
except Exception as e:
|
||||
print("Modifier node group could not be loaded:", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_modifiers_to_apply(context, obj, new_modifiers) -> list:
|
||||
"""Returns the list of modifiers that need to be applied based on add-on preferences."""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
if prefs.apply_order == 'ALL':
|
||||
modifiers = [mod for mod in obj.modifiers]
|
||||
elif prefs.apply_order == 'BOOLEANS':
|
||||
modifiers = new_modifiers
|
||||
elif prefs.apply_order == 'BEFORE':
|
||||
modifiers = list_pre_boolean_modifiers(obj)
|
||||
|
||||
return modifiers
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
import bpy
|
||||
import bmesh
|
||||
import mathutils
|
||||
from contextlib import contextmanager
|
||||
from .. import __package__ as base_package
|
||||
|
||||
|
||||
#### ------------------------------ FUNCTIONS ------------------------------ ####
|
||||
|
||||
def set_cutter_properties(context, canvas, cutter, mode, parent=True, hide=False, collection=True):
|
||||
def set_cutter_properties(context, cutter, mode, display='BOUNDS', collection=True):
|
||||
"""Ensures cutter is properly set: has right properties, is hidden, in a collection & parented"""
|
||||
|
||||
prefs = context.preferences.addons[base_package].preferences
|
||||
|
||||
# Hide Cutters
|
||||
cutter.hide_render = True
|
||||
cutter.display_type = 'WIRE' if prefs.wireframe else 'BOUNDS'
|
||||
cutter.display_type = display
|
||||
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()
|
||||
@@ -103,12 +94,18 @@ def delete_cutter(cutter):
|
||||
bpy.data.meshes.remove(orphaned_mesh)
|
||||
|
||||
|
||||
def change_parent(object, parent):
|
||||
def change_parent(obj, parent, force=False, inverse=False):
|
||||
"""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
|
||||
if obj.parent is not None:
|
||||
if not force:
|
||||
return
|
||||
|
||||
matrix_copy = obj.matrix_world.copy()
|
||||
obj.parent = parent
|
||||
if inverse:
|
||||
obj.matrix_parent_inverse = parent.matrix_world.inverted()
|
||||
obj.matrix_world = matrix_copy
|
||||
|
||||
|
||||
def create_slice(context, canvas, modifier=False):
|
||||
@@ -136,14 +133,49 @@ def create_slice(context, canvas, modifier=False):
|
||||
return slice
|
||||
|
||||
|
||||
def set_object_origin(obj, position=False):
|
||||
def set_object_origin(obj, bm, point='CENTER', custom=None):
|
||||
"""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())
|
||||
# Center of the bounding box.
|
||||
if point == 'CENTER_OBJ':
|
||||
position_local = 0.125 * sum((mathutils.Vector(b) for b in obj.bound_box), mathutils.Vector())
|
||||
position_world = obj.matrix_world @ position_local
|
||||
|
||||
mat = mathutils.Matrix.Translation(position - obj.location)
|
||||
obj.location = position
|
||||
obj.data.transform(mat.inverted())
|
||||
obj.data.update()
|
||||
# Center of the geometry.
|
||||
elif point == 'CENTER_MESH':
|
||||
if len(bm.verts) > 0:
|
||||
position_local = sum((v.co for v in bm.verts), mathutils.Vector()) / len(bm.verts)
|
||||
else:
|
||||
position_local = mathutils.Vector((0, 0, 0))
|
||||
position_world = obj.matrix_world @ position_local
|
||||
|
||||
# Custom origin point (should be local Vector).
|
||||
elif point == 'CUSTOM':
|
||||
position_local = custom
|
||||
position_world = obj.matrix_world @ custom
|
||||
|
||||
mat = mathutils.Matrix.Translation(position_local)
|
||||
bmesh.ops.transform(bm, matrix=mat.inverted(), verts=bm.verts)
|
||||
bm.to_mesh(obj.data)
|
||||
|
||||
obj.location = position_world
|
||||
|
||||
|
||||
@contextmanager
|
||||
def hide_objects(context, exceptions: list):
|
||||
"""Hides objects during the context, and restores their visibility afterwards."""
|
||||
|
||||
hidden_objects = []
|
||||
for obj in context.scene.objects:
|
||||
if obj in exceptions:
|
||||
continue
|
||||
if obj.hide_get() == False:
|
||||
hidden_objects.append(obj)
|
||||
obj.hide_set(True)
|
||||
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
for obj in hidden_objects:
|
||||
obj.hide_set(False)
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from bpy_extras import view3d_utils
|
||||
|
||||
from .math import get_bounding_box
|
||||
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, 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"""
|
||||
|
||||
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':
|
||||
# get_bounding_box_of_the_shape
|
||||
min_x, min_y, max_x, max_y = get_bounding_box(self.verts)
|
||||
|
||||
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:
|
||||
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 is_instanced_data(obj):
|
||||
self.report({'ERROR'}, f"Modifiers cannot be applied to {obj.name} because it has instanced object data")
|
||||
continue
|
||||
|
||||
intersecting_objects.append(obj)
|
||||
|
||||
return intersecting_objects
|
||||
@@ -0,0 +1,23 @@
|
||||
import bpy
|
||||
import mathutils
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
|
||||
#### ------------------------------ CLASSES ------------------------------ ####
|
||||
|
||||
class Ray:
|
||||
"""Class object for storing raycast results."""
|
||||
|
||||
def __init__(self,
|
||||
hit: bool,
|
||||
location: Vector,
|
||||
normal: Vector,
|
||||
index: int,
|
||||
obj,
|
||||
matrix: Matrix):
|
||||
self.hit = hit
|
||||
self.location = location if location is not None else mathutils.Vector()
|
||||
self.normal = normal if normal is not None else mathutils.Vector()
|
||||
self.index = index
|
||||
self.obj = obj
|
||||
self.matrix = matrix if matrix is not None else mathutils.Matrix()
|
||||
Reference in New Issue
Block a user