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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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}};
-
+