From 60a06138d2c710b0798159affdf02cbe049469e7 Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Fri, 23 Aug 2024 19:34:15 +0300 Subject: [PATCH 001/556] edit index.css for color consistency --- .../bonsai/bim/data/webui/static/css/index.css | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/index.css b/src/bonsai/bonsai/bim/data/webui/static/css/index.css index 02cb6cf3e4..f83fac08e1 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/index.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/index.css @@ -281,22 +281,29 @@ button:hover { -:root.blender .tabulator-selected .tabulator-cell { +:root.blender .tabulator-selected .tabulator-cell, +:root.blender .tabulator-row.tabulator-selected { background-color: var(--blender-active-highlight) !important; color: var(--blender-active-text-highlight) !important; } -:root.blender .tabulator-selected:hover .tabulator-cell { +:root.blender .tabulator-selected:hover .tabulator-cell, +:root.blender .tabulator-row.tabulator-selected:hover { filter: brightness(1.2) } -.tabulator-menu-item { +:root.blender .tabulator-popup-container { + background: var(--blender-panel-background); + border: var(--blender-tab-outline); +} + +:root.blender .tabulator-menu-item { color: var(--blender-text) !important; background-color: var(--blender-panel-background) !important; transition: filter 0.1s ease; } -.tabulator-menu-item:hover { +:root.blender .tabulator-menu-item:hover { filter: brightness(1.4); -} +} \ No newline at end of file From 2ca3f86fc94068d5222a4b076697dc852447fd1b Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Fri, 23 Aug 2024 21:30:21 +0300 Subject: [PATCH 002/556] add RMB context menu to tables to set top calc type --- .../bonsai/bim/data/webui/static/js/index.js | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/index.js b/src/bonsai/bonsai/bim/data/webui/static/js/index.js index 3fac376f28..d4b6d7fb73 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/js/index.js +++ b/src/bonsai/bonsai/bim/data/webui/static/js/index.js @@ -207,8 +207,35 @@ function addTableElement(blenderId, csvData, filename) { return menu; } - var containerHeight=$('.table-container').height(); - var h3Height=$('.table-container>h3').height(); + // create right click context menu for choosing column top calculation + function createColumnContextMenu(column) { + const field = column.field; + const calculations = ["sum", "avg", "max", "min"]; + + // label is the item label in the menu + // update column defination on clicking an item + // specifically the topCalc and pass calc type to the topCalcFormatter function + // uisng the topCalcFormatterParams field + return calculations.map((calc) => ({ + label: `Show ${ + calc.charAt(0).toUpperCase() + calc.slice(1) + } for ${field}`, + action: (e, cell) => + cell + .getColumn() + .updateDefinition({ topCalc: calc, topCalcFormatterParams: calc }), + })); + } + + function calcFormatter(cell, formatterParams, onRendered) { + // formatterParams is anything in the topCalcFormatterParams field in the column definition + // so we pass the calculation type to display what the calculation type is + return `${formatterParams}: ${cell.getValue()}`; + } + + var containerHeight = $(".table-container").height(); + var h3Height = $(".table-container>h3").height(); + var table = new Tabulator("#table-" + blenderId, { placeholder: "No data to display", resizableColumnGuide: true, @@ -222,14 +249,21 @@ function addTableElement(blenderId, csvData, filename) { movableColumns: true, // Our fields are never nested, and we use the "." character in queries nestedFieldSeparator: false, + // defines column defualts that are applied to all columns + columnDefaults: { + visible: true, + headerFilter: true, + topCalc: "sum", + topCalcFormatter: calcFormatter, + topCalcFormatterParams: "sum", + }, + // defines a callback to edit the column definition generated by autoColumns + // that cannot be set as default autoColumnsDefinitions: function (definitions) { - menu = createHeaderMenu(definitions, table); + headerMenu = createHeaderMenu(definitions, table); definitions.forEach((column) => { - column.visible = true; - column.headerFilter = true; - column.headerMenu = menu; - // TODO: more user control to choose function, style at bottomCalc - column.topCalc = 'sum'; + column.headerMenu = headerMenu; + column.contextMenu = createColumnContextMenu(column); }); return definitions; }, @@ -239,7 +273,9 @@ function addTableElement(blenderId, csvData, filename) { .css("margin-left", "10px") .css("cursor", "pointer"); tableTitle.append(downloadCsv); - downloadCsv.on('click', function() { table.download("csv", "data.csv"); }) + downloadCsv.on("click", function () { + table.download("csv", "data.csv"); + }); table.on("rowSelected", function (row) { var index = row.getIndex(); // the guid of the object From 42fc72bd4f4c6ea08cf0376541b4f66b814b08fa Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:12:13 +0300 Subject: [PATCH 003/556] add favicon to web UI --- src/bonsai/bonsai/bim/data/webui/templates/demo.html | 5 +++++ src/bonsai/bonsai/bim/data/webui/templates/drawings.html | 5 +++++ src/bonsai/bonsai/bim/data/webui/templates/gantt.html | 5 +++++ src/bonsai/bonsai/bim/data/webui/templates/index.html | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/src/bonsai/bonsai/bim/data/webui/templates/demo.html b/src/bonsai/bonsai/bim/data/webui/templates/demo.html index 151a7948e8..da67d65d5c 100644 --- a/src/bonsai/bonsai/bim/data/webui/templates/demo.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/demo.html @@ -5,6 +5,11 @@ Bonsai Web UI + diff --git a/src/bonsai/bonsai/bim/data/webui/templates/drawings.html b/src/bonsai/bonsai/bim/data/webui/templates/drawings.html index 858f1edcf9..9d70584ed1 100644 --- a/src/bonsai/bonsai/bim/data/webui/templates/drawings.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/drawings.html @@ -4,6 +4,11 @@ Bonsai Web UI + Bonsai Web UI + Bonsai Web UI + Date: Sat, 24 Aug 2024 18:48:58 +0300 Subject: [PATCH 004/556] make container padding/margin consistent over all pages --- src/bonsai/bonsai/bim/data/webui/static/css/demo.css | 5 +---- src/bonsai/bonsai/bim/data/webui/static/css/drawings.css | 5 +---- src/bonsai/bonsai/bim/data/webui/static/css/gantt.css | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/demo.css b/src/bonsai/bonsai/bim/data/webui/static/css/demo.css index 9a2460c90f..975bb318d1 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/demo.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/demo.css @@ -54,10 +54,7 @@ body { #container { flex: 1; - margin-top: var(--margin-medium); - margin-left: var(--margin-small); - margin-right: var(--margin-small); - margin-bottom: var(--margin-medium); + padding: var(--margin-small); } h3 { diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/drawings.css b/src/bonsai/bonsai/bim/data/webui/static/css/drawings.css index c2779d7e9e..17485412d3 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/drawings.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/drawings.css @@ -55,10 +55,7 @@ body { } #container { - margin-top: var(--margin-medium); - margin-left: var(--margin-tiny); - margin-right: var(--margin-tiny); - margin-bottom: var(--margin-medium); + padding: var(--margin-small); height: calc(100vh - var(--nav-height)); box-sizing: border-box; display: flex; diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css b/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css index 9f97eb6b82..c90d2dbcb4 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/gantt.css @@ -72,10 +72,7 @@ body { #container { flex: 1; - margin-top: var(--margin-medium); - margin-left: var(--margin-small); - margin-right: var(--margin-small); - margin-bottom: var(--margin-medium); + padding: var(--margin-small); } h3 { From 1745a4fd39c1b2edb5312fb3d473b69201ca4342 Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Sun, 25 Aug 2024 17:50:26 +0300 Subject: [PATCH 005/556] fix theme data not being sent when all Bonsai's are disconnected --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index c663e7d091..29f373f750 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -38,9 +38,9 @@ class WebNamespace(socketio.AsyncNamespace): async def on_connect(self, sid, environ): print(f"Web client connected: {sid}") + await sio.emit("theme_data", blender_theme, namespace="/web", room=sid) if blender_messages: await sio.emit("connected_clients", list(blender_messages.keys()), namespace="/web", room=sid) - await sio.emit("theme_data", blender_theme, namespace="/web", room=sid) await self.send_cached_messages(sid) async def on_disconnect(self, sid): @@ -71,6 +71,7 @@ class WebNamespace(socketio.AsyncNamespace): if "demo_data" in messages: await self.emit("demo_data", {"blenderId": blenderId, "data": messages["demo_data"]}, room=sid) + # Blender namespace class BlenderNamespace(socketio.AsyncNamespace): def __init__(self, namespace): @@ -115,8 +116,7 @@ class BlenderNamespace(socketio.AsyncNamespace): blender_theme = data await sio.emit("theme_data", data, namespace="/web") - - # this function will be called when the event demo_data is emitted + # this function will be called when the event demo_data is emitted async def on_demo_data(self, sid, data): print(f"Demo data from Blender client {sid}") blender_messages[sid]["demo_data"] = data From f26f6dfe5e5a12575bbbfd83b7ad498e9358ed62 Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:30:20 +0300 Subject: [PATCH 006/556] fix broken display for multiple spreadsheets due to absolute position removed absolute positioning as it made spreadsheets on top of each other and made the spreadsheet's width and height as full screen as possible. also, add a separating margin between two spreadsheets. --- src/bonsai/bonsai/bim/data/webui/static/css/index.css | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/index.css b/src/bonsai/bonsai/bim/data/webui/static/css/index.css index f83fac08e1..510354f2bd 100644 --- a/src/bonsai/bonsai/bim/data/webui/static/css/index.css +++ b/src/bonsai/bonsai/bim/data/webui/static/css/index.css @@ -202,11 +202,10 @@ footer p { } .table-container { - position: absolute; display: block; - height: calc(100% - var(--margin-small) * 2); - width: calc(100% - var(--margin-small) * 2); - overflow-x: scroll; + height: calc(100vh - var(--margin-small)); + width: 100%; + margin-bottom: var(--margin-medium); } .csv-table { From 47a15011f62b010d58ac7ff9cd6e1aba5a76c2b1 Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:38:15 +0300 Subject: [PATCH 007/556] add button to drawings panel to open documentation web ui page might need a different icon since the URL icon is used by the open_drawing button --- src/bonsai/bonsai/bim/module/drawing/__init__.py | 1 + src/bonsai/bonsai/bim/module/drawing/operator.py | 13 +++++++++++++ src/bonsai/bonsai/bim/module/drawing/ui.py | 3 +++ 3 files changed, 17 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/__init__.py b/src/bonsai/bonsai/bim/module/drawing/__init__.py index cb7bb771d5..a3578ed702 100644 --- a/src/bonsai/bonsai/bim/module/drawing/__init__.py +++ b/src/bonsai/bonsai/bim/module/drawing/__init__.py @@ -88,6 +88,7 @@ classes = ( operator.SelectAllDrawings, operator.SelectAssignedProduct, operator.SelectDocIfcFile, + operator.OpenDocumentationWebUi, prop.Variable, prop.Drawing, prop.Document, diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 72014d838c..07738f425a 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -2857,3 +2857,16 @@ class ConvertSVGToDXF(bpy.types.Operator): self.report({"INFO"}, f"{len(drawing_uris)} drawings were converted to .dxf.") return {"FINISHED"} + + +class OpenDocumentationWebUi(bpy.types.Operator): + bl_idname = "bim.open_documentation_web_ui" + bl_label = "Open Documentation Web UI" + bl_description = "Open the documentation web UI page" + + def execute(self, context): + if not context.scene.WebProperties.is_connected: + bpy.ops.bim.connect_websocket_server(page="documentation") + else: + bpy.ops.bim.bim.open_web_browser(page="documentation") + return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 47a6f67b70..398e4cbb45 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -289,6 +289,9 @@ class BIM_PT_drawings(Panel): create_drawing_button = row.row(align=True) create_drawing_button.operator("bim.create_drawing", text="", icon="OUTPUT") create_drawing_button.enabled = active_drawing.ifc_definition_id > 0 + + # might need a different icon since the URL icon is already used by the open_drawing + row.operator("bim.open_documentation_web_ui", icon="URL", text="") self.layout.template_list( "BIM_UL_drawinglist", "", self.props, "drawings", self.props, "active_drawing_index" ) From 5831c563802fa977bfafc3af3a9f0bf425129be3 Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:29:09 +0300 Subject: [PATCH 008/556] remove left over print :) --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index 29f373f750..3764da1824 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -55,7 +55,6 @@ class WebNamespace(socketio.AsyncNamespace): ) async def on_get_svg(self, sid, data): - print("hello world!") file_path = data["path"] with open(file_path, "r") as file: svg_data = file.read() From 8c19b695ea6c96e957722489e3f62013e792cee8 Mon Sep 17 00:00:00 2001 From: Ziad-I <68874104+Ziad-I@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:07:50 +0300 Subject: [PATCH 009/556] fix drawings and sheets paths not being normalized which lead to the paths sometimes having both / and \\ --- src/bonsai/bonsai/tool/web.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py index ebb1c3fd6d..8ee49093ab 100644 --- a/src/bonsai/bonsai/tool/web.py +++ b/src/bonsai/bonsai/tool/web.py @@ -387,6 +387,7 @@ class Web(bonsai.core.tool.Web): continue reference_name = os.path.basename(reference.Location) reference_path = os.path.join(ifc_file_dir, reference.Location) + reference_path = os.path.normpath(reference_path) sheets_data.append({"name": reference_name, "path": reference_path}) drawings = [e for e in tool.Ifc.get().by_type("IfcAnnotation") if e.ObjectType == "DRAWING"] @@ -394,6 +395,7 @@ class Web(bonsai.core.tool.Web): document = tool.Drawing.get_drawing_document(drawing) reference_name = os.path.basename(document.Location) reference_path = os.path.join(ifc_file_dir, document.Location) + reference_path = os.path.normpath(reference_path) drawings_data.append({"name": reference_name, "path": reference_path}) cls.send_webui_data( From 6fa87a16aba0317e0ec239f24716c5942b8a7679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 13:32:02 -0300 Subject: [PATCH 010/556] Polytool refactor for selecting plane and axis snap. User can now select X, Y, Z to lock to an axis, and S-X, S-Y and S-Z to lock to a plane. --- .../bonsai/bim/module/model/decorator.py | 2 - src/bonsai/bonsai/bim/module/model/wall.py | 10 ++- .../bonsai/bim/module/project/operator.py | 16 +++- src/bonsai/bonsai/tool/snap.py | 80 ++++++++++++++----- 4 files changed, 82 insertions(+), 26 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index e2eae33d3e..6298ab66ae 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -396,9 +396,7 @@ class PolylineDecorator: distance = (snap_vector - last_point).length if distance > 0: - print(snap_vector, last_point, second_to_last_point) angle = tool.Cad.angle_3_vectors(snap_vector, last_point, second_to_last_point, degrees=True) - print("A", angle) if cls.input_panel: cls.input_panel["X"] = str(round(snap_vector.x, 4)) cls.input_panel["Y"] = str(round(snap_vector.y, 4)) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index d34fd01262..0ed29202af 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -319,7 +319,7 @@ class DrawPolylineWall(bpy.types.Operator): self.number_output = "" self.number_is_negative = False self.is_input_on = False - self.input_options = ["D", "A", "X", "Y"] + self.input_options = ["D", "A"] self.input_type = "OFF" self.input_value_xy = [None, None] self.input_panel = {"D": "", "A": "", "X": "", "Y": ""} @@ -405,6 +405,14 @@ class DrawPolylineWall(bpy.types.Operator): tool.Snap.insert_polyline_point(self.input_panel) tool.Blender.update_viewport() + if event.value == "PRESS" and event.type == "X": + tool.Snap.set_snap_axis_method("X") + tool.Blender.update_viewport() + + if event.value == "PRESS" and event.type == "Y": + tool.Snap.set_snap_axis_method("Y") + tool.Blender.update_viewport() + if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() PolylineDecorator.set_input_panel(self.input_panel, self.input_type) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index f04c4c98db..9b7b73cc7d 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2328,7 +2328,7 @@ class MeasureTool(bpy.types.Operator): self.number_output = "" self.number_is_negative = False self.is_input_on = False - self.input_options = ["D", "A", "X", "Y", "Z"] + self.input_options = ["D", "A"] self.input_type = "OFF" self.input_value_xy = [None, None] self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""} @@ -2389,6 +2389,18 @@ class MeasureTool(bpy.types.Operator): tool.Snap.insert_polyline_point(self.input_panel) tool.Blender.update_viewport() + if event.value == "PRESS" and event.type == "X": + tool.Snap.set_snap_axis_method("X") + tool.Blender.update_viewport() + + if event.value == "PRESS" and event.type == "Y": + tool.Snap.set_snap_axis_method("Y") + tool.Blender.update_viewport() + + if event.value == "PRESS" and event.type == "Z": + tool.Snap.set_snap_axis_method("Z") + tool.Blender.update_viewport() + if event.value == "PRESS" and event.type == "C": tool.Snap.close_polyline() PolylineDecorator.set_input_panel(self.input_panel, self.input_type) @@ -2500,7 +2512,7 @@ class MeasureTool(bpy.types.Operator): PolylineDecorator.install(context) tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) - tool.Snap.set_snap_plane_method("No Plane") + tool.Snap.set_snap_plane_method(None) PolylineDecorator.set_input_panel(self.input_panel, self.input_type) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 0b827ba684..7f34af9c31 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -30,7 +30,8 @@ class Snap(bonsai.core.tool.Snap): mouse_pos = None snap_angle = None use_default_container = False - snap_plane_method = "No Plane" + snap_plane_method = None + snap_axis_method = None @classmethod def set_use_default_container(cls, value=True): @@ -38,8 +39,18 @@ class Snap(bonsai.core.tool.Snap): @classmethod def set_snap_plane_method(cls, value=True): + if cls.snap_plane_method == value: + cls.snap_plane_method = None + return cls.snap_plane_method = value + @classmethod + def set_snap_axis_method(cls, value=True): + if cls.snap_axis_method == value: + cls.snap_axis_method = None + return + cls.snap_axis_method = value + @classmethod def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index): matrix = obj.matrix_world.copy() @@ -181,7 +192,7 @@ class Snap(bonsai.core.tool.Snap): def create_axis_line_data(rot_mat, origin): length = 1000 direction = Vector((1, 0, 0)) - if cls.snap_plane_method == "YZ": + if cls.snap_plane_method == "YZ" or cls.snap_axis_method == "Z": direction = Vector((0, 0, 1)) rot_dir = rot_mat.inverted() @ direction start = origin + rot_dir * length @@ -233,8 +244,6 @@ class Snap(bonsai.core.tool.Snap): if cls.snap_plane_method == "YZ": pivot_axis = "X" - rectangle_data = create_axis_rectangle_data(last_point) - PolylineDecorator.set_axis_rectangle(rectangle_data) for axis in snap_axis: rot_mat = Matrix.Rotation(math.radians(360 - axis), 3, pivot_axis) start, end = create_axis_line_data(rot_mat, last_point) @@ -317,13 +326,14 @@ class Snap(bonsai.core.tool.Snap): def select_plane_method(): if not last_polyline_point: plane_origin = Vector((0, 0, 0)) + plane_normal = Vector((0, 0, 1)) - if cls.snap_plane_method == "No Plane": + if not cls.snap_plane_method: camera_rotation = rv3d.view_rotation plane_origin = Vector((0, 0, 0)) plane_normal = Vector((0, 1, 1, 1)) * Vector((camera_rotation)) - if cls.snap_plane_method == "XY": + if cls.snap_plane_method == "XY" or (not cls.snap_plane_method and cls.snap_axis_method in {"X", "Y"}): if cls.use_default_container: plane_origin = Vector((0, 0, elevation)) elif not last_polyline_point: @@ -332,7 +342,7 @@ class Snap(bonsai.core.tool.Snap): plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_normal = Vector((0, 0, 1)) - elif cls.snap_plane_method == "XZ": + elif cls.snap_plane_method == "XZ" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"): if last_polyline_point: plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z)) plane_normal = Vector((0, 1, 0)) @@ -417,10 +427,35 @@ class Snap(bonsai.core.tool.Snap): axis_start = None axis_end = None - if cls.snap_plane_method in {"XY", "XZ", "YZ"}: - rot_intersection, snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) - if rot_intersection: - detected_snaps.append({"Axis": rot_intersection}) + + # TODO It only work for XY plane. Make it work also for None plane_method + rot_intersection = None + if not cls.snap_plane_method: + if cls.snap_axis_method == "X": + cls.snap_angle = 180 + if cls.snap_axis_method == "Y": + cls.snap_angle = 90 + if cls.snap_axis_method == "Z": + cls.snap_angle = 90 + if cls.snap_axis_method: + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) + + if cls.snap_plane_method: + if cls.snap_plane_method in {"XY", "XZ"} and cls.snap_axis_method == "X": + cls.snap_angle = 180 + if cls.snap_plane_method in {"XY", "YZ"} and cls.snap_axis_method == "Y": + cls.snap_angle = 90 + if cls.snap_plane_method in {"YZ"} and cls.snap_axis_method == "Z": + cls.snap_angle = 180 + if cls.snap_plane_method in {"XZ"} and cls.snap_axis_method == "Z": + cls.snap_angle = 90 + if event.shift or cls.snap_axis_method: + # Doesn't update snap_angle so that it keeps in the same axis + rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) + else: + rot_intersection, cls.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, None) + if rot_intersection: + detected_snaps.append({"Axis": rot_intersection}) detected_snaps.append({"Plane": intersection}) @@ -448,39 +483,42 @@ class Snap(bonsai.core.tool.Snap): for op in options: snapping_points.append(op) - cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - return snapping_points + # return snapping_points elif "Edge-Vertex" in list(origin.keys()): snap_obj, options = origin['Edge-Vertex'] for op in options: snapping_points.append(op) - cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - return snapping_points + # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + # return snapping_points elif "Polyline" in list(origin.keys()): options = origin['Polyline'] for op in options: snapping_points.append(op) - cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - return snapping_points + # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + # return snapping_points elif "Axis" in list(origin.keys()): intersection = origin['Axis'] snapping_points.append((intersection, "Axis")) - cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - return snapping_points + # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + # return snapping_points elif "Plane" in list(origin.keys()): intersection = origin['Plane'] snapping_points.append((intersection, "Plane")) - cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - return snapping_points + # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + # return snapping_points + + cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + return snapping_points @classmethod From 1be025ed867d61080553f8f24ed08e7065953d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 13:35:24 -0300 Subject: [PATCH 011/556] Fixed typo --- src/bonsai/bonsai/tool/snap.py | 64 +++++++++++----------------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 7f34af9c31..48ca53137c 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -100,7 +100,7 @@ class Snap(bonsai.core.tool.Snap): return snap_point @classmethod - def update_snaping_point(cls, snap_point, snap_type): + def update_snapping_point(cls, snap_point, snap_type): try: snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0] except: @@ -112,7 +112,7 @@ class Snap(bonsai.core.tool.Snap): snap_vertex.snap_type = snap_type @classmethod - def update_snaping_ref(cls, snap_point, snap_type): + def update_snapping_ref(cls, snap_point, snap_type): try: snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_ref[0] except: @@ -299,9 +299,6 @@ class Snap(bonsai.core.tool.Snap): return best_result, "Mix" - # except Exception as e: - # cls.update_snaping_point(snap_point[0], snap_point[1]) - @classmethod def detect_snapping_points(cls, context, event, objs_2d_bbox): region = context.region @@ -387,7 +384,6 @@ class Snap(bonsai.core.tool.Snap): else: return None, None, None - ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event) objs_to_raycast = [] @@ -395,7 +391,7 @@ class Snap(bonsai.core.tool.Snap): if obj.type == "MESH" and bbox_2d: if tool.Raycast.in_view_2d_bounding_box(cls.mouse_pos, bbox_2d): objs_to_raycast.append(obj) - # Obj + # Obj snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast) if hit is not None: detected_snaps.append({"Object": (snap_obj, hit, face_index)}) @@ -461,14 +457,13 @@ class Snap(bonsai.core.tool.Snap): return detected_snaps - @classmethod def select_snapping_points(cls, context, event, detected_snaps): snapping_points = [] for origin in detected_snaps: - + if "Object" in list(origin.keys()): - snap_obj, hit, face_index = origin['Object'] + snap_obj, hit, face_index = origin["Object"] matrix = snap_obj.matrix_world.copy() face = snap_obj.data.polygons[face_index] @@ -483,54 +478,36 @@ class Snap(bonsai.core.tool.Snap): for op in options: snapping_points.append(op) - # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - - # return snapping_points - elif "Edge-Vertex" in list(origin.keys()): - snap_obj, options = origin['Edge-Vertex'] + snap_obj, options = origin["Edge-Vertex"] for op in options: snapping_points.append(op) - # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - # return snapping_points - elif "Polyline" in list(origin.keys()): - options = origin['Polyline'] + options = origin["Polyline"] for op in options: snapping_points.append(op) - # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - # return snapping_points - elif "Axis" in list(origin.keys()): - intersection = origin['Axis'] + intersection = origin["Axis"] snapping_points.append((intersection, "Axis")) - # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - # return snapping_points - elif "Plane" in list(origin.keys()): - intersection = origin['Plane'] + intersection = origin["Plane"] snapping_points.append((intersection, "Plane")) - # cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - # return snapping_points - - cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + cls.update_snapping_point(snapping_points[0][0], snapping_points[0][1]) return snapping_points - @classmethod def modify_snapping_point_selection(cls, snapping_points): shifted_list = snapping_points[1:] + snapping_points[:1] - cls.update_snaping_point(shifted_list[0][0], shifted_list[0][1]) + cls.update_snapping_point(shifted_list[0][0], shifted_list[0][1]) return shifted_list - @classmethod def validate_input(cls, input_number, input_type): - + grammar_imperial = """ start: FORMULA? dim expr? dim: imperial @@ -581,17 +558,17 @@ class Snap(bonsai.core.tool.Snap): def inches(self, args): if len(args) > 1: - result = (args[0] + args[1]) + result = args[0] + args[1] else: result = args[0] return result / 12 - + def feet(self, args): return args[0] - + def imperial(self, args): if len(args) > 1: - result = (args[0] + args[1]) + result = args[0] + args[1] else: result = args[0] return result @@ -625,24 +602,23 @@ class Snap(bonsai.core.tool.Snap): if len(args) > 1: raise ValueError("Invalid input.") dimension = args[i] - if len(args) > i+1: + if len(args) > i + 1: expression = args[i + 1] return expression(dimension) * factor else: return dimension * factor - try: - if bpy.context.scene.unit_settings.system == 'IMPERIAL': + if bpy.context.scene.unit_settings.system == "IMPERIAL": parser = Lark(grammar_imperial) factor = 0.3048 else: parser = Lark(grammar_metric) factor = 1 - if bpy.context.scene.unit_settings.length_unit == 'MILLIMETERS': + if bpy.context.scene.unit_settings.length_unit == "MILLIMETERS": factor = 0.001 - if input_type == 'A': + if input_type == "A": parser = Lark(grammar_metric) factor = 1 From 6dcbd98769b54ebc267af07896715c46f20657ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 15:38:44 -0300 Subject: [PATCH 012/556] Fixed error for measure tool when working with top or side view. --- src/bonsai/bonsai/tool/snap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 48ca53137c..80ce36ac58 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -328,7 +328,8 @@ class Snap(bonsai.core.tool.Snap): if not cls.snap_plane_method: camera_rotation = rv3d.view_rotation plane_origin = Vector((0, 0, 0)) - plane_normal = Vector((0, 1, 1, 1)) * Vector((camera_rotation)) + view_direction = Vector((0, 0, -1)) @ camera_rotation.to_matrix().transposed() + plane_normal = view_direction.normalized() if cls.snap_plane_method == "XY" or (not cls.snap_plane_method and cls.snap_axis_method in {"X", "Y"}): if cls.use_default_container: From 2a1bc30f1900f7b992555b5c5333b48a559e679a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 15:39:19 -0300 Subject: [PATCH 013/556] Polytool improved axis lock and mixed snapping You can now lock into an axis while snapping to an object. The result will be to closer point. In the future, there should be a way for the user to select between different points. --- .../bonsai/bim/module/project/operator.py | 7 ++++ src/bonsai/bonsai/tool/snap.py | 33 ++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 9b7b73cc7d..ab5b2107cc 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2478,16 +2478,22 @@ class MeasureTool(bpy.types.Operator): tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) tool.Snap.set_snap_plane_method("YZ") + tool.Snap.set_snap_axis_method(None) + tool.Blender.update_viewport() if event.shift and event.value == "PRESS" and event.type == "Y": tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) tool.Snap.set_snap_plane_method("XZ") + tool.Snap.set_snap_axis_method(None) + tool.Blender.update_viewport() if event.shift and event.value == "PRESS" and event.type == "Z": tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) tool.Snap.set_snap_plane_method("XY") + tool.Snap.set_snap_axis_method(None) + tool.Blender.update_viewport() if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: return {"PASS_THROUGH"} @@ -2513,6 +2519,7 @@ class MeasureTool(bpy.types.Operator): tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) tool.Snap.set_snap_plane_method(None) + tool.Snap.set_snap_axis_method(None) PolylineDecorator.set_input_panel(self.input_panel, self.input_type) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 80ce36ac58..f72581963c 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -192,7 +192,7 @@ class Snap(bonsai.core.tool.Snap): def create_axis_line_data(rot_mat, origin): length = 1000 direction = Vector((1, 0, 0)) - if cls.snap_plane_method == "YZ" or cls.snap_axis_method == "Z": + if cls.snap_plane_method == "YZ": direction = Vector((0, 0, 1)) rot_dir = rot_mat.inverted() @ direction start = origin + rot_dir * length @@ -452,7 +452,7 @@ class Snap(bonsai.core.tool.Snap): else: rot_intersection, cls.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, None) if rot_intersection: - detected_snaps.append({"Axis": rot_intersection}) + detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)}) detected_snaps.append({"Plane": intersection}) @@ -479,26 +479,41 @@ class Snap(bonsai.core.tool.Snap): for op in options: snapping_points.append(op) - elif "Edge-Vertex" in list(origin.keys()): + if "Edge-Vertex" in list(origin.keys()): snap_obj, options = origin["Edge-Vertex"] for op in options: snapping_points.append(op) - elif "Polyline" in list(origin.keys()): + if "Polyline" in list(origin.keys()): options = origin["Polyline"] for op in options: snapping_points.append(op) - elif "Axis" in list(origin.keys()): + if "Axis" in list(origin.keys()): intersection = origin["Axis"] - snapping_points.append((intersection, "Axis")) + axis_start = intersection[1] + axis_end = intersection[2] + snapping_points.append((intersection[0], "Axis")) - elif "Plane" in list(origin.keys()): + if "Plane" in list(origin.keys()): intersection = origin["Plane"] snapping_points.append((intersection, "Plane")) - cls.update_snapping_point(snapping_points[0][0], snapping_points[0][1]) - return snapping_points + + # Make Axis first priority + if event.shift or cls.snap_axis_method in {"X", "Y", "Z"}: + cls.update_snapping_ref(snapping_points[0][0], snapping_points[0][1]) + for point in snapping_points: + if point[1] == "Axis": + if snapping_points[0][1] not in {"Axis", "Plane"}: + mixed_snap = cls.mix_snap_and_axis(snapping_points[0], axis_start, axis_end, 0) + cls.update_snapping_point(mixed_snap[0], mixed_snap[1]) + return snapping_points + cls.update_snapping_point(point[0], point[1]) + return snapping_points + + cls.update_snapping_point(snapping_points[0][0], snapping_points[0][1]) + return snapping_points @classmethod def modify_snapping_point_selection(cls, snapping_points): From 197d0b3258d652e7a7566cbed0f3dcb8c0fd1027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 16:44:34 -0300 Subject: [PATCH 014/556] Fixed issue with X, Y and Z input for polytool. This issue was caused by the change to X, Y, Z keys for lock axis. --- src/bonsai/bonsai/bim/module/model/wall.py | 4 ++-- src/bonsai/bonsai/bim/module/project/operator.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 0ed29202af..7736a65ca7 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -319,7 +319,7 @@ class DrawPolylineWall(bpy.types.Operator): self.number_output = "" self.number_is_negative = False self.is_input_on = False - self.input_options = ["D", "A"] + self.input_options = ["D", "A", "X", "Y"] self.input_type = "OFF" self.input_value_xy = [None, None] self.input_panel = {"D": "", "A": "", "X": "", "Y": ""} @@ -443,7 +443,7 @@ class DrawPolylineWall(bpy.types.Operator): PolylineDecorator.set_input_panel(self.input_panel, self.input_type) tool.Blender.update_viewport() - if event.value == "RELEASE" and event.type in self.input_options: + if event.value == "RELEASE" and event.type in {"D", "A"}: self.recalculate_inputs(context) self.is_input_on = True self.input_type = event.type diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index ab5b2107cc..61dfeb2875 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2328,7 +2328,7 @@ class MeasureTool(bpy.types.Operator): self.number_output = "" self.number_is_negative = False self.is_input_on = False - self.input_options = ["D", "A"] + self.input_options = ["D", "A", "X", "Y", "Z"] self.input_type = "OFF" self.input_value_xy = [None, None] self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""} @@ -2431,7 +2431,7 @@ class MeasureTool(bpy.types.Operator): PolylineDecorator.set_input_panel(self.input_panel, self.input_type) tool.Blender.update_viewport() - if event.value == "PRESS" and event.type in self.input_options and not event.shift: + if event.value == "PRESS" and event.type in {"D", "A"} and not event.shift: self.recalculate_inputs(context) self.is_input_on = True self.input_type = event.type From 48b74ebb7609334bfa8942837490ee2b5d5ca431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 16:45:30 -0300 Subject: [PATCH 015/556] Polytool: creates a decorator to show instructions on the screen. --- .../bonsai/bim/module/model/decorator.py | 27 +++++++++++++++++-- src/bonsai/bonsai/bim/module/model/wall.py | 8 ++++++ .../bonsai/bim/module/project/operator.py | 9 +++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 6298ab66ae..a2e9e4ce22 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -307,13 +307,15 @@ class PolylineDecorator: angle_snap_mat = None angle_snap_loc = None use_default_container = False - + instructions = None + @classmethod def install(cls, context): if cls.is_installed: cls.uninstall() handler = cls() cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_panel, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append(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, (context,), "WINDOW", "POST_VIEW")) cls.is_installed = True @@ -354,6 +356,10 @@ class PolylineDecorator: cls.plane_origin = plane_origin cls.plane_normal = plane_normal + @classmethod + def set_instructions(cls, instructions): + cls.instructions = instructions + @classmethod def calculate_distance_and_angle(cls, context, is_input_on): @@ -556,7 +562,7 @@ class PolylineDecorator: self.addon_prefs = tool.Blender.get_addon_preferences() - self.font_id = 0 + self.font_id = 1 blf.size(self.font_id, 12) blf.enable(self.font_id, blf.SHADOW) blf.shadow(self.font_id, 6, 0, 0, 0, 1) @@ -592,6 +598,23 @@ 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 + + self.addon_prefs = tool.Blender.get_addon_preferences() + self.font_id = 2 + blf.size(self.font_id, 12) + 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) + + text_w, text_h = blf.dimensions(0, self.instructions) + position = (region.width / 2) - (text_w / 2) + blf.position(self.font_id, position, 10, 0) + blf.draw(self.font_id, self.instructions) + + def __call__(self, context): self.addon_prefs = tool.Blender.get_addon_preferences() diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 7736a65ca7..f58f64c11e 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -325,6 +325,13 @@ class DrawPolylineWall(bpy.types.Operator): self.input_panel = {"D": "", "A": "", "X": "", "Y": ""} self.snap_angle = None self.snapping_points = [] + self.instructions = """TAB: Cycle Input + M: Modify Snap Point + C: Close + Backspace: Remove + X Y: Axis + Shift: Lock axis +""" def recalculate_inputs(self, context): if self.number_input: @@ -518,6 +525,7 @@ class DrawPolylineWall(bpy.types.Operator): tool.Snap.set_use_default_container(True) PolylineDecorator.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) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 61dfeb2875..1b7af87064 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2334,6 +2334,14 @@ class MeasureTool(bpy.types.Operator): self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""} self.snap_angle = None self.snapping_points = [] + self.instructions = """TAB: Cycle Input + M: Modify Snap Point + C: Close + Backspace: Remove + X Y Z: Axis + S-(X Y Z): Plane + Shift: Lock axis +""" def recalculate_inputs(self, context): if self.number_input: @@ -2520,6 +2528,7 @@ class MeasureTool(bpy.types.Operator): PolylineDecorator.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) self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: From 32ae12b45a1ba511ca56e9ca6e7fb57d7d08f9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 17:09:49 -0300 Subject: [PATCH 016/556] Polytool: added on screen snap information. --- src/bonsai/bonsai/bim/module/model/decorator.py | 10 ++++++++++ src/bonsai/bonsai/tool/snap.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index a2e9e4ce22..7393a09434 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -308,6 +308,7 @@ class PolylineDecorator: angle_snap_loc = None use_default_container = False instructions = None + snap_info = None @classmethod def install(cls, context): @@ -360,6 +361,10 @@ class PolylineDecorator: 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): @@ -614,6 +619,11 @@ class PolylineDecorator: blf.position(self.font_id, position, 10, 0) blf.draw(self.font_id, self.instructions) + text_w, text_h = blf.dimensions(0, self.snap_info) + position = (region.width / 2) - (text_w / 2) + blf.position(self.font_id, position, 30, 0) + blf.draw(self.font_id, self.snap_info) + def __call__(self, context): diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index f72581963c..519e94d54b 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -106,6 +106,11 @@ class Snap(bonsai.core.tool.Snap): except: snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point.add() + info = f"""Snap: {snap_type} + Axis:{cls.snap_axis_method} + Plane:{cls.snap_plane_method} +""" + PolylineDecorator.set_snap_info(info) snap_vertex.x = snap_point[0] snap_vertex.y = snap_point[1] snap_vertex.z = snap_point[2] @@ -456,6 +461,7 @@ class Snap(bonsai.core.tool.Snap): detected_snaps.append({"Plane": intersection}) + return detected_snaps @classmethod From b717abf6392f12950da1815766836e06c6d0de83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 26 Aug 2024 17:10:46 -0300 Subject: [PATCH 017/556] Polytool: small fix for Z axis line decorator --- src/bonsai/bonsai/tool/snap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 519e94d54b..23e999d51e 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -197,7 +197,7 @@ class Snap(bonsai.core.tool.Snap): def create_axis_line_data(rot_mat, origin): length = 1000 direction = Vector((1, 0, 0)) - if cls.snap_plane_method == "YZ": + if cls.snap_plane_method == "YZ" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"): direction = Vector((0, 0, 1)) rot_dir = rot_mat.inverted() @ direction start = origin + rot_dir * length From fefe325eceb7d39a6974be2a179412cc3fdd6084 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 11:12:35 +0500 Subject: [PATCH 018/556] Fix workflow for tests requiring Sun Position extension --- .github/workflows/ci-bonsai-daily.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci-bonsai-daily.yml b/.github/workflows/ci-bonsai-daily.yml index a0e5640c2c..fa0a892a6b 100644 --- a/.github/workflows/ci-bonsai-daily.yml +++ b/.github/workflows/ci-bonsai-daily.yml @@ -142,6 +142,12 @@ jobs: sverchok_zip="$(pwd)/dist/$(ls dist)" blender --command extension install-file -r user_default $sverchok_zip + # Install Sun Position extension. + blender --online-mode --command extension sync + blender --online-mode --background --python-expr "import bpy; \ + bpy.ops.extensions.package_install(repo_index=0, pkg_id='sun_position'); \ + bpy.ops.preferences.addon_enable(module='bl_ext.blender_org.sun_position'); bpy.ops.wm.save_userpref()" + cd ../bonsai pip install pytest-blender blender --background --python scripts/setup_pytest.py From 5e95985d19482dc6aa74ccde1873b39e09f82b72 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 11:31:01 +0500 Subject: [PATCH 019/556] Fix error setting integer properties in bim tests --- src/bonsai/test/bim/test_feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 23d28a8805..c0d9438755 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -307,7 +307,7 @@ def i_set_the_prop_property_to_value(prop, value): setattr(spied_prop["props"], spied_prop["name"], False) elif spied_prop["prop_type"] == "FLOAT": setattr(spied_prop["props"], spied_prop["name"], float(value)) - elif spied_prop["prop_type"] == "INTEGER": + elif spied_prop["prop_type"] == "INT": setattr(spied_prop["props"], spied_prop["name"], int(value)) elif spied_prop["prop_type"] == "ENUM": enum_identifier = [i for i in spied_prop["enum_items"] if i is not None and i[1] == value] From 8309b13db0d8e65dddd66fb10193babfce213f42 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 17:49:59 +0500 Subject: [PATCH 020/556] reveal element filters and drawing underlay panels just if drawing is active Those two panels were requiring drawing's camera to be active object which was a bit confusing since "Active Drawing" section above worked fine without selecting camera explicitly. https://i.imgur.com/wcmA73Z.png --- src/bonsai/bonsai/bim/module/drawing/operator.py | 5 +++-- src/bonsai/bonsai/bim/module/drawing/ui.py | 16 +++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index 72014d838c..e606b1e1cb 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -1779,7 +1779,7 @@ class SaveDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): if self.index: index = int(self.index) else: - index = context.active_object.data.BIMCameraProperties.active_drawing_style_index + index = context.scene.camera.data.BIMCameraProperties.active_drawing_style_index scene.DocProperties.drawing_styles[index].raster_style = json.dumps(style) bpy.ops.bim.save_drawing_styles_data() @@ -2678,8 +2678,9 @@ class EditElementFilter(bpy.types.Operator, tool.Ifc.Operator): filter_mode: bpy.props.StringProperty() def _execute(self, context): - props = context.active_object.data.BIMCameraProperties obj = bpy.context.scene.camera + assert obj + props = obj.data.BIMCameraProperties element = tool.Ifc.get_entity(obj) pset = tool.Pset.get_element_pset(element, "EPset_Drawing") if self.filter_mode == "INCLUDE": diff --git a/src/bonsai/bonsai/bim/module/drawing/ui.py b/src/bonsai/bonsai/bim/module/drawing/ui.py index 47a6f67b70..0313286f20 100644 --- a/src/bonsai/bonsai/bim/module/drawing/ui.py +++ b/src/bonsai/bonsai/bim/module/drawing/ui.py @@ -106,11 +106,7 @@ class BIM_PT_element_filters(Panel): @classmethod def poll(cls, context): - return ( - context.scene.camera - and context.active_object - and hasattr(context.active_object.data, "BIMCameraProperties") - ) + return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera)) def draw(self, context): if not ElementFiltersData.is_loaded: @@ -160,17 +156,15 @@ class BIM_PT_drawing_underlay(Panel): @classmethod def poll(cls, context): - return ( - context.scene.camera - and context.active_object - and hasattr(context.active_object.data, "BIMCameraProperties") - ) + return bool((camera := context.scene.camera) and tool.Ifc.get_entity(camera)) def draw(self, context): layout = self.layout layout.use_property_split = True + camera = context.scene.camera + assert camera dprops = context.scene.DocProperties - props = context.active_object.data.BIMCameraProperties + props = camera.data.BIMCameraProperties drawing_index_is_valid = props.active_drawing_style_index < len(dprops.drawing_styles) if not DrawingsData.is_loaded: From 7daec254542d6f4c1fd53df74aaa31b483fda5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 09:59:26 -0300 Subject: [PATCH 021/556] Polytool, fixed decorator to show the mouse snapping point on top. --- src/bonsai/bonsai/bim/module/model/decorator.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 7393a09434..e9e1cfd3b4 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -655,13 +655,6 @@ class PolylineDecorator: except: ref_point = None - if snap_prop.snap_type in ["Face", "Plane"]: - self.draw_batch("POINTS", mouse_point, decorator_color_unselected) - else: - self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0)) - - if ref_point: - self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0)) default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z projection_point = [] @@ -716,3 +709,12 @@ class PolylineDecorator: 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"]: + self.draw_batch("POINTS", mouse_point, decorator_color_unselected) + else: + self.draw_batch("POINTS", mouse_point, (1.0, 0.6, 0.0, 1.0)) + + if ref_point: + self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0)) From 24aaefb92791eec17ad8bea817746917d5147dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 10:01:39 -0300 Subject: [PATCH 022/556] Polytool, fixed error with setting the plane method --- src/bonsai/bonsai/bim/module/model/wall.py | 1 + src/bonsai/bonsai/bim/module/project/operator.py | 8 +++++--- src/bonsai/bonsai/tool/snap.py | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index f58f64c11e..98f70c0901 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -512,6 +512,7 @@ class DrawPolylineWall(bpy.types.Operator): tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: + tool.Snap.set_snap_axis_method(None) PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 1b7af87064..29abb21190 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2485,21 +2485,21 @@ class MeasureTool(bpy.types.Operator): if event.shift and event.value == "PRESS" and event.type == "X": tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) - tool.Snap.set_snap_plane_method("YZ") + tool.Snap.cycle_snap_plane_method("YZ") tool.Snap.set_snap_axis_method(None) tool.Blender.update_viewport() if event.shift and event.value == "PRESS" and event.type == "Y": tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) - tool.Snap.set_snap_plane_method("XZ") + tool.Snap.cycle_snap_plane_method("XZ") tool.Snap.set_snap_axis_method(None) tool.Blender.update_viewport() if event.shift and event.value == "PRESS" and event.type == "Z": tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) - tool.Snap.set_snap_plane_method("XY") + tool.Snap.cycle_snap_plane_method("XY") tool.Snap.set_snap_axis_method(None) tool.Blender.update_viewport() @@ -2514,6 +2514,8 @@ class MeasureTool(bpy.types.Operator): tool.Blender.update_viewport() else: if event.value == "RELEASE" and event.type in {"ESC"}: + tool.Snap.set_snap_plane_method(None) + tool.Snap.set_snap_axis_method(None) PolylineDecorator.uninstall() tool.Snap.clear_polyline() tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 23e999d51e..eb2fd56ec2 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -39,6 +39,10 @@ class Snap(bonsai.core.tool.Snap): @classmethod def set_snap_plane_method(cls, value=True): + cls.snap_plane_method = value + + @classmethod + def cycle_snap_plane_method(cls, value=True): if cls.snap_plane_method == value: cls.snap_plane_method = None return From 27aee31adf4a82088388ec4f246de68b364da69f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 26 Aug 2024 17:23:51 +0500 Subject: [PATCH 023/556] typing --- src/bonsai/bonsai/bim/ifc.py | 2 ++ src/bonsai/bonsai/core/debug.py | 15 ++++++++++++--- src/bonsai/bonsai/core/geometry.py | 1 + src/bonsai/bonsai/tool/debug.py | 18 ++++++++++++------ src/bonsai/test/bim/test_feature.py | 17 +++++++++-------- .../ifcopenshell/express/__init__.py | 8 +++++++- 6 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index da67773ace..f652abb487 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -140,6 +140,8 @@ class IfcStore: def update_cache(): if not IfcStore.cache: return + assert IfcStore.cache_path + assert IfcStore.file ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() new_cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5") diff --git a/src/bonsai/bonsai/core/debug.py b/src/bonsai/bonsai/core/debug.py index 94f78d4a9f..ccf569798c 100644 --- a/src/bonsai/bonsai/core/debug.py +++ b/src/bonsai/bonsai/core/debug.py @@ -17,15 +17,24 @@ # along with Bonsai. If not, see . -def parse_express(debug, filename): +from __future__ import annotations +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + import bpy + import ifcopenshell + import bonsai.tool as tool + + +def parse_express(debug: tool.Debug, filename: str) -> None: debug.add_schema_identifier(debug.load_express(filename)) -def purge_hdf5_cache(debug): +def purge_hdf5_cache(debug: tool.Debug) -> None: debug.purge_hdf5_cache() -def purge_unused_elements(ifc, debug, ifc_class): +def purge_unused_elements(ifc, debug: tool.Debug, ifc_class: str) -> int: ifc_file = ifc.get() unused_elements = [i for i in ifc_file.by_type(ifc_class) if ifc_file.get_total_inverses(i) == 0] unused_elements_amount = len(unused_elements) diff --git a/src/bonsai/bonsai/core/geometry.py b/src/bonsai/bonsai/core/geometry.py index 73625af636..7476c56381 100644 --- a/src/bonsai/bonsai/core/geometry.py +++ b/src/bonsai/bonsai/core/geometry.py @@ -122,6 +122,7 @@ def switch_representation( return entity = ifc.get_entity(obj) + assert entity current_obj_data = geometry.get_object_data(obj) if not current_obj_data and geometry.is_text_literal(representation): diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 17125a2113..41923bd908 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -19,31 +19,37 @@ import os import bpy import ifcopenshell.express +import ifcopenshell.express.schema +import ifcopenshell.express.schema_class +import ifcopenshell.util.element import bonsai.core.tool import bonsai.tool as tool from bonsai.bim.ifc import IfcStore +from mathutils import Vector class Debug(bonsai.core.tool.Debug): @classmethod - def add_schema_identifier(cls, schema): + def add_schema_identifier(cls, schema: ifcopenshell.express.schema_class.SchemaClass) -> None: IfcStore.schema_identifiers.append(schema.schema_name) @classmethod - def load_express(cls, filename): + def load_express(cls, filename: str) -> ifcopenshell.express.schema_class.SchemaClass: schema = ifcopenshell.express.parse(filename) ifcopenshell.register_schema(schema) return schema @classmethod - def purge_hdf5_cache(cls): + def purge_hdf5_cache(cls) -> None: cache_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache") filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")] for f in filelist: os.remove(os.path.join(cache_dir, f)) @classmethod - def debug_geometry(cls, verts=[], edges=[], name="Debug"): + def debug_geometry( + cls, verts: list[Vector] = [], edges: list[tuple[int, int]] = [], name: str = "Debug" + ) -> bpy.types.Object: mesh = bpy.data.meshes.new("Debug") mesh.from_pydata(verts, edges, []) obj = bpy.data.objects.new(name, mesh) @@ -51,13 +57,13 @@ class Debug(bonsai.core.tool.Debug): return obj @classmethod - def remove_unused_elements(cls, elements): + def remove_unused_elements(cls, elements: list[ifcopenshell.entity_instance]) -> None: ifc_file = tool.Ifc.get() for element in elements: ifcopenshell.util.element.remove_deep2(ifc_file, element) @classmethod - def print_unused_elements_stats(cls, requested_ifc_class="", ignore_classes=tuple()): + def print_unused_elements_stats(cls, requested_ifc_class: str = "", ignore_classes: tuple[str] = tuple()) -> int: ifc_file = tool.Ifc.get() # get list of ifc classes used in model diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index c0d9438755..0d3194154f 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -33,7 +33,7 @@ from pytest_bdd import scenarios, given, when, then, parsers from mathutils import Vector from math import radians from pathlib import Path -from typing import Union +from typing import Union, Any scenarios("feature") @@ -49,18 +49,18 @@ webbrowser.open = lambda x: True class PanelSpy: - def __init__(self, panel): + def __init__(self, panel: type[bpy.types.Panel]): self.is_spy_dirty = True self.panel = panel def refresh_spy(self): if self.is_spy_dirty: self.is_spy_dirty = False - self.spied_attr = None - self.spied_labels = [] - self.spied_props = [] - self.spied_operators = [] - self.spied_lists = [] + self.spied_attr: Union[str, None] = None + self.spied_labels: list[str] = [] + self.spied_props: list[dict[str, Any]] = [] + self.spied_operators: list[dict[str, Any]] = [] + self.spied_lists: list[dict[str, Any]] = [] self.panel.draw(self, bpy.context) def __getattr__(self, attr): @@ -91,6 +91,7 @@ class PanelSpy: return self elif self.spied_attr == "prop": props, name = args + props: bpy.types.bpy_struct text = kwargs.get("text", props.bl_rna.properties[name].name) icon = kwargs.get("icon", None) prop_type = props.bl_rna.properties[name].type @@ -152,7 +153,7 @@ class TemplateListSpy: panel_name_cache = {} -panel_spy = None +panel_spy: PanelSpy = None def replace_variables(value): diff --git a/src/ifcopenshell-python/ifcopenshell/express/__init__.py b/src/ifcopenshell-python/ifcopenshell/express/__init__.py index d7a0ceebc4..91aa35fd82 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/express/__init__.py @@ -16,9 +16,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . +from __future__ import annotations import os import sys import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import schema_class d = os.path.abspath(os.path.dirname(__file__)) sys.path.append(d) @@ -30,8 +35,9 @@ if not os.path.exists(exp_parser_fn): subprocess.call([sys.executable, "bootstrap.py"], cwd=d, stdout=f) -def parse(fn): +def parse(fn: str) -> schema_class.SchemaClass: import express_parser import schema_class + mapping = express_parser.parse(fn) return schema_class.SchemaClass(mapping, schema_class.LateBoundSchemaInstantiator).code From 48f0dc6a005e58ecf362ccb79a063f482419c084 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 11:59:56 +0500 Subject: [PATCH 024/556] bim.purge_hdf5_cache to skip currently loaded cache Previously it would fail with: PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: 'bonsai\\bim\\data\\cache\\988c190483ae6bab7cf6b00cee78694d.h5' --- src/bonsai/bonsai/bim/module/debug/operator.py | 2 ++ src/bonsai/bonsai/tool/debug.py | 5 ++++- src/bonsai/test/tool/test_debug.py | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py index c01546b0ab..85d5efd61d 100644 --- a/src/bonsai/bonsai/bim/module/debug/operator.py +++ b/src/bonsai/bonsai/bim/module/debug/operator.py @@ -440,9 +440,11 @@ class SelectExpressFile(bpy.types.Operator): class PurgeHdf5Cache(bpy.types.Operator): bl_idname = "bim.purge_hdf5_cache" bl_label = "Purge HDF5 Cache" + bl_description = "Clean up HDF5 cache files except the ones that currently loaded" def execute(self, context): core.purge_hdf5_cache(tool.Debug) + self.report({"INFO"}, "HDF5 cache purged.") return {"FINISHED"} diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py index 41923bd908..99bf884aa9 100644 --- a/src/bonsai/bonsai/tool/debug.py +++ b/src/bonsai/bonsai/tool/debug.py @@ -44,7 +44,10 @@ class Debug(bonsai.core.tool.Debug): cache_dir = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache") filelist = [f for f in os.listdir(cache_dir) if f.endswith(".h5")] for f in filelist: - os.remove(os.path.join(cache_dir, f)) + try: + os.remove(os.path.join(cache_dir, f)) + except PermissionError: + pass @classmethod def debug_geometry( diff --git a/src/bonsai/test/tool/test_debug.py b/src/bonsai/test/tool/test_debug.py index 7f4c71f0e3..d0238b6592 100644 --- a/src/bonsai/test/tool/test_debug.py +++ b/src/bonsai/test/tool/test_debug.py @@ -57,5 +57,10 @@ class TestPurgeHdf5Cache(NewFile): test_file = cache_dir / "test.h5" test_file.parent.mkdir(parents=True, exist_ok=True) test_file.touch() + + # Ensure it can skip currently loaded cache. + loaded_file_path = test_file.with_stem("test_loaded") + loaded_file = open(loaded_file_path, "w") + subject.purge_hdf5_cache() - assert not [f for f in cache_dir.iterdir() if f.suffix == ".h5"] + assert [f for f in cache_dir.iterdir() if f.suffix == ".h5"] == [loaded_file_path] From 8bb78959ef65b2d60c691d5dbbe4032d7316cab9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 12:56:38 +0500 Subject: [PATCH 025/556] h5 cache - improve logs Now there are logs when cache was successfully loaded/created/failed to load. --- src/bonsai/bonsai/bim/ifc.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/bonsai/bonsai/bim/ifc.py b/src/bonsai/bonsai/bim/ifc.py index f652abb487..358ef37aef 100644 --- a/src/bonsai/bonsai/bim/ifc.py +++ b/src/bonsai/bonsai/bim/ifc.py @@ -117,22 +117,34 @@ class IfcStore: ifc_key = IfcStore.path + IfcStore.file.wrapped_data.header.file_name.time_stamp ifc_hash = hashlib.md5(ifc_key.encode("utf-8")).hexdigest() IfcStore.cache_path = os.path.join(bpy.context.scene.BIMProperties.data_dir, "cache", f"{ifc_hash}.h5") + cache_path = Path(IfcStore.cache_path) cache_settings = ifcopenshell.geom.settings() serializer_settings = ifcopenshell.geom.serializer_settings() + cache_preexists = cache_path.exists() try: IfcStore.cache = ifcopenshell.geom.serializers.hdf5( IfcStore.cache_path, cache_settings, serializer_settings ) - except: - if os.path.exists(IfcStore.cache_path): - os.remove(IfcStore.cache_path) - try: - IfcStore.cache = ifcopenshell.geom.serializers.hdf5( - IfcStore.cache_path, cache_settings, serializer_settings - ) - except: - return + if cache_preexists: + print(f"Successfully loaded existing cache: {cache_path.name}.") else: + print("New cache was created.") + except Exception as e: + if cache_preexists: + print(f"Failed to create a cache from existing file '{cache_path.name}': {str(e)}.") + else: + print(f"Failed to create a cache: {str(e)}.") + # No point to trying again the same operation. + return + + os.remove(IfcStore.cache_path) + try: + IfcStore.cache = ifcopenshell.geom.serializers.hdf5( + IfcStore.cache_path, cache_settings, serializer_settings + ) + print("New cache was created.") + except Exception as e: + print(f"Failed to create a cache: {str(e)}.") return return IfcStore.cache From d2efb2ab78387e3ac5d2f853b4d528fe8b6953ee Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 17:31:35 +0500 Subject: [PATCH 026/556] operator to toggle container elements Example - https://imgur.com/a/Sy83Tnz --- .../bonsai/bim/module/spatial/__init__.py | 1 + .../bonsai/bim/module/spatial/operator.py | 11 +++++++ src/bonsai/bonsai/bim/module/spatial/prop.py | 2 ++ src/bonsai/bonsai/bim/module/spatial/ui.py | 3 +- src/bonsai/bonsai/core/spatial.py | 5 ++++ src/bonsai/bonsai/tool/spatial.py | 29 ++++++++++++++----- 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/__init__.py b/src/bonsai/bonsai/bim/module/spatial/__init__.py index 99d9bae24c..bef4f21e46 100644 --- a/src/bonsai/bonsai/bim/module/spatial/__init__.py +++ b/src/bonsai/bonsai/bim/module/spatial/__init__.py @@ -38,6 +38,7 @@ classes = ( operator.SelectSimilarContainer, operator.SetContainerVisibility, operator.SetDefaultContainer, + operator.ToggleContainerElement, prop.Element, prop.BIMObjectSpatialProperties, prop.BIMContainer, diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 9ba7831940..aeaf6033fe 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -243,6 +243,17 @@ class DeleteContainer(bpy.types.Operator, tool.Ifc.Operator): core.delete_container(tool.Ifc, tool.Spatial, tool.Geometry, container=tool.Ifc.get().by_id(self.container)) +class ToggleContainerElement(bpy.types.Operator): + bl_idname = "bim.toggle_container_element" + bl_label = "Toggle Container Element" + bl_options = {"REGISTER", "UNDO"} + ifc_class: bpy.props.StringProperty() + + def execute(self, context): + core.toggle_container_element(tool.Spatial, ifc_class=self.ifc_class) + return {"FINISHED"} + + class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.select_decomposed_elements" bl_label = "Select Children" diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 12384f08c2..4f04f1bb48 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -112,6 +112,7 @@ class Element(PropertyGroup): is_type: BoolProperty(name="Is Type", default=False) ifc_definition_id: IntProperty(name="IFC Definition ID") total: IntProperty(name="Total") + is_expanded: BoolProperty(name="Is Expanded", default=False) class BIMSpatialDecompositionProperties(PropertyGroup): @@ -122,6 +123,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup): active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index) element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"}) elements: CollectionProperty(name="Elements", type=Element) + contracted_classes: StringProperty(name="Contracted Classes", default="[]") active_element_index: IntProperty(name="Active Element Index") total_elements: IntProperty(name="Total Elements") subelement_class: bpy.props.EnumProperty(items=get_subelement_class, name="Subelement Class") diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index a7d3e0a51a..da35223b33 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -262,7 +262,8 @@ class BIM_UL_elements(UIList): if item: row = layout.row(align=True) if item.is_class: - row.label(text="", icon="DISCLOSURE_TRI_DOWN") + icon_id = "DISCLOSURE_TRI_DOWN" if item.is_expanded else "DISCLOSURE_TRI_RIGHT" + row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).ifc_class = item.name row.label(text=item.name) col = row.column() col.alignment = "RIGHT" diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index 4bad423e30..1bf19d386e 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -141,6 +141,11 @@ def delete_container( spatial.import_spatial_decomposition() +def toggle_container_element(spatial: tool.Spatial, ifc_class: str) -> None: + spatial.toggle_container_element(ifc_class) + spatial.load_contained_elements() + + def select_decomposed_elements( spatial: tool.Spatial, container: ifcopenshell.entity_instance, diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 499508e9bd..d8e6410dfe 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -221,22 +221,27 @@ class Spatial(bonsai.core.tool.Spatial): results.setdefault(ifc_class, {}).setdefault(ifc_definition_id, {"total": 0, "type_name": type_name}) results[ifc_class][ifc_definition_id]["total"] += 1 + contracted_classes = json.loads(props.contracted_classes) total_elements = 0 for ifc_class in sorted(results.keys()): new = props.elements.add() new.name = ifc_class new.is_class = True + class_is_expanded = ifc_class not in contracted_classes + new.is_expanded = class_is_expanded total = 0 for ifc_definition_id in sorted( results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"] ): - new2 = props.elements.add() - new2.is_type = True - new2.name = results[ifc_class][ifc_definition_id]["type_name"] - new2.ifc_class = ifc_class - new2.total = results[ifc_class][ifc_definition_id]["total"] - new2.ifc_definition_id = ifc_definition_id - total += new2.total + total2 = results[ifc_class][ifc_definition_id]["total"] + if class_is_expanded: + new2 = props.elements.add() + new2.is_type = True + new2.name = results[ifc_class][ifc_definition_id]["type_name"] + new2.ifc_class = ifc_class + new2.total = total2 + new2.ifc_definition_id = ifc_definition_id + total += total2 new.total = total total_elements += total @@ -336,6 +341,16 @@ class Spatial(bonsai.core.tool.Spatial): contracted_containers.remove(container.id()) props.contracted_containers = json.dumps(contracted_containers) + @classmethod + def toggle_container_element(cls, ifc_class: str) -> None: + props = bpy.context.scene.BIMSpatialDecompositionProperties + contracted_classes: list[str] = json.loads(props.contracted_classes) + if ifc_class in contracted_classes: + contracted_classes.remove(ifc_class) + else: + contracted_classes.append(ifc_class) + props.contracted_classes = json.dumps(contracted_classes) + # HERE STARTS SPATIAL TOOL @classmethod From 002c50e29de569d2781fa4bd8dcd6ead104d5f25 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 17:31:41 +0500 Subject: [PATCH 027/556] black format --- src/bonsai/bonsai/bim/module/spatial/operator.py | 2 +- src/bonsai/bonsai/tool/spatial.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index aeaf6033fe..2301c97182 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -289,7 +289,7 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): ifc_class=ifc_class, relating_type=relating_type, is_untyped=is_untyped, - element_filter = element_filter + element_filter=element_filter, ) diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index d8e6410dfe..df0ae65120 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -257,6 +257,7 @@ class Spatial(bonsai.core.tool.Spatial): keyword: str | None, ) -> filter[ifcopenshell.entity_instance]: keyword = keyword.lower() if keyword else keyword + def filter_element(element): if ifc_class: if not element.is_a(ifc_class): @@ -273,6 +274,7 @@ class Spatial(bonsai.core.tool.Spatial): if keyword not in f"{element.is_a()} {type_name}".lower(): return False return True + return filter(filter_element, elements) @classmethod From 76853db32c734821f1b0f996d0272261e6642d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 13:08:58 -0300 Subject: [PATCH 028/556] Fix mixed snap for Polytool --- src/bonsai/bonsai/tool/cad.py | 8 +++++++ src/bonsai/bonsai/tool/snap.py | 42 ++++++++++++---------------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/bonsai/bonsai/tool/cad.py b/src/bonsai/bonsai/tool/cad.py index 6693509d90..6c8d1fa4c6 100644 --- a/src/bonsai/bonsai/tool/cad.py +++ b/src/bonsai/bonsai/tool/cad.py @@ -142,6 +142,14 @@ class Cad: def are_vectors_equal(cls, v1: Vector, v2: Vector, tolerance: float | None = None) -> bool: return cls.is_x((v2 - v1).length, 0, tolerance) + @classmethod + def intersect_edge_plane(cls, v1, v2, plane_co, plane_no): + """ + > takes an edges as two vector, and a plane as origin point and normal + < return the intersection point or None + """ + return geometry.intersect_line_plane(v1, v2, plane_co, plane_no) + @classmethod def intersect_edges(cls, edge1, edge2): """ diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index eb2fd56ec2..2e2b9feac0 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -278,35 +278,17 @@ class Snap(bonsai.core.tool.Snap): return None, None, None, None @classmethod - def mix_snap_and_axis(cls, snap_point, axis_start, axis_end, elevation): + def mix_snap_and_axis(cls, snap_point, axis_start, axis_end): # Creates a mixed snap point between the locked axis and # the object snap - # TODO Use ALT key to give the user the option to choose between the two results. - # TODO Create decorator for this - snap_point_vector = Vector((snap_point[0].x, snap_point[0].y, snap_point[0].z)) - snap_point_axis_1 = ( - Vector((snap_point[0].x + 1000, snap_point[0].y, elevation)), - Vector((snap_point[0].x - 1000, snap_point[0].y, elevation)), - ) - snap_point_axis_2 = ( - Vector((snap_point[0].x, snap_point[0].y + 1000, elevation)), - Vector((snap_point[0].x, snap_point[0].y - 1000, elevation)), - ) - snap_angle_axis = (axis_start, axis_end) - result_1 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_1) - result_1 = Vector((result_1[0].x, result_1[0].y, elevation)) - distance_1 = (result_1 - snap_point_vector).length + intersections = [] + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((1, 0, 0)))) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 1, 0)))) + intersections.append(tool.Cad.intersect_edge_plane(axis_start, axis_end, snap_point[0], Vector((0, 0, 1)))) + sorted_intersections = sorted(i for i in intersections if i is not None) + if sorted_intersections[0]: + return sorted_intersections[0], "Mix" - result_2 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_2) - result_2 = Vector((result_2[0].x, result_2[0].y, elevation)) - distance_2 = (result_2 - snap_point_vector).length - - if distance_1 < distance_2: - best_result = result_1 - else: - best_result = result_2 - - return best_result, "Mix" @classmethod def detect_snapping_points(cls, context, event, objs_2d_bbox): @@ -472,7 +454,6 @@ class Snap(bonsai.core.tool.Snap): def select_snapping_points(cls, context, event, detected_snaps): snapping_points = [] for origin in detected_snaps: - if "Object" in list(origin.keys()): snap_obj, hit, face_index = origin["Object"] @@ -489,16 +470,21 @@ class Snap(bonsai.core.tool.Snap): for op in options: snapping_points.append(op) + break + if "Edge-Vertex" in list(origin.keys()): snap_obj, options = origin["Edge-Vertex"] for op in options: snapping_points.append(op) + break if "Polyline" in list(origin.keys()): options = origin["Polyline"] for op in options: snapping_points.append(op) + break + for origin in detected_snaps: if "Axis" in list(origin.keys()): intersection = origin["Axis"] axis_start = intersection[1] @@ -516,7 +502,7 @@ class Snap(bonsai.core.tool.Snap): for point in snapping_points: if point[1] == "Axis": if snapping_points[0][1] not in {"Axis", "Plane"}: - mixed_snap = cls.mix_snap_and_axis(snapping_points[0], axis_start, axis_end, 0) + mixed_snap = cls.mix_snap_and_axis(snapping_points[0], axis_start, axis_end) cls.update_snapping_point(mixed_snap[0], mixed_snap[1]) return snapping_points cls.update_snapping_point(point[0], point[1]) From 89d27bdf5fdc48673a6baa2793649a4eba46cc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 18:16:53 -0300 Subject: [PATCH 029/556] Fixed edge snap. The `intersect_line_line` were returning points that were outside the object edges, so it was added a function to check if the point return is on the object edge. --- src/bonsai/bonsai/tool/raycast.py | 45 +++++++++++++++---------------- src/bonsai/bonsai/tool/snap.py | 12 ++++----- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 996a9d8c3d..1e432a9336 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -108,7 +108,7 @@ class Raycast(bonsai.core.tool.Raycast): @classmethod - def ray_cast_by_proximity(cls, context, event, obj, mesh=None): + def ray_cast_by_proximity(cls, context, event, obj, face=None): region = context.region rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y @@ -121,26 +121,15 @@ class Raycast(bonsai.core.tool.Raycast): loc = Vector((0, 0, 0)) bm = bmesh.new() - if mesh is None: # Object with faces + 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 mesh.vertices] + verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices] bm.faces.new(verts) - for edge in bm.edges: - v1 = edge.verts[0].co - v2 = edge.verts[1].co - world_v1 = obj.matrix_world @ v1 - world_v2 = obj.matrix_world @ v2 - division_point = (world_v1 + world_v2) / 2 # TODO Make it work for different divisions - intersection, _ = mathutils.geometry.intersect_point_line(division_point, ray_target, loc) - distance = (division_point - intersection).length - if distance < 0.2: - points.append((division_point, "Edge Center")) - for vertex in bm.verts: - world_vertex = obj.matrix_world @ vertex.co - intersection, _ = mathutils.geometry.intersect_point_line(world_vertex, ray_target, loc) + world_vertex = obj.matrix_world.copy() @ vertex.co + intersection = tool.Cad.point_on_edge(world_vertex, (ray_target, loc)) distance = (world_vertex - intersection).length if distance < 0.2: points.append((world_vertex, "Vertex")) @@ -148,14 +137,24 @@ class Raycast(bonsai.core.tool.Raycast): for edge in bm.edges: v1 = edge.verts[0].co v2 = edge.verts[1].co - world_v1 = obj.matrix_world @ v1 - world_v2 = obj.matrix_world @ v2 - intersection = mathutils.geometry.intersect_line_line(ray_target, loc, world_v1, world_v2) - if intersection: - distance = (intersection[0] - intersection[1]).length - if distance < 0.2: - points.append((intersection[1], "Edge")) + 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 + intersection = tool.Cad.point_on_edge(division_point, (ray_target, loc)) + distance = (division_point - intersection).length + if distance < 0.2: + points.append((division_point, "Edge Center")) + + intersection = tool.Cad.intersect_edges((ray_target, loc), (world_v1, world_v2)) + if intersection: + if tool.Cad.is_point_on_edge(intersection[1], (world_v1, world_v2)): + distance = (intersection[1] - intersection[0]).length + if distance < 0.8: + points.append((intersection[1], "Edge")) + + + bm.free() return points diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 2e2b9feac0..6214a2c15b 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -390,11 +390,12 @@ class Snap(bonsai.core.tool.Snap): # Edge-Vertex for obj in objs_to_raycast: - options = tool.Raycast.ray_cast_by_proximity(context, event, obj) - snap_obj = obj - if options: - detected_snaps.append({"Edge-Vertex": (snap_obj, options)}) - break + if len(obj.data.polygons) == 0: + options = tool.Raycast.ray_cast_by_proximity(context, event, obj) + snap_obj = obj + if options: + detected_snaps.append({"Edge-Vertex": (snap_obj, options)}) + break # Polyline try: polyline_data = bpy.context.scene.BIMModelProperties.polyline_point @@ -456,7 +457,6 @@ class Snap(bonsai.core.tool.Snap): for origin in detected_snaps: if "Object" in list(origin.keys()): snap_obj, hit, face_index = origin["Object"] - matrix = snap_obj.matrix_world.copy() face = snap_obj.data.polygons[face_index] verts = [] From 0faacccf6d69758a7b421347311270e0a542a89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 18:18:30 -0300 Subject: [PATCH 030/556] Raycast now filters objects that are not in the active local view. --- src/bonsai/bonsai/tool/snap.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 6214a2c15b..b8e40148f2 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -294,6 +294,7 @@ class Snap(bonsai.core.tool.Snap): def detect_snapping_points(cls, context, event, objs_2d_bbox): region = context.region rv3d = context.region_data + space = context.space_data cls.mouse_pos = event.mouse_region_x, event.mouse_region_y detected_snaps = [] @@ -382,7 +383,11 @@ class Snap(bonsai.core.tool.Snap): for obj, bbox_2d in objs_2d_bbox: if obj.type == "MESH" and bbox_2d: if tool.Raycast.in_view_2d_bounding_box(cls.mouse_pos, bbox_2d): - objs_to_raycast.append(obj) + if space.local_view: + if obj.local_view_get(context.space_data): + objs_to_raycast.append(obj) + else: + objs_to_raycast.append(obj) # Obj snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast) if hit is not None: From 994c85a05ab40024618cb1f500e9bbf857e62c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 18:22:43 -0300 Subject: [PATCH 031/556] black format --- .../bonsai/bim/module/model/decorator.py | 33 ++++++++++--------- .../bonsai/bim/module/project/operator.py | 2 +- src/bonsai/bonsai/tool/raycast.py | 10 ++---- src/bonsai/bonsai/tool/snap.py | 5 +-- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index e9e1cfd3b4..0effb48e4a 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -309,14 +309,16 @@ class PolylineDecorator: use_default_container = False instructions = None snap_info = None - + @classmethod def install(cls, context): if cls.is_installed: cls.uninstall() handler = cls() cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_panel, (context,), "WINDOW", "POST_PIXEL")) - cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_on_screen_menu, (context,), "WINDOW", "POST_PIXEL")) + cls.handlers.append( + 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, (context,), "WINDOW", "POST_VIEW")) cls.is_installed = True @@ -463,7 +465,6 @@ class PolylineDecorator: cls.input_panel["AREA"] = str(round(area, 4)) return cls.input_panel - @classmethod def calculate_x_y_and_z(cls, context): try: @@ -522,7 +523,7 @@ class PolylineDecorator: batch.draw(shader) def draw_input_panel(self, context): - texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"} + texts = {"D": "Distance:", "A": "Angle:", "X": "X coord:", "Y": "Y coord:", "Z": "Z coord:", "AREA": "Area:"} unit_system = tool.Drawing.get_unit_system() if unit_system == "IMPERIAL": @@ -545,9 +546,11 @@ class PolylineDecorator: if key != "A" and key != self.input_type: value = float(value) - if context.scene.unit_settings.length_unit == 'MILLIMETERS': + if context.scene.unit_settings.length_unit == "MILLIMETERS": value = value * 1000 - formatted_value = format_distance(value*factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) + formatted_value = format_distance( + value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True + ) else: formatted_value = value @@ -565,7 +568,6 @@ class PolylineDecorator: rv3d = region.data measurement_prop = context.scene.BIMModelProperties.polyline_measurement - self.addon_prefs = tool.Blender.get_addon_preferences() self.font_id = 1 blf.size(self.font_id, 12) @@ -573,12 +575,11 @@ class PolylineDecorator: blf.shadow(self.font_id, 6, 0, 0, 0, 1) color = self.addon_prefs.decorations_colour - blf.color(self.font_id, *color) 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 + 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) unit_system = tool.Drawing.get_unit_system() @@ -589,16 +590,18 @@ class PolylineDecorator: precision = None factor = 1 - value = measurement_prop[i].dim + value = measurement_prop[i].dim value = float(value) - if context.scene.unit_settings.length_unit == 'MILLIMETERS': + if context.scene.unit_settings.length_unit == "MILLIMETERS": value = value * 1000 - formatted_value = format_distance(value*factor, precision=precision, suppress_zero_inches=True, in_unit_length=True) - + formatted_value = format_distance( + value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True + ) + blf.position(self.font_id, coords_dim[0], coords_dim[1], 0) blf.draw(self.font_id, "d: " + formatted_value) - pos_angle = measurement_prop[i-1].position + 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) blf.draw(self.font_id, "a: " + measurement_prop[i].angle) @@ -624,7 +627,6 @@ class PolylineDecorator: blf.position(self.font_id, position, 30, 0) blf.draw(self.font_id, self.snap_info) - def __call__(self, context): self.addon_prefs = tool.Blender.get_addon_preferences() @@ -655,7 +657,6 @@ class PolylineDecorator: except: ref_point = None - default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z projection_point = [] if self.use_default_container: diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 29abb21190..1215fda519 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2481,7 +2481,7 @@ class MeasureTool(bpy.types.Operator): if event.value == "PRESS" and event.type == "M": self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) tool.Blender.update_viewport() - + if event.shift and event.value == "PRESS" and event.type == "X": tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 1e432a9336..bb855decec 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -106,7 +106,6 @@ class Raycast(bonsai.core.tool.Raycast): else: return None, None, None - @classmethod def ray_cast_by_proximity(cls, context, event, obj, face=None): region = context.region @@ -121,9 +120,9 @@ class Raycast(bonsai.core.tool.Raycast): loc = Vector((0, 0, 0)) bm = bmesh.new() - if face is None: # Object with faces + if face is None: # Object with faces bm.from_mesh(obj.data) - else: # Object without faces + else: # Object without faces verts = [bm.verts.new(obj.data.vertices[i].co) for i in face.vertices] bm.faces.new(verts) @@ -139,7 +138,7 @@ class Raycast(bonsai.core.tool.Raycast): 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 + division_point = (world_v1 + world_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 @@ -153,11 +152,9 @@ class Raycast(bonsai.core.tool.Raycast): if distance < 0.8: points.append((intersection[1], "Edge")) - bm.free() return points - @classmethod def ray_cast_to_polyline(cls, context, event): region = context.region @@ -175,7 +172,6 @@ class Raycast(bonsai.core.tool.Raycast): for point_data in polyline_data: point = Vector((point_data.x, point_data.y, point_data.z)) - intersection, _ = mathutils.geometry.intersect_point_line(point, ray_target, loc) distance = (point - intersection).length if distance < 0.2: diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index b8e40148f2..6d8f88ca9a 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -289,7 +289,6 @@ class Snap(bonsai.core.tool.Snap): if sorted_intersections[0]: return sorted_intersections[0], "Mix" - @classmethod def detect_snapping_points(cls, context, event, objs_2d_bbox): region = context.region @@ -453,7 +452,6 @@ class Snap(bonsai.core.tool.Snap): detected_snaps.append({"Plane": intersection}) - return detected_snaps @classmethod @@ -500,14 +498,13 @@ class Snap(bonsai.core.tool.Snap): intersection = origin["Plane"] snapping_points.append((intersection, "Plane")) - # Make Axis first priority if event.shift or cls.snap_axis_method in {"X", "Y", "Z"}: cls.update_snapping_ref(snapping_points[0][0], snapping_points[0][1]) for point in snapping_points: if point[1] == "Axis": if snapping_points[0][1] not in {"Axis", "Plane"}: - mixed_snap = cls.mix_snap_and_axis(snapping_points[0], axis_start, axis_end) + mixed_snap = cls.mix_snap_and_axis(snapping_points[0], axis_start, axis_end) cls.update_snapping_point(mixed_snap[0], mixed_snap[1]) return snapping_points cls.update_snapping_point(point[0], point[1]) From 79eaf895f79474bdd221123d1bb628d3413601c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 19:05:30 -0300 Subject: [PATCH 032/556] Fix polyline to work only with walls. This is temporary until we enable polyline to work with other tools. --- src/bonsai/bonsai/bim/module/model/workspace.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index b7d4312853..8442559b08 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -1135,7 +1135,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): self.props.z = self.z def hotkey_S_P(self): - bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT") + mode = bpy.context.mode + current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode) + if current_tool.idname == 'bim.wall_tool': + bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT") def hotkey_S_L(self): if AuthoringData.data["active_class"] in ("IfcOpeningElement",): From b74811b3f44ddae6753590af30a4aaa20df5956c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 19:53:03 -0300 Subject: [PATCH 033/556] Polytool, changed decorator order to show created polyline first. --- .../bonsai/bim/module/model/decorator.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 0effb48e4a..e1c47253a3 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -669,7 +669,7 @@ class PolylineDecorator: edges = [[0, 1]] self.draw_batch("LINES", mouse_point + projection_point, (1.0, 0.6, 0.0, 1.0), edges) - # Polyline with selected points + # Create polyline with selected points polyline_data = context.scene.BIMModelProperties.polyline_point polyline_points = [] polyline_edges = [] @@ -680,18 +680,6 @@ class PolylineDecorator: for i in range(len(polyline_points) - 1): polyline_edges.append([i, i + 1]) - self.line_shader.uniform_float("lineWidth", 2.0) - self.draw_batch("POINTS", polyline_points, decorator_color_selected) - if len(polyline_points) > 1: - self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges) - - # Line between last polyline point and mouse - edges = [[0, 1]] - if polyline_points: - if snap_prop.snap_type != "Plane" and projection_point: - self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges) - else: - self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges) # Line for angle axis snap if snap_prop.snap_type == "Axis": @@ -719,3 +707,17 @@ class PolylineDecorator: if ref_point: self.draw_batch("POINTS", ref_point, (1.0, 0.6, 0.0, 1.0)) + + # Draw polyline with selected points + self.line_shader.uniform_float("lineWidth", 2.0) + self.draw_batch("POINTS", polyline_points, decorator_color_selected) + if len(polyline_points) > 1: + self.draw_batch("LINES", polyline_points, decorator_color_selected, polyline_edges) + + # Line between last polyline point and mouse + edges = [[0, 1]] + if polyline_points: + if snap_prop.snap_type != "Plane" and projection_point: + self.draw_batch("LINES", [polyline_points[-1]] + projection_point, decorator_color_unselected, edges) + else: + self.draw_batch("LINES", [polyline_points[-1]] + mouse_point, decorator_color_unselected, edges) From dabb85c4669404cb5da887c25314016bae30d36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Tue, 27 Aug 2024 19:59:55 -0300 Subject: [PATCH 034/556] Fix polytool input for distance zero --- src/bonsai/bonsai/bim/module/model/wall.py | 7 +++---- src/bonsai/bonsai/bim/module/project/operator.py | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 98f70c0901..96c1fbcc29 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -345,10 +345,9 @@ class DrawPolylineWall(bpy.types.Operator): self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) elif self.input_type in {"D", "A"}: self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) - - self.input_panel[self.input_type] = self.number_output - - PolylineDecorator.set_input_panel(self.input_panel, self.input_type) + self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + else: + self.input_panel[self.input_type] = self.number_output tool.Blender.update_viewport() return is_valid diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 1215fda519..039443cea4 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2355,8 +2355,9 @@ class MeasureTool(bpy.types.Operator): self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) elif self.input_type in {"D", "A"}: self.input_panel = PolylineDecorator.calculate_x_y_and_z(context) - - self.input_panel[self.input_type] = self.number_output + self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) + else: + self.input_panel[self.input_type] = self.number_output PolylineDecorator.set_input_panel(self.input_panel, self.input_type) tool.Blender.update_viewport() From 1b49515d9f352f24a58c61dcff628d9c18b9527b Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 11:14:47 +0500 Subject: [PATCH 035/556] Replace ordered_set with orderly_set deepdiff deprecated use of ordered_set and switched to orderly_set (ordered_set fork), so we switch too see https://github.com/seperman/deepdiff/releases/tag/8.0.0 --- src/ifcdiff/ifcdiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ifcdiff/ifcdiff.py b/src/ifcdiff/ifcdiff.py index a4f654623a..0f7c01cfc4 100755 --- a/src/ifcdiff/ifcdiff.py +++ b/src/ifcdiff/ifcdiff.py @@ -34,7 +34,7 @@ import ifcopenshell.util.placement import ifcopenshell.util.classification import ifcopenshell.util.representation from deepdiff import DeepDiff -from ordered_set import OrderedSet +from orderly_set import OrderedSet from typing import Optional, Union, Literal, Any From 747a39b4403168b6edb530ab9353159b9f5d18b0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 11:29:36 +0500 Subject: [PATCH 036/556] 48f0dc6a00 to pass on unix too --- src/bonsai/test/tool/test_debug.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bonsai/test/tool/test_debug.py b/src/bonsai/test/tool/test_debug.py index d0238b6592..582b1a63ef 100644 --- a/src/bonsai/test/tool/test_debug.py +++ b/src/bonsai/test/tool/test_debug.py @@ -63,4 +63,6 @@ class TestPurgeHdf5Cache(NewFile): loaded_file = open(loaded_file_path, "w") subject.purge_hdf5_cache() - assert [f for f in cache_dir.iterdir() if f.suffix == ".h5"] == [loaded_file_path] + # On Unix loaded files are not locked. + paths = [loaded_file_path] if os.name == "nt" else [] + assert [f for f in cache_dir.iterdir() if f.suffix == ".h5"] == paths From 4e1a9568309108b702c17d270e27f97c3eec3989 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 11:38:18 +0500 Subject: [PATCH 037/556] Fix #5233 --- src/bonsai/bonsai/tool/search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index ef70b81a60..e7deaacb94 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -23,9 +23,9 @@ class Search(bonsai.core.tool.Search): elif module == "diff": return bpy.context.scene.DiffProperties.filter_groups elif module == "drawing_include": - return bpy.context.active_object.data.BIMCameraProperties.include_filter_groups + return bpy.context.scene.camera.data.BIMCameraProperties.include_filter_groups elif module == "drawing_exclude": - return bpy.context.active_object.data.BIMCameraProperties.exclude_filter_groups + return bpy.context.scene.camera.data.BIMCameraProperties.exclude_filter_groups elif module.startswith("clash"): _, clash_set_index, ab, clash_source_index = module.split("_") return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[ From 1aed2a6b3c0c91dc25cd62257008f0f641b14217 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 12:21:23 +0500 Subject: [PATCH 038/556] Fix error not excluding annotations from other drawings after 3848a21 Mentioned in #5233 Also some refactor: - checking IfcProduct instead of IfcProject. Not sure when this issue occur in general but if it occurs then it might fail for other non-IfcProducts too, not just IfcProject - small performance optimization --- src/bonsai/bonsai/tool/drawing.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index 0b8dc7533e..e9f6ec315d 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1669,20 +1669,17 @@ class Drawing(bonsai.core.tool.Drawing): elements = {e for e in (elements & base_elements) if e.is_a() != "IfcSpace"} updated_set = set() - for i in elements: # exclude annotations to avoid including annotations from other drawings if not i.is_a("IfcAnnotation"): updated_set.add(i) # add aggregate too, if element is host by one - if i.Decomposes: - aggregate = i.Decomposes[0].RelatingObject + if decomposes := i.Decomposes: + aggregate = decomposes[0].RelatingObject # remove IfcProject for class iterator. See https://github.com/IfcOpenShell/IfcOpenShell/issues/4361#issuecomment-2081223615 - if not aggregate.is_a("IfcProject"): + if aggregate.is_a("IfcProduct"): updated_set.add(aggregate) - - # After the iteration is complete, update elements with updated set - elements.update(updated_set) + elements = updated_set # add annotations from the current drawing annotations = tool.Drawing.get_group_elements(tool.Drawing.get_drawing_group(drawing)) From 64cba98c3d839e77eed8d3780c5f67551a5f0722 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 14:37:55 +0500 Subject: [PATCH 039/556] Fix #5235 --- src/bonsai/bonsai/bim/module/drawing/prop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index b0190e0165..cef66d139b 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -531,9 +531,10 @@ def get_relating_type_id(self, context): def update_annotation_object_type(self, context): - self.relating_type_id = "0" - # changing enum doesn't trigger refresh by itself + # Refresh enum items before changing property, + # otherwise it might map to the wrong item. AnnotationData.is_loaded = False + self.relating_type_id = "0" def update_sheet_data(self, context): From 84a2d4266d9f30e3b4279718fef938293b706895 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 15:38:19 +0500 Subject: [PATCH 040/556] Autodocument hotkeys in the BIM tool hotkeys descriptions Addition to #5223 Example - https://i.imgur.com/vGVHNPL.png --- .../bonsai/bim/module/model/workspace.py | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 8442559b08..5b107ea14e 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -17,6 +17,7 @@ # along with Bonsai. If not, see . import os +import sys import bpy import bpy.utils.previews import bonsai.tool as tool @@ -239,12 +240,14 @@ class CableTool(BimTool): BimToolUI.draw(context, layout, ifc_element_type=cls.ifc_element_type) +MODIFIERS = { + "A": ("EVENT_ALT", "OPTION" if sys.platform == "Darwin" else "ALT"), + "C": ("EVENT_CTRL", "CTRL"), + "S": ("EVENT_SHIFT", "⇧"), +} + + def add_layout_hotkey_operator(layout, text, hotkey, description, ui_context=""): - modifiers = { - "A": "EVENT_ALT", - "C": "EVENT_CTRL", - "S": "EVENT_SHIFT", - } modifier, key = hotkey.split("_") try: @@ -252,14 +255,21 @@ def add_layout_hotkey_operator(layout, text, hotkey, description, ui_context="") except KeyError: custom_icon = custom_icon_previews["IFC"].icon_id + modifier_icon, modifier_str = MODIFIERS[modifier] + if ui_context == "TOOL_HEADER": op = layout.operator("bim.hotkey", text="", icon_value=custom_icon) else: row = layout.row(align=True) op = row.operator("bim.hotkey", text=text, icon_value=custom_icon) - row.label(text="", icon=modifiers[modifier]) + row.label(text="", icon=modifier_icon) row.label(text="", icon=f"EVENT_{key}") + hotkey_description = f"Hotkey: {modifier_str} {key}" + if description: + description += "\n\n" + description += hotkey_description + op.hotkey = hotkey op.description = description return op @@ -370,9 +380,7 @@ class BimToolUI: cls.layout.separator() - add_layout_hotkey_operator( - cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__ + "\n⇧ G", ui_context - ) + add_layout_hotkey_operator(cls.layout, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__, ui_context) cls.layout.separator() @@ -380,21 +388,21 @@ class BimToolUI: cls.layout, "Extend", "S_E", - "Extends/reduces element to 3D cursor\n⇧ E", + "Extends/reduces element to 3D cursor", ui_context, ) add_layout_hotkey_operator( cls.layout, "Butt", "S_T", - "Intersects two non-parallel elements to a butt corner junction\n⇧ T", + "Intersects two non-parallel elements to a butt corner junction", ui_context, ) add_layout_hotkey_operator( cls.layout, "Mitre", "S_Y", - "Intersects two non-parallel elements to a mitred corner junction\n⇧ Y", + "Intersects two non-parallel elements to a mitred corner junction", ui_context, ) @@ -404,12 +412,10 @@ class BimToolUI: ) cls.layout.separator() - add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__ + "\n⇧ M", ui_context) - add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__ + "\n⇧ K", ui_context) - add_layout_hotkey_operator( - cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__ + "\n⇧ R", ui_context - ) - add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__ + "\n⇧ F", ui_context) + add_layout_hotkey_operator(cls.layout, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__, ui_context) + add_layout_hotkey_operator(cls.layout, "Split", "S_K", bpy.ops.bim.split_wall.__doc__, ui_context) + add_layout_hotkey_operator(cls.layout, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context) + add_layout_hotkey_operator(cls.layout, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__, ui_context) # row.operator("bim.unjoin_walls", icon="X", text="") @@ -596,37 +602,37 @@ class BimToolUI: op.x_angle = cls.props.x_angle row = cls.layout.row() - add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__ + "\n⇧ G", ui_context) + add_layout_hotkey_operator(row, "Regen", "S_G", bpy.ops.bim.recalculate_wall.__doc__, ui_context) row = cls.layout.row(align=True) add_layout_hotkey_operator( row, "Extend", "S_E", - "Extends/reduces element to 3D cursor\n⇧ E", + "Extends/reduces element to 3D cursor", ui_context, ) add_layout_hotkey_operator( row, "Butt", "S_T", - "Intersects two non-parallel elements to a butt corner junction\n⇧ T", + "Intersects two non-parallel elements to a butt corner junction", ui_context, ) add_layout_hotkey_operator( row, "Mitre", "S_Y", - "Intersects two non-parallel elements to a mitred corner junction\n⇧ Y", + "Intersects two non-parallel elements to a mitred corner junction", ui_context, ) row.operator("bim.unjoin_walls", text="", icon_value=custom_icon_previews["UNJOIN_WALLS"].icon_id) row = cls.layout.row(align=True) - add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__ + "\n⇧ M", ui_context) - add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__ + "\n⇧ K", ui_context) - add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__ + "\n⇧ R", ui_context) - add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__ + "\n⇧ F", ui_context) + add_layout_hotkey_operator(row, "Merge", "S_M", bpy.ops.bim.merge_wall.__doc__, ui_context) + add_layout_hotkey_operator(row, "Split", "S_K", bpy.ops.bim.split_wall.__doc__, ui_context) + add_layout_hotkey_operator(row, "Rotate 90", "S_R", bpy.ops.bim.rotate_90.__doc__, ui_context) + add_layout_hotkey_operator(row, "Flip", "S_F", bpy.ops.bim.flip_wall.__doc__, ui_context) elif AuthoringData.data["active_material_usage"] == "LAYER3": if len(context.selected_objects) == 1: @@ -848,7 +854,7 @@ class BimToolUI: class Hotkey(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.hotkey" - bl_label = "Hotkey" + bl_label = "" bl_options = {"REGISTER", "UNDO"} hotkey: bpy.props.StringProperty() description: bpy.props.StringProperty() @@ -1137,7 +1143,7 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): def hotkey_S_P(self): mode = bpy.context.mode current_tool = bpy.context.workspace.tools.from_space_view3d_mode(mode) - if current_tool.idname == 'bim.wall_tool': + if current_tool.idname == "bim.wall_tool": bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT") def hotkey_S_L(self): From 174ff00a6df0ea47a61b8f9024b3916fec8ddfa0 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 16:18:17 +0500 Subject: [PATCH 041/556] Fix errors trying to setup decorations running tests --- src/bonsai/bonsai/bim/module/drawing/prop.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bonsai/bonsai/bim/module/drawing/prop.py b/src/bonsai/bonsai/bim/module/drawing/prop.py index cef66d139b..0ae5e45aac 100644 --- a/src/bonsai/bonsai/bim/module/drawing/prop.py +++ b/src/bonsai/bonsai/bim/module/drawing/prop.py @@ -259,8 +259,12 @@ def update_should_draw_decorations(self, context): continue tool.Drawing.update_text_value(obj) refresh_drawing_data() + if bpy.app.background: + return decoration.DecorationsHandler.install(context) else: + if bpy.app.background: + return decoration.DecorationsHandler.uninstall() From c113061742639a351096078ef416ef14ccdb1c02 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 16:13:17 +0500 Subject: [PATCH 042/556] Preserve visibility status for non-ifc objects when drawing is activated Mentioned in #3402 --- src/bonsai/bonsai/tool/drawing.py | 12 ++++++----- src/bonsai/test/bim/feature/drawing.feature | 23 ++++++++++++++++++++ src/bonsai/test/bim/test_feature.py | 24 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py index e9f6ec315d..93dfbaf732 100644 --- a/src/bonsai/bonsai/tool/drawing.py +++ b/src/bonsai/bonsai/tool/drawing.py @@ -1779,6 +1779,7 @@ class Drawing(bonsai.core.tool.Drawing): @classmethod def activate_drawing(cls, camera: bpy.types.Object) -> None: selected_objects_before = bpy.context.selected_objects + non_ifc_objects_hide = {o: o.hide_get() for o in bpy.context.view_layer.objects if not tool.Ifc.get_entity(o)} # Sync viewport objects visibility with selectors from EPset_Drawing/Include and /Exclude drawing = tool.Ifc.get_entity(camera) @@ -1859,11 +1860,12 @@ class Drawing(bonsai.core.tool.Drawing): element_obj_names.add(obj.name) # Note that render visibility is only set on drawing generation time for speed. - [ - obj.hide_set(False) # Show the object - for obj in bpy.context.view_layer.objects - if obj.name in element_obj_names or not tool.Ifc.get_entity(obj) - ] + for obj in bpy.context.view_layer.objects: + if obj.name in element_obj_names: + obj.hide_set(False) # Show the object + continue + if (hide := non_ifc_objects_hide.get(obj)) is not None: + obj.hide_set(hide) cls.import_camera_props(drawing, camera.data) diff --git a/src/bonsai/test/bim/feature/drawing.feature b/src/bonsai/test/bim/feature/drawing.feature index 762be4e8ef..f5839dfe8e 100644 --- a/src/bonsai/test/bim/feature/drawing.feature +++ b/src/bonsai/test/bim/feature/drawing.feature @@ -51,6 +51,29 @@ Scenario: Create drawing after deleting a duplicated object When I press "bim.create_drawing" Then nothing happens +Scenario: Activate drawing preserves visibility for non-ifc objects + Given an empty IFC project + And I add a cube + And I add a cube + And the object "Cube" is visible + And the object "Cube.001" is not visible + And I press "bim.add_drawing" + And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()" + And I set "scene.DocProperties.active_drawing_index" to "0" + When I press "bim.activate_drawing(drawing={drawing})" + Then the object "Cube" is visible + And the object "Cube.001" is not visible + +Scenario: Activate drawing preserves selection + Given an empty IFC project + And I add a cube + And the object "Cube" is selected + And I press "bim.add_drawing" + And the variable "drawing" is "IfcStore.get_file().by_type('IfcAnnotation')[0].id()" + And I set "scene.DocProperties.active_drawing_index" to "0" + When I press "bim.activate_drawing(drawing={drawing})" + Then the object "Cube" is selected + Scenario: Remove drawing Given an empty IFC project And I add a cube diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index 0d3194154f..c9f3a99265 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -840,6 +840,30 @@ def the_object_name_is_not_a_void(name): assert False, "A void was found" +@given(parsers.parse('the object "{name}" is visible')) +def given_the_object_name_is_visible(name): + obj = the_object_name_exists(name) + obj.hide_set(False) + + +@given(parsers.parse('the object "{name}" is not visible')) +def given_the_object_name_is_not_visible(name): + obj = the_object_name_exists(name) + obj.hide_set(True) + + +@then(parsers.parse('the object "{name}" is visible')) +def the_object_name_is_visible(name): + obj = the_object_name_exists(name) + assert obj.hide_get() == False + + +@then(parsers.parse('the object "{name}" is not visible')) +def the_object_name_is_not_visible(name): + obj = the_object_name_exists(name) + assert obj.hide_get() == True + + @then(parsers.parse('the object "{name}" is an "{ifc_class}"')) def the_object_name_is_an_ifc_class(name, ifc_class): ifc = an_ifc_file_exists() From 99e67d528fe726d092cb0da8bf50c49dbc14b76f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 16:42:51 +0500 Subject: [PATCH 043/556] Remove duplicated then decorator from b9855b2 'then the object name is selected' were already defined in then_the_object_name_is_selected --- src/bonsai/test/bim/test_feature.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bonsai/test/bim/test_feature.py b/src/bonsai/test/bim/test_feature.py index c9f3a99265..682bbefce4 100644 --- a/src/bonsai/test/bim/test_feature.py +++ b/src/bonsai/test/bim/test_feature.py @@ -536,7 +536,6 @@ def i_deselect_all_objects(): @given(parsers.parse('the object "{name}" is selected')) @when(parsers.parse('the object "{name}" is selected')) -@then(parsers.parse('the object "{name}" is selected')) def the_object_name_is_selected(name): i_deselect_all_objects() additionally_the_object_name_is_selected(name) From fc04c6e41ef206b933abcd63d614bfa2a87cfd38 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 16:46:55 +0500 Subject: [PATCH 044/556] Remove unused property --- src/bonsai/bonsai/bim/module/spatial/prop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index 4f04f1bb48..ef91769928 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -119,7 +119,6 @@ class BIMSpatialDecompositionProperties(PropertyGroup): container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"}) containers: CollectionProperty(name="Containers", type=BIMContainer) contracted_containers: StringProperty(name="Contracted containers", default="[]") - expanded_containers: StringProperty(name="Expanded containers", default="[]") active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index) element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"}) elements: CollectionProperty(name="Elements", type=Element) From 760816b7f04da7c4aa16a558de898f0b0a586124 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 27 Aug 2024 18:25:17 +0500 Subject: [PATCH 045/556] Show occurrences in spatial manager decomposed elements Example - https://imgur.com/a/PNFUbv0 --- .../bonsai/bim/module/spatial/operator.py | 28 +++++--- src/bonsai/bonsai/bim/module/spatial/prop.py | 19 +++-- src/bonsai/bonsai/bim/module/spatial/ui.py | 19 +++-- src/bonsai/bonsai/core/spatial.py | 4 +- src/bonsai/bonsai/tool/covering.py | 4 ++ src/bonsai/bonsai/tool/spatial.py | 69 ++++++++++++++----- 6 files changed, 102 insertions(+), 41 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py index 2301c97182..8d59dfa223 100644 --- a/src/bonsai/bonsai/bim/module/spatial/operator.py +++ b/src/bonsai/bonsai/bim/module/spatial/operator.py @@ -247,10 +247,10 @@ class ToggleContainerElement(bpy.types.Operator): bl_idname = "bim.toggle_container_element" bl_label = "Toggle Container Element" bl_options = {"REGISTER", "UNDO"} - ifc_class: bpy.props.StringProperty() + element_index: bpy.props.IntProperty() def execute(self, context): - core.toggle_container_element(tool.Spatial, ifc_class=self.ifc_class) + core.toggle_container_element(tool.Spatial, element_index=self.element_index) return {"FINISHED"} @@ -271,21 +271,29 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator): return self.execute(context) def _execute(self, context): - ifc_class = relating_type = is_untyped = None + ifc_class = relating_type = None + is_untyped = False + ifc_file = tool.Ifc.get() if self.should_filter: active_element = context.scene.BIMSpatialDecompositionProperties.active_element - if active_element.is_class: + element_type = active_element.type + if element_type == "CLASS": ifc_class = active_element.name - elif relating_type := active_element.ifc_definition_id: + elif element_type == "TYPE": ifc_class = active_element.ifc_class - relating_type = tool.Ifc.get().by_id(relating_type) - else: - ifc_class = active_element.ifc_class - is_untyped = True + if ifc_id := active_element.ifc_definition_id: + relating_type = ifc_file.by_id(ifc_id) + else: # OCCURRENCE + occurrence = ifc_file.by_id(active_element.ifc_definition_id) + obj = tool.Ifc.get_object(occurrence) + assert isinstance(obj, bpy.types.Object) + tool.Blender.set_active_object(obj) + return + element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter core.select_decomposed_elements( tool.Spatial, - container=tool.Ifc.get().by_id(self.container), + container=ifc_file.by_id(self.container), ifc_class=ifc_class, relating_type=relating_type, is_untyped=is_untyped, diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py index ef91769928..76f539afc8 100644 --- a/src/bonsai/bonsai/bim/module/spatial/prop.py +++ b/src/bonsai/bonsai/bim/module/spatial/prop.py @@ -107,12 +107,21 @@ class BIMContainer(PropertyGroup): class Element(PropertyGroup): name: StringProperty(name="Name") - ifc_class: StringProperty(name="Name") - is_class: BoolProperty(name="Is Class", default=False) - is_type: BoolProperty(name="Is Type", default=False) - ifc_definition_id: IntProperty(name="IFC Definition ID") + ifc_class: StringProperty(name="Name", description="Type or element IFC class, empty if 'type' is 'CLASS'") + ifc_definition_id: IntProperty( + name="IFC Definition ID", + description="ID of the element type / occurrence. 0 if 'type' is 'CLASS' or if it's 'TYPE' but it represents untyped elements", + ) total: IntProperty(name="Total") is_expanded: BoolProperty(name="Is Expanded", default=False) + type: EnumProperty( + name="Element Type", + items=( + ("CLASS", "CLASS", "CLASS"), + ("TYPE", "TYPE", "TYPE"), + ("OCCURRENCE", "OCCURRENCE", "OCCURRENCE"), + ), + ) class BIMSpatialDecompositionProperties(PropertyGroup): @@ -122,7 +131,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup): active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index) element_filter: StringProperty(name="Element Filter", default="", options={"TEXTEDIT_UPDATE"}) elements: CollectionProperty(name="Elements", type=Element) - contracted_classes: StringProperty(name="Contracted Classes", default="[]") + expanded_elements: StringProperty(name="Expanded Elements", default="{}") active_element_index: IntProperty(name="Active Element Index") total_elements: IntProperty(name="Total Elements") subelement_class: bpy.props.EnumProperty(items=get_subelement_class, name="Subelement Class") diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py index da35223b33..164ab89c01 100644 --- a/src/bonsai/bonsai/bim/module/spatial/ui.py +++ b/src/bonsai/bonsai/bim/module/spatial/ui.py @@ -258,22 +258,31 @@ class BIM_UL_elements(UIList): def __init__(self): self.use_filter_show = True - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + def draw_toggle(self, row: bpy.types.UILayout, is_expanded: bool, index: int): + icon_id = "DISCLOSURE_TRI_DOWN" if is_expanded else "DISCLOSURE_TRI_RIGHT" + row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).element_index = index + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag): if item: row = layout.row(align=True) - if item.is_class: - icon_id = "DISCLOSURE_TRI_DOWN" if item.is_expanded else "DISCLOSURE_TRI_RIGHT" - row.operator("bim.toggle_container_element", text="", emboss=False, icon=icon_id).ifc_class = item.name + item_type = item.type + if item_type == "CLASS": + self.draw_toggle(row, item.is_expanded, index) row.label(text=item.name) col = row.column() col.alignment = "RIGHT" col.label(text=str(item.total)) - elif item.is_type: + elif item_type == "TYPE": row.label(text="", icon="BLANK1") + self.draw_toggle(row, item.is_expanded, index) row.label(text=item.name) col = row.column() col.alignment = "RIGHT" col.label(text=str(item.total)) + else: # OCCURRENCE + for _ in range(2): + row.label(text="", icon="BLANK1") + row.label(text=item.name) def draw_filter(self, context, layout): row = layout.row() diff --git a/src/bonsai/bonsai/core/spatial.py b/src/bonsai/bonsai/core/spatial.py index 1bf19d386e..9a200626e4 100644 --- a/src/bonsai/bonsai/core/spatial.py +++ b/src/bonsai/bonsai/core/spatial.py @@ -141,8 +141,8 @@ def delete_container( spatial.import_spatial_decomposition() -def toggle_container_element(spatial: tool.Spatial, ifc_class: str) -> None: - spatial.toggle_container_element(ifc_class) +def toggle_container_element(spatial: tool.Spatial, element_index: int) -> None: + spatial.toggle_container_element(element_index) spatial.load_contained_elements() diff --git a/src/bonsai/bonsai/tool/covering.py b/src/bonsai/bonsai/tool/covering.py index a0461e99f7..d682761ae3 100644 --- a/src/bonsai/bonsai/tool/covering.py +++ b/src/bonsai/bonsai/tool/covering.py @@ -49,6 +49,8 @@ class Covering(bonsai.core.tool.Covering): def covering_poll_wall_selected( cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str ) -> bool: + if not tool.Ifc.get(): + return False if not context.selected_objects or not context.active_object: operator.poll_message_set("No objects selected.") return False @@ -62,6 +64,8 @@ class Covering(bonsai.core.tool.Covering): def covering_poll_relating_type_check( cls, operator: type[bpy.types.Operator], context: bpy.types.Context, covering_type: str ) -> bool: + if not tool.Ifc.get(): + return False props = context.scene.BIMModelProperties relating_type_id = tool.Blender.get_enum_safe(props, "relating_type_id") if relating_type_id is not None: diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index df0ae65120..9b8cdbf46c 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -39,7 +39,8 @@ import json from math import pi from mathutils import Vector, Matrix from shapely import Polygon -from typing import Generator, Optional, Union, Literal, List +from typing import Generator, Optional, Union, Literal, List, Any, Iterable +from collections import defaultdict class Spatial(bonsai.core.tool.Spatial): @@ -139,7 +140,7 @@ class Spatial(bonsai.core.tool.Spatial): target_obj.matrix_world = relative_to_obj.matrix_world @ matrix @classmethod - def select_products(cls, products: list[ifcopenshell.entity_instance], unhide: bool = False) -> None: + def select_products(cls, products: Iterable[ifcopenshell.entity_instance], unhide: bool = False) -> None: bpy.ops.object.select_all(action="DESELECT") for product in products: obj = tool.Ifc.get_object(product) @@ -206,7 +207,7 @@ class Spatial(bonsai.core.tool.Spatial): container = tool.Ifc.get().by_id(container.ifc_definition_id) - results = {} + results: defaultdict[str, dict[int, Any]] = defaultdict(dict) if props.should_include_children: elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True) else: @@ -218,29 +219,47 @@ class Spatial(bonsai.core.tool.Spatial): ifc_class = element.is_a() ifc_definition_id = element_type.id() if element_type else 0 type_name = element_type.Name or "Unnamed" if element_type else f"Untyped {element.is_a()}" - results.setdefault(ifc_class, {}).setdefault(ifc_definition_id, {"total": 0, "type_name": type_name}) - results[ifc_class][ifc_definition_id]["total"] += 1 + class_data = results.setdefault(ifc_class, {}) + type_data = class_data.setdefault(ifc_definition_id, {"type_name": type_name, "elements": []}) + type_data["elements"].append(element) - contracted_classes = json.loads(props.contracted_classes) + expanded_elements = json.loads(props.expanded_elements) + expanded_classes = expanded_elements.get("CLASS", []) + expanded_ifc_ids = expanded_elements.get("IFC_ID", []) + expanded_untyped = expanded_elements.get("UNTYPED_CLASSES", []) total_elements = 0 for ifc_class in sorted(results.keys()): new = props.elements.add() new.name = ifc_class - new.is_class = True - class_is_expanded = ifc_class not in contracted_classes + new.type = "CLASS" + class_is_expanded = ifc_class in expanded_classes new.is_expanded = class_is_expanded total = 0 for ifc_definition_id in sorted( results[ifc_class].keys(), key=lambda x: results[ifc_class][x]["type_name"] ): - total2 = results[ifc_class][ifc_definition_id]["total"] + type_data = results[ifc_class][ifc_definition_id] + total2 = len(type_data["elements"]) if class_is_expanded: new2 = props.elements.add() - new2.is_type = True - new2.name = results[ifc_class][ifc_definition_id]["type_name"] + new2.type = "TYPE" + new2.name = type_data["type_name"] new2.ifc_class = ifc_class new2.total = total2 new2.ifc_definition_id = ifc_definition_id + if ifc_definition_id == 0: + type_is_expanded = ifc_class in expanded_untyped + else: + type_is_expanded = ifc_definition_id in expanded_ifc_ids + new2.is_expanded = type_is_expanded + + if type_is_expanded: + for element in type_data["elements"]: + occurrence = props.elements.add() + occurrence.name = element.Name or "Unnamed" + occurrence.ifc_definition_id = element.id() + occurrence.type = "OCCURRENCE" + total += total2 new.total = total total_elements += total @@ -253,12 +272,12 @@ class Spatial(bonsai.core.tool.Spatial): elements: List[ifcopenshell.entity_instance], ifc_class: str | None, relating_type: ifcopenshell.entity_instance | None, - is_untyped: bool | None, + is_untyped: bool, keyword: str | None, ) -> filter[ifcopenshell.entity_instance]: keyword = keyword.lower() if keyword else keyword - def filter_element(element): + def filter_element(element: ifcopenshell.entity_instance) -> bool: if ifc_class: if not element.is_a(ifc_class): return False @@ -344,14 +363,26 @@ class Spatial(bonsai.core.tool.Spatial): props.contracted_containers = json.dumps(contracted_containers) @classmethod - def toggle_container_element(cls, ifc_class: str) -> None: + def toggle_container_element(cls, element_index: int) -> None: props = bpy.context.scene.BIMSpatialDecompositionProperties - contracted_classes: list[str] = json.loads(props.contracted_classes) - if ifc_class in contracted_classes: - contracted_classes.remove(ifc_class) + expanded_elements: dict[str, list[Union[str, int]]] = json.loads(props.expanded_elements) + element = props.elements[element_index] + if element.type == "CLASS": + element_type = "CLASS" + filtered_item = element.name else: - contracted_classes.append(ifc_class) - props.contracted_classes = json.dumps(contracted_classes) + if element.ifc_definition_id == 0: + element_type = "UNTYPED_CLASSES" + filtered_item = element.ifc_class + else: + element_type = "IFC_ID" + filtered_item = element.ifc_definition_id + expanded_elements_list: list[Union[str, int]] = expanded_elements.setdefault(element_type, []) + if filtered_item in expanded_elements_list: + expanded_elements_list.remove(filtered_item) + else: + expanded_elements_list.append(filtered_item) + props.expanded_elements = json.dumps(expanded_elements) # HERE STARTS SPATIAL TOOL From 691dfb45f0e3646e69ef6568307a94cf14099070 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 18:42:05 +0500 Subject: [PATCH 046/556] bim.activate_model to unhide all objects #5232 Restore old behaviour (new behaviour was introduced by accident in 786b796) where it would unhide all objects instead of keeping only objects in the current drawing. --- src/bonsai/bonsai/bim/module/drawing/operator.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index e606b1e1cb..dd3f94f567 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -1471,9 +1471,17 @@ class ActivateModel(bpy.types.Operator): CutDecorator.uninstall() - # save current visibility statuses + # Preserve current visibility statuses for: + # - non-ifc objects + # - type product + # - annotations (so we won't unhide other drawings) + ifc_file = tool.Ifc.get() visibility_status: dict[bpy.types.Object, bool] = {} + drawing_groups = [g for g in ifc_file.by_type("IfcGroup", include_subtypes=False)] for obj in bpy.data.objects: + element = tool.Ifc.get_entity(obj) + if element and not element.is_a("IfcAnnotation") and not element.is_a("IfcTypeProduct"): + continue visibility_status[obj] = obj.hide_get() if not bpy.app.background: From 42ef06aae1b8966c9f8b18108f2d0ed6863c7cdd Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 28 Aug 2024 11:38:31 +0500 Subject: [PATCH 047/556] typing --- src/bonsai/bonsai/bim/module/drawing/operator.py | 9 +++++---- src/bonsai/bonsai/tool/search.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index dd3f94f567..912d2748ce 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -1883,7 +1883,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): bonsai.bim.handler.refresh_ui_data() return {"FINISHED"} - def set_raster_style(self, context): + def set_raster_style(self, context: bpy.types.Context) -> None: scene = context.scene # Do not remove. It is used in exec later space = self.get_view_3d(context) # Do not remove. It is used in exec later style = json.loads(self.drawing_style.raster_style) @@ -1897,7 +1897,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): # Differences in Blender versions mean result in failures here print(f"Failed to set shading style {path} to {value}") - def set_query(self, context): + def set_query(self, context: bpy.types.Context) -> None: self.include_global_ids = [] self.exclude_global_ids = [] for ifc_file in context.scene.DocProperties.ifc_files: @@ -1916,7 +1916,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): if self.drawing_style.exclude_query: self.parse_filter_query("EXCLUDE", context) - def parse_filter_query(self, mode, context): + def parse_filter_query(self, mode: Literal["INCLUDE", "EXCLUDE"], context: bpy.types.Context) -> None: if mode == "INCLUDE": objects = context.scene.objects elif mode == "EXCLUDE": @@ -1935,7 +1935,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): if global_id in self.exclude_global_ids: obj.hide_viewport = True # Note: this breaks alt-H - def get_view_3d(self, context): + def get_view_3d(self, context: bpy.types.Context) -> bpy.types.Space: for area in context.screen.areas: if area.type != "VIEW_3D": continue @@ -1943,6 +1943,7 @@ class ActivateDrawingStyle(bpy.types.Operator, tool.Ifc.Operator): if space.type != "VIEW_3D": continue return space + assert False, "Space is not found." class RemoveSheet(bpy.types.Operator, tool.Ifc.Operator): diff --git a/src/bonsai/bonsai/tool/search.py b/src/bonsai/bonsai/tool/search.py index e7deaacb94..179ad210dd 100644 --- a/src/bonsai/bonsai/tool/search.py +++ b/src/bonsai/bonsai/tool/search.py @@ -31,6 +31,7 @@ class Search(bonsai.core.tool.Search): return getattr(bpy.context.scene.BIMClashProperties.clash_sets[int(clash_set_index)], ab)[ int(clash_source_index) ].filter_groups + assert False, f"Unsupported module: {module}" @classmethod def import_filter_query(cls, query: str, filter_groups: bpy.types.bpy_prop_collection) -> None: From 03ab88b5bbcb3325d0159c2263b2617f45bd8384 Mon Sep 17 00:00:00 2001 From: Yassine Oualid Date: Wed, 28 Aug 2024 18:54:18 +0100 Subject: [PATCH 048/556] First Draft Cost Schedule Web UI: - display cost schedules - load cost items - Add cost items - Edit cost item names --- src/bonsai/bonsai/bim/data/webui/sioserver.py | 17 +- .../data/webui/static/css/components/card.css | 58 +++ .../bonsai/bim/data/webui/static/js/cost.js | 168 +++++++ .../data/webui/static/js/utilities/costui.js | 464 ++++++++++++++++++ .../bim/data/webui/templates/costing.html | 120 +++++ .../bim/data/webui/templates/drawings.html | 5 + .../bim/data/webui/templates/gantt.html | 8 +- .../bim/data/webui/templates/index.html | 5 + src/bonsai/bonsai/tool/cost.py | 45 ++ src/bonsai/bonsai/tool/web.py | 46 ++ 10 files changed, 934 insertions(+), 2 deletions(-) create mode 100644 src/bonsai/bonsai/bim/data/webui/static/css/components/card.css create mode 100644 src/bonsai/bonsai/bim/data/webui/static/js/cost.js create mode 100644 src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js create mode 100644 src/bonsai/bonsai/bim/data/webui/templates/costing.html diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py index c663e7d091..37dcc81741 100644 --- a/src/bonsai/bonsai/bim/data/webui/sioserver.py +++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py @@ -115,13 +115,21 @@ class BlenderNamespace(socketio.AsyncNamespace): blender_theme = data await sio.emit("theme_data", data, namespace="/web") - # this function will be called when the event demo_data is emitted async def on_demo_data(self, sid, data): print(f"Demo data from Blender client {sid}") blender_messages[sid]["demo_data"] = data await sio.emit("demo_data", {"blenderId": sid, "data": data}, namespace="/web") + async def on_cost_items(self, sid, data): + print(f"Cost items data from Blender client {sid}") + blender_messages[sid]["cost_items"] = data + await sio.emit("cost_items", {"blenderId": sid, "data": data}, namespace="/web") + + async def on_cost_schedules(self, sid, data): + print(f"Cost schedule info from Blender client {sid}") + blender_messages[sid]["cost_schedules"] = data + await sio.emit("cost_schedules", {"blenderId": sid, "data": data}, namespace="/web") async def schedules(request): with open("templates/index.html", "r") as f: @@ -129,6 +137,12 @@ async def schedules(request): html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version}) return web.Response(text=html_content, content_type="text/html") +async def costing(request): + with open("templates/costing.html", "r") as f: + template = f.read() + html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version}) + return web.Response(text=html_content, content_type="text/html") + async def sequencing(request): with open("templates/gantt.html", "r") as f: @@ -179,6 +193,7 @@ sio.register_namespace(BlenderNamespace("/blender")) app.router.add_get("/", schedules) app.router.add_get("/documentation", documentation) app.router.add_get("/sequencing", sequencing) +app.router.add_get("/costing", costing) app.router.add_get("/demo", demo) # Add static files diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/components/card.css b/src/bonsai/bonsai/bim/data/webui/static/css/components/card.css new file mode 100644 index 0000000000..152606e72f --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/css/components/card.css @@ -0,0 +1,58 @@ +:root.blender .flex-row { + display: flex; + flex-direction: row; + justify-content: space-between; + } + +/* CSS for the work schedule cards */ +:root.blender #work_schedules { + display: flex; + flex-wrap: wrap; + gap: 20px; + padding: 20px; + } + +:root.blender .card { + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 300px; + margin: 10px; + transition: transform 0.2s; + } + +:root.blender .card:hover { + transform: scale(1.05); + } + +:root.blender .card-body { + padding: 20px; + } + +:root.blender .card-title { + font-size: 1.25rem; + margin-bottom: 10px; + } + +:root.blender .card-text { + font-size: 1rem; + margin-bottom: 20px; + } + +:root.blender .btn-primary { + background-color: #007bff; + border: none; + color: white; + padding: 10px 20px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 1rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s; + } + +:root.blender .btn-primary:hover { + background-color: #0056b3; + } \ No newline at end of file diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js new file mode 100644 index 0000000000..1ea91590ed --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js @@ -0,0 +1,168 @@ +import { CostUI } from './utilities/costui.js'; + +const connectedClients = {}; +let socket; + +$(document).ready(function () { + var defaultTheme = "blender"; + var theme = localStorage.getItem("theme") || defaultTheme; + setTheme(theme); + + connectSocket(); + CostUI.createColorPicker(); +}); + +function connectSocket() { + const url = "ws://localhost:" + SOCKET_PORT + "/web"; + socket = io(url); + + socket.on("blender_connect", handleBlenderConnect); + socket.on("blender_disconnect", handleBlenderDisconnect); + socket.on("connected_clients", handleConnectedClients); + socket.on("theme_data", handleThemeData); + socket.on("connect", handleWebConnect); + socket.on("cost_schedules", handleCostSchedulesData); + socket.on("cost_items", handleCostItemsData); +} + +function handleBlenderConnect(blenderId) { + if (!connectedClients.hasOwnProperty(blenderId)) { + connectedClients[blenderId] = { shown: false, ifc_file: "" }; + } + + $("#blender-count").text(function (i, text) { + return parseInt(text, 10) + 1; + }); +} + +function handleBlenderDisconnect(blenderId) { + if (connectedClients.hasOwnProperty(blenderId)) { + delete connectedClients[blenderId]; + removeTableElement(blenderId); + } + + $("#blender-count").text(function (i, text) { + return parseInt(text, 10) - 1; + }); +} + + + +function handleConnectedClients(data) { + $("#blender-count").text(data.length); + + data.forEach(function (id) { + connectedClients[id] = { shown: false, ifc_file: "" }; + }); +} + +function handleThemeData(themeData) { + function arrayToRgbString(arr) { + const [r, g, b, a] = arr.map((num) => Math.round(num * 255)); + if (a !== undefined) { + return `rgba(${r}, ${g}, ${b}, ${a})`; + } + return `rgb(${r}, ${g}, ${b})`; + } + + function generateCssVariableRule(theme) { + let cssVariables = ":root.blender {\n"; + for (const key in theme) { + const cssVariableName = `--blender-${key.replace(/_/g, "-")}`; + const cssVariableValue = arrayToRgbString(theme[key]); + cssVariables += ` ${cssVariableName}: ${cssVariableValue};\n`; + } + cssVariables += "}"; + return cssVariables; + } + + const cssRule = generateCssVariableRule(themeData.theme); + + var styleElement = $("#index-stylesheet")[0]; + if (styleElement) { + var sheet = styleElement.sheet || styleElement.styleSheet; + sheet.insertRule(cssRule, sheet.cssRules.length); + } +} + +function setTheme(theme) { + $("html").removeClass("light dark blender").addClass(theme); + $(":root").css("color-scheme", theme); + if (theme === "light") { + $("#toggle-theme").html(''); + } else if (theme === "dark") { + $("#toggle-theme").html(''); + } else if (theme === "blender") { + $("#toggle-theme").html(''); + } + localStorage.setItem("theme", theme); +} + +function addCostItem(costItemId) { + console.log("addCostItem", costItemId); + executeOperator({ type: "addCostItem", costItemId: costItemId }); +} + +function editCostItemName(costItemId, name) { + executeOperator({ type: "editCostItemName", costItemId: costItemId, name: name }); +} + +function selectAssignedElements(costItemId) { + executeOperator({ type: "selectAssignedElements", costItemId: costItemId }); +} + +function handleWebConnect() { + getCostSchedules(); +} + +function handleCostSchedulesData(data) { + const blenderId = data.blenderId; + const costSchedules = data.data["cost_schedules"]["cost_schedules"]; + const currency = data.data["cost_schedules"]["currency"]["name"]; + + console.log(data.data["cost_schedules"]); + + const costScheduleDiv = $("#cost-schedules"); + + costSchedules.forEach((costSchedule) => { + costSchedule.UpdateDate = new Date(costSchedule.UpdateDate); + const mainContainer = CostUI.text("Updated On: " + costSchedule.UpdateDate); + const callback = () => loadCostSchedule(costSchedule.id, blenderId); + + + const card = CostUI.createCard(costSchedule.Name, mainContainer, callback); + costScheduleDiv.append(card); + }); +} + +function handleCostItemsData(data) { + console.log(data); + CostUI.createCostSchedule({ + data: data.data["cost_items"], + blenderID: data.blenderId, + callbacks: { + "addCostItem": addCostItem, + "selectAssignedElements": selectAssignedElements, + 'editCostItemName': editCostItemName, + } + }); +} + +function executeOperator(operator, blenderId) { + const msg = { + sourcePage: "cost", + operator: operator, + }; + if (blenderId !== undefined) { + msg.BlenderId = blenderId; + } + socket.emit("web_operator", msg); +} + +function loadCostSchedule(costScheduleId, blenderId) { + executeOperator({ type: "loadCostSchedule", costScheduleId: costScheduleId }, blenderId); +} + +function getCostSchedules(blenderId) { + executeOperator({ type: "getCostSchedules" }, blenderId); +} diff --git a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js new file mode 100644 index 0000000000..61779592bd --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js @@ -0,0 +1,464 @@ +export class CostUI { + constructor() {} + + createButton() { + console.log("Button created"); + } + + createInput() { + console.log("Input created"); + } + + static isCostScheduleLoaded(id) { + const existingTable = document.getElementById('cost-items-' + id); + return existingTable !== null; + } + + static removeCostSchedule(id) { + document.getElementById("cost-items-" + id).remove(); + } + static createTable(id) { + CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null; + + const table = document.createElement("table"); + table.id = 'cost-items-' + id; + const tbody = document.createElement("tbody"); + tbody.setAttribute("id", "cost-items"); + + const columnHeaders = ["Name", "Quantity", "Unit", "Cost", "Total Cost", "Action"]; + const tr = document.createElement("tr"); + + for (let i = 0; i < columnHeaders.length; i++) { + const th = document.createElement("th"); + th.textContent = columnHeaders[i]; + th.style.position = "relative"; // Required for the resizer handle + tr.appendChild(th); + + // Add resizer handle + if (i < columnHeaders.length - 1) { // No resizer for the last column + const resizer = document.createElement("div"); + resizer.classList.add("resizer"); + th.appendChild(resizer); + CostUI.addResizer(resizer); + } + } + + tbody.appendChild(tr); + table.appendChild(tbody); + document.getElementById("cost-items").appendChild(table); + + // Add CSS to set column widths, resizer styles, hover effect, and color scheme + CostUI.addTableStyles(id); + + // Create context menu + CostUI.createContextMenu(); + + table.get_blender_id = function() { + return this.getAttribute("id").split("-")[2]; + }; + + return [table, tbody]; + } + + static addTableStyles(id) { + const style = document.createElement("style"); + style.textContent = ` + #cost-items-${id} th:nth-child(1), + #cost-items-${id} td:nth-child(1) { + width: auto; + } + #cost-items-${id} th:not(:nth-child(1)), + #cost-items-${id} td:not(:nth-child(1)) { + width: 100px; /* Set a fixed width for other columns */ + } + th { + position: relative; + } + .resizer { + position: absolute; + right: 0; + top: 0; + width: 5px; + height: 100%; + cursor: col-resize; + user-select: none; + } + .context-menu { + display: none; + position: absolute; + background-color: white; + border: 1px solid #ccc; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + z-index: 1000; + } + .context-menu button { + display: block; + width: 100%; + padding: 8px; + border: none; + background: none; + text-align: left; + cursor: pointer; + } + .context-menu button:hover { + background-color: #f0f0f0; + } + #cost-items tr:hover { + background-color: #f0f0f0; /* Change this color to your desired hover color */ + } + `; + document.head.appendChild(style); + } + + static createContextMenu() { + // Create context menu + const contextMenu = document.createElement("div"); + contextMenu.id = "context-menu"; + contextMenu.classList.add("context-menu"); + contextMenu.innerHTML = ` + + + + `; + document.body.appendChild(contextMenu); + + // Add event listeners for context menu + document.addEventListener("contextmenu", function(event) { + event.preventDefault(); + const targetRow = event.target.closest("tr"); + if (targetRow && targetRow.parentElement.id === "cost-items") { + const contextMenu = document.getElementById("context-menu"); + contextMenu.style.display = "block"; + contextMenu.style.left = `${event.pageX}px`; + contextMenu.style.top = `${event.pageY}px`; + + // Store the target row in the context menu for later use + contextMenu.targetRow = targetRow; + } else { + document.getElementById("context-menu").style.display = "none"; + } + }); + + document.addEventListener("click", function(event) { + const contextMenu = document.getElementById("context-menu"); + if (!contextMenu.contains(event.target)) { + contextMenu.style.display = "none"; + } + }); + + document.getElementById("edit-button").addEventListener("click", function() { + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + // Implement your edit action here + console.log("Edit row:", targetRow.getAttribute("id")); + } + }); + + document.getElementById("delete-button").addEventListener("click", function() { + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + // Implement your delete action here + console.log("Delete row:", targetRow.getAttribute("id")); + targetRow.remove(); + } + }); + + document.getElementById("duplicate-button").addEventListener("click", function() { + const targetRow = document.getElementById("context-menu").targetRow; + if (targetRow) { + // Implement your duplicate action here + console.log("Duplicate row:", targetRow.getAttribute("id")); + const newRow = targetRow.cloneNode(true); + targetRow.parentElement.appendChild(newRow); + } + }); + } + + static addResizer(resizer) { + let startX, startWidth, th; + + resizer.addEventListener("mousedown", function(e) { + th = e.target.parentElement; + startX = e.pageX; + startWidth = th.offsetWidth; + document.addEventListener("mousemove", resizeColumn); + document.addEventListener("mouseup", stopResize); + }); + + function resizeColumn(e) { + const newWidth = startWidth + (e.pageX - startX); + th.style.width = newWidth + "px"; + } + + function stopResize() { + document.removeEventListener("mousemove", resizeColumn); + document.removeEventListener("mouseup", stopResize); + } + } + + static generateColorScheme(baseColor) { + // This function generates a color scheme based on the base color + // For simplicity, we'll just lighten the base color for each level + const levels = 7; // Number of levels of nesting + const colorScheme = []; + for (let i = 0; i < levels; i++) { + colorScheme.push(CostUI.lightenColor(baseColor, i * 7)); + } + return colorScheme; + } + + static lightenColor(color, percent) { + // This function lightens a color by a given percentage + const num = parseInt(color.slice(1), 16), + amt = Math.round(2.55 * percent), + R = (num >> 16) + amt, + G = (num >> 8 & 0x00FF) + amt, + B = (num & 0x0000FF) + amt; + return `#${(0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1).toUpperCase()}`; + } + + static applyColorScheme(tableId, colorScheme) { + const rows = document.querySelectorAll(`#${tableId} tbody tr`); + rows.forEach((row, index) => { + const level = index % colorScheme.length; // Assuming level is determined by index for simplicity + row.style.backgroundColor = colorScheme[level]; + }); + } + + + static createColorPicker() { + const colorPicker = document.createElement("input"); + colorPicker.type = "color"; + colorPicker.id = "color-picker"; + colorPicker.value = "#ff0000"; // Default color + const colorText = document.createElement("p"); + colorText.textContent = "Select a color to change row color:"; + document.getElementById("UI").appendChild(colorText); + document.getElementById("UI").appendChild(colorPicker); + + colorPicker.addEventListener("input", function() { + const baseColor = colorPicker.value; + const colorScheme = CostUI.generateColorScheme(baseColor); + CostUI.applyColorScheme("cost-items", colorScheme); + }); + const colorScheme = CostUI.generateColorScheme("#000000"); + CostUI.applyColorScheme("cost-items", colorScheme); + } + + static createCostSchedule({ data, blenderID, title, callbacks = {} }) { + const [table, tbody] = CostUI.createTable(blenderID); + CostUI.createCostItem(data, tbody, 0, null, callbacks); + CostUI.applyExpandedState(); + } + + static createCostItem(data, container, nestingLevel = 0, parentID = null, callbacks = {}) { + data.forEach(obj => { + const row = CostUI.createRow(obj, nestingLevel, parentID, callbacks); + container.appendChild(row); + + if (obj.is_nested_by && obj.is_nested_by.length > 0) { + CostUI.createCostItem(obj.is_nested_by, container, nestingLevel + 1, obj.id, callbacks); + } + }); + } + + static createRow(obj, nestingLevel, parentID, callbacks = {}) { + const row = document.createElement("tr"); + row.setAttribute("id", obj.id); + row.setAttribute("parent-id", parentID); + if (nestingLevel > 0) { + row.classList.add("nested"); + row.classList.add(`level-${nestingLevel}`); + } + const expandButton = document.createElement("button"); + expandButton.classList.add("toggle-button"); + if (obj.is_nested_by && obj.is_nested_by.length > 0) { + expandButton.textContent = ">"; + } else { + expandButton.style.visibility = "hidden"; + } + //row.appendChild(expandButton); + + expandButton.addEventListener("click", function() { + CostUI.contractExpandRow.call(this, obj.id); + }); + + const nameCell = document.createElement("td"); + const nameInput = document.createElement("input"); + nameInput.value = obj.name ? obj.name : "Unnamed"; + + nameInput.addEventListener("change", function() { + callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null; + }); + + nameInput.addEventListener("keydown", function(event) { + if (event.key === "Enter") { + callbacks.editCostItemName ? callbacks.editCostItemName(obj.id, this.value) : null; + } + }); + nameCell.style.paddingLeft = nestingLevel * 20 + "px"; + nameCell.appendChild(expandButton); + nameCell.appendChild(nameInput); + row.appendChild(nameCell); + + const totalCostQuantityCell = document.createElement("td"); + totalCostQuantityCell.textContent = obj.TotalCostQuantity; + row.appendChild(totalCostQuantityCell); + + const unitSymbolCell = document.createElement("td"); + unitSymbolCell.textContent = obj.UnitSymbol; + row.appendChild(unitSymbolCell); + + const totalAppliedValueCell = document.createElement("td"); + totalAppliedValueCell.textContent = obj.TotalAppliedValue; + row.appendChild(totalAppliedValueCell); + + const totalCostCell = document.createElement("td"); + const totalCost = parseFloat(obj.TotalCost).toFixed(2); + + totalCostCell.textContent = obj.is_sum ? totalCost + " (Σ)" : totalCost; + + row.appendChild(totalCostCell); + + const divFlex = document.createElement("div"); + divFlex.classList.add("flex-container"); + const addButton = document.createElement("button"); + addButton.textContent = "+"; + addButton.classList.add("add-button"); + addButton.addEventListener("click", function(e) { + e.stopPropagation(); + callbacks.addCostItem ? callbacks.addCostItem(obj.id) : null; + }); + + const selectButton = document.createElement("button"); + selectButton.textContent = "Select"; + selectButton.addEventListener("click", function(e) { + e.stopPropagation(); + callbacks.selectAssignedElements ? callbacks.selectAssignedElements(obj.id) : null; + }); + + divFlex.appendChild(addButton); + divFlex.appendChild(selectButton); + + const flexContainerCell = document.createElement("td"); + flexContainerCell.appendChild(divFlex); + row.appendChild(flexContainerCell); + + row.get_id = function() { + return this.getAttribute("id"); + }; + + row.get_parent = function() { + const parentId = this.getAttribute("parent-id"); + return parentId ? document.getElementById(parentId) : null; + }; + + return row; + } + + static hideNestedRows(parentId) { + const rows = document.querySelectorAll(`[parent-id='${parentId}']`); + rows.forEach(row => { + row.classList.add("nested"); + const childId = row.getAttribute('id'); + if (childId) { + CostUI.hideNestedRows(childId); + } + }); + } + + static showNestedRows(parentId) { + const rows = document.querySelectorAll(`[parent-id='${parentId}']`); + rows.forEach(row => { + row.classList.remove("nested"); + const childId = row.getAttribute('id'); + if (childId && CostUI.isRowExpanded(childId)) { + CostUI.showNestedRows(childId); + } + }); + } + + static contractExpandRow(id) { + const rows = document.querySelectorAll(`[parent-id='${id}']`); + if (rows.length === 0) { + return; + } + + let isVisible = false; + rows.forEach(row => { + if (!row.classList.contains("nested")) { + isVisible = true; + } + }); + + if (isVisible) { + CostUI.hideNestedRows(id); + this.textContent = ">"; + CostUI.updateExpandedState(id, false); + } else { + CostUI.showNestedRows(id); + this.textContent = "^"; + CostUI.updateExpandedState(id, true); + } + } + + static updateExpandedState(id, isExpanded) { + const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {}; + expandedState[id] = isExpanded; + localStorage.setItem('expandedState', JSON.stringify(expandedState)); + } + + static applyExpandedState() { + const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {}; + Object.keys(expandedState).forEach(id => { + if (expandedState[id]) { + CostUI.showNestedRows(id); + const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`); + if (toggleButton) { + toggleButton.textContent = "^"; + } + } else { + CostUI.hideNestedRows(id); + const toggleButton = document.querySelector(`[id='${id}'] .toggle-button`); + if (toggleButton) { + toggleButton.textContent = ">"; + } + } + }); + } + + static isRowExpanded(id) { + const expandedState = JSON.parse(localStorage.getItem('expandedState')) || {}; + return expandedState[id] || false; + } + + static text(label) { + const text = document.createElement("p"); + text.textContent = label; + return text; + } + + static createCard(title, mainContainer, callback) { + const card = document.createElement("div"); + card.classList.add("card"); + + const cardBody = document.createElement("div"); + cardBody.classList.add("card-body"); + + const cardTitle = document.createElement("h5"); + cardTitle.classList.add("card-title"); + cardTitle.textContent = title; + + const cardButton = document.createElement("button"); + cardButton.classList.add("btn", "btn-primary"); + cardButton.textContent = "Load"; + cardButton.addEventListener("click", callback); + + cardBody.appendChild(cardTitle); + cardBody.appendChild(mainContainer); + cardBody.appendChild(cardButton); + card.appendChild(cardBody); + + return card; + } +} diff --git a/src/bonsai/bonsai/bim/data/webui/templates/costing.html b/src/bonsai/bonsai/bim/data/webui/templates/costing.html new file mode 100644 index 0000000000..6bd6ddeee7 --- /dev/null +++ b/src/bonsai/bonsai/bim/data/webui/templates/costing.html @@ -0,0 +1,120 @@ + + + + + + BlenderBIM Web UI + + + + + + + + + + + + + +
+ +
+
+
+
+
+ +
+

BlenderBIM Version: {{version}}

+
+ + diff --git a/src/bonsai/bonsai/bim/data/webui/templates/drawings.html b/src/bonsai/bonsai/bim/data/webui/templates/drawings.html index 858f1edcf9..d45d9b51bf 100644 --- a/src/bonsai/bonsai/bim/data/webui/templates/drawings.html +++ b/src/bonsai/bonsai/bim/data/webui/templates/drawings.html @@ -53,6 +53,11 @@
  • Schedules
  • +
  • + Costing +
  • Construction Sequencing var SOCKET_PORT = {{port}}; - +
    -
    -
    + +