mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 22:02:21 +00:00
snap: remove custom edge BVH (SnapObj)
This commit is contained in:
committed by
Bruno Perdigão
parent
d227f45663
commit
50c1a0049c
@@ -462,7 +462,6 @@ class PolylineOperator:
|
|||||||
self.tool_state.axis_method = None
|
self.tool_state.axis_method = None
|
||||||
self.tool_state.plane_method = None
|
self.tool_state.plane_method = None
|
||||||
self.tool_state.mode = "Mouse"
|
self.tool_state.mode = "Mouse"
|
||||||
tool.Raycast.clear_snap_objs()
|
|
||||||
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
||||||
for obj in self.visible_objs:
|
for obj in self.visible_objs:
|
||||||
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
if bbox_2d := tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj):
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import bmesh
|
import bmesh
|
||||||
@@ -334,8 +333,6 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
(0, -offset),
|
(0, -offset),
|
||||||
(offset, -offset),
|
(offset, -offset),
|
||||||
)
|
)
|
||||||
snap_objs = []
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_visible_objects(cls, context: bpy.types.Context):
|
def get_visible_objects(cls, context: bpy.types.Context):
|
||||||
depsgraph = context.evaluated_depsgraph_get()
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
@@ -655,9 +652,6 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
"""
|
"""
|
||||||
global _encoding_shader, _offscreen, _obj_list
|
global _encoding_shader, _offscreen, _obj_list
|
||||||
|
|
||||||
new_objs = []
|
|
||||||
for o in objs_to_raycast:
|
|
||||||
new_objs.append(o.obj)
|
|
||||||
if bpy.app.background:
|
if bpy.app.background:
|
||||||
return [], None
|
return [], None
|
||||||
|
|
||||||
@@ -676,7 +670,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
render_ops: list[tuple[GPUBatch, Matrix, int]] = []
|
render_ops: list[tuple[GPUBatch, Matrix, int]] = []
|
||||||
|
|
||||||
if tris:
|
if tris:
|
||||||
for snap_obj in new_objs:
|
for snap_obj in objs_to_raycast:
|
||||||
if snap_obj.type != "MESH":
|
if snap_obj.type != "MESH":
|
||||||
continue
|
continue
|
||||||
if not hasattr(snap_obj.data, "polygons"):
|
if not hasattr(snap_obj.data, "polygons"):
|
||||||
@@ -697,7 +691,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
slot = 1 # slot 0 = background
|
slot = 1 # slot 0 = background
|
||||||
obj_slots: list[tuple] = [] # [(snap_obj, pts_start, n_pts, lines_start, n_lines), ...]
|
obj_slots: list[tuple] = [] # [(snap_obj, pts_start, n_pts, lines_start, n_lines), ...]
|
||||||
|
|
||||||
for snap_obj in new_objs:
|
for snap_obj in objs_to_raycast:
|
||||||
batches = _ensure_wireframe_batches(snap_obj)
|
batches = _ensure_wireframe_batches(snap_obj)
|
||||||
if not batches:
|
if not batches:
|
||||||
continue
|
continue
|
||||||
@@ -949,176 +943,6 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
return cls.get_gpu_detection_snaps(context, event, objs_to_raycast)
|
return cls.get_gpu_detection_snaps(context, event, objs_to_raycast)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def ray_cast_by_proximity_2d(
|
|
||||||
cls,
|
|
||||||
context: bpy.types.Context,
|
|
||||||
event: bpy.types.Event,
|
|
||||||
snap_obj: SnapObj,
|
|
||||||
):
|
|
||||||
|
|
||||||
def divide_vector(start, end, n):
|
|
||||||
points = []
|
|
||||||
delta = (end - start) / n
|
|
||||||
for i in range(1, n):
|
|
||||||
point = start + i * delta
|
|
||||||
points.append(point)
|
|
||||||
return points
|
|
||||||
|
|
||||||
region = context.region
|
|
||||||
rv3d = context.region_data
|
|
||||||
mouse_pos = event.mouse_region_x, event.mouse_region_y
|
|
||||||
ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
|
|
||||||
points = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, mouse_pos, ray_direction)
|
|
||||||
except:
|
|
||||||
loc = Vector((0, 0, 0))
|
|
||||||
|
|
||||||
snap_obj._ensure_bvh()
|
|
||||||
intersected = snap_obj.raycast_boxes(
|
|
||||||
context, event, snap_obj.root, intersected=[], rays=(ray_origin, ray_direction)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Collect edges from intersected BVH boxes
|
|
||||||
edges = []
|
|
||||||
for it in intersected:
|
|
||||||
edges.extend(it.edges)
|
|
||||||
edges = set(edges)
|
|
||||||
|
|
||||||
# Build only the vertices indices that belong to these edges
|
|
||||||
verts_idx: set[int] = set()
|
|
||||||
for e in edges:
|
|
||||||
ev = snap_obj.obj.data.edges[e].vertices
|
|
||||||
verts_idx.add(ev[0])
|
|
||||||
verts_idx.add(ev[1])
|
|
||||||
|
|
||||||
# Lazily project only the needed vertices to 2D screen space
|
|
||||||
verts_2d: dict[int, Vector] = {}
|
|
||||||
for idx in verts_idx:
|
|
||||||
v2d = view3d_utils.location_3d_to_region_2d(region, rv3d, snap_obj.verts_3d[idx])
|
|
||||||
if v2d is not None:
|
|
||||||
verts_2d[idx] = v2d
|
|
||||||
|
|
||||||
edge_verts = {}
|
|
||||||
for e in edges:
|
|
||||||
verts_idx = snap_obj.obj.data.edges[e].vertices
|
|
||||||
v1 = snap_obj.verts_3d[verts_idx[0]]
|
|
||||||
v2 = snap_obj.verts_3d[verts_idx[1]]
|
|
||||||
v1_2d = verts_2d.get(verts_idx[0])
|
|
||||||
v2_2d = verts_2d.get(verts_idx[1])
|
|
||||||
if (v1_2d is None) ^ (v2_2d is None):
|
|
||||||
point, _ = cls.intersect_edge_region_border(region, context.space_data, rv3d, v1, v2)
|
|
||||||
if v1_2d is None:
|
|
||||||
edge_verts[e] = (point, v2_2d)
|
|
||||||
else:
|
|
||||||
edge_verts[e] = (v1_2d, point)
|
|
||||||
else:
|
|
||||||
edge_verts[e] = (v1_2d, v2_2d)
|
|
||||||
|
|
||||||
snap_threshold = 10.0
|
|
||||||
|
|
||||||
# Check all vertices for proximity to mouse position.
|
|
||||||
# Re-use the 2D projections already computed for edge endpoints.
|
|
||||||
for i, v3d in enumerate(snap_obj.verts_3d):
|
|
||||||
if i in verts_2d:
|
|
||||||
v2d = verts_2d[i]
|
|
||||||
else:
|
|
||||||
v2d = view3d_utils.location_3d_to_region_2d(region, rv3d, v3d)
|
|
||||||
if v2d is None:
|
|
||||||
continue
|
|
||||||
distance = (Vector(mouse_pos) - v2d).length
|
|
||||||
if distance <= snap_threshold:
|
|
||||||
snap_point = {
|
|
||||||
"object": snap_obj.obj,
|
|
||||||
"type": "Vertex",
|
|
||||||
"point": snap_obj.verts_3d[i],
|
|
||||||
"distance": distance / 10,
|
|
||||||
}
|
|
||||||
points.append(snap_point)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
selected_edges = {}
|
|
||||||
for e in edges:
|
|
||||||
p0, p1 = edge_verts[e]
|
|
||||||
p0x, p0y = p0
|
|
||||||
p1x, p1y = p1
|
|
||||||
px, py = mouse_pos
|
|
||||||
|
|
||||||
# segment vector = p1 - p0
|
|
||||||
sx = p1x - p0x
|
|
||||||
sy = p1y - p0y
|
|
||||||
|
|
||||||
# seg length squared
|
|
||||||
seg_len_sq = sx * sx + sy * sy
|
|
||||||
|
|
||||||
if seg_len_sq == 0.0:
|
|
||||||
# degenerate segment: skip it
|
|
||||||
continue
|
|
||||||
|
|
||||||
# project (p - p0) onto seg: t = dot(p-p0, seg) / |seg|^2
|
|
||||||
apx = px - p0x
|
|
||||||
apy = py - p0y
|
|
||||||
t = (apx * sx + apy * sy) / seg_len_sq
|
|
||||||
|
|
||||||
# clamp to segment
|
|
||||||
if t <= 0.0:
|
|
||||||
t_clamped = 0.0
|
|
||||||
cx, cy = p0x, p0y
|
|
||||||
elif t >= 1.0:
|
|
||||||
t_clamped = 1.0
|
|
||||||
cx, cy = p1x, p1y
|
|
||||||
else:
|
|
||||||
t_clamped = t
|
|
||||||
cx = p0x + sx * t_clamped
|
|
||||||
cy = p0y + sy * t_clamped
|
|
||||||
|
|
||||||
dx = px - cx
|
|
||||||
dy = py - cy
|
|
||||||
dist = math.hypot(dx, dy)
|
|
||||||
if dist <= snap_threshold:
|
|
||||||
selected_edges[dist] = e
|
|
||||||
|
|
||||||
if selected_edges:
|
|
||||||
min_dist = float("inf")
|
|
||||||
for key in selected_edges:
|
|
||||||
if key < min_dist:
|
|
||||||
min_dist = key
|
|
||||||
|
|
||||||
idx = snap_obj.obj.data.edges[selected_edges[min_dist]].vertices
|
|
||||||
edge_verts = (snap_obj.verts_3d[idx[0]], snap_obj.verts_3d[idx[1]])
|
|
||||||
division_points = divide_vector(
|
|
||||||
edge_verts[0], edge_verts[1], 2
|
|
||||||
) # TODO Make it work for different divisions
|
|
||||||
for division_point in division_points:
|
|
||||||
intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc))
|
|
||||||
distance = (division_point - intersection).length
|
|
||||||
if distance < snap_threshold:
|
|
||||||
snap_point = {
|
|
||||||
"object": snap_obj.obj,
|
|
||||||
"type": "Edge Center",
|
|
||||||
"point": division_point.copy(),
|
|
||||||
"distance": distance,
|
|
||||||
}
|
|
||||||
points.append(snap_point)
|
|
||||||
|
|
||||||
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), edge_verts)
|
|
||||||
if intersection[0]:
|
|
||||||
if tool.Cad.is_point_on_edge(intersection[1], edge_verts):
|
|
||||||
distance = (intersection[1] - intersection[0]).length
|
|
||||||
if distance < snap_threshold:
|
|
||||||
snap_point = {
|
|
||||||
"object": snap_obj.obj,
|
|
||||||
"type": "Edge",
|
|
||||||
"point": intersection[1].copy(),
|
|
||||||
"edge_verts": edge_verts,
|
|
||||||
"distance": distance,
|
|
||||||
}
|
|
||||||
points.append(snap_point)
|
|
||||||
|
|
||||||
return points
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ray_cast_by_proximity(
|
def ray_cast_by_proximity(
|
||||||
cls,
|
cls,
|
||||||
@@ -1344,9 +1168,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
if bbox_2d:
|
if bbox_2d:
|
||||||
if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d):
|
if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d):
|
||||||
if tool.Raycast.object_is_visible_in_clipping_plane(obj):
|
if tool.Raycast.object_is_visible_in_clipping_plane(obj):
|
||||||
snap_obj = cls.create_snap_obj(obj)
|
objs_to_raycast.append(obj)
|
||||||
if snap_obj is not None:
|
|
||||||
objs_to_raycast.append(snap_obj)
|
|
||||||
|
|
||||||
return objs_to_raycast
|
return objs_to_raycast
|
||||||
|
|
||||||
@@ -1399,12 +1221,12 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
|
|
||||||
for snap_obj in objs_to_raycast:
|
for snap_obj in objs_to_raycast:
|
||||||
if not include_wireframes and (
|
if not include_wireframes and (
|
||||||
snap_obj.obj.type in {"EMPTY", "CURVE"}
|
snap_obj.type in {"EMPTY", "CURVE"}
|
||||||
or (hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0)
|
or (hasattr(snap_obj.data, "polygons") and len(snap_obj.data.polygons) == 0)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj.obj)
|
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj)
|
||||||
|
|
||||||
if hit is not None:
|
if hit is not None:
|
||||||
length_squared = (hit - ray_origin).length_squared
|
length_squared = (hit - ray_origin).length_squared
|
||||||
@@ -1429,13 +1251,14 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
ray_origin: Vector,
|
ray_origin: Vector,
|
||||||
closest_snaps: list,
|
closest_snaps: list,
|
||||||
):
|
):
|
||||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj)
|
||||||
hit_obj = None
|
hit_obj = None
|
||||||
hit = None
|
hit = None
|
||||||
if snap_points:
|
if snap_points:
|
||||||
closest_length_squared = float("inf")
|
closest_length_squared = float("inf")
|
||||||
for point in snap_points:
|
for point in snap_points:
|
||||||
point["group"] = "Wireframe"
|
point["group"] = "Wireframe"
|
||||||
|
point["object"] = snap_obj
|
||||||
closest_snaps.append(point)
|
closest_snaps.append(point)
|
||||||
length = (point["point"] - ray_origin).length_squared
|
length = (point["point"] - ray_origin).length_squared
|
||||||
if length < closest_length_squared:
|
if length < closest_length_squared:
|
||||||
@@ -1472,15 +1295,15 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
wireframe_objs = []
|
wireframe_objs = []
|
||||||
solid_objs = []
|
solid_objs = []
|
||||||
for snap_obj in objs_to_raycast:
|
for snap_obj in objs_to_raycast:
|
||||||
if snap_obj.obj.type in {"EMPTY", "CURVE"} or (
|
if snap_obj.type in {"EMPTY", "CURVE"} or (
|
||||||
hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0
|
hasattr(snap_obj.data, "polygons") and len(snap_obj.data.polygons) == 0
|
||||||
):
|
):
|
||||||
wireframe_objs.append(snap_obj)
|
wireframe_objs.append(snap_obj)
|
||||||
else:
|
else:
|
||||||
solid_objs.append(snap_obj)
|
solid_objs.append(snap_obj)
|
||||||
|
|
||||||
# Rough distance - object origin to ray origin
|
# Rough distance - object origin to ray origin
|
||||||
solid_objs.sort(key=lambda so: (so.obj.matrix_world.translation - ray_origin).length_squared)
|
solid_objs.sort(key=lambda so: (so.matrix_world.translation - ray_origin).length_squared)
|
||||||
|
|
||||||
# Process wireframe objects first (all of them, always collected)
|
# Process wireframe objects first (all of them, always collected)
|
||||||
for snap_obj in wireframe_objs:
|
for snap_obj in wireframe_objs:
|
||||||
@@ -1495,7 +1318,7 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
|
|
||||||
# Process solid objects in distance order, stop at first hit
|
# Process solid objects in distance order, stop at first hit
|
||||||
for snap_obj in solid_objs:
|
for snap_obj in solid_objs:
|
||||||
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj.obj)
|
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj)
|
||||||
|
|
||||||
if hit:
|
if hit:
|
||||||
snap_point = {
|
snap_point = {
|
||||||
@@ -1520,14 +1343,14 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
else:
|
else:
|
||||||
# Xray mode - process all objects (all snaps are kept by the caller)
|
# Xray mode - process all objects (all snaps are kept by the caller)
|
||||||
for snap_obj in objs_to_raycast:
|
for snap_obj in objs_to_raycast:
|
||||||
if snap_obj.obj.type in {"EMPTY", "CURVE"} or (
|
if snap_obj.type in {"EMPTY", "CURVE"} or (
|
||||||
hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0
|
hasattr(snap_obj.data, "polygons") and len(snap_obj.data.polygons) == 0
|
||||||
):
|
):
|
||||||
hit_obj, hit = cls.process_wireframe_snap_obj(context, event, snap_obj, ray_origin, closest_snaps)
|
hit_obj, hit = cls.process_wireframe_snap_obj(context, event, snap_obj, ray_origin, closest_snaps)
|
||||||
face_index = None
|
face_index = None
|
||||||
else:
|
else:
|
||||||
# Solid objects
|
# Solid objects
|
||||||
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj.obj)
|
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj)
|
||||||
|
|
||||||
if hit:
|
if hit:
|
||||||
snap_point = {
|
snap_point = {
|
||||||
@@ -1567,280 +1390,3 @@ class Raycast(bonsai.core.tool.Raycast):
|
|||||||
if lens < 50:
|
if lens < 50:
|
||||||
snap_threshold *= value
|
snap_threshold *= value
|
||||||
return snap_threshold
|
return snap_threshold
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_snap_obj(cls, obj):
|
|
||||||
if obj.data is None or not isinstance(obj.data, bpy.types.Mesh):
|
|
||||||
return None
|
|
||||||
for i, snap_obj in enumerate(cls.snap_objs):
|
|
||||||
if obj.name == snap_obj.obj.name:
|
|
||||||
# Handle objects modified while a modal operator is active.
|
|
||||||
# Example: adding a door or window alters the wall geometry.
|
|
||||||
if len(obj.data.vertices) != len(snap_obj.verts_3d):
|
|
||||||
cls.snap_objs.pop(i)
|
|
||||||
snap_obj = SnapObj(obj)
|
|
||||||
cls.snap_objs.append(snap_obj)
|
|
||||||
for v1, v2 in zip(obj.data.vertices, snap_obj.verts_3d):
|
|
||||||
if (obj.matrix_world @ v1.co) != v2:
|
|
||||||
cls.snap_objs.pop(i)
|
|
||||||
snap_obj = SnapObj(obj)
|
|
||||||
cls.snap_objs.append(snap_obj)
|
|
||||||
return snap_obj
|
|
||||||
snap_obj = SnapObj(obj)
|
|
||||||
cls.snap_objs.append(snap_obj)
|
|
||||||
return snap_obj
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def clear_snap_objs(cls):
|
|
||||||
TreeNode.__clear_all__()
|
|
||||||
SnapObj.__clear_all__()
|
|
||||||
cls.snap_objs.clear()
|
|
||||||
|
|
||||||
|
|
||||||
class TreeNode:
|
|
||||||
all = []
|
|
||||||
|
|
||||||
def __init__(self, box: tuple):
|
|
||||||
self.__class__.all.append(self)
|
|
||||||
self.box = box
|
|
||||||
self.child_a = None
|
|
||||||
self.child_b = None
|
|
||||||
self.edges = []
|
|
||||||
|
|
||||||
def __clear_all__():
|
|
||||||
for instance in TreeNode.all:
|
|
||||||
del instance
|
|
||||||
TreeNode.all.clear()
|
|
||||||
|
|
||||||
|
|
||||||
class SnapObj:
|
|
||||||
max_depth = 9
|
|
||||||
all = []
|
|
||||||
|
|
||||||
def __init__(self, obj: bpy.types.Object):
|
|
||||||
self.__class__.all.append(self)
|
|
||||||
self.obj = obj
|
|
||||||
self.root = None
|
|
||||||
self._bvh_built = False
|
|
||||||
self.verts_3d = [obj.matrix_world @ v.co for v in obj.data.vertices]
|
|
||||||
self.snap_points = []
|
|
||||||
|
|
||||||
def _ensure_bvh(self):
|
|
||||||
if self._bvh_built:
|
|
||||||
return
|
|
||||||
self.root = self._create_root_node()
|
|
||||||
self.root.edges = [e.index for e in self.obj.data.edges]
|
|
||||||
self.split_box(self.root, 0)
|
|
||||||
self._bvh_built = True
|
|
||||||
|
|
||||||
def __clear_all__():
|
|
||||||
for instance in SnapObj.all:
|
|
||||||
del instance
|
|
||||||
SnapObj.all.clear()
|
|
||||||
|
|
||||||
def _create_root_node(self) -> TreeNode:
|
|
||||||
bbox = tool.Blender.get_object_bounding_box(self.obj)
|
|
||||||
min_point = self.obj.matrix_world @ bbox["min_point"]
|
|
||||||
max_point = self.obj.matrix_world @ bbox["max_point"]
|
|
||||||
new_bbox = self.expand_bounding_box((min_point, max_point))
|
|
||||||
return TreeNode(new_bbox)
|
|
||||||
|
|
||||||
def divide_bounding_box_along_longest_axis(
|
|
||||||
self, min_pt: Vector, max_pt: Vector
|
|
||||||
) -> Union[tuple[Vector, Vector], tuple[Vector, Vector]]:
|
|
||||||
"""
|
|
||||||
Divide a bounding box into two equal parts along the axis with the longest dimension.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
min_pt: The minimum point of the bounding box.
|
|
||||||
max_pt: The maximum point of the bounding box.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: A list of two tuples, each containing the minimum and maximum points of the divided boxes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Calculate the dimensions of the box
|
|
||||||
dx = max_pt.x - min_pt.x
|
|
||||||
dy = max_pt.y - min_pt.y
|
|
||||||
dz = max_pt.z - min_pt.z
|
|
||||||
|
|
||||||
# Determine the axis with the longest dimension
|
|
||||||
if dx >= dy and dx >= dz:
|
|
||||||
# Divide along the x-axis
|
|
||||||
mid_x = min_pt.x + dx / 2
|
|
||||||
box1 = (min_pt, Vector((mid_x, max_pt.y, max_pt.z)))
|
|
||||||
box2 = (Vector((mid_x, min_pt.y, min_pt.z)), max_pt)
|
|
||||||
elif dy >= dx and dy >= dz:
|
|
||||||
# Divide along the y-axis
|
|
||||||
mid_y = min_pt.y + dy / 2
|
|
||||||
box1 = (min_pt, Vector((max_pt.x, mid_y, max_pt.z)))
|
|
||||||
box2 = (Vector((min_pt.x, mid_y, min_pt.z)), max_pt)
|
|
||||||
else:
|
|
||||||
# Divide along the z-axis
|
|
||||||
mid_z = min_pt.z + dz / 2
|
|
||||||
box1 = (min_pt, Vector((max_pt.x, max_pt.y, mid_z)))
|
|
||||||
box2 = (Vector((min_pt.x, min_pt.y, mid_z)), max_pt)
|
|
||||||
|
|
||||||
return [box1, box2]
|
|
||||||
|
|
||||||
def expand_bounding_box(self, box: tuple[Vector, Vector], offset: float = 0.1) -> tuple[Vector, Vector]:
|
|
||||||
"""
|
|
||||||
Expand a 3D bounding box by a given offset.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
min_pt: The minimum point of the bounding box.
|
|
||||||
max_pt: The maximum point of the bounding box.
|
|
||||||
offset: The offset to expand the bounding box by.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: A tuple containing the new minimum and maximum points of the expanded bounding box.
|
|
||||||
"""
|
|
||||||
|
|
||||||
min_pt, max_pt = box
|
|
||||||
# Calculate the new minimum and maximum points
|
|
||||||
new_min_pt = Vector((min_pt.x - offset, min_pt.y - offset, min_pt.z - offset))
|
|
||||||
new_max_pt = Vector((max_pt.x + offset, max_pt.y + offset, max_pt.z + offset))
|
|
||||||
|
|
||||||
return new_min_pt, new_max_pt
|
|
||||||
|
|
||||||
def split_box(self, parent: TreeNode, depth: int):
|
|
||||||
"""
|
|
||||||
Splits the bounding box creating two child nodes to compose a BVH Tree recursively.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: the TreeNode instance that represents the parent node of a BVH Tree.
|
|
||||||
depth: the depth of the BVH Tree no be used in recursion.
|
|
||||||
"""
|
|
||||||
if depth > self.max_depth:
|
|
||||||
return
|
|
||||||
box_a, box_b = self.divide_bounding_box_along_longest_axis(parent.box[0], parent.box[1])
|
|
||||||
parent.child_a = TreeNode(box_a)
|
|
||||||
parent.child_b = TreeNode(box_b)
|
|
||||||
edges_a = []
|
|
||||||
edges_b = []
|
|
||||||
for e in parent.edges:
|
|
||||||
verts_idx = [v for v in self.obj.data.edges[e].vertices]
|
|
||||||
verts_coords = []
|
|
||||||
for idx in verts_idx:
|
|
||||||
if idx < len(self.obj.data.vertices):
|
|
||||||
verts_coords.append(self.obj.matrix_world @ self.obj.data.vertices[idx].co)
|
|
||||||
if self.line_intersects_box(verts_coords[0], verts_coords[1], parent.child_a.box):
|
|
||||||
edges_a.append(e)
|
|
||||||
if self.line_intersects_box(verts_coords[0], verts_coords[1], parent.child_b.box):
|
|
||||||
edges_b.append(e)
|
|
||||||
parent.child_a.edges = edges_a
|
|
||||||
parent.child_b.edges = edges_b
|
|
||||||
self.split_box(parent.child_a, depth + 1)
|
|
||||||
self.split_box(parent.child_b, depth + 1)
|
|
||||||
|
|
||||||
def raycast_box(
|
|
||||||
self, context: bpy.types.Context, event: bpy.types.Event, node: TreeNode, rays: tuple[Vector, Vector]
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Raycast bounding box.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: Blender context.
|
|
||||||
event: Blender event.
|
|
||||||
node: a TreeNode instance.
|
|
||||||
rays: tuple containing ray origin and ray direction
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if hits the box or False otherwise.
|
|
||||||
"""
|
|
||||||
box = node.box
|
|
||||||
min_v = box[0]
|
|
||||||
max_v = box[1]
|
|
||||||
t_min = 0.0
|
|
||||||
t_max = float("inf")
|
|
||||||
ray_origin, ray_dir = rays
|
|
||||||
inv_dir = Vector((1.0 / r if r != 0.0 else 1e32) for r in (ray_dir.x, ray_dir.y, ray_dir.z))
|
|
||||||
# X
|
|
||||||
tx1 = (min_v.x - ray_origin.x) * inv_dir[0]
|
|
||||||
tx2 = (max_v.x - ray_origin.x) * inv_dir[0]
|
|
||||||
tmin = min(tx1, tx2)
|
|
||||||
tmax = max(tx1, tx2)
|
|
||||||
# Y
|
|
||||||
ty1 = (min_v.y - ray_origin.y) * inv_dir[1]
|
|
||||||
ty2 = (max_v.y - ray_origin.y) * inv_dir[1]
|
|
||||||
tmin = max(tmin, min(ty1, ty2))
|
|
||||||
tmax = min(tmax, max(ty1, ty2))
|
|
||||||
# Z
|
|
||||||
tz1 = (min_v.z - ray_origin.z) * inv_dir[2]
|
|
||||||
tz2 = (max_v.z - ray_origin.z) * inv_dir[2]
|
|
||||||
tmin = max(tmin, min(tz1, tz2))
|
|
||||||
tmax = min(tmax, max(tz1, tz2))
|
|
||||||
return (tmax >= max(tmin, t_min)) and (tmin <= t_max)
|
|
||||||
|
|
||||||
def line_intersects_box(self, v1: mathutils.Vector, v2: mathutils.Vector, box: tuple) -> bool:
|
|
||||||
"""
|
|
||||||
Check if a line segment intersects an axis-aligned bounding box (AABB).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
v1: The first endpoint of the line segment as a mathutils.Vector.
|
|
||||||
v2: The second endpoint of the line segment as a mathutils.Vector.
|
|
||||||
box: A tuple containing the minimum and maximum points of the AABB, where each point is a mathutils.Vector.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the segment [v1, v2] intersects the AABB; otherwise, False.
|
|
||||||
"""
|
|
||||||
bmin, bmax = box
|
|
||||||
dir = v2 - v1
|
|
||||||
tmin = 0.0
|
|
||||||
tmax = 1.0
|
|
||||||
|
|
||||||
for i in range(3):
|
|
||||||
if abs(dir[i]) < 1e-12:
|
|
||||||
# Line is parallel to slab. If origin not within slab -> no hit.
|
|
||||||
if v1[i] < bmin[i] or v1[i] > bmax[i]:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
ood = 1.0 / dir[i]
|
|
||||||
t1 = (bmin[i] - v1[i]) * ood
|
|
||||||
t2 = (bmax[i] - v1[i]) * ood
|
|
||||||
if t1 > t2:
|
|
||||||
t1, t2 = t2, t1
|
|
||||||
if t1 > tmin:
|
|
||||||
tmin = t1
|
|
||||||
if t2 < tmax:
|
|
||||||
tmax = t2
|
|
||||||
if tmin > tmax:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If any overlap in [0,1] exists, there's intersection
|
|
||||||
return (tmax >= 0.0) and (tmin <= 1.0)
|
|
||||||
|
|
||||||
def raycast_boxes(
|
|
||||||
self,
|
|
||||||
context: bpy.types.Context,
|
|
||||||
event: bpy.Types.Event,
|
|
||||||
node: TreeNode,
|
|
||||||
intersected: Union[TreeNode] = [],
|
|
||||||
rays: tuple[Vector, Vector] = (),
|
|
||||||
) -> Union[TreeNode]:
|
|
||||||
"""
|
|
||||||
Raycast bounding box subdivisions recursively.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: Blender context.
|
|
||||||
event: Blender event.
|
|
||||||
node: a TreeNode instance.
|
|
||||||
intersected: list of intersected boxes to use in recursion.
|
|
||||||
rays: tuple containing ray origin and ray direction
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: a list of TreeNode instances that represent the subdivided boxes hit by the ray cast.
|
|
||||||
"""
|
|
||||||
if not node.child_a:
|
|
||||||
intersected.append(node)
|
|
||||||
return intersected
|
|
||||||
|
|
||||||
intersects_a = self.raycast_box(context, event, node.child_a, rays)
|
|
||||||
intersects_b = self.raycast_box(context, event, node.child_b, rays)
|
|
||||||
if intersects_a:
|
|
||||||
intersected = self.raycast_boxes(context, event, node.child_a, intersected, rays)
|
|
||||||
|
|
||||||
if intersects_b:
|
|
||||||
intersected = self.raycast_boxes(context, event, node.child_b, intersected, rays)
|
|
||||||
|
|
||||||
return intersected
|
|
||||||
|
|||||||
@@ -404,14 +404,8 @@ class Snap(bonsai.core.tool.Snap):
|
|||||||
|
|
||||||
for snap_obj in objs_to_raycast:
|
for snap_obj in objs_to_raycast:
|
||||||
for snap in closest_snaps:
|
for snap in closest_snaps:
|
||||||
if snap_obj.obj == snap["object"]:
|
if snap_obj == snap["object"]:
|
||||||
if xray_mode:
|
if not xray_mode:
|
||||||
if "face_index" in snap and snap["face_index"] is not None:
|
|
||||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
|
||||||
for point in snap_points:
|
|
||||||
point["group"] = "Object"
|
|
||||||
detected_snaps.append(point)
|
|
||||||
else:
|
|
||||||
# If it is a solid object that is closest to camera it ignores all the rest
|
# If it is a solid object that is closest to camera it ignores all the rest
|
||||||
if (
|
if (
|
||||||
"is_closest_to_camera" in snap
|
"is_closest_to_camera" in snap
|
||||||
@@ -419,11 +413,6 @@ class Snap(bonsai.core.tool.Snap):
|
|||||||
and snap["group"] == "Object"
|
and snap["group"] == "Object"
|
||||||
):
|
):
|
||||||
closest_snap = [snap] # discards objects that aren't the closest
|
closest_snap = [snap] # discards objects that aren't the closest
|
||||||
if "face_index" in snap and snap["face_index"] is not None:
|
|
||||||
snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj)
|
|
||||||
for point in snap_points:
|
|
||||||
point["group"] = "Object"
|
|
||||||
closest_snap.append(point)
|
|
||||||
detected_snaps = closest_snap
|
detected_snaps = closest_snap
|
||||||
|
|
||||||
# snap to cut geometry (e.g. in plan view)
|
# snap to cut geometry (e.g. in plan view)
|
||||||
|
|||||||
Reference in New Issue
Block a user