Initial implementation of measure tool modes: single, polyline and area.

- Single: draws a single measurement that shows the lines and dimensions for x, y and z.
- Polyline: it's how it was already working
- Area: show the area value in the input panel and creates the polygon shape that represents the area. Only works for coplanar points.

The mode can be chosen by clicking the option icon in the workspace menu.
This commit is contained in:
Bruno Perdigão
2024-10-03 21:45:26 -03:00
parent 864cc10b24
commit 9b5f20e1dc
6 changed files with 149 additions and 23 deletions
+109 -16
View File
@@ -319,6 +319,9 @@ class PolylineDecorator:
handler = cls() 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_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_measurements, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_measurement_axis, (context,), "WINDOW", "POST_VIEW")
)
cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL")) cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append( cls.handlers.append(
SpaceView3D.draw_handler_add(handler.draw_product_preview, (context,), "WINDOW", "POST_VIEW") SpaceView3D.draw_handler_add(handler.draw_product_preview, (context,), "WINDOW", "POST_VIEW")
@@ -350,14 +353,50 @@ class PolylineDecorator:
cls.axis_start = start cls.axis_start = start
cls.axis_end = end cls.axis_end = end
# @classmethod
# def set_axis_rectangle(cls, corners):
# cls.axis_rectangle = [*corners]
@classmethod @classmethod
def set_tool_state(cls, tool_state): def set_tool_state(cls, tool_state):
cls.tool_state = tool_state cls.tool_state = tool_state
def calculate_measurement_x_y_and_z(self, context):
measurement_prop = context.scene.BIMPolylineProperties.polyline_point
if len(measurement_prop) == 0 or len(measurement_prop) > 2:
return None, None
start = measurement_prop[0]
if len(measurement_prop) == 1:
end = context.scene.BIMPolylineProperties.snap_mouse_point[0]
else:
end = measurement_prop[1]
x_axis = (Vector((start.x, start.y, start.z)), Vector((end.x, start.y, start.z)))
y_axis = (Vector((end.x, start.y, start.z)), Vector((end.x, end.y, start.z)))
z_axis = (Vector((end.x, end.y, start.z)), Vector((end.x, end.y, end.z)))
x_middle = (x_axis[1] + x_axis[0]) / 2
y_middle = (y_axis[1] + y_axis[0]) / 2
z_middle = (z_axis[1] + z_axis[0]) / 2
return (x_axis, y_axis, z_axis), (x_middle, y_middle, z_middle)
def calculate_polygon(self, points):
bm = bmesh.new()
new_verts = [bm.verts.new(v) for v in points]
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(points) - 1)]
bm.verts.index_update()
bm.edges.index_update()
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
bm.verts.index_update()
bm.edges.index_update()
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
bm.free()
return tris
def draw_batch(self, shader_type, content_pos, color, indices=None): def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader shader = self.line_shader if shader_type == "LINES" else self.shader
batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices) batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
@@ -424,7 +463,6 @@ class PolylineDecorator:
bm.verts.index_update() bm.verts.index_update()
bm.edges.index_update() bm.edges.index_update()
edges = [[v.index for v in e.verts] for e in bm.edges] edges = [[v.index for v in e.verts] for e in bm.edges]
print(edges)
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()] tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
all_edges.extend(edges) all_edges.extend(edges)
all_tris.extend(tris) all_tris.extend(tris)
@@ -491,6 +529,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
measure_type = context.scene.MeasureToolSettings.measure_type
measurement_prop = context.scene.BIMPolylineProperties.polyline_point measurement_prop = context.scene.BIMPolylineProperties.polyline_point
self.addon_prefs = tool.Blender.get_addon_preferences() self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -526,8 +565,65 @@ class PolylineDecorator:
text_dim = blf.dimensions(self.font_id, text) text_dim = blf.dimensions(self.font_id, text)
self.draw_text_background(context, coords_angle, text_dim) self.draw_text_background(context, coords_angle, text_dim)
blf.draw(self.font_id, text) blf.draw(self.font_id, text)
if measure_type == "SINGLE":
axis, axis_middle = self.calculate_measurement_x_y_and_z(context)
for i, measurement in enumerate(axis_middle):
coords_measurement = view3d_utils.location_3d_to_region_2d(region, rv3d, measurement)
blf.position(self.font_id, coords_measurement[0], coords_measurement[1], 0)
value = round((axis[i][1] - axis[i][0]).length, 4)
direction = axis[i][1] - axis[i][0]
if (i == 0 and direction.x < 0) or (i == 1 and direction.y < 0) or (i == 2 and direction.z < 0):
value = -value
prefix = "xyz"[i]
text = f"{prefix}: {str(value)}"
text_dim = blf.dimensions(self.font_id, text)
self.draw_text_background(context, coords_measurement, text_dim)
blf.draw(self.font_id, text)
blf.disable(self.font_id, blf.SHADOW) blf.disable(self.font_id, blf.SHADOW)
def draw_measurement_axis(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)
self.font_id = 1
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_type = context.scene.MeasureToolSettings.measure_type
if measure_type == "SINGLE":
axis, _ = self.calculate_measurement_x_y_and_z(context)
x_axis, y_axis, z_axis = axis
self.draw_batch("LINES", [*x_axis], decorator_color_x_axis, [(0, 1)])
self.draw_batch("LINES", [*y_axis], decorator_color_y_axis, [(0, 1)])
self.draw_batch("LINES", [*z_axis], decorator_color_z_axis, [(0, 1)])
def draw_snap_point(self, context): def draw_snap_point(self, context):
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") 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.bind() # required to be able to change uniforms of the shader
@@ -648,18 +744,15 @@ class PolylineDecorator:
self.line_shader.uniform_float("lineWidth", 0.75) self.line_shader.uniform_float("lineWidth", 0.75)
self.draw_batch("LINES", [self.axis_start, self.axis_end], axis_color, [(0, 1)]) self.draw_batch("LINES", [self.axis_start, self.axis_end], axis_color, [(0, 1)])
# try:
# self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)])
# except:
# pass
# Area highlight # Area highlight
# if "AREA" in list(self.input_panel.keys()): # TODO Change to input_ui try:
# if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0: # TODO Change to input_ui has_area = self.input_ui.init_area
# edges = [] except:
# for i in range(1, len(polyline_points) - 1): has_area = False
# edges.append((0, i, i + 1)) if has_area:
# self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges) if self.input_ui.get_number_value("AREA") > 0:
tris = self.calculate_polygon(polyline_points)
self.draw_batch("TRIS", polyline_points, transparent_color(decorator_color_special), tris)
# Mouse points # Mouse points
if snap_prop.snap_type in ["Plane", "Axis", "Mix"]: if snap_prop.snap_type in ["Plane", "Axis", "Mix"]:
@@ -65,6 +65,7 @@ classes = (
prop.FilterCategory, prop.FilterCategory,
prop.Link, prop.Link,
prop.BIMProjectProperties, prop.BIMProjectProperties,
prop.MeasureToolSettings,
ui.BIM_MT_new_project, ui.BIM_MT_new_project,
ui.BIM_MT_project, ui.BIM_MT_project,
ui.BIM_PT_new_project_wizard, ui.BIM_PT_new_project_wizard,
@@ -92,6 +93,7 @@ def register():
if not bpy.app.background: if not bpy.app.background:
bpy.utils.register_tool(workspace.ExploreTool, after={"builtin.transform"}, separator=True, group=False) bpy.utils.register_tool(workspace.ExploreTool, after={"builtin.transform"}, separator=True, group=False)
bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties) bpy.types.Scene.BIMProjectProperties = bpy.props.PointerProperty(type=prop.BIMProjectProperties)
bpy.types.Scene.MeasureToolSettings = bpy.props.PointerProperty(type=prop.MeasureToolSettings)
bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load) bpy.app.handlers.load_post.append(decorator.toggle_decorations_on_load)
bpy.types.TOPBAR_MT_file_import.append(ui.file_import_menu) bpy.types.TOPBAR_MT_file_import.append(ui.file_import_menu)
bpy.types.TOPBAR_MT_file.prepend(ui.file_menu) bpy.types.TOPBAR_MT_file.prepend(ui.file_menu)
@@ -115,6 +117,7 @@ def unregister():
if not bpy.app.background: if not bpy.app.background:
bpy.utils.unregister_tool(workspace.ExploreTool) bpy.utils.unregister_tool(workspace.ExploreTool)
del bpy.types.Scene.BIMProjectProperties del bpy.types.Scene.BIMProjectProperties
del bpy.types.Scene.MeasureToolSettings
bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load) bpy.app.handlers.load_post.remove(decorator.toggle_decorations_on_load)
bpy.types.TOPBAR_MT_file.remove(ui.file_menu) bpy.types.TOPBAR_MT_file.remove(ui.file_menu)
bpy.types.TOPBAR_MT_file_context_menu.remove(ui.file_menu) bpy.types.TOPBAR_MT_file_context_menu.remove(ui.file_menu)
@@ -2372,13 +2372,18 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Tool" bl_label = "Measure Tool"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty()
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.space_data.type == "VIEW_3D" return context.space_data.type == "VIEW_3D"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.input_ui = tool.Polyline.create_input_ui(init_z=True) if self.measure_type == 'AREA':
self.input_ui = tool.Polyline.create_input_ui(init_z=True, init_area=True)
else:
self.input_ui = tool.Polyline.create_input_ui(init_z=True)
self.input_options = ["D", "A", "X", "Y", "Z"] self.input_options = ["D", "A", "X", "Y", "Z"]
self.instructions = """TAB: Cycle Input self.instructions = """TAB: Cycle Input
D: Distance Input D: Distance Input
@@ -2405,11 +2410,15 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
self.handle_snap_selection(context, event) self.handle_snap_selection(context, event)
single_mode = False
if self.measure_type == "SINGLE" and len(context.scene.BIMPolylineProperties.polyline_point) >= 2:
single_mode = True
if ( if (
not self.tool_state.is_input_on not self.tool_state.is_input_on
and event.value == "RELEASE" and event.value == "RELEASE"
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"} and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
): ) or single_mode:
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.move_polyline_to_measure()
@@ -2421,6 +2430,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
self.handle_keyboard_input(context, event) self.handle_keyboard_input(context, event)
self.handle_inserting_polyline(context, event) self.handle_inserting_polyline(context, event)
tool.Polyline.calculate_area(context, self.input_ui)
if event.type == "E": if event.type == "E":
context.scene.BIMPolylineProperties.measure_polyline.clear() context.scene.BIMPolylineProperties.measure_polyline.clear()
@@ -229,3 +229,13 @@ class BIMProjectProperties(PropertyGroup):
def get_library_element_index(self, lib_element): def get_library_element_index(self, lib_element):
return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element)) return next((i for i in range(len(self.library_elements)) if self.library_elements[i] == lib_element))
class MeasureToolSettings(PropertyGroup):
measure_type_items = [
("SINGLE", "SINGLE", "Single", "FIXED_SIZE",1),
("POLYLINE", "POLYLINE", "Polyline", "DRIVER_ROTATIONAL_DIFFERENCE", 2),
("AREA", "AREA", "Area", "OUTLINER_DATA_LIGHTPROBE", 3),
]
measure_type: bpy.props.EnumProperty(items=measure_type_items, default="POLYLINE")
@@ -58,9 +58,16 @@ class ExploreTool(bpy.types.WorkSpaceTool):
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_ALT") row.label(text="", icon="EVENT_ALT")
row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C") row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C")
prop = context.scene.MeasureToolSettings
row = layout.row(align=True) row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT") row.label(text="", icon="EVENT_SHIFT")
row.label(text="Measure Tool", icon="EVENT_M") row.label(text="", icon="EVENT_M")
row = layout.row(align=True)
op = row.operator("bim.explore_hotkey", text="Measure Tool", icon="CON_DISTLIMIT")
op.hotkey = "S_M"
row = layout.row(align=True)
row.prop(prop, "measure_type", text="Measure Type", expand=True, icon_only=True, emboss=True)
class ExploreHotkey(bpy.types.Operator): class ExploreHotkey(bpy.types.Operator):
@@ -94,4 +101,7 @@ class ExploreHotkey(bpy.types.Operator):
bpy.ops.bim.enable_culling("INVOKE_DEFAULT") bpy.ops.bim.enable_culling("INVOKE_DEFAULT")
def hotkey_S_M(self): def hotkey_S_M(self):
bpy.ops.bim.measure_tool("INVOKE_DEFAULT") for obj in tool.Blender.get_selected_objects():
obj.select_set(False)
measure_type = bpy.context.scene.MeasureToolSettings.measure_type
bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type=measure_type)
+3 -3
View File
@@ -45,7 +45,7 @@ class Polyline(bonsai.core.tool.Polyline):
if self.init_z: if self.init_z:
self._Z = "" self._Z = ""
if self.init_area: if self.init_area:
self._AREA = "" self._AREA = "0"
def set_value(self, attribute_name, value): def set_value(self, attribute_name, value):
value = str(value) value = str(value)
@@ -222,8 +222,8 @@ class Polyline(bonsai.core.tool.Polyline):
else: else:
area = 0 area = 0
if input_ui.get_text_value("A") is not None: if input_ui.get_text_value("AREA") is not None:
input_ui.set_value("A", area) input_ui.set_value("AREA", area)
return return
@classmethod @classmethod