diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 6951ef2c67..7d55436035 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -120,7 +120,7 @@ classes = ( mep.RegenerateDistributionElement, prop.SnapMousePoint, prop.PolylinePoint, - prop.PolylineMeasurement, + prop.MeasurePolyline, prop.BIMModelProperties, prop.BIMArrayProperties, prop.BIMStairProperties, diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 317169a66e..1f82fb337f 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -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 diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index fa7817042e..9299075021 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -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) diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py index d9912d51a7..bea8e9d5c6 100644 --- a/src/bonsai/bonsai/bim/module/project/decorator.py +++ b/src/bonsai/bonsai/bim/module/project/decorator.py @@ -17,10 +17,12 @@ # along with Bonsai. If not, see . 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) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 63afa634de..1499136eab 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -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"} + diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index e4f6db536e..b30c283085 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -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 diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 7ffbbf6806..71ad67b7f9 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -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