Measure tool persists on screen and can be snapped.

The user has an option to clean previous measurements polylines by pressing "E" while using the measure tool.
This commit is contained in:
Bruno Perdigão
2024-09-27 16:48:28 -03:00
parent 314534436d
commit 8d08d358af
7 changed files with 219 additions and 56 deletions
@@ -120,7 +120,7 @@ classes = (
mep.RegenerateDistributionElement,
prop.SnapMousePoint,
prop.PolylinePoint,
prop.PolylineMeasurement,
prop.MeasurePolyline,
prop.BIMModelProperties,
prop.BIMArrayProperties,
prop.BIMStairProperties,
@@ -454,7 +454,7 @@ class PolylineDecorator:
def draw_measurements(self, context):
region = context.region
rv3d = region.data
measurement_prop = context.scene.BIMPolylineProperties.polyline_measurement
measurement_prop = context.scene.BIMPolylineProperties.polyline_point
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 1
+3 -4
View File
@@ -774,17 +774,16 @@ class PolylinePoint(PropertyGroup):
x: bpy.props.FloatProperty(name="X")
y: bpy.props.FloatProperty(name="Y")
z: bpy.props.FloatProperty(name="Z")
class PolylineMeasurement(PropertyGroup):
dim: bpy.props.StringProperty(name="Dimension")
angle: bpy.props.StringProperty(name="Angle")
position: bpy.props.FloatVectorProperty(name="Decorator Position", size=3)
class MeasurePolyline(PropertyGroup):
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
class BIMPolylineProperties(PropertyGroup):
snap_mouse_point: bpy.props.CollectionProperty(type=SnapMousePoint)
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
polyline_measurement: bpy.props.CollectionProperty(type=PolylineMeasurement)
product_preview: bpy.props.CollectionProperty(type=PolylinePoint)
measure_polyline: bpy.props.CollectionProperty(type=MeasurePolyline)
@@ -17,10 +17,12 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import gpu
import blf
import bpy
import bmesh
import bonsai.tool as tool
from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
from bpy.app.handlers import persistent
@@ -201,3 +203,133 @@ class ClippingPlaneDecorator:
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
class MeasureDecorator:
is_installed = False
handlers = []
@classmethod
def install(cls, context):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@classmethod
def uninstall(cls):
for handler in cls.handlers:
try:
SpaceView3D.draw_handler_remove(handler, "WINDOW")
except ValueError:
pass
cls.is_installed = False
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
shader.uniform_float("color", color)
batch.draw(shader)
def draw_text_background(self, context, coords_dim, text_dim):
padding = 5
theme = context.preferences.themes.items()[0][1]
color = (*theme.user_interface.wcol_menu_back.inner[:3], 0.5) # unwrap color values and adds alpha
top_left = (coords_dim[0] - padding, coords_dim[1] + text_dim[1] + padding)
bottom_left = (coords_dim[0] - padding, coords_dim[1] - padding)
top_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] + text_dim[1] + padding)
bottom_right = (coords_dim[0] + text_dim[0] + padding, coords_dim[1] - padding)
verts = [top_left, bottom_left, top_right, bottom_right]
gpu.state.blend_set("ALPHA")
self.draw_batch("TRIS", verts, color, [(0, 1, 2), (1, 2, 3)])
def draw_measurements(self, context):
region = context.region
rv3d = region.data
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 1
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
font_size = tool.Blender.scale_font_size(12)
blf.size(self.font_id, font_size)
blf.enable(self.font_id, blf.SHADOW)
blf.shadow(self.font_id, 6, 0, 0, 0, 1)
color = self.addon_prefs.decorations_colour
blf.color(self.font_id, *color)
measure_data = context.scene.BIMPolylineProperties.measure_polyline
for data in measure_data:
measurement_prop = data.polyline_point
for i in range(len(measurement_prop)):
if i == 0:
continue
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
formatted_value = measurement_prop[i].dim
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
text = "d: " + formatted_value
text_dim = blf.dimensions(self.font_id, text)
self.draw_text_background(context, coords_dim, text_dim)
blf.draw(self.font_id, text)
if i == 1:
continue
pos_angle = measurement_prop[i - 1].position
coords_angle = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_angle)
blf.position(self.font_id, coords_angle[0], coords_angle[1], 0)
text = "a: " + measurement_prop[i].angle
text_dim = blf.dimensions(self.font_id, text)
self.draw_text_background(context, coords_angle, text_dim)
blf.draw(self.font_id, text)
blf.disable(self.font_id, blf.SHADOW)
def __call__(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences()
decorator_color = self.addon_prefs.decorations_colour
decorator_color_special = self.addon_prefs.decorator_color_special
decorator_color_selected = self.addon_prefs.decorator_color_selected
decorator_color_error = self.addon_prefs.decorator_color_error
decorator_color_unselected = self.addon_prefs.decorator_color_unselected
decorator_color_background = self.addon_prefs.decorator_color_background
theme = context.preferences.themes.items()[0][1]
decorator_color_object_active = (*theme.view_3d.object_active, 1) # unwrap color values and adds alpha=1
decorator_color_x_axis = (*theme.user_interface.axis_x, 1)
decorator_color_y_axis = (*theme.user_interface.axis_y, 1)
decorator_color_z_axis = (*theme.user_interface.axis_z, 1)
gpu.state.blend_set("ALPHA")
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
# POLYLINE_UNIFORM_COLOR specific uniforms
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
gpu.state.point_size_set(6)
measure_data = context.scene.BIMPolylineProperties.measure_polyline
for data in measure_data:
polyline_data = data.polyline_point
polyline_points = []
polyline_edges = []
for point_prop in polyline_data:
point = Vector((point_prop.x, point_prop.y, point_prop.z))
polyline_points.append(point)
for i in range(len(polyline_points) - 1):
polyline_edges.append([i, i + 1])
# Draw polyline with selected points
self.line_shader.uniform_float("lineWidth", 2.0)
self.draw_batch("POINTS", polyline_points, decorator_color_unselected)
if len(polyline_points) > 1:
self.draw_batch("LINES", polyline_points, decorator_color_unselected, polyline_edges)
@@ -51,7 +51,7 @@ from mathutils import Vector, Matrix
from bpy.app.handlers import persistent
from ifcopenshell.geom import ShapeElementType
from bonsai.bim.module.project.data import LinksData
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union
@@ -2340,6 +2340,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
A: Angle Input
M: Modify Snap Point
C: Close Polyline
E: Erase previous polylines
BACKSPACE: Remove Point
X, Y, Z: Choose Axis
S-X, S-Y, S-Z: Choose Plane
@@ -2366,7 +2367,9 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
):
context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall()
tool.Snap.move_polyline_to_measure()
tool.Snap.clear_polyline()
MeasureDecorator.install(context)
tool.Blender.update_viewport()
return {"FINISHED"}
@@ -2374,6 +2377,11 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
self.handle_inserting_polyline(context, event)
if event.type == "E":
context.scene.BIMPolylineProperties.measure_polyline.clear()
MeasureDecorator.uninstall()
tool.Blender.update_viewport()
result = self.handle_cancelation(context, event)
if result is not None:
return result
@@ -2383,3 +2391,4 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
def invoke(self, context, event):
super().invoke(context, event)
return {"RUNNING_MODAL"}
+41 -16
View File
@@ -133,7 +133,7 @@ class Raycast(bonsai.core.tool.Raycast):
return None, None, None
@classmethod
def ray_cast_by_proximity(cls, context, event, obj, face=None):
def ray_cast_by_proximity(cls, context, event, obj, face=None, custom_bmesh=None):
region = context.region
rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y
@@ -149,35 +149,42 @@ class Raycast(bonsai.core.tool.Raycast):
except:
loc = Vector((0, 0, 0))
bm = bmesh.new()
if face is None: # Object with faces
bm.from_mesh(obj.data)
else: # Object without faces
verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices]
bm.faces.new(verts)
if not custom_bmesh:
bm = bmesh.new()
if face is None: # Object without faces
bm.from_mesh(obj.data)
else: # Object with faces
verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices]
bm.faces.new(verts)
else:
# Measure polylines
bm = custom_bmesh
for vertex in bm.verts:
world_vertex = obj.matrix_world.copy() @ vertex.co
intersection = tool.Cad.point_on_edge(world_vertex, (ray_target, loc))
distance = (world_vertex - intersection).length
v = vertex.co
if obj:
v = obj.matrix_world.copy() @ v
intersection = tool.Cad.point_on_edge(v, (ray_target, loc))
distance = (v - intersection).length
if distance < 0.2:
points.append([distance - stick_factor, (world_vertex, "Vertex")])
points.append([distance - stick_factor, (v, "Vertex")])
for edge in bm.edges:
v1 = edge.verts[0].co
v2 = edge.verts[1].co
world_v1 = obj.matrix_world.copy() @ v1
world_v2 = obj.matrix_world.copy() @ v2
division_point = (world_v1 + world_v2) / 2 # TODO Make it work for different divisions
if obj:
v1 = obj.matrix_world.copy() @ v1
v2 = obj.matrix_world.copy() @ v2
division_point = (v1 + v2) / 2 # TODO Make it work for different divisions
intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc))
distance = (division_point - intersection).length
if distance < 0.2:
points.append([distance, (division_point, "Edge Center")])
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (world_v1, world_v2))
intersection = tool.Cad.intersect_edges_v2((ray_target, loc), (v1, v2))
if intersection[0]:
if tool.Cad.is_point_on_edge(intersection[1], (world_v1, world_v2)):
if tool.Cad.is_point_on_edge(intersection[1], (v1, v2)):
distance = (intersection[1] - intersection[0]).length
if distance < 0.8:
points.append([distance + 4 * stick_factor, (intersection[1], "Edge")])
@@ -217,6 +224,24 @@ class Raycast(bonsai.core.tool.Raycast):
return polyline_points
@classmethod
def ray_cast_to_measure(cls, context, event, points):
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
indices = list(range(len(points)-1))
edges = [(i, i+1) for i in range(len(points)-1)]
new_verts = [bm.verts.new(Vector((point.x, point.y, point.z))) for point in points]
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in edges]
bm.verts.index_update()
bm.edges.index_update()
snapping_points = cls.ray_cast_by_proximity(context, event, None, custom_bmesh=bm)
bm.free()
return snapping_points
@classmethod
def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal):
region = context.region
+31 -33
View File
@@ -60,28 +60,6 @@ class Snap(bonsai.core.tool.Snap):
return snap_point
# TODO Remove this function
@classmethod
def select_snap_point(cls, snap_points, hit, threshold):
shortest_distance = None
snap_point = None
for point, snap_type in snap_points.items():
point = Vector(point)
distance = (point - hit).length
if distance > threshold:
continue
if shortest_distance and distance < shortest_distance:
shortest_distance = distance
snap_point = (point, snap_type)
elif not shortest_distance:
shortest_distance = distance
snap_point = (point, snap_type)
else:
pass
return snap_point
@classmethod
def update_snapping_point(cls, snap_point, snap_type):
try:
@@ -140,6 +118,7 @@ class Snap(bonsai.core.tool.Snap):
for point in polyline_data[1:]: # The first can be repeated to form a wall loop
if (x, y, z) == (point.x, point.y, point.z):
return "Cannot create two points at the same location"
# TODO move this limitation to be Wall tool specific. Right now it also affects Measure tool
# Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points
length = (
Vector((x, y, z)) - Vector((polyline_data[-1].x, polyline_data[-1].y, polyline_data[-1].z))
@@ -152,10 +131,9 @@ class Snap(bonsai.core.tool.Snap):
polyline_point.y = y
polyline_point.z = z
polyline_measurement = bpy.context.scene.BIMPolylineProperties.polyline_measurement.add()
polyline_measurement.dim = d
polyline_measurement.angle = a
polyline_measurement.position = Vector((x, y, z))
polyline_point.dim = d
polyline_point.angle = a
polyline_point.position = Vector((x, y, z))
@classmethod
def close_polyline(cls):
@@ -172,15 +150,25 @@ class Snap(bonsai.core.tool.Snap):
@classmethod
def clear_polyline(cls):
bpy.context.scene.BIMPolylineProperties.polyline_point.clear()
bpy.context.scene.BIMPolylineProperties.polyline_measurement.clear()
@classmethod
def remove_last_polyline_point(cls):
polyline_data = bpy.context.scene.BIMPolylineProperties.polyline_point
polyline_data.remove(len(polyline_data) - 1)
polyline_measurement = bpy.context.scene.BIMPolylineProperties.polyline_measurement
polyline_measurement.remove(len(polyline_measurement) - 1)
@classmethod
def move_polyline_to_measure(cls):
polyline_data = bpy.context.scene.BIMPolylineProperties.polyline_point
measure_data = bpy.context.scene.BIMPolylineProperties.measure_polyline.add()
for point in polyline_data:
measure_point = measure_data.polyline_point.add()
measure_point.x = point.x
measure_point.y = point.y
measure_point.z = point.z
measure_point.dim = point.dim
measure_point.angle = point.angle
measure_point.position = point.position
@classmethod
def snap_on_axis(cls, intersection, tool_state, lock_angle=False):
@@ -399,10 +387,20 @@ class Snap(bonsai.core.tool.Snap):
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})
if polyline_data:
snap_points = tool.Raycast.ray_cast_to_polyline(context, event)
if snap_points:
detected_snaps.append({"Polyline": snap_points})
# Measure
measure_data = context.scene.BIMPolylineProperties.measure_polyline
for measure in measure_data:
measure_points = measure.polyline_point
print('points', measure_points)
snap_points = tool.Raycast.ray_cast_to_measure(context, event, measure_points)
if snap_points:
detected_snaps.append({"Polyline": snap_points})
# Axis and Plane
elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z