mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-29 08:13:14 +00:00
snap: refactor GPU snap detection into helpers
This commit is contained in:
+321
-225
@@ -317,6 +317,322 @@ def _ensure_wireframe_batches(obj) -> dict[str, tuple[GPUBatch, int, list]]:
|
||||
_wireframe_batch_cache[cache_key] = batches
|
||||
return batches
|
||||
|
||||
def _get_tris_render_ops(objs_to_raycast):
|
||||
"""Build render ops for solid (triangle) objects to raycast.
|
||||
|
||||
Each mesh contributes a single TRIANGLES batch and a slot base that
|
||||
encodes its index in the global ``_obj_list`` (slot 0 is reserved for
|
||||
the background). The batch is drawn unlit so the object index can be
|
||||
read back from the framebuffer.
|
||||
|
||||
Args:
|
||||
objs_to_raycast: iterable of candidate objects.
|
||||
|
||||
Returns:
|
||||
list[tuple[GPUBatch, Matrix, int]]: ``(batch, world_matrix, slot_base)``
|
||||
for every mesh with faces. Populates ``_obj_list`` as a side effect.
|
||||
"""
|
||||
global _obj_list
|
||||
|
||||
render_ops: list[tuple[GPUBatch, Matrix, int]] = []
|
||||
|
||||
for snap_obj in objs_to_raycast:
|
||||
if snap_obj.type != "MESH":
|
||||
continue
|
||||
if not hasattr(snap_obj.data, "polygons"):
|
||||
continue
|
||||
if len(snap_obj.data.polygons) == 0:
|
||||
continue
|
||||
|
||||
batch, cut = _ensure_triangle_batches(snap_obj)
|
||||
if batch is None:
|
||||
continue
|
||||
|
||||
obj_index = len(_obj_list)
|
||||
_obj_list.append(snap_obj)
|
||||
slot_base = obj_index + 1 # slot 0 = background
|
||||
render_ops.append((batch, snap_obj.matrix_world.copy(), slot_base))
|
||||
return render_ops
|
||||
|
||||
def _create_tris_snaps(context, event, mouse_read_rect, buffers_list, last_buf, xray_mode):
|
||||
"""Decode the triangle readback buffer(s) into face snaps.
|
||||
|
||||
In xray mode each object is read back as a single pixel under the
|
||||
cursor (``buffers_list``); otherwise the center pixel of the readback
|
||||
region (``last_buf``) is decoded. Every hit object is then ray cast for
|
||||
real to find the exact face, producing one ``Face`` snap per hit.
|
||||
|
||||
Args:
|
||||
context: Blender context.
|
||||
event: the event carrying the cursor position.
|
||||
mouse: ``(mx, read_x, my, read_y)`` cursor and readback origin.
|
||||
buffers_list: per-object single-pixel buffers (xray mode only).
|
||||
last_buf: full readback region buffer (non-xray mode).
|
||||
xray_mode: whether solid xray rendering is active.
|
||||
|
||||
Returns:
|
||||
tuple[list[dict], bpy.types.Object | None]: the face snaps and the
|
||||
closest hit object, or ``([], None)`` when nothing was hit.
|
||||
"""
|
||||
|
||||
global _obj_list
|
||||
|
||||
w, h, mx, my, read_x, read_y = mouse_read_rect
|
||||
# Decode hits
|
||||
hits: set[int] = set()
|
||||
|
||||
if xray_mode:
|
||||
vals_read: set[int] = set()
|
||||
# Each buffer is a single pixel read back right under the
|
||||
# cursor. When the cursor is outside the region there is
|
||||
# nothing to snap to, matching the previous bounds check.
|
||||
if not (0 <= mx < w and 0 <= my < h):
|
||||
return [], None
|
||||
for buf in buffers_list:
|
||||
pixel_data = buf.to_list()
|
||||
if not pixel_data or not pixel_data[0]:
|
||||
return [], None
|
||||
px = pixel_data[0][0]
|
||||
val = _decode_wireframe_pixel(px[0], px[1], px[2], px[3])
|
||||
if val in vals_read: # avoid getting all the tris from the same object
|
||||
continue
|
||||
vals_read.add(val)
|
||||
if val > 0:
|
||||
obj_index = val - 1
|
||||
if obj_index < len(_obj_list):
|
||||
hits.add(obj_index)
|
||||
else:
|
||||
pixel_data = last_buf.to_list()
|
||||
if not pixel_data or not pixel_data[0]:
|
||||
return [], None
|
||||
centre_x = mx - int(read_x)
|
||||
centre_y = my - int(read_y)
|
||||
if 0 <= centre_y < len(pixel_data) and 0 <= centre_x < len(pixel_data[0]):
|
||||
px = pixel_data[centre_y][centre_x]
|
||||
val = _decode_wireframe_pixel(px[0], px[1], px[2], px[3])
|
||||
if val > 0:
|
||||
obj_index = val - 1
|
||||
if obj_index < len(_obj_list):
|
||||
hits.add(obj_index)
|
||||
|
||||
if not hits:
|
||||
return [], None
|
||||
|
||||
snaps: list[dict] = []
|
||||
closest_obj = None
|
||||
closest_dist = float("inf")
|
||||
ray_origin, _, _ = tool.Raycast.get_viewport_ray_data(context, event)
|
||||
for obj_index in hits:
|
||||
obj = _obj_list[obj_index]
|
||||
hit_obj, hit, face_index = tool.Raycast.cast_rays_to_single_object(context, event, obj)
|
||||
if hit:
|
||||
snap: dict = {
|
||||
"point": hit,
|
||||
"type": "Face",
|
||||
"group": "Object",
|
||||
"object": hit_obj,
|
||||
"face_index": face_index,
|
||||
# "is_cut": cut, # Used later in snap
|
||||
"distance": 9, # High value so it has low priority
|
||||
}
|
||||
dist = (hit - ray_origin).length
|
||||
if dist < closest_dist:
|
||||
closest_dist = dist
|
||||
closest_obj = obj
|
||||
|
||||
snaps.append(snap)
|
||||
|
||||
return snaps, closest_obj
|
||||
|
||||
def _get_wireframe_render_ops(objs_to_raycast):
|
||||
"""Build render ops for wireframe (non-solid) objects.
|
||||
|
||||
Boundary points and lines of each object are assigned sequential slot
|
||||
IDs across all objects, so every vertex and edge gets a unique encoded
|
||||
ID. Per-object slot ranges are recorded in ``obj_slots`` for decoding.
|
||||
|
||||
Args:
|
||||
objs_to_raycast: iterable of candidate objects.
|
||||
|
||||
Returns:
|
||||
tuple[list[tuple[GPUBatch, Matrix, int]], list[tuple]]:
|
||||
``(render_ops, obj_slots)`` where ``render_ops`` holds
|
||||
``(batch, world_matrix, slot_base)`` and each ``obj_slots``
|
||||
entry is ``(snap_obj, pts_start, n_pts, lines_start, n_lines)``.
|
||||
"""
|
||||
|
||||
render_ops: list[tuple[GPUBatch, Matrix, int]] = []
|
||||
obj_slots: list[tuple] = [] # [(snap_obj, pts_start, n_pts, lines_start, n_lines), ...]
|
||||
|
||||
slot = 1 # slot 0 = background
|
||||
|
||||
for snap_obj in objs_to_raycast:
|
||||
# avoids creating batches for solid objects
|
||||
if hasattr(snap_obj.data, "polygons") and len(snap_obj.data.polygons) > 0:
|
||||
continue
|
||||
|
||||
batches = _ensure_wireframe_batches(snap_obj)
|
||||
if not batches:
|
||||
continue
|
||||
|
||||
world_mat = snap_obj.matrix_world.copy()
|
||||
pts_start = 0
|
||||
n_pts = 0
|
||||
lines_start = 0
|
||||
n_lines = 0
|
||||
|
||||
pts_data = batches.get("POINTS")
|
||||
if pts_data is not None:
|
||||
batch, n_pts, _ = pts_data
|
||||
pts_start = slot
|
||||
render_ops.append((batch, world_mat, slot))
|
||||
slot += n_pts
|
||||
|
||||
lines_data = batches.get("LINES")
|
||||
if lines_data is not None:
|
||||
batch, n_lines, _ = lines_data
|
||||
lines_start = slot
|
||||
render_ops.append((batch, world_mat, slot))
|
||||
slot += n_lines
|
||||
|
||||
if n_pts > 0 or n_lines > 0:
|
||||
obj_slots.append((snap_obj, pts_start, n_pts, lines_start, n_lines))
|
||||
|
||||
return render_ops, obj_slots
|
||||
|
||||
def _create_wireframe_snaps(context, event, mouse_read_rect, obj_slots, last_buf):
|
||||
"""Decode the wireframe readback buffer into vertex/edge snaps.
|
||||
|
||||
Finds the closest non-zero pixel to the cursor, maps its encoded slot ID
|
||||
back to a vertex or edge via ``obj_slots``, then builds the candidate
|
||||
snaps (Vertex, Edge, Edge Center, plus endpoint Vertex snaps within the
|
||||
snap threshold).
|
||||
|
||||
Args:
|
||||
context: Blender context.
|
||||
event: the event carrying the cursor position.
|
||||
mouse: ``(mx, read_x, my, read_y)`` cursor and readback origin.
|
||||
obj_slots: ``[(snap_obj, pts_start, n_pts, lines_start, n_lines), ...]``.
|
||||
last_buf: full readback region buffer.
|
||||
|
||||
Returns:
|
||||
tuple[list[dict], None]: the wireframe snaps, or ``([], None)`` when
|
||||
no non-zero pixel is found near the cursor.
|
||||
"""
|
||||
global _wireframe_batch_cache
|
||||
|
||||
w, h, mx, my, read_x, read_y = mouse_read_rect
|
||||
centre = (mx - int(read_x), my - int(read_y))
|
||||
pixel_data = last_buf.to_list()
|
||||
best = _find_closest_wireframe_pixel(pixel_data, *centre)
|
||||
if best is None:
|
||||
return [], None
|
||||
encoded, dx, dy = best
|
||||
|
||||
# Decode and build snap dicts
|
||||
|
||||
rv3d = context.region_data
|
||||
snaps: list[dict] = []
|
||||
snap_threshold = tool.Raycast.calculate_snap_threshold(rv3d.view_distance)
|
||||
|
||||
# Compute view ray for 3D proximity calculations
|
||||
_, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event)
|
||||
try:
|
||||
loc = tool.Cad.region_2d_to_location_3d_np(context.region, rv3d, (mx, my), ray_direction)
|
||||
except Exception:
|
||||
loc = ray_target
|
||||
|
||||
for snap_obj, pts_start, n_pts, lines_start, n_lines in obj_slots:
|
||||
if n_pts > 0 and pts_start <= encoded < pts_start + n_pts:
|
||||
vi = encoded - pts_start
|
||||
batches = _wireframe_batch_cache.get(id(snap_obj))
|
||||
if batches:
|
||||
pts_data = batches.get("POINTS")
|
||||
if pts_data:
|
||||
_, _, coords = pts_data
|
||||
if vi < len(coords):
|
||||
local_pos = Vector(coords[vi])
|
||||
world_pos = snap_obj.matrix_world @ local_pos
|
||||
# Compute proper 3D distance from vertex to view ray
|
||||
proj = tool.Cad.point_on_edge(world_pos, (ray_target, loc))
|
||||
distance = (world_pos - proj).length
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Vertex",
|
||||
"point": world_pos,
|
||||
"distance": distance,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
if n_lines > 0 and lines_start <= encoded < lines_start + n_lines:
|
||||
ei = encoded - lines_start
|
||||
batches = _wireframe_batch_cache.get(id(snap_obj))
|
||||
if batches:
|
||||
lines_data = batches.get("LINES")
|
||||
if lines_data:
|
||||
_, _, edge_pairs = lines_data
|
||||
if ei < len(edge_pairs):
|
||||
c0, c1 = edge_pairs[ei]
|
||||
mw = snap_obj.matrix_world
|
||||
v0 = mw @ Vector(c0)
|
||||
v1 = mw @ Vector(c1)
|
||||
|
||||
# Compute closest point on edge to view ray
|
||||
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (v0, v1))
|
||||
if intersection[0] is not None and tool.Cad.is_point_on_edge(intersection[1], (v0, v1)):
|
||||
edge_point = intersection[1].copy()
|
||||
distance = (intersection[1] - intersection[0]).length
|
||||
else:
|
||||
# Fallback to midpoint if lines are parallel
|
||||
edge_point = (v0 + v1) / 2
|
||||
proj = tool.Cad.point_on_edge(edge_point, (ray_target, loc))
|
||||
distance = (edge_point - proj).length
|
||||
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Edge",
|
||||
"point": edge_point,
|
||||
"edge_verts": (v0, v1),
|
||||
"distance": distance,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
|
||||
# Edge Center snap (midpoint)
|
||||
mid = (v0 + v1) / 2 # TODO Allow divisions by other values
|
||||
mid_proj = tool.Cad.point_on_edge(mid, (ray_target, loc))
|
||||
mid_dist = (mid - mid_proj).length
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Edge Center",
|
||||
"point": mid,
|
||||
"distance": mid_dist,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
|
||||
# Also include vertex snaps for edge endpoints
|
||||
for vtx in (v0, v1):
|
||||
proj = tool.Cad.point_on_edge(vtx, (ray_target, loc))
|
||||
vtx_dist = (vtx - proj).length
|
||||
if vtx_dist < snap_threshold:
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Vertex",
|
||||
"point": vtx,
|
||||
"distance": vtx_dist,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
return snaps, None
|
||||
|
||||
class Raycast(bonsai.core.tool.Raycast):
|
||||
offset = 10
|
||||
@@ -670,57 +986,9 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
obj_slots: list[tuple] = [] # [(snap_obj, pts_start, n_pts, lines_start, n_lines), ...]
|
||||
|
||||
if tris:
|
||||
for snap_obj in objs_to_raycast:
|
||||
if snap_obj.type != "MESH":
|
||||
continue
|
||||
if not hasattr(snap_obj.data, "polygons"):
|
||||
continue
|
||||
if len(snap_obj.data.polygons) == 0:
|
||||
continue
|
||||
|
||||
batch_info = _ensure_triangle_batches(snap_obj)
|
||||
if batch_info is None:
|
||||
continue
|
||||
|
||||
batch, _ = batch_info
|
||||
obj_index = len(_obj_list)
|
||||
_obj_list.append(snap_obj)
|
||||
slot_base = obj_index + 1 # slot 0 = background
|
||||
render_ops.append((batch, snap_obj.matrix_world.copy(), slot_base))
|
||||
render_ops = _get_tris_render_ops(objs_to_raycast)
|
||||
else:
|
||||
slot = 1 # slot 0 = background
|
||||
|
||||
for snap_obj in objs_to_raycast:
|
||||
# avoids creating batches for solid objects
|
||||
if hasattr(snap_obj.data, "polygons") and len(snap_obj.data.polygons) > 0:
|
||||
continue
|
||||
|
||||
batches = _ensure_wireframe_batches(snap_obj)
|
||||
if not batches:
|
||||
continue
|
||||
|
||||
world_mat = snap_obj.matrix_world.copy()
|
||||
pts_start = 0
|
||||
n_pts = 0
|
||||
lines_start = 0
|
||||
n_lines = 0
|
||||
|
||||
pts_data = batches.get("POINTS")
|
||||
if pts_data is not None:
|
||||
batch, n_pts, _ = pts_data
|
||||
pts_start = slot
|
||||
render_ops.append((batch, world_mat, slot))
|
||||
slot += n_pts
|
||||
|
||||
lines_data = batches.get("LINES")
|
||||
if lines_data is not None:
|
||||
batch, n_lines, _ = lines_data
|
||||
lines_start = slot
|
||||
render_ops.append((batch, world_mat, slot))
|
||||
slot += n_lines
|
||||
|
||||
if n_pts > 0 or n_lines > 0:
|
||||
obj_slots.append((snap_obj, pts_start, n_pts, lines_start, n_lines))
|
||||
render_ops, obj_slots = _get_wireframe_render_ops(objs_to_raycast)
|
||||
|
||||
if not render_ops:
|
||||
return [], None
|
||||
@@ -788,183 +1056,11 @@ class Raycast(bonsai.core.tool.Raycast):
|
||||
gpu.state.depth_mask_set(True)
|
||||
gpu.state.depth_test_set("LESS")
|
||||
|
||||
mouse_read_rect = (w, h, mx, my, read_x, read_y)
|
||||
if tris:
|
||||
# Decode hits
|
||||
hits: set[int] = set()
|
||||
|
||||
if xray_mode:
|
||||
vals_read: set[int] = set()
|
||||
# Each buffer is a single pixel read back right under the
|
||||
# cursor. When the cursor is outside the region there is
|
||||
# nothing to snap to, matching the previous bounds check.
|
||||
if not (0 <= mx < w and 0 <= my < h):
|
||||
return [], None
|
||||
for buf in buffers_list:
|
||||
pixel_data = buf.to_list()
|
||||
if not pixel_data or not pixel_data[0]:
|
||||
return [], None
|
||||
px = pixel_data[0][0]
|
||||
val = _decode_wireframe_pixel(px[0], px[1], px[2], px[3])
|
||||
if val in vals_read: # avoid getting all the tris from the same object
|
||||
continue
|
||||
vals_read.add(val)
|
||||
if val > 0:
|
||||
obj_index = val - 1
|
||||
if obj_index < len(_obj_list):
|
||||
hits.add(obj_index)
|
||||
else:
|
||||
pixel_data = last_buf.to_list()
|
||||
if not pixel_data or not pixel_data[0]:
|
||||
return [], None
|
||||
centre_x = mx - int(read_x)
|
||||
centre_y = my - int(read_y)
|
||||
if 0 <= centre_y < len(pixel_data) and 0 <= centre_x < len(pixel_data[0]):
|
||||
px = pixel_data[centre_y][centre_x]
|
||||
val = _decode_wireframe_pixel(px[0], px[1], px[2], px[3])
|
||||
if val > 0:
|
||||
obj_index = val - 1
|
||||
if obj_index < len(_obj_list):
|
||||
hits.add(obj_index)
|
||||
|
||||
if not hits:
|
||||
return [], None
|
||||
|
||||
snaps: list[dict] = []
|
||||
closest_obj = None
|
||||
closest_dist = float("inf")
|
||||
ray_origin, _, _ = cls.get_viewport_ray_data(context, event)
|
||||
for obj_index in hits:
|
||||
obj = _obj_list[obj_index]
|
||||
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, obj)
|
||||
if hit:
|
||||
snap: dict = {
|
||||
"point": hit,
|
||||
"type": "Face",
|
||||
"group": "Object",
|
||||
"object": hit_obj,
|
||||
"face_index": face_index,
|
||||
"distance": 9, # High value so it has low priority
|
||||
}
|
||||
dist = (hit - ray_origin).length
|
||||
if dist < closest_dist:
|
||||
closest_dist = dist
|
||||
closest_obj = obj
|
||||
|
||||
snaps.append(snap)
|
||||
|
||||
return snaps, closest_obj
|
||||
|
||||
return _create_tris_snaps(context, event, mouse_read_rect, buffers_list, last_buf, xray_mode)
|
||||
else:
|
||||
centre = (mx - int(read_x), my - int(read_y))
|
||||
pixel_data = last_buf.to_list()
|
||||
best = _find_closest_wireframe_pixel(pixel_data, *centre)
|
||||
if best is None:
|
||||
return [], None
|
||||
encoded, dx, dy = best
|
||||
|
||||
# Decode and build snap dicts
|
||||
|
||||
snaps: list[dict] = []
|
||||
snap_threshold = cls.calculate_snap_threshold(rv3d.view_distance)
|
||||
|
||||
# Compute view ray for 3D proximity calculations
|
||||
_, ray_target, ray_direction = cls.get_viewport_ray_data(context, event)
|
||||
try:
|
||||
loc = tool.Cad.region_2d_to_location_3d_np(region, rv3d, (mx, my), ray_direction)
|
||||
except Exception:
|
||||
loc = ray_target
|
||||
|
||||
for snap_obj, pts_start, n_pts, lines_start, n_lines in obj_slots:
|
||||
if n_pts > 0 and pts_start <= encoded < pts_start + n_pts:
|
||||
vi = encoded - pts_start
|
||||
batches = _wireframe_batch_cache.get(id(snap_obj))
|
||||
if batches:
|
||||
pts_data = batches.get("POINTS")
|
||||
if pts_data:
|
||||
_, _, coords = pts_data
|
||||
if vi < len(coords):
|
||||
local_pos = Vector(coords[vi])
|
||||
world_pos = snap_obj.matrix_world @ local_pos
|
||||
# Compute proper 3D distance from vertex to view ray
|
||||
proj = tool.Cad.point_on_edge(world_pos, (ray_target, loc))
|
||||
distance = (world_pos - proj).length
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Vertex",
|
||||
"point": world_pos,
|
||||
"distance": distance,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
if n_lines > 0 and lines_start <= encoded < lines_start + n_lines:
|
||||
ei = encoded - lines_start
|
||||
batches = _wireframe_batch_cache.get(id(snap_obj))
|
||||
if batches:
|
||||
lines_data = batches.get("LINES")
|
||||
if lines_data:
|
||||
_, _, edge_pairs = lines_data
|
||||
if ei < len(edge_pairs):
|
||||
c0, c1 = edge_pairs[ei]
|
||||
mw = snap_obj.matrix_world
|
||||
v0 = mw @ Vector(c0)
|
||||
v1 = mw @ Vector(c1)
|
||||
|
||||
# Compute closest point on edge to view ray
|
||||
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (v0, v1))
|
||||
if intersection[0] is not None and tool.Cad.is_point_on_edge(intersection[1], (v0, v1)):
|
||||
edge_point = intersection[1].copy()
|
||||
distance = (intersection[1] - intersection[0]).length
|
||||
else:
|
||||
# Fallback to midpoint if lines are parallel
|
||||
edge_point = (v0 + v1) / 2
|
||||
proj = tool.Cad.point_on_edge(edge_point, (ray_target, loc))
|
||||
distance = (edge_point - proj).length
|
||||
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Edge",
|
||||
"point": edge_point,
|
||||
"edge_verts": (v0, v1),
|
||||
"distance": distance,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
|
||||
# Edge Center snap (midpoint)
|
||||
mid = (v0 + v1) / 2
|
||||
mid_proj = tool.Cad.point_on_edge(mid, (ray_target, loc))
|
||||
mid_dist = (mid - mid_proj).length
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Edge Center",
|
||||
"point": mid,
|
||||
"distance": mid_dist,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
|
||||
# Also include vertex snaps for edge endpoints
|
||||
for vtx in (v0, v1):
|
||||
proj = tool.Cad.point_on_edge(vtx, (ray_target, loc))
|
||||
vtx_dist = (vtx - proj).length
|
||||
if vtx_dist < snap_threshold:
|
||||
snaps.append(
|
||||
{
|
||||
"object": snap_obj,
|
||||
"type": "Vertex",
|
||||
"point": vtx,
|
||||
"distance": vtx_dist,
|
||||
"group": "Wireframe",
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
return snaps, None
|
||||
return _create_wireframe_snaps(context, event, mouse_read_rect, obj_slots, last_buf)
|
||||
|
||||
@classmethod
|
||||
def get_gpu_solid_snaps(cls, context, event, objs_to_raycast):
|
||||
|
||||
Reference in New Issue
Block a user