From 8dc77735a58fad2f54386a2044b42b393f6d8db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sun, 8 Sep 2024 12:10:01 -0300 Subject: [PATCH] fix rebase conflict --- .../bonsai/bim/module/model/decorator.py | 222 ++------------ src/bonsai/bonsai/bim/module/model/wall.py | 74 ++--- .../bonsai/bim/module/project/operator.py | 65 ++-- src/bonsai/bonsai/core/tool.py | 3 + src/bonsai/bonsai/tool/__init__.py | 1 + src/bonsai/bonsai/tool/polyline.py | 284 ++++++++++++++++++ src/bonsai/bonsai/tool/snap.py | 14 +- 7 files changed, 388 insertions(+), 275 deletions(-) create mode 100644 src/bonsai/bonsai/tool/polyline.py diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index f3160453a8..b14878a690 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -302,8 +302,8 @@ class PolylineDecorator: is_installed = False handlers = [] mouse_pos = None - input_panel = None input_type = None + input_ui = None angle_snap_mat = None angle_snap_loc = None use_default_container = False @@ -319,7 +319,7 @@ class PolylineDecorator: SpaceView3D.draw_handler_add(handler.draw_on_screen_menu, (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_panel, (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")) cls.is_installed = True @@ -337,8 +337,8 @@ class PolylineDecorator: cls.mouse_pos = event.mouse_region_x, event.mouse_region_y @classmethod - def set_input_panel(cls, input_panel, input_type): - cls.input_panel = input_panel + def set_input_ui(cls, input_ui, input_type): + cls.input_ui = input_ui cls.input_type = input_type @classmethod @@ -354,11 +354,6 @@ class PolylineDecorator: def set_use_default_container(cls, value=False): cls.use_default_container = value - @classmethod - def set_plane(cls, plane_origin, plane_normal): - cls.plane_origin = plane_origin - cls.plane_normal = plane_normal - @classmethod def set_instructions(cls, instructions): cls.instructions = instructions @@ -367,169 +362,6 @@ class PolylineDecorator: def set_snap_info(cls, snap_info): cls.snap_info = snap_info - @classmethod - def calculate_distance_and_angle(cls, context, is_input_on): - - try: - polyline_data = context.scene.BIMModelProperties.polyline_point - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z - last_point_data = polyline_data[len(polyline_data) - 1] - except: - default_container_elevation = 0 - last_point_data = None - - snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] - - if last_point_data: - last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) - else: - last_point = Vector((0, 0, 0)) - - if is_input_on: - if cls.use_default_container: - snap_vector = Vector( - (float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation) - ) - else: - snap_vector = Vector( - (float(cls.input_panel["X"]), float(cls.input_panel["Y"]), float(cls.input_panel["Z"])) - ) - else: - if cls.use_default_container: - snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) - else: - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - - second_to_last_point = None - if len(polyline_data) > 1: - second_to_last_point_data = polyline_data[len(polyline_data) - 2] - second_to_last_point = Vector( - (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) - ) - else: - # Creates a fake "second to last" point away from the first point but in the same x axis - # this allows to calculate the angle relative to x axis when there is only one point - second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) - - distance = (snap_vector - last_point).length - if distance > 0: - angle = tool.Cad.angle_3_vectors( - second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True - ) - - # Round angle to the nearest 0.05 - angle = round(angle / 0.05) * 0.05 - - if cls.input_panel: - cls.input_panel["X"] = str(round(snap_vector.x, 3)) - cls.input_panel["Y"] = str(round(snap_vector.y, 3)) - if "Z" in list(cls.input_panel.keys()): - cls.input_panel["Z"] = str(round(snap_vector.z, 3)) - cls.input_panel["D"] = str(round(distance, 3)) - cls.input_panel["A"] = str(round(angle, 3)) - - return cls.input_panel - - return cls.input_panel - - @classmethod - def calculate_area(cls, context): - try: - polyline_data = context.scene.BIMModelProperties.polyline_point - except: - return cls.input_panel - - if len(polyline_data) < 3: - return cls.input_panel - - points = [] - for data in polyline_data: - points.append(Vector((data.x, data.y, data.z))) - - if points[0] == points[-1]: - points = points[1:] - - # TODO move this to CAD - # Calculate the normal vector of the plane formed by the first three vertices - v1, v2, v3 = points[:3] - normal = (v2 - v1).cross(v3 - v1).normalized() - - # Check if all points are coplanar - is_coplanar = True - tolerance = 1e-6 # Adjust this value as needed - for v in points: - if abs((v - v1).dot(normal)) > tolerance: - is_coplanar = False - - if is_coplanar: - area = 0 - for i in range(len(points)): - j = (i + 1) % len(points) - area += points[i].cross(points[j]).dot(normal) - - area = abs(area) / 2 - else: - area = 0 - - if "AREA" in list(cls.input_panel.keys()): - cls.input_panel["AREA"] = str(round(area, 4)) - return cls.input_panel - - @classmethod - def calculate_x_y_and_z(cls, context): - try: - polyline_data = context.scene.BIMModelProperties.polyline_point - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z - last_point_data = polyline_data[len(polyline_data) - 1] - last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) - except: - default_container_elevation = 0 - last_point = Vector((0, 0, 0)) - - snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - - if cls.use_default_container: - snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) - else: - snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) - - if len(polyline_data) > 1: - second_to_last_point_data = polyline_data[len(polyline_data) - 2] - second_to_last_point = Vector( - (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) - ) - else: - # Creates a fake "second to last" point away from the first point but in the same x axis - # this allows to calculate the angle relative to x axis when there is only one point - second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) - - distance = float(cls.input_panel["D"]) - - if distance < 0 or distance > 0: - angle = radians(float(cls.input_panel["A"])) - - rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True) - - coords = rot_vector * distance + last_point - - x = coords[0] - y = coords[1] - z = coords[2] - if cls.input_panel: - cls.input_panel["X"] = str(round(x, 3)) - cls.input_panel["Y"] = str(round(y, 3)) - if "Z" in list(cls.input_panel.keys()): - cls.input_panel["Z"] = str(round(z, 3)) - - return cls.input_panel - - cls.input_panel["X"] = str(round(last_point.x, 3)) - cls.input_panel["Y"] = str(round(last_point.y, 3)) - if "Z" in list(cls.input_panel.keys()): - cls.input_panel["Z"] = str(round(last_point.z, 3)) - - return cls.input_panel def draw_batch(self, shader_type, content_pos, color, indices=None): shader = self.line_shader if shader_type == "LINES" else self.shader @@ -537,21 +369,8 @@ class PolylineDecorator: shader.uniform_float("color", color) batch.draw(shader) - @classmethod - def format_input_panel_units(cls, context, value): - unit_system = tool.Drawing.get_unit_system() - if unit_system == "IMPERIAL": - precision = context.scene.DocProperties.imperial_precision - factor = 3.28084 - else: - precision = None - factor = 1 - if context.scene.unit_settings.length_unit == "MILLIMETERS": - factor = 1000 - return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) - - def draw_input_panel(self, context): + def draw_input_ui(self, context): texts = { "D": "Distance: ", "A": "Angle: ", @@ -571,22 +390,22 @@ class PolylineDecorator: color_highlight = self.addon_prefs.decorator_color_special offset = 20 new_line = 20 - for i, (key, value) in enumerate(self.input_panel.items()): + for i, (key, field_name) in enumerate(texts.items()): - if key != "A" and key != self.input_type: - value = float(value) - formatted_value = self.format_input_panel_units(context, value) + if key != self.input_type: + formatted_value = self.input_ui.get_formatted_value(key) else: - formatted_value = value + formatted_value = self.input_ui.get_text_value(key) - if key not in list(texts.keys()): + if formatted_value is None: continue if key == self.input_type: blf.color(self.font_id, *color_highlight) else: blf.color(self.font_id, *color) blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] - (new_line * i), 0) - blf.draw(self.font_id, texts[key] + formatted_value) + blf.draw(self.font_id, field_name + formatted_value) + def draw_measurements(self, context): region = context.region @@ -608,9 +427,7 @@ class PolylineDecorator: 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) - value = measurement_prop[i].dim - value = float(value) - formatted_value = self.format_input_panel_units(context, value) + formatted_value = measurement_prop[i].dim blf.position(self.font_id, coords_dim[0], coords_dim[1], 0) blf.draw(self.font_id, "d: " + formatted_value) @@ -622,6 +439,7 @@ class PolylineDecorator: blf.position(self.font_id, coords_angle[0], coords_angle[1], 0) blf.draw(self.font_id, "a: " + measurement_prop[i].angle) + def draw_on_screen_menu(self, context): region = context.region @@ -708,12 +526,12 @@ class PolylineDecorator: pass # Area highlight - if "AREA" in list(self.input_panel.keys()): - if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0: - edges = [] - for i in range(1, len(polyline_points) - 1): - edges.append((0, i, i + 1)) - self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges) + # if "AREA" in list(self.input_panel.keys()): # TODO Change to input_ui + # if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0: # TODO Change to input_ui + # edges = [] + # for i in range(1, len(polyline_points) - 1): + # edges.append((0, i, i + 1)) + # self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges) # Mouse points if snap_prop.snap_type in ["Face", "Plane"]: diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 21b57f8f4f..5dbc53351a 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -324,9 +324,9 @@ class DrawPolylineWall(bpy.types.Operator): self.number_is_negative = False self.is_input_on = False self.input_options = ["D", "A", "X", "Y"] - self.input_type = "OFF" + self.input_type = None self.input_value_xy = [None, None] - self.input_panel = {"D": "", "A": "", "X": "", "Y": ""} + self.input_ui = tool.Polyline.create_input_ui() self.snap_angle = None self.snapping_points = [] self.instructions = """TAB: Cycle Input @@ -340,18 +340,18 @@ class DrawPolylineWall(bpy.types.Operator): def recalculate_inputs(self, context): if self.number_input: is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) if not is_valid: self.report({"WARNING"}, "The number typed is not valid.") return is_valid else: if self.input_type in {"X", "Y"}: - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) elif self.input_type in {"D", "A"}: - self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_x_y_and_z(context, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) else: - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) tool.Blender.update_viewport() return is_valid @@ -387,8 +387,8 @@ class DrawPolylineWall(bpy.types.Operator): if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE": self.mousemove_count += 1 self.is_input_on = False - self.input_type = "OFF" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_type = None + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: @@ -403,7 +403,7 @@ class DrawPolylineWall(bpy.types.Operator): detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() return {"RUNNING_MODAL"} @@ -412,7 +412,7 @@ class DrawPolylineWall(bpy.types.Operator): tool.Blender.update_viewport() if event.value == "RELEASE" and event.type == "LEFTMOUSE": - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "X": @@ -425,7 +425,7 @@ class DrawPolylineWall(bpy.types.Operator): if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.is_input_on and event.value == "PRESS" and event.type == "TAB": @@ -434,15 +434,15 @@ class DrawPolylineWall(bpy.types.Operator): size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) if self.input_type != "A": - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) + self.number_output = self.input_ui.get_formatted_value(self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB": @@ -450,13 +450,14 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = True self.input_type = "D" - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) + print("NI", self.number_input) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) - self.input_panel[self.input_type] = self.number_output + self.number_output = self.input_ui.get_formatted_value(self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.ascii in self.number_options: @@ -464,7 +465,8 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = True self.input_type = "D" self.number_input = [] - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + print("WALL", self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "RELEASE" and event.type in {"D", "A"}: @@ -472,8 +474,8 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = True self.input_type = event.type self.number_input = [] - self.input_panel[self.input_type] = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, "") + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.input_type in self.input_options: @@ -486,16 +488,16 @@ class DrawPolylineWall(bpy.types.Operator): self.number_output = "".join(self.number_input) if not self.number_input: self.number_output = "0" - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: self.number_input.append(event.ascii) self.number_output = "".join(self.number_input) if self.number_input: - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: @@ -508,18 +510,18 @@ class DrawPolylineWall(bpy.types.Operator): if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: is_valid = self.recalculate_inputs(context) if is_valid: - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) self.is_input_on = False - self.input_type = "OFF" + self.input_type = None self.number_input = [] self.number_output = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: @@ -529,8 +531,8 @@ class DrawPolylineWall(bpy.types.Operator): if event.value == "RELEASE" and event.type in {"ESC"}: self.recalculate_inputs(context) self.is_input_on = False - self.input_type = "OFF" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_type = None + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: @@ -547,16 +549,18 @@ class DrawPolylineWall(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.set_use_default_container(True) PolylineDecorator.set_use_default_container(True) + tool.Polyline.set_use_default_container(True) # <--- tool.Snap.set_snap_plane_method("XY") PolylineDecorator.set_instructions(self.instructions) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) + tool.Blender.update_viewport() context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index f8acb4d7e1..0758348d04 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2310,7 +2310,7 @@ class MeasureTool(bpy.types.Operator): self.input_options = ["D", "A", "X", "Y", "Z"] self.input_type = None self.input_value_xy = [None, None] - self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""} + self.input_ui = tool.Polyline.create_input_ui(init_z=True) self.snap_angle = None self.snapping_points = [] self.instructions = """TAB: Cycle Input @@ -2325,18 +2325,18 @@ class MeasureTool(bpy.types.Operator): def recalculate_inputs(self, context): if self.number_input: is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) if not is_valid: self.report({"WARNING"}, "The number typed is not valid.") return is_valid else: if self.input_type in {"X", "Y", "Z"}: - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) elif self.input_type in {"D", "A"}: - self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) - # self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_x_y_and_z(context, self.input_ui) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) else: - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) tool.Blender.update_viewport() return is_valid @@ -2347,7 +2347,7 @@ class MeasureTool(bpy.types.Operator): self.mousemove_count += 1 self.is_input_on = False self.input_type = None - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: @@ -2362,7 +2362,7 @@ class MeasureTool(bpy.types.Operator): detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() return {"RUNNING_MODAL"} @@ -2371,7 +2371,7 @@ class MeasureTool(bpy.types.Operator): tool.Blender.update_viewport() if event.value == "RELEASE" and event.type == "LEFTMOUSE": - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "X": @@ -2388,7 +2388,7 @@ class MeasureTool(bpy.types.Operator): if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.is_input_on and event.value == "PRESS" and event.type == "TAB": @@ -2397,15 +2397,15 @@ class MeasureTool(bpy.types.Operator): size = len(self.input_options) self.input_type = self.input_options[((index + 1) % size)] - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) if self.input_type != "A": - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) + self.number_output = self.input_ui.get_formatted_value(self.input_type) - self.input_panel[self.input_type] = self.number_output + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB": @@ -2413,13 +2413,13 @@ class MeasureTool(bpy.types.Operator): self.is_input_on = True self.input_type = "D" - self.number_input = self.input_panel[self.input_type] + self.number_input = self.input_ui.get_text_value(self.input_type) self.number_input = list(self.number_input) self.number_output = "".join(self.number_input) - self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output)) - self.input_panel[self.input_type] = self.number_output + self.number_output = self.input_ui.get_formatted_value(self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.ascii in self.number_options: @@ -2427,7 +2427,7 @@ class MeasureTool(bpy.types.Operator): self.is_input_on = True self.input_type = "D" self.number_input = [] - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "RELEASE" and event.type in {"D", "A"}: @@ -2435,8 +2435,8 @@ class MeasureTool(bpy.types.Operator): self.is_input_on = True self.input_type = event.type self.number_input = [] - self.input_panel[self.input_type] = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, "") + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if self.input_type in self.input_options: @@ -2447,16 +2447,18 @@ class MeasureTool(bpy.types.Operator): else: self.number_input = self.number_input[:-1] self.number_output = "".join(self.number_input) - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + if not self.number_input: + self.number_output = "0" + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: self.number_input.append(event.ascii) self.number_output = "".join(self.number_input) if self.number_input: - self.input_panel[self.input_type] = self.number_output - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_ui.set_value(self.input_type, self.number_output) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: @@ -2468,18 +2470,18 @@ class MeasureTool(bpy.types.Operator): if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}: is_valid = self.recalculate_inputs(context) if is_valid: - tool.Snap.insert_polyline_point(self.input_panel) + tool.Snap.insert_polyline_point(self.input_ui) self.is_input_on = False self.input_type = None self.number_input = [] self.number_output = "" - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() if event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() if event.shift and event.value == "PRESS" and event.type == "X": @@ -2511,7 +2513,7 @@ class MeasureTool(bpy.types.Operator): self.recalculate_inputs(context) self.is_input_on = False self.input_type = None - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: @@ -2529,17 +2531,18 @@ class MeasureTool(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) + tool.Polyline.set_use_default_container(False) # <--- tool.Snap.set_snap_plane_method(None) tool.Snap.set_snap_axis_method(None) PolylineDecorator.set_instructions(self.instructions) - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + PolylineDecorator.set_input_ui(self.input_ui, self.input_type) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj)) detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) - self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + tool.Polyline.calculate_distance_and_angle(context, self.is_input_on, self.input_ui) tool.Blender.update_viewport() context.window_manager.modal_handler_add(self) return {"RUNNING_MODAL"} diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index e3566b72cf..0c675f684d 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -580,6 +580,9 @@ class Nest: class Patch: def run_migrate_patch(cls, infile, outfile, schema): pass +@interface +class Polyline: + pass @interface class Owner: diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py index a7d28cab44..16a64716e4 100644 --- a/src/bonsai/bonsai/tool/__init__.py +++ b/src/bonsai/bonsai/tool/__init__.py @@ -44,6 +44,7 @@ from bonsai.tool.model import Model from bonsai.tool.nest import Nest from bonsai.tool.owner import Owner from bonsai.tool.patch import Patch +from bonsai.tool.polyline import Polyline from bonsai.tool.project import Project from bonsai.tool.profile import Profile from bonsai.tool.pset import Pset diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py new file mode 100644 index 0000000000..8934ffe4c7 --- /dev/null +++ b/src/bonsai/bonsai/tool/polyline.py @@ -0,0 +1,284 @@ +# Bonsai - OpenBIM Blender Add-on +# Copyright (C) 2022 Cyril Waechter +# +# This file is part of Bonsai. +# +# Bonsai is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Bonsai is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Bonsai. If not, see . + +import bpy +import bonsai.core.tool +import bonsai.tool as tool +from bonsai.bim.module.drawing.helper import format_distance +from dataclasses import dataclass, field +from math import sin, cos, radians, degrees, atan2, acos +from mathutils import Vector, Matrix +from typing import Optional + + +@dataclass +class PolylineUI: + _D: str = "" + _A: str = "" + _X: str = "" + _Y: str = "" + _Z: Optional[str] = None + _AREA: Optional[str] = None + init_z: bool = False + init_area: bool = False + + def __post_init__(self): + if self.init_z: + self._Z = "" + if self.init_area: + self._AREA = "" + + def set_value(self, attribute_name, value): + if isinstance(value, float): + value = round(value, 3) + value = str(value) + setattr(self, f"_{attribute_name}", value) + + def get_text_value(self, attribute_name): + return getattr(self, f"_{attribute_name}") + + def get_number_value(self, attribute_name): + value = getattr(self, f"_{attribute_name}") + if value: + return float(value) + else: + return value + + def get_formatted_value(self, attribute_name): + value = self.get_number_value(attribute_name) + context = bpy.context + if value is None: + return None + if attribute_name == 'A': + return self.get_text_value(attribute_name) + else: + return self.format_input_ui_units(context, value) + + def format_input_ui_units(cls, context, value): + unit_system = tool.Drawing.get_unit_system() + if unit_system == "IMPERIAL": + precision = context.scene.DocProperties.imperial_precision + factor = 3.28084 + else: + precision = None + factor = 1 + if context.scene.unit_settings.length_unit == "MILLIMETERS": + factor = 1000 + + return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) + + +class Polyline(bonsai.core.tool.Polyline): + + @classmethod + def create_input_ui(cls, init_z=False, init_area=False): + return PolylineUI(init_z=init_z, init_area=init_area) + + @classmethod + def set_mouse_position(cls, event): + cls.mouse_pos = event.mouse_region_x, event.mouse_region_y + + @classmethod + def set_angle_axis_line(cls, start, end): + cls.axis_start = start + cls.axis_end = end + + @classmethod + def set_axis_rectangle(cls, corners): + cls.axis_rectangle = [*corners] + + @classmethod + def set_use_default_container(cls, value=False): + cls.use_default_container = value + + @classmethod + def set_plane(cls, plane_origin, plane_normal): + cls.plane_origin = plane_origin + cls.plane_normal = plane_normal + + @classmethod + def set_instructions(cls, instructions): + cls.instructions = instructions + + @classmethod + def set_snap_info(cls, snap_info): + cls.snap_info = snap_info + + @classmethod + def calculate_distance_and_angle(cls, context, is_input_on, input_ui): + + try: + polyline_data = context.scene.BIMModelProperties.polyline_point + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + last_point_data = polyline_data[len(polyline_data) - 1] + except: + default_container_elevation = 0 + last_point_data = None + + snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] + + if last_point_data: + last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) + else: + last_point = Vector((0, 0, 0)) + + if is_input_on: + if cls.use_default_container: + snap_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation) + ) + else: + snap_vector = Vector( + (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z")) + ) + else: + if cls.use_default_container: + snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + else: + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + + second_to_last_point = None + if len(polyline_data) > 1: + second_to_last_point_data = polyline_data[len(polyline_data) - 2] + second_to_last_point = Vector( + (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) + ) + else: + # Creates a fake "second to last" point away from the first point but in the same x axis + # this allows to calculate the angle relative to x axis when there is only one point + second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + + distance = (snap_vector - last_point).length + if distance > 0: + angle = tool.Cad.angle_3_vectors( + second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True + ) + + # Round angle to the nearest 0.05 + angle = round(angle / 0.05) * 0.05 + + if input_ui: + input_ui.set_value("X", snap_vector.x) + input_ui.set_value("Y", snap_vector.y) + if input_ui.get_number_value("Z") is not None: + input_ui.set_value("Z", snap_vector.z) + + input_ui.set_value("D", distance) + input_ui.set_value("A", angle) + return + + return + + @classmethod + def calculate_area(cls, context, input_ui): + try: + polyline_data = context.scene.BIMModelProperties.polyline_point + except: + return input_ui + + if len(polyline_data) < 3: + return input_ui + + points = [] + for data in polyline_data: + points.append(Vector((data.x, data.y, data.z))) + + if points[0] == points[-1]: + points = points[1:] + + # TODO move this to CAD + # Calculate the normal vector of the plane formed by the first three vertices + v1, v2, v3 = points[:3] + normal = (v2 - v1).cross(v3 - v1).normalized() + + # Check if all points are coplanar + is_coplanar = True + tolerance = 1e-6 # Adjust this value as needed + for v in points: + if abs((v - v1).dot(normal)) > tolerance: + is_coplanar = False + + if is_coplanar: + area = 0 + for i in range(len(points)): + j = (i + 1) % len(points) + area += points[i].cross(points[j]).dot(normal) + + area = abs(area) / 2 + else: + area = 0 + + if input_ui.get_text_value("A") is not None: + input_ui.set_value("A", area) + return + + @classmethod + def calculate_x_y_and_z(cls, context, input_ui): + try: + polyline_data = context.scene.BIMModelProperties.polyline_point + default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z + last_point_data = polyline_data[len(polyline_data) - 1] + last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z)) + except: + default_container_elevation = 0 + last_point = Vector((0, 0, 0)) + + snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0] + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + + if cls.use_default_container: + snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation)) + else: + snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z)) + + if len(polyline_data) > 1: + second_to_last_point_data = polyline_data[len(polyline_data) - 2] + second_to_last_point = Vector( + (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z) + ) + else: + # Creates a fake "second to last" point away from the first point but in the same x axis + # this allows to calculate the angle relative to x axis when there is only one point + second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z)) + + distance = input_ui.get_number_value("D") + + if distance < 0 or distance > 0: + angle = radians(input_ui.get_number_value("A")) + + rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True) + + coords = rot_vector * distance + last_point + + x = coords[0] + y = coords[1] + z = coords[2] + if input_ui: + input_ui.set_value("X", x) + input_ui.set_value("Y", y) + if input_ui.get_number_value("Z") is not None: + input_ui.set_value("Z", z) + + return + + input_ui.set_value("X", last_point.x) + input_ui.set_value("Y", last_point.y) + if input_ui.get_number_value("Z") is not None: + input_ui.set_value("Z", last_point.z) + + return diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 4f51e85ca6..9f0cc5f9bf 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -70,6 +70,7 @@ 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 @@ -125,15 +126,15 @@ class Snap(bonsai.core.tool.Snap): bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear() @classmethod - def insert_polyline_point(cls, input_panel): - x = float(input_panel["X"]) - y = float(input_panel["Y"]) + def insert_polyline_point(cls, input_ui): + x = input_ui.get_number_value("X") + y = input_ui.get_number_value("Y") try: - z = float(input_panel["Z"]) + z = input_ui.get_number_value("Z") except: z = Vector((0, 0, 0)) - d = input_panel["D"] - a = input_panel["A"] + d = input_ui.get_formatted_value("D") + a = input_ui.get_formatted_value("A") snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0] if cls.use_default_container: @@ -405,7 +406,6 @@ class Snap(bonsai.core.tool.Snap): elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z plane_origin, plane_normal = select_plane_method() - PolylineDecorator.set_plane(plane_origin, plane_normal) intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal) axis_start = None