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, mep.RegenerateDistributionElement,
prop.SnapMousePoint, prop.SnapMousePoint,
prop.PolylinePoint, prop.PolylinePoint,
prop.PolylineMeasurement, prop.MeasurePolyline,
prop.BIMModelProperties, prop.BIMModelProperties,
prop.BIMArrayProperties, prop.BIMArrayProperties,
prop.BIMStairProperties, prop.BIMStairProperties,
@@ -454,7 +454,7 @@ class PolylineDecorator:
def draw_measurements(self, context): def draw_measurements(self, context):
region = context.region region = context.region
rv3d = region.data 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.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 1 self.font_id = 1
+3 -4
View File
@@ -774,17 +774,16 @@ class PolylinePoint(PropertyGroup):
x: bpy.props.FloatProperty(name="X") x: bpy.props.FloatProperty(name="X")
y: bpy.props.FloatProperty(name="Y") y: bpy.props.FloatProperty(name="Y")
z: bpy.props.FloatProperty(name="Z") z: bpy.props.FloatProperty(name="Z")
class PolylineMeasurement(PropertyGroup):
dim: bpy.props.StringProperty(name="Dimension") dim: bpy.props.StringProperty(name="Dimension")
angle: bpy.props.StringProperty(name="Angle") angle: bpy.props.StringProperty(name="Angle")
position: bpy.props.FloatVectorProperty(name="Decorator Position", size=3) position: bpy.props.FloatVectorProperty(name="Decorator Position", size=3)
class MeasurePolyline(PropertyGroup):
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
class BIMPolylineProperties(PropertyGroup): class BIMPolylineProperties(PropertyGroup):
snap_mouse_point: bpy.props.CollectionProperty(type=SnapMousePoint) snap_mouse_point: bpy.props.CollectionProperty(type=SnapMousePoint)
snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint) snap_mouse_ref: bpy.props.CollectionProperty(type=SnapMousePoint)
polyline_point: bpy.props.CollectionProperty(type=PolylinePoint) polyline_point: bpy.props.CollectionProperty(type=PolylinePoint)
polyline_measurement: bpy.props.CollectionProperty(type=PolylineMeasurement)
product_preview: bpy.props.CollectionProperty(type=PolylinePoint) 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/>. # along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import gpu import gpu
import blf
import bpy import bpy
import bmesh import bmesh
import bonsai.tool as tool import bonsai.tool as tool
from bpy.types import SpaceView3D from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from mathutils import Vector from mathutils import Vector
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
@@ -201,3 +203,133 @@ class ClippingPlaneDecorator:
if selected_edges: if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, 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) 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 bpy.app.handlers import persistent
from ifcopenshell.geom import ShapeElementType from ifcopenshell.geom import ShapeElementType
from bonsai.bim.module.project.data import LinksData 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.decorator import PolylineDecorator
from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union from typing import Union
@@ -2340,6 +2340,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
A: Angle Input A: Angle Input
M: Modify Snap Point M: Modify Snap Point
C: Close Polyline C: Close Polyline
E: Erase previous polylines
BACKSPACE: Remove Point BACKSPACE: Remove Point
X, Y, Z: Choose Axis X, Y, Z: Choose Axis
S-X, S-Y, S-Z: Choose Plane 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) context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall() PolylineDecorator.uninstall()
tool.Snap.move_polyline_to_measure()
tool.Snap.clear_polyline() tool.Snap.clear_polyline()
MeasureDecorator.install(context)
tool.Blender.update_viewport() tool.Blender.update_viewport()
return {"FINISHED"} return {"FINISHED"}
@@ -2374,6 +2377,11 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
self.handle_inserting_polyline(context, event) 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) result = self.handle_cancelation(context, event)
if result is not None: if result is not None:
return result return result
@@ -2383,3 +2391,4 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
def invoke(self, context, event): def invoke(self, context, event):
super().invoke(context, event) super().invoke(context, event)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
+41 -16
View File
@@ -133,7 +133,7 @@ class Raycast(bonsai.core.tool.Raycast):
return None, None, None return None, None, None
@classmethod @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 region = context.region
rv3d = context.region_data rv3d = context.region_data
mouse_pos = event.mouse_region_x, event.mouse_region_y mouse_pos = event.mouse_region_x, event.mouse_region_y
@@ -149,35 +149,42 @@ class Raycast(bonsai.core.tool.Raycast):
except: except:
loc = Vector((0, 0, 0)) loc = Vector((0, 0, 0))
bm = bmesh.new() if not custom_bmesh:
if face is None: # Object with faces bm = bmesh.new()
bm.from_mesh(obj.data) if face is None: # Object without faces
else: # Object without faces bm.from_mesh(obj.data)
verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices] else: # Object with faces
bm.faces.new(verts) 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: for vertex in bm.verts:
world_vertex = obj.matrix_world.copy() @ vertex.co v = vertex.co
intersection = tool.Cad.point_on_edge(world_vertex, (ray_target, loc)) if obj:
distance = (world_vertex - intersection).length v = obj.matrix_world.copy() @ v
intersection = tool.Cad.point_on_edge(v, (ray_target, loc))
distance = (v - intersection).length
if distance < 0.2: if distance < 0.2:
points.append([distance - stick_factor, (world_vertex, "Vertex")]) points.append([distance - stick_factor, (v, "Vertex")])
for edge in bm.edges: for edge in bm.edges:
v1 = edge.verts[0].co v1 = edge.verts[0].co
v2 = edge.verts[1].co v2 = edge.verts[1].co
world_v1 = obj.matrix_world.copy() @ v1 if obj:
world_v2 = obj.matrix_world.copy() @ v2 v1 = obj.matrix_world.copy() @ v1
division_point = (world_v1 + world_v2) / 2 # TODO Make it work for different divisions 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)) intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc))
distance = (division_point - intersection).length distance = (division_point - intersection).length
if distance < 0.2: if distance < 0.2:
points.append([distance, (division_point, "Edge Center")]) 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 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 distance = (intersection[1] - intersection[0]).length
if distance < 0.8: if distance < 0.8:
points.append([distance + 4 * stick_factor, (intersection[1], "Edge")]) points.append([distance + 4 * stick_factor, (intersection[1], "Edge")])
@@ -217,6 +224,24 @@ class Raycast(bonsai.core.tool.Raycast):
return polyline_points 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 @classmethod
def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal): def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal):
region = context.region region = context.region
+31 -33
View File
@@ -60,28 +60,6 @@ class Snap(bonsai.core.tool.Snap):
return snap_point 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 @classmethod
def update_snapping_point(cls, snap_point, snap_type): def update_snapping_point(cls, snap_point, snap_type):
try: 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 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): if (x, y, z) == (point.x, point.y, point.z):
return "Cannot create two points at the same location" 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 # Avoids creating segments smaller then 0.1. This is a limitation from create_wall_from_2_points
length = ( length = (
Vector((x, y, z)) - Vector((polyline_data[-1].x, polyline_data[-1].y, polyline_data[-1].z)) 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.y = y
polyline_point.z = z polyline_point.z = z
polyline_measurement = bpy.context.scene.BIMPolylineProperties.polyline_measurement.add() polyline_point.dim = d
polyline_measurement.dim = d polyline_point.angle = a
polyline_measurement.angle = a polyline_point.position = Vector((x, y, z))
polyline_measurement.position = Vector((x, y, z))
@classmethod @classmethod
def close_polyline(cls): def close_polyline(cls):
@@ -172,15 +150,25 @@ class Snap(bonsai.core.tool.Snap):
@classmethod @classmethod
def clear_polyline(cls): def clear_polyline(cls):
bpy.context.scene.BIMPolylineProperties.polyline_point.clear() bpy.context.scene.BIMPolylineProperties.polyline_point.clear()
bpy.context.scene.BIMPolylineProperties.polyline_measurement.clear()
@classmethod @classmethod
def remove_last_polyline_point(cls): def remove_last_polyline_point(cls):
polyline_data = bpy.context.scene.BIMPolylineProperties.polyline_point polyline_data = bpy.context.scene.BIMPolylineProperties.polyline_point
polyline_data.remove(len(polyline_data) - 1) 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 @classmethod
def snap_on_axis(cls, intersection, tool_state, lock_angle=False): 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] last_polyline_point = polyline_data[len(polyline_data) - 1]
except: except:
last_polyline_point = None last_polyline_point = None
snap_points = tool.Raycast.ray_cast_to_polyline(context, event) if polyline_data:
# snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold) snap_points = tool.Raycast.ray_cast_to_polyline(context, event)
if snap_points: if snap_points:
detected_snaps.append({"Polyline": 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 # Axis and Plane
elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z