Refactor snapping functions and allow to choose snapping point.

By pressing "M", the user can now cycle through active snapping points when
there are many options close to each other. Still lacks a text to give
information about the active point.
This commit is contained in:
Bruno Perdigão
2024-08-25 18:23:22 -03:00
parent fcaac3787b
commit 6bbf3d3917
4 changed files with 148 additions and 68 deletions
+10 -3
View File
@@ -324,6 +324,7 @@ class DrawPolylineWall(bpy.types.Operator):
self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": ""}
self.snap_angle = None
self.snapping_points = []
def recalculate_inputs(self, context):
if self.number_input:
@@ -378,7 +379,7 @@ class DrawPolylineWall(bpy.types.Operator):
self.is_input_on = False
self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snaping_ref()
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
else:
self.mousemove_count = 0
@@ -389,7 +390,8 @@ class DrawPolylineWall(bpy.types.Operator):
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
if self.mousemove_count > 3:
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
@@ -480,6 +482,10 @@ class DrawPolylineWall(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "M":
self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
tool.Blender.update_viewport()
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
return {"PASS_THROUGH"}
@@ -508,7 +514,8 @@ class DrawPolylineWall(bpy.types.Operator):
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
@@ -2333,6 +2333,7 @@ class MeasureTool(bpy.types.Operator):
self.input_value_xy = [None, None]
self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
self.snap_angle = None
self.snapping_points = []
def recalculate_inputs(self, context):
if self.number_input:
@@ -2361,7 +2362,7 @@ class MeasureTool(bpy.types.Operator):
self.is_input_on = False
self.input_type = "OFF"
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Snap.clear_snaping_ref()
tool.Snap.clear_snapping_ref()
tool.Blender.update_viewport()
else:
self.mousemove_count = 0
@@ -2372,7 +2373,8 @@ class MeasureTool(bpy.types.Operator):
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
if self.mousemove_count > 3:
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
@@ -2456,6 +2458,10 @@ class MeasureTool(bpy.types.Operator):
PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
tool.Blender.update_viewport()
if event.value == "PRESS" and event.type == "M":
self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
tool.Blender.update_viewport()
if event.shift and event.value == "PRESS" and event.type == "X":
tool.Snap.set_use_default_container(False)
PolylineDecorator.set_use_default_container(False)
@@ -2499,7 +2505,8 @@ class MeasureTool(bpy.types.Operator):
self.visible_objs = tool.Raycast.get_visible_objects(context)
for obj in self.visible_objs:
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
tool.Snap.snaping_movement(context, event, self.objs_2d_bbox)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
PolylineDecorator.set_mouse_position(event)
self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
tool.Blender.update_viewport()
+33 -6
View File
@@ -106,12 +106,14 @@ class Raycast(bonsai.core.tool.Raycast):
else:
return None, None, None
@classmethod
def ray_cast_by_proximity(cls, context, event, obj, mesh=None):
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 = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
@@ -119,9 +121,9 @@ class Raycast(bonsai.core.tool.Raycast):
loc = Vector((0, 0, 0))
bm = bmesh.new()
if mesh is None:
if mesh is None: # Object with faces
bm.from_mesh(obj.data)
else:
else: # Object without faces
verts = [bm.verts.new(obj.data.vertices[i].co) for i in mesh.vertices]
bm.faces.new(verts)
@@ -134,14 +136,14 @@ class Raycast(bonsai.core.tool.Raycast):
intersection, _ = mathutils.geometry.intersect_point_line(division_point, ray_target, loc)
distance = (division_point - intersection).length
if distance < 0.2:
return division_point, "Edge Center"
points.append((division_point, "Edge Center"))
for vertex in bm.verts:
world_vertex = obj.matrix_world @ vertex.co
intersection, _ = mathutils.geometry.intersect_point_line(world_vertex, ray_target, loc)
distance = (world_vertex - intersection).length
if distance < 0.2:
return world_vertex, "Vertex"
points.append((world_vertex, "Vertex"))
for edge in bm.edges:
v1 = edge.verts[0].co
@@ -151,11 +153,36 @@ class Raycast(bonsai.core.tool.Raycast):
intersection = mathutils.geometry.intersect_line_line(ray_target, loc, world_v1, world_v2)
distance = (intersection[0] - intersection[1]).length
if distance < 0.2:
return intersection[1], "Edge"
points.append((intersection[1], "Edge"))
return None, None
return points
@classmethod
def ray_cast_to_polyline(cls, context, event):
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)
try:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction)
except:
loc = Vector((0, 0, 0))
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
polyline_points = []
for point_data in polyline_data:
point = Vector((point_data.x, point_data.y, point_data.z))
intersection, _ = mathutils.geometry.intersect_point_line(point, ray_target, loc)
distance = (point - intersection).length
if distance < 0.2:
polyline_points.append((point, "Vertex"))
return polyline_points
@classmethod
def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal):
region = context.region
+95 -56
View File
@@ -113,7 +113,7 @@ class Snap(bonsai.core.tool.Snap):
snap_vertex.snap_type = snap_type
@classmethod
def clear_snaping_ref(cls):
def clear_snapping_ref(cls):
bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear()
@classmethod
@@ -294,10 +294,11 @@ class Snap(bonsai.core.tool.Snap):
# cls.update_snaping_point(snap_point[0], snap_point[1])
@classmethod
def snaping_movement(cls, context, event, objs_2d_bbox):
def detect_snapping_points(cls, context, event, objs_2d_bbox):
region = context.region
rv3d = context.region_data
cls.mouse_pos = event.mouse_region_x, event.mouse_region_y
detected_snaps = []
offset = 15
snap_threshold = 0.3
@@ -376,12 +377,38 @@ class Snap(bonsai.core.tool.Snap):
else:
return None, None, None
ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event)
objs_to_raycast = []
for obj, bbox_2d in objs_2d_bbox:
if obj.type == "MESH" and bbox_2d:
if tool.Raycast.in_view_2d_bounding_box(cls.mouse_pos, bbox_2d):
objs_to_raycast.append(obj)
# Obj
snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast)
if hit is not None:
detected_snaps.append({"Object": (snap_obj, hit, face_index)})
# Edge-Vertex
for obj in objs_to_raycast:
options = tool.Raycast.ray_cast_by_proximity(context, event, obj)
snap_obj = obj
if options:
detected_snaps.append({"Edge-Vertex": (snap_obj, options)})
break
# Polyline
try:
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
last_polyline_point = polyline_data[len(polyline_data) - 1]
except:
last_polyline_point = None
snap_points = tool.Raycast.ray_cast_to_polyline(context, event)
# snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold)
if snap_points:
detected_snaps.append({"Polyline": snap_points})
# Axis and Plane
elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
plane_origin, plane_normal = select_plane_method()
@@ -391,67 +418,79 @@ class Snap(bonsai.core.tool.Snap):
axis_start = None
axis_end = None
if cls.snap_plane_method in {"XY", "XZ", "YZ"}:
# Locks snap into an angle axis
if event.shift:
rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle)
else:
cls.snap_angle = None
rot_intersection, cls.snap_angle, _, _ = cls.snap_on_axis(intersection)
else:
rot_intersection = None
rot_intersection, snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle)
if rot_intersection:
detected_snaps.append({"Axis": rot_intersection})
ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event)
detected_snaps.append({"Plane": intersection})
objs_to_raycast = []
for obj, bbox_2d in objs_2d_bbox:
if obj.type == "MESH" and bbox_2d:
if tool.Raycast.in_view_2d_bounding_box(cls.mouse_pos, bbox_2d):
objs_to_raycast.append(obj)
for item in detected_snaps:
# Try to get object using object raycast method
# If it fails use the raycast by proximity
snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast)
if snap_obj is None:
for obj in objs_to_raycast:
hit, hit_type = tool.Raycast.ray_cast_by_proximity(context, event, obj)
snap_obj = obj
if hit is not None:
break
return detected_snaps
if snap_obj is not None:
# Objects with faces
if face_index is not None:
snap_point = cls.get_snap_points_on_raycasted_face(context, event, snap_obj, face_index)
if snap_point[0] is None:
cls.update_snaping_point(hit, "Face")
return
# Objects with only edges or verts
else:
if hit is not None:
snap_point = (hit, hit_type)
@classmethod
def select_snapping_points(cls, context, event, detected_snaps):
snapping_points = []
for origin in detected_snaps:
if "Object" in list(origin.keys()):
snap_obj, hit, face_index = origin['Object']
matrix = snap_obj.matrix_world.copy()
face = snap_obj.data.polygons[face_index]
verts = []
for i in face.vertices:
verts.append(matrix @ snap_obj.data.vertices[i].co)
options = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj, face)
if not options:
snapping_points.append((hit, "Face"))
else:
snap_point = None
for op in options:
snapping_points.append(op)
# If there are no objects to snap to, it will try snaping in the polyline
else:
snap_points = cls.get_snap_points_on_polyline()
snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold)
cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1])
if snap_point:
if event.shift and axis_start:
snap_result, snap_type = cls.mix_snap_and_axis(snap_point, axis_start, axis_end, elevation)
cls.update_snaping_point(snap_result, snap_type)
cls.update_snaping_ref(snap_point[0], snap_point[1])
return
else:
cls.update_snaping_point(snap_point[0], snap_point[1])
return
# If theres no snapping point from objects and polyline
# It will snap to the plane
if rot_intersection:
cls.update_snaping_point(rot_intersection, "Axis")
else:
cls.update_snaping_point(intersection, "Plane")
return snapping_points
elif "Edge-Vertex" in list(origin.keys()):
snap_obj, options = origin['Edge-Vertex']
for op in options:
snapping_points.append(op)
cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1])
return snapping_points
elif "Polyline" in list(origin.keys()):
options = origin['Polyline']
for op in options:
snapping_points.append(op)
cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1])
return snapping_points
elif "Axis" in list(origin.keys()):
intersection = origin['Axis']
snapping_points.append((intersection, "Axis"))
cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1])
return snapping_points
elif "Plane" in list(origin.keys()):
intersection = origin['Plane']
snapping_points.append((intersection, "Plane"))
cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1])
return snapping_points
@classmethod
def modify_snapping_point_selection(cls, snapping_points):
shifted_list = snapping_points[1:] + snapping_points[:1]
cls.update_snaping_point(shifted_list[0][0], shifted_list[0][1])
return shifted_list
@classmethod
def validate_input(cls, input_number, input_type):