Add "Face Area" option to measure tool. See #6163.

This commit is contained in:
Bruno Perdigão
2025-03-13 16:47:34 -03:00
parent c80c7c6d36
commit 46a31386da
7 changed files with 166 additions and 9 deletions
@@ -33,6 +33,7 @@ from gpu_extras.batch import batch_for_shader
from gpu_extras.presets import draw_circle_2d
from typing import Union
from bonsai.bim.module.drawing.helper import format_distance
from itertools import chain
def transparent_color(color, alpha=0.1):
@@ -320,14 +321,15 @@ class PolylineDecorator:
relating_type = None
@classmethod
def install(cls, context):
def install(cls, context, ui_only=False):
if cls.is_installed:
cls.uninstall()
handler = cls()
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_measurements, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
if not ui_only:
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_snap_point, (context,), "WINDOW", "POST_PIXEL"))
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
@@ -376,6 +378,7 @@ class PolylineDecorator:
return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle)
@classmethod
def calculate_polygon(self, points):
bm = bmesh.new()
@@ -389,8 +392,8 @@ class PolylineDecorator:
bm.verts.index_update()
bm.edges.index_update()
verts = bm.verts
edges = bm.edges
verts = [v.co for v in bm.verts]
edges = [[v.index for v in e.verts] for e in bm.edges]
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
bm.free()
@@ -970,3 +973,58 @@ class SlabDirectionDecorator:
base = [obj.matrix_world @ Vector(d) for d in base]
self.draw_batch("LINES", dir, selected_elements_color, [(0, 1), (1, 2), (1, 3)])
self.draw_batch("LINES", base, selected_elements_color, [(0, 1)])
class FaceAreaDecorator:
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_face_area, (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_face_area(self, context):
def transparent_color(color, alpha=0.1):
color = [i for i in color]
color[3] = alpha
return color
self.addon_prefs = tool.Blender.get_addon_preferences()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.line_shader.uniform_float("lineWidth", 2.0)
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
decorator_color = self.addon_prefs.decorator_color_special
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
for i, polyline in enumerate(polyline_data):
vertices = []
for point in polyline.polyline_points:
vertices.append((point.x, point.y, point.z))
data = PolylineDecorator.calculate_polygon(vertices)
if data:
self.draw_batch("POINTS", data["verts"], decorator_color)
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color, alpha=0.5), data["tris"])
@@ -1119,6 +1119,7 @@ class PolylinePoint(PropertyGroup):
class Polyline(PropertyGroup):
id: bpy.props.StringProperty(name="Id")
polyline_points: bpy.props.CollectionProperty(type=PolylinePoint)
measurement_type: bpy.props.StringProperty(name="Measurement Type")
area: bpy.props.StringProperty(name="Measured Area")
@@ -48,6 +48,7 @@ classes = (
operator.LoadProject,
operator.LoadProjectElements,
operator.MeasureTool,
operator.MeasureFaceAreaTool,
operator.ClearMeasurement,
operator.NewProject,
operator.QueryLinkedElement,
@@ -61,7 +61,7 @@ from ifcopenshell.geom import ShapeElementType
from bonsai.bim.module.project.data import LinksData, ProjectLibraryData
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator, MeasureDecorator
from bonsai.bim.module.project.prop import BreadcrumbType
from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator, FaceAreaDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union, TYPE_CHECKING, Literal, get_args
@@ -2681,6 +2681,97 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
return {"RUNNING_MODAL"}
class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
bl_idname = "bim.measure_face_area_tool"
bl_label = "Measure Face Area Tool"
bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return context.space_data.type == "VIEW_3D"
def __init__(self):
super().__init__()
self.input_options = ["AREA"]
self.input_ui = tool.Polyline.create_input_ui(input_options=self.input_options)
self.clicked_faces = []
self.total_area = 0
if tool.Ifc.get():
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
else:
self.unit_scale = tool.Blender.get_unit_scale()
def modal(self, context, event):
def select_face(mouse_pos):
objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, self.objs_2d_bbox)
obj, _, face_index = tool.Raycast.cast_rays_and_get_best_object(
context, event, objs_to_raycast, include_wireframes=False
)
if face_index:
return obj, face_index
return None, None
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
tool.Blender.update_viewport()
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
self.handle_mouse_move(context, event)
return {"PASS_THROUGH"}
self.handle_mouse_move(context, event)
if event.value == "PRESS" and event.type == "LEFTMOUSE":
tool.Blender.update_viewport()
mouse_pos = event.mouse_region_x, event.mouse_region_y
obj, face_index = select_face(mouse_pos)
if face_index:
if obj.data.polygons[face_index] not in self.clicked_faces:
self.clicked_faces.append(obj.data.polygons[face_index])
self.total_area += obj.data.polygons[face_index].area
self.input_ui.set_value("AREA", self.total_area)
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline.add()
polyline_data.id = obj.name + str(face_index)
for v_id in obj.data.polygons[face_index].vertices:
vertex = obj.matrix_world @ obj.data.vertices[v_id].co
polyline_point = polyline_data.polyline_points.add()
polyline_point.x = vertex.x
polyline_point.y = vertex.y
polyline_point.z = vertex.z
tool.Blender.update_viewport()
if event.shift and (event.value == "PRESS" and event.type == "LEFTMOUSE"):
mouse_pos = event.mouse_region_x, event.mouse_region_y
obj, face_index = select_face(mouse_pos)
if face_index:
if obj.data.polygons[face_index] in self.clicked_faces:
self.clicked_faces.remove(obj.data.polygons[face_index])
self.total_area -= obj.data.polygons[face_index].area
self.input_ui.set_value("AREA", self.total_area)
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
for i, polyline in enumerate(polyline_data):
if polyline.id == obj.name + str(face_index):
polyline_data.remove(i)
tool.Blender.update_viewport()
if event.value == "RELEASE" and event.type in {"ESC", "RIGHTMOUSE"}:
bpy.context.scene.BIMPolylineProperties.insertion_polyline.clear()
PolylineDecorator.uninstall()
FaceAreaDecorator.uninstall()
tool.Blender.update_viewport()
return {"CANCELLED"}
return {"RUNNING_MODAL"}
def invoke(self, context, event):
super().invoke(context, event)
PolylineDecorator.uninstall()
PolylineDecorator.install(context, ui_only=True)
FaceAreaDecorator.install(context)
return {"RUNNING_MODAL"}
class ClearMeasurement(bpy.types.Operator):
bl_idname = "bim.clear_measurement"
bl_label = "Clear measurement from the screen"
+2 -1
View File
@@ -499,7 +499,8 @@ class MeasureToolSettings(PropertyGroup):
measurement_type_items = [
("SINGLE", "SINGLE", "Single", "FIXED_SIZE", 1),
("POLYLINE", "POLYLINE", "Polyline", "DRIVER_ROTATIONAL_DIFFERENCE", 2),
("AREA", "AREA", "Area", "OUTLINER_DATA_LIGHTPROBE", 3),
("POLY_AREA", "POLY_AREA", "Poyline Area", "OUTLINER_DATA_LIGHTPROBE", 3),
("FACE_AREA", "FACE_AREA", "Face Area", "FACESEL", 4),
]
measurement_type: bpy.props.EnumProperty(items=measurement_type_items, default="POLYLINE")
@@ -106,4 +106,7 @@ class ExploreHotkey(bpy.types.Operator):
for obj in tool.Blender.get_selected_objects():
obj.select_set(False)
measure_type = bpy.context.scene.MeasureToolSettings.measurement_type
bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type)
if measure_type == "FACE_AREA":
bpy.ops.bim.measure_face_area_tool("INVOKE_DEFAULT")
else:
bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type)
+2
View File
@@ -66,6 +66,8 @@ class Polyline(bonsai.core.tool.Polyline):
if attribute_name == "A":
value = float(self.get_text_value(attribute_name))
return f"{value:.2f}°"
if attribute_name == "AREA":
return Polyline.format_input_ui_units(value, True)
else:
return Polyline.format_input_ui_units(value)