diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index f77ffaa117..75d7fe5fa6 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -730,6 +730,10 @@ if(MSVC)
# endif()
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE)
+ # See #5158.
+ if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40)
+ add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
+ endif()
else()
add_definitions(-Wall -Wextra)
diff --git a/nix/build-all.py b/nix/build-all.py
index 8f63612434..756b6b3c1b 100644
--- a/nix/build-all.py
+++ b/nix/build-all.py
@@ -966,7 +966,7 @@ if "IfcOpenShell-Python" in targets:
logger.info(f"\rBuilding python {python_version} wrapper... ")
- run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "_ifcopenshell_wrapper"], cwd=python_dir)
+ run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
if python_executable:
diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile
index 524ac9f91c..8a67f787de 100644
--- a/src/bonsai/Makefile
+++ b/src/bonsai/Makefile
@@ -81,7 +81,7 @@ BLENDER_PLATFORM:=windows-x64
endif
# Current build commit hash.
-OLD:=03935a9
+OLD:=f5e02d1
.PHONY: bump
bump:
cd . && $(SED) -b "s/$(OLD)/$(NEW)/" Makefile
diff --git a/src/bonsai/bonsai/bim/data/webui/sioserver.py b/src/bonsai/bonsai/bim/data/webui/sioserver.py
index 91cec745e5..e80173c474 100644
--- a/src/bonsai/bonsai/bim/data/webui/sioserver.py
+++ b/src/bonsai/bonsai/bim/data/webui/sioserver.py
@@ -183,10 +183,10 @@ class BlenderNamespace(socketio.AsyncNamespace):
blender_messages[sid]["predefined_types"] = data
await sio.emit("predefined_types", {"blenderId": sid, "data": data}, namespace="/web")
- async def on_selected_products(self, sid, data):
+ async def on_quantities(self, sid, data):
print(f"Selected products from Blender client {sid}")
- blender_messages[sid]["selected_products"] = data
- await sio.emit("selected_products", {"blenderId": sid, "data": data}, namespace="/web")
+ blender_messages[sid]["quantities"] = data
+ await sio.emit("quantities", {"blenderId": sid, "data": data}, namespace="/web")
async def schedules(request):
with open("templates/index.html", "r") as f:
diff --git a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css
index bf2a85a9df..eac13d1945 100644
--- a/src/bonsai/bonsai/bim/data/webui/static/css/cost.css
+++ b/src/bonsai/bonsai/bim/data/webui/static/css/cost.css
@@ -20,6 +20,22 @@ body {
box-sizing: border-box;
}
+.floating-form {
+ width: 50vw;
+ max-height: 80vh;
+ position: absolute;
+ background-color: var(--background-color);
+ border: 1px solid black;
+ z-index: 9999;
+ cursor: move;
+ user-select: none;
+ padding-bottom: var(--padding-medium);
+}
+
+[id^="enable-editing-quantities"].floating-form {
+ height: 70vh;
+}
+
.form-header {
display: flex;
justify-content: flex-start;
@@ -30,37 +46,44 @@ body {
font-size: large;
border-bottom: 1px solid #ddd;
border-radius: 6px 6px 0 0;
+ height: 5%;
}
-.floating-form span {
- padding: var(--padding-small);
- margin-left: var(--margin-small);
+.form-container {
+ height: 95%;
+ padding-left: var(--padding-medium);
+ padding-right: var(--padding-medium);
}
-.floating-form {
- width: 50vw;
- max-height: 80vh;
- position: absolute;
- background-color: var(--background-color);
- border: 1px solid black;
- z-index: 9999;
- cursor: move;
- user-select: none;
- resize: both;
+.form-section {
+ margin-bottom: 1em;
+ height: 65%;
+ overflow-y: auto;
+ overflow-x: hidden;
}
+.form-section h3 {
+ margin-bottom: 0.5em;
+}
+
+.summary-section {
+ margin-top: 1em;
+}
+
+.summary-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 14px;
+ text-align: left;
+}
+
+
.floating-form table tr {
margin-bottom: 10px;
}
-
-.form-container {
- padding: var(--padding-medium);
- overflow-y: auto;
- overflow-x: hidden;
-}
-
.action-button {
+ position:relative;
background-color: var(--button-background);
border: none;
color: #fff;
@@ -73,6 +96,32 @@ body {
transition: background-color 0.3s ease;
width: fit-content;
margin: 0.5em;
+ z-index: 11000;
+}
+
+.action-button::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: -30px;
+ background-color: #333;
+ color: #fff;
+ padding: 5px 10px;
+ border-radius: 5px;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s;
+ font-size: 12px;
+}
+
+.action-button:hover::after {
+ opacity: 1;
+}
+
+.active-btn {
+ background-color: #887821;
+ /* make a nic shadow */
+ box-shadow: 0 0 10px #887821;
}
.action-button:hover {
@@ -142,7 +191,7 @@ body {
}
.floating-form .table-container {
- max-height: 70%;
+ max-height: 80%;
}
#cost-items {
@@ -150,12 +199,10 @@ body {
}
[id^="cost-values-form"] table {
- /* fixed */
max-height: 50%;
table-layout: fixed;
}
-
[id^="selected-products"] .form-container {
height: 50vh;
}
@@ -186,9 +233,9 @@ tbody {
td,
th {
- border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
text-align: center;
+ height: 100%;
}
th {
@@ -224,15 +271,7 @@ select {
color: var(--blender-button-text, var(--text-color));
border-color: var(--blender-button-border, var(--border-color));
transition: filter 0.2s ease;
- border-radius: 6px;
- padding: 6px 8px;
- font-size: 14px;
- line-height: 20px;
transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1);
- margin-left: 0.5em;
- margin-right: 0.5em;
- font-size: var(--fontSize);
- min-width: 50px;
}
input:focus {
@@ -242,6 +281,31 @@ input:focus {
color: #ddd;
}
+#cost-items input, #cost-items
+select {
+ border-radius: 6px;
+ padding: 6px 8px;
+ font-size: 14px;
+ line-height: 20px;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ font-size: var(--fontSize);
+ min-width: 50px;
+}
+
+form input {
+ border-radius: 6px;
+ padding: 3px 4px;
+ font-size: 12px;
+ line-height: 10px;
+ margin-left: 0.2em;
+ margin-right: 0.2em;
+ font-size: inherit;
+ height: inherit;
+ width: inherit;
+ box-sizing: border-box;
+}
+
#cost-items tr:hover,
.highlighted {
background-color: #28a74657;
@@ -304,8 +368,38 @@ form table {
max-height: 50%;
}
-
.subtotal-row {
background-color: var(--button-hover-background);
font-weight: bold;
+}
+
+[id^="cost-items"] th:nth-child(1),
+[id^="cost-items"] td:nth-child(1) {
+ width: auto;
+}
+[id^="cost-items"] th:not(:nth-child(1)),
+[id^="cost-items"] td:not(:nth-child(1)) {
+ width: 10%;
+}
+
+[id^="cost-items"] th:last-child,
+[id^="cost-items"] td:last-child {
+ width: fit-content;
+ min-width: fit-content;
+ max-width: 50%;
+}
+
+.actions-column {
+ transition: opacity 0.3s ease-in-out;
+ opacity: 0;
+}
+
+[id^="cost-items"] tr:hover .actions-column {
+ opacity: 1;
+}
+
+.clickable-cell:hover {
+ border: 1px solid #94a728;
+ border-radius: 6px;
+ cursor: pointer;
}
\ 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
index 22fca33d5a..be078a8d3c 100644
--- a/src/bonsai/bonsai/bim/data/webui/static/js/cost.js
+++ b/src/bonsai/bonsai/bim/data/webui/static/js/cost.js
@@ -28,53 +28,57 @@ function connectSocket() {
socket.on("cost_items", handleCostItemsData);
socket.on("cost_values", handleCostValuesData);
socket.on("cost_value", handleCostValueData);
- socket.on("selected_products", handleSelectedProducts);
+ socket.on("quantities", handleEditQuantities);
}
-function handleSelectedProducts(data) {
- const costItemId = data.data["selected_products"]["cost_item_id"];
- const products = data.data["selected_products"]["selected_products"];
- const assigned_products = data.data["selected_products"]["assigned_products"];
- const quantityNames =
- data.data["selected_products"]["product_quantity_names"];
- const formId = "selected-products-" + costItemId;
- const form = CostUI.Form({
- id: formId,
- name: "Edit Product Assignments for: " + CostUI.getCostItemName(costItemId),
- icon: "fa-solid fa-box",
- });
- const numberOfProducts = CostUI.Text(
- "Selection basket : " + products.length + " products",
- "fa-solid fa-cart-shopping",
- "large"
- );
- form.appendChild(numberOfProducts);
- CostUI.highlightElement(costItemId);
- const selectedProductsTable = CostUI.createProductTable({
- form,
- products,
- quantityNames,
- costItemId,
+function handleEditQuantities(data) {
+ const costItemId = data.data["quantities"]["cost_item_id"];
+ const products = data.data["quantities"]["selected_products"];
+ const assigned_products = data.data["quantities"]["assigned_products"];
+ const quantityNames = data.data["quantities"]["product_quantity_names"];
+ const costQuantities = data.data["quantities"]["cost_quantities"];
+
+ CostUI.enableEditingQuantities({
+ costItemId: costItemId,
+ selectedProducts: products,
+ assignedProducts: assigned_products,
+ quantityNames: quantityNames,
+ costQuantities: costQuantities,
callbacks: {
addProductAssignments: addProductAssignments,
- getSelectedProducts: getSelectedProducts,
+ enableEditingQuantities: enableEditingQuantities,
+ addQuantity: addQuantity,
+ editQuantity: editQuantity,
+ deleteQuantity: deleteQuantity,
},
});
+}
- if (assigned_products.length > 0) {
- const assignedProductsText = CostUI.Text(
- "Assigned Products",
- "fa-solid fa-solid fa-paperclip",
- "large"
- );
- form.appendChild(assignedProductsText);
- const assignmentsTable = CostUI.createAssignmentsTable({
- form,
- products: assigned_products,
- quantityNames,
- costItemId,
- });
- }
+function addQuantity(costItemId, ifcClass) {
+ executeOperator({
+ type: "AddCostItemQuantity",
+ costItemId: costItemId,
+ ifcClass: ifcClass,
+ });
+
+ //CostUI.addQuantity(costItemId, quantityName);
+}
+
+function editQuantity(costItemId, quantityId, attributes) {
+ executeOperator({
+ type: "editCostItemQuantity",
+ costItemId: costItemId,
+ quantityId: quantityId,
+ attributes: attributes,
+ });
+}
+
+function deleteQuantity(costItemId, quantityId) {
+ executeOperator({
+ type: "deleteCostItemQuantity",
+ costItemId: costItemId,
+ quantityId: quantityId,
+ });
}
function handlePredefinedTypes(data) {
@@ -322,9 +326,6 @@ function handleCostSchedulesData(data) {
if (costSchedules.length === 0) {
return;
}
- const currency = data.data["cost_schedules"]["currency"]
- ? data.data["cost_schedules"]["currency"]["name"]
- : "Undefined";
const costScheduleDiv = document.getElementById("cost-schedules");
costScheduleDiv.innerHTML = "";
costSchedules.forEach((costSchedule) => {
@@ -368,8 +369,13 @@ function handleCostItemsData(data) {
loadedSchedules[data.blenderId] = costScheduleId;
CostUI.highlightElement("schedule-" + costScheduleId);
+ const currency = data.data["cost_items"]["currency"]
+ ? data.data["cost_items"]["currency"]["name"]
+ : "Undefined";
+
CostUI.createCostSchedule({
- data: data.data["cost_items"]["cost_items"],
+ costItems: data.data["cost_items"]["cost_items"],
+ currency: currency,
costScheduleId: costScheduleId,
blenderID: data.blenderId,
callbacks: {
@@ -380,13 +386,13 @@ function handleCostItemsData(data) {
editCostItemName: editCostItemName,
enableEditingCostValues: enableEditingCostValues,
addSummaryCostItem: addSummaryCostItem,
- getSelectedProducts: getSelectedProducts,
+ enableEditingQuantities: enableEditingQuantities,
},
});
}
-function getSelectedProducts(costItemId) {
- executeOperator({ type: "getSelectedProducts", costItemId: costItemId });
+function enableEditingQuantities(costItemId) {
+ executeOperator({ type: "enableEditingQuantities", costItemId: costItemId });
}
function duplicateCostItem(costItemId) {
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
index a3f6c76882..7a99b57c8a 100644
--- a/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js
+++ b/src/bonsai/bonsai/bim/data/webui/static/js/utilities/costui.js
@@ -9,7 +9,7 @@ export class CostUI {
static removeCostSchedule(id) {
document.getElementById("cost-items-" + id).remove();
}
- static createTable(id, callbacks) {
+ static createCostTable(id, currency, callbacks) {
CostUI.isCostScheduleLoaded(id) ? CostUI.removeCostSchedule(id) : null;
const table = document.createElement("table");
@@ -21,9 +21,9 @@ export class CostUI {
"Name",
"Quantity",
"Unit",
- "Cost",
- "Total Cost",
- "Action",
+ "Cost (" + currency + ")",
+ "Total Cost (" + currency + ")",
+ "Actions",
];
const thead = document.createElement("thead");
const tr = document.createElement("tr");
@@ -47,7 +47,6 @@ export class CostUI {
table.appendChild(tbody);
document.getElementById("cost-items").appendChild(table);
- CostUI.addTableStyles(id);
CostUI.createContextMenu(callbacks);
table.get_blender_id = function () {
return this.getAttribute("id").split("-")[2];
@@ -56,20 +55,12 @@ export class CostUI {
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 */
- }
-
- `;
- document.head.appendChild(style);
+ static deleteCostItem(costItemId, callback) {
+ const costItemRow = document.getElementById(costItemId);
+ let expandedState = JSON.parse(localStorage.getItem("expandedState")) || {};
+ expandedState = CostUI.deleteCostItemRow(costItemRow, expandedState);
+ localStorage.setItem("expandedState", JSON.stringify(expandedState));
+ callback(costItemId);
}
static createContextMenu(callbacks) {
@@ -139,21 +130,17 @@ export class CostUI {
addButton.dataset.listenerAdded = "true";
}
- const deleteCostItem = document.getElementById("delete-cost-item");
- if (deleteCostItem && !deleteCostItem.hasListener) {
- deleteCostItem.addEventListener("click", function () {
+ const deleteCostItemButton = document.getElementById("delete-cost-item");
+ if (deleteCostItemButton && !deleteCostItemButton.hasListener) {
+ deleteCostItemButton.addEventListener("click", function () {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
- let expandedState =
- JSON.parse(localStorage.getItem("expandedState")) || {};
- expandedState = CostUI.deleteCostItemRow(targetRow, expandedState);
- localStorage.setItem("expandedState", JSON.stringify(expandedState));
- callbacks.deleteCostItem(costItemId);
+ CostUI.deleteCostItem(costItemId, callbacks.deleteCostItem);
}
document.getElementById("context-menu").style.display = "none";
});
- deleteCostItem.hasListener = true;
+ deleteCostItemButton.hasListener = true;
}
const duplicateButton = document.getElementById("duplicate-button");
@@ -175,7 +162,7 @@ export class CostUI {
const targetRow = document.getElementById("context-menu").targetRow;
if (targetRow) {
const costItemId = parseInt(targetRow.getAttribute("id"));
- callbacks.getSelectedProducts(costItemId);
+ callbacks.enableEditingQuantities(costItemId);
}
document.getElementById("context-menu").style.display = "none";
});
@@ -192,7 +179,7 @@ export class CostUI {
document
.getElementById("cost-items")
- .addEventListener("dblclick", function (event) {
+ .addEventListener("click", function (event) {
const targetRow = event.target.closest("tr");
const targetCell = event.target.closest("td");
if (targetRow && targetCell) {
@@ -202,11 +189,16 @@ export class CostUI {
const costItemId = parseInt(targetRow.getAttribute("id"));
const columnName = getColumnNames("cost-items")[columnIndex];
- if (columnName === "Cost") {
+ if (columnName.includes("Cost") && !columnName.includes("Total")) {
callbacks.enableEditingCostValues
? callbacks.enableEditingCostValues(costItemId)
: null;
}
+ if (columnName === "Quantity") {
+ callbacks.enableEditingQuantities
+ ? callbacks.enableEditingQuantities(costItemId)
+ : null;
+ }
}
});
}
@@ -363,13 +355,18 @@ export class CostUI {
}
static createCostSchedule({
- data,
+ costItems,
+ currency,
costScheduleId,
blenderID,
callbacks = {},
}) {
- const [table, tbody] = CostUI.createTable(blenderID, callbacks);
- if (data.length === 0) {
+ const [table, tbody] = CostUI.createCostTable(
+ blenderID,
+ currency,
+ callbacks
+ );
+ if (costItems.length === 0) {
const tr = document.createElement("tr");
const td = document.createElement("td");
td.colSpan = 6;
@@ -387,24 +384,29 @@ export class CostUI {
td.appendChild(addSummaryCostItemButton);
addSummaryCostItemButton.classList.add("action-button");
} else {
- CostUI.createCostItem(data, tbody, 0, null, callbacks);
+ CostUI.createCostTree(costItems, tbody, 0, null, callbacks);
CostUI.applyExpandedState();
}
}
- static createCostItem(
- data,
+ static createCostTree(
+ costItems,
container,
nestingLevel = 0,
parentID = null,
callbacks = {}
) {
- data.forEach((costItem) => {
- const row = CostUI.createRow(costItem, nestingLevel, parentID, callbacks);
+ costItems.forEach((costItem) => {
+ const row = CostUI.addCostItemRow(
+ costItem,
+ nestingLevel,
+ parentID,
+ callbacks
+ );
container.appendChild(row);
if (costItem.is_nested_by && costItem.is_nested_by.length > 0) {
- CostUI.createCostItem(
+ CostUI.createCostTree(
costItem.is_nested_by,
container,
nestingLevel + 1,
@@ -415,7 +417,7 @@ export class CostUI {
});
}
- static createRow(costItem, nestingLevel, parentID, callbacks = {}) {
+ static addCostItemRow(costItem, nestingLevel, parentID, callbacks = {}) {
const totalQuantity = costItem.TotalCostQuantity
? parseFloat(costItem.TotalCostQuantity).toFixed(2)
: "-";
@@ -431,20 +433,21 @@ export class CostUI {
callbacks
);
const totalCostQuantityCell = CostUI.createTableCell(totalQuantity);
+ totalCostQuantityCell.classList.add("clickable-cell");
const unitSymbolCell = CostUI.createTableCell(costItem.UnitSymbol);
const totalAppliedValueCell = CostUI.createTableCell(appliedValue);
+ totalAppliedValueCell.classList.add("clickable-cell");
const totalCostCell = CostUI.createTotalCostCell(costItem);
- const flexContainerCell = CostUI.createFlexContainerCell(
- costItem,
- callbacks
- );
+ const actionsCell = CostUI.costItemActions(costItem, callbacks);
+
+ actionsCell.classList.add("actions-column");
row.appendChild(nameCell);
row.appendChild(totalCostQuantityCell);
row.appendChild(unitSymbolCell);
row.appendChild(totalAppliedValueCell);
row.appendChild(totalCostCell);
- row.appendChild(flexContainerCell);
+ row.appendChild(actionsCell);
row.get_id = function () {
return this.getAttribute("id");
@@ -636,30 +639,92 @@ export class CostUI {
return totalCostCell;
}
- static createFlexContainerCell(costItem, callbacks) {
+ static costItemActions(costItem, callbacks) {
const divFlex = document.createElement("div");
divFlex.classList.add("row-container");
- const selectButton = CostUI.createSelectButton(costItem, callbacks);
- divFlex.appendChild(selectButton);
+ const addCostItem = CostUI.addCostItemButton(
+ costItem.id,
+ callbacks.addCostItem
+ );
+ const selectButton = CostUI.createSelectButton(
+ costItem.id,
+ callbacks.selectAssignedElements
+ );
+ const deleteButton = CostUI.deleteCostItemButton(
+ costItem.id,
+ callbacks.deleteCostItem
+ );
+ const duplicateButton = CostUI.duplicateCostItemButton(
+ costItem.id,
+ callbacks.duplicateCostItem
+ );
- const flexContainerCell = document.createElement("td");
- flexContainerCell.appendChild(divFlex);
- return flexContainerCell;
+ [addCostItem, duplicateButton, selectButton, deleteButton].forEach(
+ (button) => {
+ divFlex.appendChild(button);
+ }
+ );
+
+ const actionsCell = document.createElement("td");
+ actionsCell.appendChild(divFlex);
+ return actionsCell;
}
- static createSelectButton(costItem, callbacks) {
- const selectButton = document.createElement("button");
- selectButton.classList.add("action-button");
- selectButton.textContent = "Select";
- selectButton.addEventListener("click", function (e) {
- e.stopPropagation();
- callbacks.selectAssignedElements
- ? callbacks.selectAssignedElements(costItem.id)
- : null;
- });
+ static duplicateCostItemButton(costItemId, duplicateCostItem) {
+ const duplicateButton = CostUI.createButton(
+ "Duplicate",
+ "fa-solid fa-copy"
+ );
+ duplicateButton.addEventListener(
+ "click",
+ duplicateCostItem.bind(null, costItemId)
+ );
+ return duplicateButton;
+ }
+
+ static addCostItemButton(costItemId, addCostItem) {
+ const addCostItemButton = CostUI.createButton(
+ "Add Sub-Cost",
+ "fa-solid fa-plus"
+ );
+ addCostItemButton.addEventListener(
+ "click",
+ addCostItem.bind(null, costItemId)
+ );
+ return addCostItemButton;
+ }
+
+ static createSelectButton(costItemId, selectAssignedElements) {
+ const selectButton = CostUI.createButton(
+ "Select",
+ "fa-solid fa-arrow-pointer"
+ );
+ selectButton.addEventListener(
+ "click",
+ selectAssignedElements.bind(null, costItemId)
+ );
return selectButton;
}
+ static deleteCostItemButton(costItemId, deleteCostItem) {
+ const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash");
+ deleteButton.addEventListener(
+ "click",
+ CostUI.deleteCostItem.bind(null, costItemId, deleteCostItem)
+ );
+ return deleteButton;
+ }
+
+ static createButton(text, icon) {
+ const button = document.createElement("button");
+ !icon ? (button.textContent = text) : null;
+ icon
+ ? button.classList.add("action-button", ...icon.split(" "))
+ : button.classList.add("action-button");
+ button.dataset.tooltip = text;
+ return button;
+ }
+
static highlightElement(id) {
const element = document.getElementById(id);
if (element) {
@@ -728,11 +793,15 @@ export class CostUI {
}
static getRowNameCell(costItemId) {
- return document.getElementById(costItemId).querySelector("td input");
+ return document.getElementById(costItemId)
+ ? document.getElementById(costItemId).querySelector("td input")
+ : null;
}
static getCostItemName(costItemId) {
- return CostUI.getRowNameCell(costItemId).value;
+ return CostUI.getRowNameCell(costItemId)
+ ? CostUI.getRowNameCell(costItemId).value
+ : "Unnamed";
}
static createCostValuesForm({ costItemId, costValues, callbacks }) {
@@ -775,7 +844,6 @@ export class CostUI {
static addNewCostValueRow(costItemId, costValueId, costValueCallbacks) {
const table = CostUI.getCostValuesTable(costItemId);
if (!table) {
- console.log("Cost values table not found for cost item ID:", costItemId);
return;
}
const tr = CostUI.createCostvaluesRow(
@@ -843,8 +911,8 @@ export class CostUI {
} else if (costValue.applied_value) {
costType = "FIXED";
}
-
- const typeCell = CostUI.createTableDropdown("type", costType);
+ const options = ["FIXED", "CATEGORY", "SUM"];
+ const typeCell = CostUI.createTableDropdown("type", options, costType);
const dropdown = typeCell.querySelector("select");
dropdown.addEventListener("change", function () {
const selectedType = this.value;
@@ -947,12 +1015,11 @@ export class CostUI {
}
}
- static createTableDropdown(name, value = "FIXED") {
+ static createTableDropdown(name, options, value = "FIXED") {
const cell = document.createElement("td");
const dropdown = document.createElement("select");
dropdown.name = name;
- const options = ["FIXED", "CATEGORY", "SUM"];
options.forEach((optionValue) => {
const option = document.createElement("option");
option.value = optionValue;
@@ -1241,7 +1308,7 @@ export class CostUI {
}
static createProductTable({
- form,
+ container,
products,
quantityNames,
costItemId,
@@ -1251,7 +1318,7 @@ export class CostUI {
const noProductsMessage = document.createElement("p");
noProductsMessage.textContent =
"Your Blender Selection is empty! Select objects first.";
- form.appendChild(noProductsMessage);
+ container.appendChild(noProductsMessage);
return;
}
const { tableContainer, table } = CostUI.addTable({
@@ -1259,7 +1326,7 @@ export class CostUI {
className: "",
id: "cost-values-table-" + costItemId,
});
- form.appendChild(tableContainer);
+ container.appendChild(tableContainer);
const tbody = document.createElement("tbody");
table.appendChild(tbody);
@@ -1367,7 +1434,7 @@ export class CostUI {
const productIds = products.map((product) => product.info.id);
callbacks.emptyForm = () => {
- form.innerHTML = "";
+ container.innerHTML = "";
};
const addProductAssignmentsButton = CostUI.addProductAssignmentsButton(
costItemId,
@@ -1376,13 +1443,13 @@ export class CostUI {
callbacks
);
- form.appendChild(quantitySelect);
- form.appendChild(addProductAssignmentsButton);
+ container.appendChild(quantitySelect);
+ container.appendChild(addProductAssignmentsButton);
return table;
}
static createAssignmentsTable({
- form,
+ container,
products,
costItemId,
quantityNames,
@@ -1394,7 +1461,7 @@ export class CostUI {
id: "cost-values-table-" + costItemId,
});
- form.appendChild(tableContainer);
+ container.appendChild(tableContainer);
const tbody = document.createElement("tbody");
table.appendChild(tbody);
@@ -1447,8 +1514,10 @@ export class CostUI {
quantitySelect,
callbacks
) {
- const addProductAssignmentsButton = document.createElement("button");
- addProductAssignmentsButton.textContent = "Add Product Assignments";
+ const addProductAssignmentsButton = CostUI.createButton(
+ "Add Product Assignments",
+ "fa-solid fa-plus"
+ );
addProductAssignmentsButton.addEventListener("click", (event) => {
event.preventDefault();
let propName = quantitySelect.value;
@@ -1463,7 +1532,7 @@ export class CostUI {
})
: null;
callbacks.emptyForm ? callbacks.emptyForm() : null;
- callbacks.getSelectedProducts(costItemId);
+ callbacks.enableEditingQuantities(costItemId);
});
addProductAssignmentsButton.classList.add("action-button");
return addProductAssignmentsButton;
@@ -1479,6 +1548,7 @@ export class CostUI {
const picker = CostUI.createColorPicker();
const tableFontSize = CostUI.createFontSizePicker();
+ const currencyPicker = CostUI.createCurrencyPicker();
settingsMenu.appendChild(picker);
settingsMenu.appendChild(tableFontSize);
document.getElementById("settings-menu").style.display = "none";
@@ -1521,6 +1591,32 @@ export class CostUI {
return div;
}
+ static createCurrencyPicker() {
+ const currency = CostUI.getCurrency();
+ const div = document.createElement("div");
+ const currencyText = document.createElement("p");
+ currencyText.textContent = "Select a currency for the table:";
+ div.appendChild(currencyText);
+ const currencyPicker = document.createElement("input");
+ currencyPicker.type = "text";
+ currencyPicker.id = "currency-picker";
+ currencyPicker.value = currency;
+ div.appendChild(currencyPicker);
+ currencyPicker.addEventListener("input", function () {
+ const currency = currencyPicker.value;
+ CostUI.saveCurrency(currency);
+ });
+ return div;
+ }
+
+ static saveCurrency(currency) {
+ localStorage.setItem("tableCurrency", currency);
+ }
+
+ static getCurrency() {
+ return localStorage.getItem("tableCurrency");
+ }
+
static saveFontSize(fontSize) {
localStorage.setItem("tableFontSize", fontSize);
}
@@ -1637,4 +1733,425 @@ export class CostUI {
},
});
}
+
+ static enableEditingQuantities({
+ costItemId,
+ selectedProducts,
+ assignedProducts,
+ quantityNames,
+ costQuantities,
+ callbacks,
+ }) {
+ CostUI.highlightElement(costItemId);
+ const form = CostUI.Form({
+ id: "enable-editing-quantities-" + costItemId,
+ name: "Editing Quantities for: " + CostUI.getCostItemName(costItemId),
+ icon: "fa-solid fa-box",
+ });
+
+ const ribbonBar = CostUI.createRibbonBar();
+ form.appendChild(ribbonBar);
+
+ const selectedProductsSection = CostUI.selectedProductsSection({
+ products: selectedProducts,
+ quantityNames,
+ costItemId,
+ callbacks,
+ });
+
+ const assignedProductsSection = CostUI.assignedProductsSection({
+ products: assignedProducts,
+ quantityNames,
+ costItemId,
+ callbacks,
+ });
+
+ const manualQuantitiesSection = CostUI.manualQuantitiesSection({
+ costItemId,
+ costQuantities,
+ has_assigned_products: assignedProducts.length > 0,
+ callbacks,
+ });
+
+ form.appendChild(selectedProductsSection);
+ form.appendChild(assignedProductsSection);
+ form.appendChild(manualQuantitiesSection);
+
+ const summarySection = CostUI.createSummarySection({
+ selectedProducts,
+ assignedProducts,
+ costQuantities,
+ });
+ form.appendChild(summarySection);
+
+ CostUI.addEventListeners({
+ switchBar: ribbonBar,
+ costItemId,
+ selectedProductsSection,
+ assignedProductsSection,
+ manualQuantitiesSection,
+ });
+
+ const lastActiveSection =
+ localStorage.getItem("lastActiveSection") || "selected-products";
+ const lastActiveButton = document.getElementById(
+ `${lastActiveSection}-btn`
+ );
+ if (lastActiveButton) {
+ lastActiveButton.click();
+ }
+ }
+
+ static createRibbonBar() {
+ const ribbonBar = document.createElement("div");
+ ribbonBar.className = "switch-bar";
+ ribbonBar.innerHTML = `
+
+
+
+ `;
+ return ribbonBar;
+ }
+
+ static createSummarySection({
+ selectedProducts,
+ assignedProducts,
+ costQuantities,
+ }) {
+ const summarySection = document.createElement("div");
+ summarySection.classList.add("summary-section");
+
+ const manualQuantities = costQuantities.quantities.filter(
+ (quantity) => !quantity.fromProduct
+ );
+ const paramaterQuantities = costQuantities.quantities.filter(
+ (quantity) => quantity.fromProduct
+ );
+
+ const table = document.createElement("table");
+ table.classList.add("summary-table");
+
+ const thead = document.createElement("thead");
+ const headerRow = document.createElement("tr");
+ const headers = ["Category", "Count"];
+ headers.forEach((headerText) => {
+ const th = document.createElement("th");
+ th.textContent = headerText;
+ headerRow.appendChild(th);
+ });
+ thead.appendChild(headerRow);
+ table.appendChild(thead);
+ const tbody = document.createElement("tbody");
+
+ const data = [
+ { category: "Assigned Products", count: assignedProducts.length },
+ { category: "Manual Quantities", count: manualQuantities.length },
+ {
+ category: "Product derived Quantities",
+ count: paramaterQuantities.length,
+ },
+ ];
+
+ data.forEach((item) => {
+ const row = document.createElement("tr");
+ const categoryCell = document.createElement("td");
+ categoryCell.textContent = item.category;
+ const countCell = document.createElement("td");
+ countCell.textContent = item.count;
+ row.appendChild(categoryCell);
+ row.appendChild(countCell);
+ tbody.appendChild(row);
+ });
+
+ table.appendChild(tbody);
+ summarySection.appendChild(table);
+
+ return summarySection;
+ }
+
+ static addEventListeners({
+ switchBar,
+ costItemId,
+ selectedProductsSection,
+ assignedProductsSection,
+ manualQuantitiesSection,
+ }) {
+ const buttons = switchBar.querySelectorAll(".action-button");
+
+ buttons.forEach((button) => {
+ button.addEventListener("click", (e) => {
+ e.preventDefault();
+ const sectionName = button.id.replace("-btn", "");
+ const sectionId = sectionName + "-section-" + costItemId;
+
+ if (selectedProductsSection)
+ selectedProductsSection.style.display = "none";
+ if (assignedProductsSection)
+ assignedProductsSection.style.display = "none";
+ if (manualQuantitiesSection)
+ manualQuantitiesSection.style.display = "none";
+
+ const section = document.getElementById(sectionId);
+ if (section) {
+ section.style.display = "block";
+ }
+ buttons.forEach((btn) => btn.classList.remove("active-btn"));
+ button.classList.add("active-btn");
+
+ localStorage.setItem("lastActiveSection", sectionName);
+ });
+ });
+ }
+
+ static getSection(costItemId, sectionName) {
+ return document.getElementById(`${sectionName}-${costItemId}`);
+ }
+
+ static selectedProductsSection({
+ products,
+ quantityNames,
+ costItemId,
+ callbacks,
+ }) {
+ const selectedProductsSection = document.createElement("div");
+ selectedProductsSection.id = "selected-products-section-" + costItemId;
+ selectedProductsSection.classList.add("form-section");
+ const numberOfProducts = CostUI.Text(
+ "Selection basket : " + products.length + " products",
+ "fa-solid fa-cart-shopping",
+ "medium"
+ );
+ selectedProductsSection.appendChild(numberOfProducts);
+
+ const selectedProductsTable = CostUI.createProductTable({
+ container: selectedProductsSection,
+ products: products,
+ quantityNames,
+ costItemId,
+ callbacks,
+ });
+ return selectedProductsSection;
+ }
+
+ static assignedProductsSection({
+ products,
+ quantityNames,
+ costItemId,
+ callbacks,
+ }) {
+ const assignedProductsSection = document.createElement("div");
+ assignedProductsSection.id = "assigned-products-section-" + costItemId;
+ assignedProductsSection.style.display = "none";
+ assignedProductsSection.classList.add("form-section");
+ if (products.length > 0) {
+ let text = " Assigned Products : " + products.length;
+ const assignedProductsText = CostUI.Text(
+ text,
+ "fa-solid fa-solid fa-paperclip",
+ "medium"
+ );
+ assignedProductsSection.appendChild(assignedProductsText);
+ const assignmentsTable = CostUI.createAssignmentsTable({
+ container: assignedProductsSection,
+ products: products,
+ quantityNames,
+ costItemId,
+ callbacks,
+ });
+ }
+
+ return assignedProductsSection;
+ }
+
+ static manualQuantitiesSection({
+ costItemId,
+ costQuantities,
+ has_assigned_products,
+ callbacks,
+ }) {
+ const manualQuantitiesSection = document.createElement("div");
+ manualQuantitiesSection.classList.add("form-section");
+ manualQuantitiesSection.id = "manual-quantities-section-" + costItemId;
+ manualQuantitiesSection.style.display = "none";
+
+ const title = CostUI.Text(
+ "Manual Quantities",
+ "fa-solid fa-ruler",
+ "medium"
+ );
+ manualQuantitiesSection.appendChild(title);
+ const unitSymbol = costQuantities.unit_symbol;
+
+ let quantityType = costQuantities.quantity_type;
+
+ if (quantityType) {
+ const text = CostUI.Text(
+ quantityType + " (" + unitSymbol + " )",
+ "fa-solid fa-ruler",
+ "small"
+ );
+ manualQuantitiesSection.appendChild(text);
+ } else {
+ // add dropdown to chose quantity type , from "IfcQuantityArea", "IfcQuantityLength", "IfcQuantityVolume", "IfcQuantityCount", "IfcQuantityWeight "
+ const quantityTypes = [
+ "IfcQuantityArea",
+ "IfcQuantityLength",
+ "IfcQuantityVolume",
+ "IfcQuantityCount",
+ "IfcQuantityWeight",
+ ];
+ const dropdown = CostUI.createTableDropdown(
+ "type",
+ quantityTypes,
+ "IfcQuantityArea"
+ );
+ dropdown.id = "quantity-type-dropdown-" + costItemId;
+ manualQuantitiesSection.appendChild(dropdown);
+ }
+
+ // if quantity is ty IfcQuantityCount, and the costQuantities
+ if (quantityType === "IfcQuantityCount" && has_assigned_products) {
+ // write text that one of the quantities is assigned to the product selection
+ const text = CostUI.Text(
+ " One of the quantities is assigned to the product selection",
+ "fa-solid fa-warning",
+ "small"
+ );
+ manualQuantitiesSection.appendChild(text);
+ }
+
+ let paramaterQuantities = costQuantities.quantities.filter(
+ (quantity) => quantity.fromProduct
+ );
+ let manualQuantities = costQuantities.quantities.filter(
+ (quantity) => !quantity.fromProduct
+ );
+
+ let headerNames = ["Name", "Value" + " (" + unitSymbol + " )", "Actions"];
+
+ if (manualQuantities.length > 0) {
+ headerNames = [];
+ Object.entries(manualQuantities[0]).forEach(([key, value]) => {
+ if (key !== "id" && key !== "fromProduct") {
+ headerNames.push(key);
+ }
+ });
+ headerNames.push("Actions");
+ }
+
+ const { tableContainer, table } = CostUI.addTable({
+ headers: headerNames,
+ className: "manual-quantities-table",
+ id: "manual-quantities-table-" + costItemId,
+ });
+
+ manualQuantities.forEach((quantity) => {
+ const tr = CostUI.createManualQuantityRow(
+ costItemId,
+ quantity,
+ callbacks
+ );
+ table.appendChild(tr);
+ });
+ const addButton = CostUI.createAddManualQuantityButton(
+ costItemId,
+ quantityType,
+ callbacks
+ );
+
+ manualQuantitiesSection.appendChild(tableContainer);
+ // manualQuantitiesSection.appendChild(tableContainer2);
+ manualQuantitiesSection.appendChild(addButton);
+ return manualQuantitiesSection;
+ }
+
+ static createManualQuantityRow(costItemId, quantity, callbacks) {
+ const tr = document.createElement("tr");
+ tr.id = quantity.id;
+
+ // get quantity keys and values to create the table row cells
+ // Label cell
+
+ Object.entries(quantity).forEach(([key, value]) => {
+ if (key !== "id" && key !== "fromProduct") {
+ if (key === "Name") {
+ // create input
+ const cell = document.createElement("td");
+ const input = document.createElement("input");
+ input.type = "text";
+ input.value = value;
+ cell.appendChild(input);
+ tr.appendChild(cell);
+
+ input.addEventListener("keydown", function (e) {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ callbacks.editQuantity(costItemId, quantity.id, {
+ [key]: input.value,
+ });
+ }
+ });
+ }
+
+ // if key contains value
+ else if (key.toLowerCase().includes("value")) {
+ const cell = document.createElement("td");
+ const input = document.createElement("input");
+ input.type = "number";
+ input.value = value;
+ cell.appendChild(input);
+
+ input.addEventListener("keydown", function (e) {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ callbacks.editQuantity(costItemId, quantity.id, {
+ [key]: parseFloat(input.value), // Dynamically set the key and parse the value as a double
+ });
+ }
+ });
+ tr.appendChild(cell);
+ } else {
+ const cell = document.createElement("td");
+ cell.textContent = value;
+ tr.appendChild(cell);
+ }
+ }
+ });
+
+ // Add delete button
+ const deleteButton = CostUI.createButton("Delete", "fa-solid fa-trash");
+ const deleteCell = document.createElement("td");
+ deleteButton.addEventListener("click", function (e) {
+ e.preventDefault();
+ callbacks.deleteQuantity(costItemId, quantity.id);
+ tr.remove();
+ });
+ deleteCell.appendChild(deleteButton);
+ tr.appendChild(deleteCell);
+
+ return tr;
+ }
+
+ static createAddManualQuantityButton(costItemId, quantityType, callbacks) {
+ const addButton = CostUI.createButton(
+ "Add Manual Quantity",
+ "fa-solid fa-plus"
+ );
+
+ addButton.addEventListener("click", function (e) {
+ e.preventDefault();
+ let type = quantityType;
+ if (!type) {
+ // get quantityType from dropdown
+ const qtoSection = document.getElementById(
+ "manual-quantities-section-" + costItemId
+ );
+ const dropdown = qtoSection.querySelector("select");
+ type = dropdown ? dropdown.value : null;
+ }
+ callbacks.addQuantity(costItemId, type);
+ });
+
+ return addButton;
+ }
}
diff --git a/src/bonsai/bonsai/bim/module/aggregate/operator.py b/src/bonsai/bonsai/bim/module/aggregate/operator.py
index 187d1cb648..d25343fad8 100644
--- a/src/bonsai/bonsai/bim/module/aggregate/operator.py
+++ b/src/bonsai/bonsai/bim/module/aggregate/operator.py
@@ -76,7 +76,7 @@ class BIM_OT_aggregate_unassign_object(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
- for obj in bpy.context.selected_objects:
+ for obj in tool.Blender.get_selected_objects():
element = tool.Ifc.get_entity(obj)
if not element:
continue
diff --git a/src/bonsai/bonsai/bim/module/debug/__init__.py b/src/bonsai/bonsai/bim/module/debug/__init__.py
index 629bb05dc6..1e19ff678a 100644
--- a/src/bonsai/bonsai/bim/module/debug/__init__.py
+++ b/src/bonsai/bonsai/bim/module/debug/__init__.py
@@ -36,6 +36,7 @@ classes = (
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeUnusedElementsByClass,
+ operator.PurgeUnusedObjects,
operator.RestartBlender,
operator.RewindInspector,
operator.SelectExpressFile,
diff --git a/src/bonsai/bonsai/bim/module/debug/operator.py b/src/bonsai/bonsai/bim/module/debug/operator.py
index 44c4eef5da..83ee47d07f 100644
--- a/src/bonsai/bonsai/bim/module/debug/operator.py
+++ b/src/bonsai/bonsai/bim/module/debug/operator.py
@@ -32,6 +32,8 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit
import bonsai.tool as tool
import bonsai.core.debug as core
+import bonsai.core.profile
+import bonsai.core.type
import bonsai.bim.handler
import bonsai.bim.import_ifc as import_ifc
from pathlib import Path
@@ -608,6 +610,57 @@ class PurgeUnusedElementsByClass(bpy.types.Operator, tool.Ifc.Operator):
tool.Ifc.get().write(self.filepath)
+class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.purge_unused_objects"
+ bl_label = "Purge Unused Objects"
+ bl_options = {"REGISTER", "UNDO"}
+
+ object_type: bpy.props.EnumProperty(
+ name="Object Type",
+ items=(
+ ("TYPE", "Type", ""),
+ ("PROFILE", "Profile", ""),
+ ("STYLE", "Style", ""),
+ ("MATERIAL", "Material", ""),
+ ),
+ )
+
+ def _execute(self, context):
+ object_type = self.object_type
+ if object_type == "TYPE":
+ purged = bonsai.core.type.purge_unused_types(tool.Ifc, tool.Type, tool.Geometry)
+ elif object_type == "PROFILE":
+ purged = bonsai.core.profile.purge_unused_profiles(tool.Ifc, tool.Profile)
+ elif object_type == "STYLE":
+ purged = tool.Debug.purge_unused_class("IfcPresentationStyle")
+ elif object_type == "MATERIAL":
+ ifc_file = tool.Ifc.get()
+ is_ifc2x3 = ifc_file.schema == "IFC2X3"
+ if is_ifc2x3:
+ purged = tool.Debug.purge_unused_class("IfcMaterial")
+ else:
+ purged = tool.Debug.purge_unused_class("IfcMaterialDefinition")
+ else:
+ self.report({"ERROR"}, f"Invalid object type {object_type}.")
+ return {"CANCELLED"}
+
+ self.report({"INFO"}, f"{purged} unused {object_type.lower()}s were purged.")
+
+ if purged == 0:
+ return
+
+ scene = context.scene
+ if object_type == "PROFILE":
+ if scene.BIMProfileProperties.is_editing:
+ bpy.ops.bim.load_profiles()
+ elif object_type == "STYLE":
+ if scene.BIMStylesProperties.is_editing:
+ bpy.ops.bim.load_styles()
+ elif object_type == "MATERIAL":
+ if scene.BIMMaterialProperties.is_editing:
+ bpy.ops.bim.load_materials()
+
+
class PipInstall(bpy.types.Operator):
bl_idname = "bim.pip_install"
bl_label = "Pip Install"
diff --git a/src/bonsai/bonsai/bim/module/drawing/scheduler.py b/src/bonsai/bonsai/bim/module/drawing/scheduler.py
index abe44ffd7a..568c3ca294 100644
--- a/src/bonsai/bonsai/bim/module/drawing/scheduler.py
+++ b/src/bonsai/bonsai/bim/module/drawing/scheduler.py
@@ -555,6 +555,8 @@ class Scheduler:
if text_color:
text_params["fill"] = text_color
+ text_params["class_"] = "schedule"
+
if len(text_lines) == 1 and not wrap_text:
text_params.update(box_alignment_params)
text_tag = self.svg.text(text_lines[0], insert=(x, y), **(text_params))
diff --git a/src/bonsai/bonsai/bim/module/geometry/__init__.py b/src/bonsai/bonsai/bim/module/geometry/__init__.py
index f07aa0b19f..1caba354ba 100644
--- a/src/bonsai/bonsai/bim/module/geometry/__init__.py
+++ b/src/bonsai/bonsai/bim/module/geometry/__init__.py
@@ -36,6 +36,7 @@ classes = (
operator.EnableEditingRepresentationItems,
operator.FlipObject,
operator.GetRepresentationIfcParameters,
+ operator.ImportRepresentationItems,
operator.OverrideDelete,
operator.OverrideDuplicateMove,
operator.OverrideDuplicateMoveLinked,
@@ -60,6 +61,7 @@ classes = (
operator.UpdateParametricRepresentation,
operator.UpdateRepresentation,
prop.RepresentationItem,
+ prop.RepresentationItemObject,
prop.ShapeAspect,
prop.BIMObjectGeometryProperties,
prop.BIMGeometryProperties,
diff --git a/src/bonsai/bonsai/bim/module/geometry/data.py b/src/bonsai/bonsai/bim/module/geometry/data.py
index 5f84838e14..0c8ba8eec1 100644
--- a/src/bonsai/bonsai/bim/module/geometry/data.py
+++ b/src/bonsai/bonsai/bim/module/geometry/data.py
@@ -52,7 +52,11 @@ class ViewportData:
("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0),
("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 1),
]
- if not obj or not tool.Blender.is_editable(obj):
+ if (
+ not obj
+ or not tool.Blender.is_editable(obj)
+ or ((element := tool.Ifc.get_entity(obj)) and tool.Geometry.is_locked(element))
+ ):
return obj_mode
return mesh_modes
diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py
new file mode 100644
index 0000000000..4db123c04e
--- /dev/null
+++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py
@@ -0,0 +1,138 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2024 Dion Moult
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
+
+import blf
+import gpu
+import json
+import bmesh
+import bonsai.tool as tool
+from bpy.types import SpaceView3D
+from mathutils import Vector
+from gpu_extras.batch import batch_for_shader
+from bpy_extras.view3d_utils import location_3d_to_region_2d
+
+
+class ItemDecorator:
+ is_installed = False
+ handlers = []
+
+ @classmethod
+ def install(cls, context):
+ if cls.is_installed:
+ cls.uninstall()
+ handler = cls()
+ cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
+ cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw, (context,), "WINDOW", "POST_VIEW"))
+ cls.is_installed = True
+
+ @classmethod
+ def uninstall(cls):
+ for handler in cls.handlers:
+ try:
+ SpaceView3D.draw_handler_remove(handler, "WINDOW")
+ except ValueError:
+ pass
+ cls.is_installed = False
+
+ def draw_batch(self, shader_type, content_pos, color, indices=None):
+ shader = self.line_shader if shader_type == "LINES" else self.shader
+ batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+ def draw_text(self, context):
+ self.addon_prefs = tool.Blender.get_addon_preferences()
+ selected_elements_color = self.addon_prefs.decorator_color_selected
+ unselected_elements_color = self.addon_prefs.decorator_color_unselected
+ special_elements_color = self.addon_prefs.decorator_color_special
+
+ font_id = 0
+ blf.size(font_id, 12)
+ blf.enable(font_id, blf.SHADOW)
+ color = selected_elements_color
+ blf.color(font_id, *color)
+
+ for item in context.scene.BIMGeometryProperties.item_objs:
+ if (obj := item.obj) and obj.hide_get() == False:
+ if obj.select_get():
+ centroid = obj.matrix_world @ Vector(obj.bound_box[0]).lerp(Vector(obj.bound_box[6]), 0.5)
+ tag = obj.name.split("/")[1]
+ coords_2d = location_3d_to_region_2d(context.region, context.region_data, centroid)
+ if coords_2d:
+ w, h = blf.dimensions(font_id, tag)
+ coords_2d -= Vector((w * 0.5, h * 0.5))
+ blf.position(font_id, coords_2d[0], coords_2d[1], 0)
+ blf.draw(font_id, tag)
+
+ def draw(self, context):
+ def transparent_color(color, alpha=0.3):
+ color = [i for i in color]
+ color[3] = alpha
+ return color
+
+ self.addon_prefs = tool.Blender.get_addon_preferences()
+ selected_elements_color = self.addon_prefs.decorator_color_selected
+ unselected_elements_color = self.addon_prefs.decorator_color_unselected
+ special_elements_color = self.addon_prefs.decorator_color_special
+
+ gpu.state.point_size_set(6)
+ gpu.state.blend_set("ALPHA")
+
+ self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
+ self.line_shader.bind() # required to be able to change uniforms of the shader
+ # POLYLINE_UNIFORM_COLOR specific uniforms
+ self.line_shader.uniform_float("viewportSize", (context.region.width, context.region.height))
+ self.line_shader.uniform_float("lineWidth", 2.0)
+
+ # general shader
+ self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
+
+ selected_verts = []
+ selected_edges = []
+ selected_tris = []
+ unselected_verts = []
+ unselected_edges = []
+ unselected_tris = []
+ for item in context.scene.BIMGeometryProperties.item_objs:
+ if (obj := item.obj) and obj.hide_get() == False:
+ if obj.select_get():
+ if context.mode != "OBJECT":
+ continue
+ edges = selected_edges
+ verts = selected_verts
+ offset = len(selected_verts)
+ selected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices])
+ selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
+ else:
+ offset = len(unselected_verts)
+ unselected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices])
+ unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
+ edges = unselected_edges
+ verts = unselected_verts
+ i = len(verts)
+ edges.extend([[ei + i for ei in e] for e in json.loads(item.edges)])
+ matrix_world = obj.matrix_world
+ verts.extend([matrix_world @ Vector(v) for v in json.loads(item.verts)])
+
+ if unselected_verts:
+ self.draw_batch("LINES", unselected_verts, transparent_color(unselected_elements_color), unselected_edges)
+ self.draw_batch("TRIS", unselected_verts, transparent_color(special_elements_color), unselected_tris)
+
+ if selected_verts:
+ self.draw_batch("LINES", selected_verts, selected_elements_color, selected_edges)
+ self.draw_batch("TRIS", selected_verts, transparent_color(selected_elements_color), selected_tris)
diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py
index 4991f18978..6a7100347f 100644
--- a/src/bonsai/bonsai/bim/module/geometry/operator.py
+++ b/src/bonsai/bonsai/bim/module/geometry/operator.py
@@ -98,10 +98,19 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_origin_set"
+ blender_op = bpy.ops.object.origin_set.get_rna_type()
bl_label = "IFC Origin Set"
+ bl_description = (
+ blender_op.description + ".\nAlso makes sure changes are in sync with IFC (opeartor works only on IFC objects)"
+ )
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
- origin_type: bpy.props.StringProperty()
+ blender_type_prop = blender_op.properties["type"]
+ origin_type: bpy.props.EnumProperty(
+ name=blender_type_prop.name,
+ default=blender_type_prop.default,
+ items=[(i.identifier, i.name, i.description) for i in blender_type_prop.enum_items],
+ )
def _execute(self, context):
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
@@ -635,6 +644,7 @@ class OverrideDelete(bpy.types.Operator):
element = tool.Ifc.get_entity(obj)
if element:
if tool.Geometry.is_locked(element):
+ self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be deleted.")
continue
if ifcopenshell.util.element.get_pset(element, "BBIM_Array"):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
@@ -766,6 +776,7 @@ class OverrideOutlinerDelete(bpy.types.Operator):
for obj in objects_to_delete:
if element := tool.Ifc.get_entity(obj):
if tool.Geometry.is_locked(element):
+ self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be deleted.")
if collection := obj.BIMObjectProperties.collection:
collections_to_delete.discard(collection)
continue
@@ -882,6 +893,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
continue # For now, don't copy drawings until we stabilise a bit more. It's tricky.
elif tool.Geometry.is_locked(element):
obj.select_set(False)
+ self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be duplicated.")
continue
linked_non_ifc_object = linked and not element
@@ -1600,6 +1612,7 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
if not element:
continue
if tool.Geometry.is_locked(element):
+ self.report({"ERROR"}, f"Element '{obj.name}' is locked and cannot be edited.")
obj.select_set(False)
continue
representation = tool.Geometry.get_active_representation(obj)
@@ -2214,3 +2227,96 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera
if not styled_item.Styles:
ifc_file.remove(styled_item)
+
+
+class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
+ bl_idname = "bim.xxx_import_representation_items"
+ bl_label = "Import Representation Items"
+ bl_options = {"REGISTER", "UNDO"}
+
+ def _execute(self, context):
+ obj = context.active_object
+ tool.Geometry.apply_item_ids_as_vertex_groups(obj)
+ tool.Geometry.dissolve_triangulated_edges(obj)
+ bm_dict = self.separate_faces_by_vertex_group(obj)
+ # bm_dict = self.separate_faces_by_id(obj)
+ props = context.scene.BIMGeometryProperties
+ props.item_objs.clear()
+
+ for item_id, bm in bm_dict.items():
+ item_mesh = bpy.data.meshes.new(f"mesh_id_{item_id}")
+ bm.to_mesh(item_mesh)
+ bm.free()
+
+ item = tool.Ifc.get().by_id(item_id)
+ item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh)
+ item_obj.matrix_world = obj.matrix_world
+ item_obj.show_in_front = True
+ bpy.context.collection.objects.link(item_obj)
+
+ new = props.item_objs.add()
+ new.obj = item_obj
+
+ verts = [list(co) for co in item_obj.bound_box]
+ edges = [(0, 3), (3, 7), (7, 4), (4, 0), (0, 1), (3, 2), (7, 6), (4, 5), (1, 2), (2, 6), (6, 5), (5, 1)]
+ new.verts = json.dumps(verts)
+ new.edges = json.dumps(edges)
+
+ def separate_faces_by_id(self, obj):
+ mesh = obj.data
+ bm = bmesh.new()
+ bm.from_mesh(mesh)
+ results = {}
+ face_ids = mesh["ios_item_ids"]
+ unique_ids = set(face_ids)
+
+ for item_id in unique_ids:
+ item_bm = bmesh.new()
+ for face, face_id in zip(bm.faces, face_ids):
+ if face_id == item_id:
+ # Copy the face and its vertices into the new BMesh
+ new_face_verts = [item_bm.verts.new(v.co) for v in face.verts]
+ item_bm.faces.new(new_face_verts)
+ item_bm.verts.ensure_lookup_table()
+ item_bm.faces.ensure_lookup_table()
+ results[item_id] = item_bm
+
+ return results
+
+ def separate_faces_by_vertex_group(self, obj):
+ mesh = obj.data
+ bm = bmesh.new()
+ bm.from_mesh(mesh)
+
+ # Ensure the bmesh has up-to-date vertex weights (vertex groups)
+ bm.verts.layers.deform.verify()
+
+ results = {}
+ for vgroup in obj.vertex_groups:
+ group_bm = bmesh.new()
+ deform_layer = bm.verts.layers.deform.active
+
+ # Iterate over the faces and check if all vertices of the face belong to the vertex group
+ for face in bm.faces:
+ face_in_group = True
+ for vert in face.verts:
+ # Get the vertex groups the vertex belongs to
+ deform = vert[deform_layer]
+
+ # If this vertex does not belong to the current vertex group, mark the face as outside the group
+ if vgroup.index not in deform:
+ face_in_group = False
+ break
+
+ # If all vertices of the face belong to the current vertex group, add the face to the new BMesh
+ if face_in_group:
+ new_face_verts = [group_bm.verts.new(v.co) for v in face.verts]
+ group_bm.faces.new(new_face_verts)
+
+ # Ensure the mesh is valid and doesn't have duplicate elements
+ group_bm.verts.ensure_lookup_table()
+ group_bm.faces.ensure_lookup_table()
+
+ results[int(vgroup.name.split("_")[3])] = group_bm
+
+ return results
diff --git a/src/bonsai/bonsai/bim/module/geometry/prop.py b/src/bonsai/bonsai/bim/module/geometry/prop.py
index 641695a16e..1bb06837d8 100644
--- a/src/bonsai/bonsai/bim/module/geometry/prop.py
+++ b/src/bonsai/bonsai/bim/module/geometry/prop.py
@@ -18,7 +18,7 @@
import bpy
import bonsai.tool as tool
-from bonsai.bim.prop import StrProperty, Attribute
+from bonsai.bim.prop import StrProperty, Attribute, ObjProperty
from bonsai.bim.module.geometry.data import RepresentationsData, ViewportData
from bpy.types import PropertyGroup
from bpy.props import (
@@ -100,6 +100,16 @@ class RepresentationItem(PropertyGroup):
tags: StringProperty(name="Tags")
+class RepresentationItemObject(PropertyGroup):
+ name: StringProperty(name="Name")
+ ifc_definition_id: IntProperty(name="IFC Definition ID")
+ obj: PointerProperty(type=bpy.types.Object)
+ verts: StringProperty(name="Verts")
+ edges: StringProperty(name="Edges")
+ special_verts: StringProperty(name="Special Verts")
+ special_edges: StringProperty(name="Special Edges")
+
+
class ShapeAspect(PropertyGroup):
name: StringProperty(
name="Name",
@@ -137,6 +147,7 @@ class BIMGeometryProperties(PropertyGroup):
should_force_triangulation: BoolProperty(name="Force Triangulation", default=False)
is_changing_mode: BoolProperty(name="Is Changing Mode", default=False)
mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode)
+ item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject)
def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool:
return bool(obj != bpy.context.active_object and obj.data)
diff --git a/src/bonsai/bonsai/bim/module/material/operator.py b/src/bonsai/bonsai/bim/module/material/operator.py
index 2575e62c6c..1e6094e5fb 100644
--- a/src/bonsai/bonsai/bim/module/material/operator.py
+++ b/src/bonsai/bonsai/bim/module/material/operator.py
@@ -32,43 +32,47 @@ from bonsai.bim.module.material.prop import purge as material_prop_purge
from bonsai.bim.ifc import IfcStore
-class LoadMaterials(bpy.types.Operator, tool.Ifc.Operator):
+class LoadMaterials(bpy.types.Operator):
bl_idname = "bim.load_materials"
bl_label = "Load Materials"
bl_description = "Display list of named materials"
bl_options = {"REGISTER", "UNDO"}
- def _execute(self, context):
+ def execute(self, context):
core.load_materials(tool.Material, context.scene.BIMMaterialProperties.material_type)
+ return {"FINISHED"}
-class DisableEditingMaterials(bpy.types.Operator, tool.Ifc.Operator):
+class DisableEditingMaterials(bpy.types.Operator):
bl_idname = "bim.disable_editing_materials"
bl_label = "Disable Editing Materials"
bl_options = {"REGISTER", "UNDO"}
- def _execute(self, context):
+ def execute(self, context):
core.disable_editing_materials(tool.Material)
+ return {"FINISHED"}
-class SelectByMaterial(bpy.types.Operator, tool.Ifc.Operator):
+class SelectByMaterial(bpy.types.Operator):
bl_idname = "bim.select_by_material"
bl_label = "Select By Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
- def _execute(self, context):
+ def execute(self, context):
core.select_by_material(tool.Material, tool.Spatial, material=tool.Ifc.get().by_id(self.material))
+ return {"FINISHED"}
-class EnableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator):
+class EnableEditingMaterial(bpy.types.Operator):
bl_idname = "bim.enable_editing_material"
bl_label = "Enable Editing Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
- def _execute(self, context):
+ def execute(self, context):
core.enable_editing_material(tool.Material, material=tool.Ifc.get().by_id(self.material))
+ return {"FINISHED"}
class EditMaterial(bpy.types.Operator, tool.Ifc.Operator):
@@ -81,14 +85,15 @@ class EditMaterial(bpy.types.Operator, tool.Ifc.Operator):
core.edit_material(tool.Ifc, tool.Material, material=tool.Ifc.get().by_id(self.material))
-class DisableEditingMaterial(bpy.types.Operator, tool.Ifc.Operator):
+class DisableEditingMaterial(bpy.types.Operator):
bl_idname = "bim.disable_editing_material"
bl_label = "Disable Editing Material"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
- def _execute(self, context):
+ def execute(self, context):
core.disable_editing_material(tool.Material)
+ return {"FINISHED"}
class AssignParameterizedProfile(bpy.types.Operator, tool.Ifc.Operator):
@@ -776,13 +781,13 @@ class ContractMaterialCategory(bpy.types.Operator):
return {"FINISHED"}
-class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
+class EnableEditingMaterialStyle(bpy.types.Operator):
bl_idname = "bim.enable_editing_material_style"
bl_label = "Enable Editing Material Style"
bl_options = {"REGISTER", "UNDO"}
material: bpy.props.IntProperty()
- def _execute(self, context):
+ def execute(self, context):
props = bpy.context.scene.BIMMaterialProperties
props.active_material_id = self.material
props.editing_material_type = "STYLE"
@@ -800,6 +805,7 @@ class EnableEditingMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
style = rep.Items[0].Styles[0]
if style.Name: # props.styles only has named styles
props.styles = str(rep.Items[0].Styles[0].id())
+ return {"FINISHED"}
class EditMaterialStyle(bpy.types.Operator, tool.Ifc.Operator):
diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py
index f3160453a8..e96bd74450 100644
--- a/src/bonsai/bonsai/bim/module/model/decorator.py
+++ b/src/bonsai/bonsai/bim/module/model/decorator.py
@@ -301,9 +301,10 @@ class ProfileDecorator:
class PolylineDecorator:
is_installed = False
handlers = []
- mouse_pos = None
- input_panel = None
+ event = None
+
input_type = None
+ input_ui = None
angle_snap_mat = None
angle_snap_loc = None
use_default_container = False
@@ -315,11 +316,8 @@ class PolylineDecorator:
if cls.is_installed:
cls.uninstall()
handler = cls()
- 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.draw_input_panel, (context,), "WINDOW", "POST_PIXEL"))
+ cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_input_ui, (context,), "WINDOW", "POST_PIXEL"))
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
@@ -333,203 +331,27 @@ class PolylineDecorator:
cls.is_installed = False
@classmethod
- def set_mouse_position(cls, event):
- cls.mouse_pos = event.mouse_region_x, event.mouse_region_y
+ def update(cls, event, tool_state, input_ui, snapping_point):
+ cls.event = event
+ cls.tool_state = tool_state
+ cls.input_ui = input_ui
@classmethod
- def set_input_panel(cls, input_panel, input_type):
- cls.input_panel = input_panel
- cls.input_type = input_type
+ def set_input_ui(cls, input_ui):
+ cls.input_ui = input_ui
@classmethod
def set_angle_axis_line(cls, start, end):
cls.axis_start = start
cls.axis_end = end
- @classmethod
- def set_axis_rectangle(cls, corners):
- cls.axis_rectangle = [*corners]
+ # @classmethod
+ # def set_axis_rectangle(cls, corners):
+ # cls.axis_rectangle = [*corners]
@classmethod
- def set_use_default_container(cls, value=False):
- cls.use_default_container = value
-
- @classmethod
- def set_plane(cls, plane_origin, plane_normal):
- cls.plane_origin = plane_origin
- cls.plane_normal = plane_normal
-
- @classmethod
- def set_instructions(cls, instructions):
- cls.instructions = instructions
-
- @classmethod
- def set_snap_info(cls, snap_info):
- cls.snap_info = snap_info
-
- @classmethod
- def calculate_distance_and_angle(cls, context, is_input_on):
-
- try:
- polyline_data = context.scene.BIMModelProperties.polyline_point
- default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
- last_point_data = polyline_data[len(polyline_data) - 1]
- except:
- default_container_elevation = 0
- last_point_data = None
-
- snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
-
- if last_point_data:
- last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
- else:
- last_point = Vector((0, 0, 0))
-
- if is_input_on:
- if cls.use_default_container:
- snap_vector = Vector(
- (float(cls.input_panel["X"]), float(cls.input_panel["Y"]), default_container_elevation)
- )
- else:
- snap_vector = Vector(
- (float(cls.input_panel["X"]), float(cls.input_panel["Y"]), float(cls.input_panel["Z"]))
- )
- else:
- if cls.use_default_container:
- snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
- else:
- snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
-
- second_to_last_point = None
- if len(polyline_data) > 1:
- second_to_last_point_data = polyline_data[len(polyline_data) - 2]
- second_to_last_point = Vector(
- (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
- )
- else:
- # Creates a fake "second to last" point away from the first point but in the same x axis
- # this allows to calculate the angle relative to x axis when there is only one point
- second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
-
- distance = (snap_vector - last_point).length
- if distance > 0:
- angle = tool.Cad.angle_3_vectors(
- second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True
- )
-
- # Round angle to the nearest 0.05
- angle = round(angle / 0.05) * 0.05
-
- if cls.input_panel:
- cls.input_panel["X"] = str(round(snap_vector.x, 3))
- cls.input_panel["Y"] = str(round(snap_vector.y, 3))
- if "Z" in list(cls.input_panel.keys()):
- cls.input_panel["Z"] = str(round(snap_vector.z, 3))
- cls.input_panel["D"] = str(round(distance, 3))
- cls.input_panel["A"] = str(round(angle, 3))
-
- return cls.input_panel
-
- return cls.input_panel
-
- @classmethod
- def calculate_area(cls, context):
- try:
- polyline_data = context.scene.BIMModelProperties.polyline_point
- except:
- return cls.input_panel
-
- if len(polyline_data) < 3:
- return cls.input_panel
-
- points = []
- for data in polyline_data:
- points.append(Vector((data.x, data.y, data.z)))
-
- if points[0] == points[-1]:
- points = points[1:]
-
- # TODO move this to CAD
- # Calculate the normal vector of the plane formed by the first three vertices
- v1, v2, v3 = points[:3]
- normal = (v2 - v1).cross(v3 - v1).normalized()
-
- # Check if all points are coplanar
- is_coplanar = True
- tolerance = 1e-6 # Adjust this value as needed
- for v in points:
- if abs((v - v1).dot(normal)) > tolerance:
- is_coplanar = False
-
- if is_coplanar:
- area = 0
- for i in range(len(points)):
- j = (i + 1) % len(points)
- area += points[i].cross(points[j]).dot(normal)
-
- area = abs(area) / 2
- else:
- area = 0
-
- if "AREA" in list(cls.input_panel.keys()):
- cls.input_panel["AREA"] = str(round(area, 4))
- return cls.input_panel
-
- @classmethod
- def calculate_x_y_and_z(cls, context):
- try:
- polyline_data = context.scene.BIMModelProperties.polyline_point
- default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
- last_point_data = polyline_data[len(polyline_data) - 1]
- last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
- except:
- default_container_elevation = 0
- last_point = Vector((0, 0, 0))
-
- snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
- snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
-
- if cls.use_default_container:
- snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
- else:
- snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
-
- if len(polyline_data) > 1:
- second_to_last_point_data = polyline_data[len(polyline_data) - 2]
- second_to_last_point = Vector(
- (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
- )
- else:
- # Creates a fake "second to last" point away from the first point but in the same x axis
- # this allows to calculate the angle relative to x axis when there is only one point
- second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
-
- distance = float(cls.input_panel["D"])
-
- if distance < 0 or distance > 0:
- angle = radians(float(cls.input_panel["A"]))
-
- rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True)
-
- coords = rot_vector * distance + last_point
-
- x = coords[0]
- y = coords[1]
- z = coords[2]
- if cls.input_panel:
- cls.input_panel["X"] = str(round(x, 3))
- cls.input_panel["Y"] = str(round(y, 3))
- if "Z" in list(cls.input_panel.keys()):
- cls.input_panel["Z"] = str(round(z, 3))
-
- return cls.input_panel
-
- cls.input_panel["X"] = str(round(last_point.x, 3))
- cls.input_panel["Y"] = str(round(last_point.y, 3))
- if "Z" in list(cls.input_panel.keys()):
- cls.input_panel["Z"] = str(round(last_point.z, 3))
-
- return cls.input_panel
+ def set_tool_state(cls, tool_state):
+ cls.tool_state = tool_state
def draw_batch(self, shader_type, content_pos, color, indices=None):
shader = self.line_shader if shader_type == "LINES" else self.shader
@@ -537,21 +359,7 @@ class PolylineDecorator:
shader.uniform_float("color", color)
batch.draw(shader)
- @classmethod
- def format_input_panel_units(cls, context, value):
- unit_system = tool.Drawing.get_unit_system()
- if unit_system == "IMPERIAL":
- precision = context.scene.DocProperties.imperial_precision
- factor = 3.28084
- else:
- precision = None
- factor = 1
- if context.scene.unit_settings.length_unit == "MILLIMETERS":
- factor = 1000
-
- return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True)
-
- def draw_input_panel(self, context):
+ def draw_input_ui(self, context):
texts = {
"D": "Distance: ",
"A": "Angle: ",
@@ -560,6 +368,7 @@ class PolylineDecorator:
"Z": "Z coord:",
"AREA": "Area: ",
}
+ mouse_pos = self.event.mouse_region_x, self.event.mouse_region_y
self.addon_prefs = tool.Blender.get_addon_preferences()
self.font_id = 0
@@ -571,22 +380,21 @@ class PolylineDecorator:
color_highlight = self.addon_prefs.decorator_color_special
offset = 20
new_line = 20
- for i, (key, value) in enumerate(self.input_panel.items()):
+ for i, (key, field_name) in enumerate(texts.items()):
- if key != "A" and key != self.input_type:
- value = float(value)
- formatted_value = self.format_input_panel_units(context, value)
+ if key != self.tool_state.input_type:
+ formatted_value = self.input_ui.get_formatted_value(key)
else:
- formatted_value = value
+ formatted_value = self.input_ui.get_text_value(key)
- if key not in list(texts.keys()):
+ if formatted_value is None:
continue
- if key == self.input_type:
+ if key == self.tool_state.input_type:
blf.color(self.font_id, *color_highlight)
else:
blf.color(self.font_id, *color)
- blf.position(self.font_id, self.mouse_pos[0] + offset, self.mouse_pos[1] - (new_line * i), 0)
- blf.draw(self.font_id, texts[key] + formatted_value)
+ blf.position(self.font_id, mouse_pos[0] + offset, mouse_pos[1] - (new_line * i), 0)
+ blf.draw(self.font_id, field_name + formatted_value)
def draw_measurements(self, context):
region = context.region
@@ -608,9 +416,7 @@ class PolylineDecorator:
pos_dim = (Vector(measurement_prop[i].position) + Vector(measurement_prop[i - 1].position)) / 2
coords_dim = view3d_utils.location_3d_to_region_2d(region, rv3d, pos_dim)
- value = measurement_prop[i].dim
- value = float(value)
- formatted_value = self.format_input_panel_units(context, value)
+ formatted_value = measurement_prop[i].dim
blf.position(self.font_id, coords_dim[0], coords_dim[1], 0)
blf.draw(self.font_id, "d: " + formatted_value)
@@ -622,28 +428,6 @@ 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
- font_size = tool.Blender.scale_font_size(12)
- blf.size(self.font_id, font_size)
- blf.enable(self.font_id, blf.SHADOW)
- blf.shadow(self.font_id, 6, 0, 0, 0, 1)
- color = self.addon_prefs.decorations_colour
- blf.color(self.font_id, *color)
-
- 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)
-
- 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):
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -676,7 +460,7 @@ class PolylineDecorator:
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
projection_point = []
- if self.use_default_container:
+ if self.tool_state.use_default_container:
# When a point is above the plane it projects the point
# to the plane and creates a line
if snap_prop.snap_type != "Plane" and snap_prop.z != 0:
@@ -702,18 +486,18 @@ class PolylineDecorator:
self.line_shader.uniform_float("lineWidth", 0.75)
self.draw_batch("LINES", [self.axis_start, self.axis_end], decorator_color_unselected, [(0, 1)])
- try:
- self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)])
- except:
- pass
+ # try:
+ # self.draw_batch("TRIS", self.axis_rectangle, (1, 1, 1, 0.1), [(0, 1, 3), (0, 2, 3)])
+ # except:
+ # pass
# Area highlight
- if "AREA" in list(self.input_panel.keys()):
- if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0:
- edges = []
- for i in range(1, len(polyline_points) - 1):
- edges.append((0, i, i + 1))
- self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges)
+ # if "AREA" in list(self.input_panel.keys()): # TODO Change to input_ui
+ # if self.input_panel["AREA"] and float(self.input_panel["AREA"]) > 0: # TODO Change to input_ui
+ # edges = []
+ # for i in range(1, len(polyline_points) - 1):
+ # edges.append((0, i, i + 1))
+ # self.draw_batch("TRIS", polyline_points, (0, 1, 0, 0.1), edges)
# Mouse points
if snap_prop.snap_type in ["Face", "Plane"]:
diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py
new file mode 100644
index 0000000000..a828309696
--- /dev/null
+++ b/src/bonsai/bonsai/bim/module/model/polyline.py
@@ -0,0 +1,353 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2024 Bruno Perdigão
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
+
+import bpy
+import copy
+import math
+import bmesh
+import ifcopenshell
+import ifcopenshell.api
+import ifcopenshell.util.unit
+import ifcopenshell.util.element
+import ifcopenshell.util.placement
+import ifcopenshell.util.representation
+import ifcopenshell.util.type
+import mathutils.geometry
+import bonsai.core.type
+import bonsai.core.root
+import bonsai.core.geometry
+import bonsai.core.model as core
+import bonsai.tool as tool
+from bonsai.bim.ifc import IfcStore
+from math import pi, sin, cos, degrees
+from mathutils import Vector, Matrix
+from bonsai.bim.module.model.opening import FilledOpeningGenerator
+from bonsai.bim.module.model.decorator import PolylineDecorator
+from typing import Optional
+from lark import Lark, Transformer
+
+
+class PolylineOperator:
+ # TODO Fill doc strings
+ """ """
+
+ @classmethod
+ def poll(cls, context):
+ return context.space_data.type == "VIEW_3D"
+
+ def __init__(self):
+ self.mousemove_count = 0
+ self.action_count = 0
+ self.visible_objs = []
+ self.objs_2d_bbox = []
+ self.number_options = {
+ "0",
+ "1",
+ "2",
+ "3",
+ "4",
+ "5",
+ "6",
+ "7",
+ "8",
+ "9",
+ " ",
+ ".",
+ "+",
+ "-",
+ "*",
+ "/",
+ "'",
+ '"',
+ "=",
+ }
+ self.number_input = []
+ self.number_output = ""
+ self.number_is_negative = False
+ self.input_options = ["D", "A", "X", "Y"]
+ self.input_type = None
+ self.input_type = None
+ self.input_value_xy = [None, None]
+ self.input_ui = tool.Polyline.create_input_ui()
+ self.is_typing = False
+ self.snap_angle = None
+ self.snapping_points = []
+ self.instructions = """TAB: Cycle Input
+ D: Distance Input
+ A: Angle Input
+ M: Modify Snap Point
+ C: Close Polyline
+ BACKSPACE: Remove Point
+ X, Y: Choose Axis
+ SHIFT: Lock axis
+ """
+ self.snap_info = """
+ Snap:
+ Axis:
+ Plane:
+ """
+
+ self.tool_state = tool.Polyline.create_tool_state()
+
+ def recalculate_inputs(self, context):
+ if self.number_input:
+ is_valid, self.number_output = tool.Polyline.validate_input(self.number_output, self.input_type)
+ self.input_ui.set_value(self.input_type, self.number_output)
+ if not is_valid:
+ self.report({"WARNING"}, "The number typed is not valid.")
+ return is_valid
+ else:
+ if self.input_type in {"X", "Y"}:
+ tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
+ elif self.input_type in {"D", "A"}:
+ tool.Polyline.calculate_x_y_and_z(context, self.input_ui, self.tool_state)
+ tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
+ else:
+ self.input_ui.set_value(self.input_type, self.number_output)
+ tool.Blender.update_viewport()
+ return is_valid
+
+ def choose_axis(self, event, x=True, y=True, z=False):
+ if x:
+ if event.value == "PRESS" and event.type == "X":
+ self.tool_state.axis_method = "X" if self.tool_state.axis_method != event.type else None
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ if y:
+ if event.value == "PRESS" and event.type == "Y":
+ self.tool_state.axis_method = "Y" if self.tool_state.axis_method != event.type else None
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+ if z:
+ if event.value == "PRESS" and event.type == "Z":
+ self.tool_state.axis_method = "Z" if self.tool_state.axis_method != event.type else None
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ def choose_plane(self, event, x=True, y=True, z=True):
+ if x:
+ if event.shift and event.value == "PRESS" and event.type == "X":
+ self.tool_state.use_default_container = False
+ self.tool_state.plane_method = "YZ"
+ self.tool_state.axis_method = None
+ tool.Blender.update_viewport()
+
+ if y:
+ if event.shift and event.value == "PRESS" and event.type == "Y":
+ self.tool_state.use_default_container = False
+ self.tool_state.plane_method = "XZ"
+ self.tool_state.axis_method = None
+ tool.Blender.update_viewport()
+
+ if z:
+ if event.shift and event.value == "PRESS" and event.type == "Z":
+ self.tool_state.use_default_container = False
+ self.tool_state.plane_method = "XY"
+ self.tool_state.axis_method = None
+ tool.Blender.update_viewport()
+
+ def handle_instructions(self, context):
+ self.snap_info = f"""|
+ Axis: {self.tool_state.axis_method}
+ Plane: {self.tool_state.plane_method}
+ Snap: {self.snapping_points[0][1]}
+ """
+ context.workspace.status_text_set(self.instructions + self.snap_info)
+
+ def handle_keyboard_input(self, context, event):
+
+ if self.tool_state.is_input_on and event.value == "PRESS" and event.type == "TAB":
+ self.recalculate_inputs(context)
+ index = self.input_options.index(self.input_type)
+ size = len(self.input_options)
+ self.input_type = self.input_options[((index + 1) % size)]
+ self.tool_state.input_type = self.input_options[((index + 1) % size)]
+ self.tool_state.mode = "Select"
+ self.is_typing = False
+ self.number_input = self.input_ui.get_formatted_value(self.input_type)
+ self.number_input = list(self.number_input)
+ self.number_output = "".join(self.number_input)
+ self.input_ui.set_value(self.input_type, self.number_output)
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ if not self.tool_state.is_input_on and event.value == "RELEASE" and event.type == "TAB":
+ self.recalculate_inputs(context)
+ self.tool_state.mode = "Select"
+ self.tool_state.is_input_on = True
+ self.input_type = "D"
+ self.tool_state.input_type = "D"
+ self.is_typing = False
+ self.number_input = self.input_ui.get_formatted_value(self.input_type)
+ self.number_input = list(self.number_input)
+ self.number_output = "".join(self.number_input)
+ self.input_ui.set_value(self.input_type, self.number_output)
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ if not self.tool_state.is_input_on and event.ascii in self.number_options:
+ self.recalculate_inputs(context)
+ self.tool_state.mode = "Edit"
+ self.tool_state.is_input_on = True
+ self.input_type = "D"
+ self.tool_state.input_type = "D"
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ if event.value == "RELEASE" and event.type in {"D", "A"}:
+ self.recalculate_inputs(context)
+ self.tool_state.mode = "Edit"
+ self.tool_state.is_input_on = True
+ self.input_type = event.type
+ self.tool_state.input_type = event.type
+ self.input_ui.set_value(self.input_type, "")
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ if self.input_type in self.input_options:
+ if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"):
+ if not self.tool_state.mode == "Edit" and not (event.ascii == "=" or event.type == "BACK_SPACE"):
+ self.number_input = []
+
+ if event.type == "BACK_SPACE":
+ if len(self.number_input) <= 1:
+ self.number_input = []
+ else:
+ self.number_input.pop(-1)
+ elif event.ascii == "=":
+ if self.number_input[0] == "=":
+ self.number_input.pop(0)
+ else:
+ self.number_input.insert(0, "=")
+ else:
+ self.number_input.append(event.ascii)
+
+ if not self.number_input:
+ self.number_output = "0"
+
+ self.tool_state.mode = "Edit"
+ self.is_typing = True
+ self.number_output = "".join(self.number_input)
+ self.input_ui.set_value(self.input_type, self.number_output)
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ def handle_inserting_polyline(self, context, event):
+ if event.value == "RELEASE" and event.type == "LEFTMOUSE":
+ tool.Snap.insert_polyline_point(self.input_ui)
+ tool.Blender.update_viewport()
+
+ if event.value == "PRESS" and event.type == "C":
+ tool.Snap.close_polyline()
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ if (
+ self.tool_state.is_input_on
+ and event.value == "RELEASE"
+ and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
+ ):
+ is_valid = self.recalculate_inputs(context)
+ if is_valid:
+ tool.Snap.insert_polyline_point(self.input_ui)
+ self.tool_state.mode = "Mouse"
+ self.tool_state.is_input_on = False
+ self.input_type = None
+ self.tool_state.input_type = None
+ self.number_input = []
+ self.number_output = ""
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ def handle_snap_selection(self, context, event):
+ if event.value == "PRESS" and event.type == "M":
+ self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
+ tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+
+ def handle_cancelation(self, context, event):
+ if self.tool_state.is_input_on:
+ if event.value == "RELEASE" and event.type in {"ESC"}:
+ self.recalculate_inputs(context)
+ self.tool_state.mode = "Mouse"
+ self.tool_state.is_input_on = False
+ self.input_type = None
+ self.tool_state.input_type = None
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+ else:
+ if event.value == "RELEASE" and event.type in {"ESC"}:
+ self.tool_state.axis_method = None
+ context.workspace.status_text_set(text=None)
+ PolylineDecorator.uninstall()
+ tool.Snap.clear_polyline()
+ tool.Blender.update_viewport()
+ return {"CANCELLED"}
+
+ def handle_mouse_move(self, context, event):
+ if not self.tool_state.is_input_on:
+ if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
+ self.mousemove_count += 1
+ self.tool_state.mode = "Mouse"
+ self.tool_state.is_input_on = False
+ self.input_type = None
+ self.tool_state.input_type = None
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Snap.clear_snapping_ref()
+ tool.Blender.update_viewport()
+ else:
+ self.mousemove_count = 0
+
+ if self.mousemove_count == 2:
+ self.objs_2d_bbox = []
+ for obj in self.visible_objs:
+ self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
+
+ if self.mousemove_count > 3:
+ detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
+ self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
+ tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
+ tool.Blender.update_viewport()
+ return {"RUNNING_MODAL"}
+
+ if event.value == "RELEASE" and event.type == "BACK_SPACE":
+ tool.Snap.remove_last_polyline_point()
+ tool.Blender.update_viewport()
+
+ def invoke(self, context, event):
+ PolylineDecorator.install(context)
+ tool.Snap.clear_snapping_point()
+
+ self.tool_state.use_default_container = False
+ self.tool_state.axis_method = None
+ self.tool_state.plane_method = None
+ self.tool_state.mode = "Mouse"
+ tool.Snap.set_tool_state(self.tool_state)
+ self.visible_objs = tool.Raycast.get_visible_objects(context)
+ for obj in self.visible_objs:
+ self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
+ detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
+ self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
+ tool.Polyline.calculate_distance_and_angle(context, self.input_ui, self.tool_state)
+
+ PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
+ tool.Blender.update_viewport()
+ context.window_manager.modal_handler_add(self)
+ # return {"RUNNING_MODAL"}
diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py
index 21b57f8f4f..a5ddd83a7a 100644
--- a/src/bonsai/bonsai/bim/module/model/wall.py
+++ b/src/bonsai/bonsai/bim/module/model/wall.py
@@ -38,6 +38,7 @@ from math import pi, sin, cos, degrees
from mathutils import Vector, Matrix
from bonsai.bim.module.model.opening import FilledOpeningGenerator
from bonsai.bim.module.model.decorator import PolylineDecorator
+from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Optional
from lark import Lark, Transformer
@@ -284,7 +285,7 @@ def recalculate_dumb_wall_origin(wall, new_origin=None):
child.matrix_parent_inverse = wall.matrix_world.inverted()
-class DrawPolylineWall(bpy.types.Operator):
+class DrawPolylineWall(bpy.types.Operator, PolylineOperator):
bl_idname = "bim.draw_polyline_wall"
bl_label = "Draw Polyline Wall"
bl_options = {"REGISTER", "UNDO"}
@@ -294,66 +295,7 @@ class DrawPolylineWall(bpy.types.Operator):
return context.space_data.type == "VIEW_3D"
def __init__(self):
- self.mousemove_count = 0
- self.action_count = 0
- self.visible_objs = []
- self.objs_2d_bbox = []
- self.number_options = {
- "0",
- "1",
- "2",
- "3",
- "4",
- "5",
- "6",
- "7",
- "8",
- "9",
- " ",
- ".",
- "+",
- "-",
- "*",
- "/",
- "'",
- '"',
- "=",
- }
- self.number_input = []
- self.number_output = ""
- self.number_is_negative = False
- self.is_input_on = False
- self.input_options = ["D", "A", "X", "Y"]
- self.input_type = "OFF"
- self.input_value_xy = [None, None]
- 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:
- is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type)
- self.input_panel[self.input_type] = self.number_output
- if not is_valid:
- self.report({"WARNING"}, "The number typed is not valid.")
- return is_valid
- else:
- if self.input_type in {"X", "Y"}:
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- elif self.input_type in {"D", "A"}:
- self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- else:
- self.input_panel[self.input_type] = self.number_output
- tool.Blender.update_viewport()
- return is_valid
+ super().__init__()
# TODO This is creating a hack in generate function from DumbWallGenerator
# Come up with a better solution
@@ -382,187 +324,44 @@ class DrawPolylineWall(bpy.types.Operator):
DumbWallJoiner().join_V(wall1["obj"], wall2["obj"])
def modal(self, context, event):
+ if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
+ return {"PASS_THROUGH"}
- if not self.is_input_on:
- if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
- self.mousemove_count += 1
- self.is_input_on = False
- self.input_type = "OFF"
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Snap.clear_snapping_ref()
- tool.Blender.update_viewport()
- else:
- self.mousemove_count = 0
+ self.handle_instructions(context)
- if self.mousemove_count == 2:
- self.objs_2d_bbox = []
- for obj in self.visible_objs:
- self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
+ self.handle_mouse_move(context, event)
- if self.mousemove_count > 3:
- detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
- self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
- PolylineDecorator.set_mouse_position(event)
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- tool.Blender.update_viewport()
- return {"RUNNING_MODAL"}
+ self.choose_axis(event)
- if event.value == "RELEASE" and event.type == "BACK_SPACE":
- tool.Snap.remove_last_polyline_point()
- tool.Blender.update_viewport()
+ self.handle_snap_selection(context, event)
- if event.value == "RELEASE" and event.type == "LEFTMOUSE":
- 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)
- tool.Blender.update_viewport()
-
- if self.is_input_on and event.value == "PRESS" and event.type == "TAB":
- self.recalculate_inputs(context)
- index = self.input_options.index(self.input_type)
- size = len(self.input_options)
- self.input_type = self.input_options[((index + 1) % size)]
-
- self.number_input = self.input_panel[self.input_type]
- self.number_input = list(self.number_input)
- self.number_output = "".join(self.number_input)
- if self.input_type != "A":
- self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
-
- self.input_panel[self.input_type] = self.number_output
-
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB":
- self.recalculate_inputs(context)
- self.is_input_on = True
- self.input_type = "D"
-
- self.number_input = self.input_panel[self.input_type]
- self.number_input = list(self.number_input)
- self.number_output = "".join(self.number_input)
- self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
- self.input_panel[self.input_type] = self.number_output
-
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if not self.is_input_on and event.ascii in self.number_options:
- self.recalculate_inputs(context)
- self.is_input_on = True
- self.input_type = "D"
- self.number_input = []
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if event.value == "RELEASE" and event.type in {"D", "A"}:
- self.recalculate_inputs(context)
- self.is_input_on = True
- self.input_type = event.type
- self.number_input = []
- self.input_panel[self.input_type] = ""
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if self.input_type in self.input_options:
- if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"):
- if event.type == "BACK_SPACE":
- if len(self.number_input) <= 1:
- self.number_input = []
- else:
- self.number_input = self.number_input[:-1]
- self.number_output = "".join(self.number_input)
- if not self.number_input:
- self.number_output = "0"
- self.input_panel[self.input_type] = self.number_output
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
- else:
- self.number_input.append(event.ascii)
- self.number_output = "".join(self.number_input)
-
- if self.number_input:
- self.input_panel[self.input_type] = self.number_output
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
+ if (
+ not self.tool_state.is_input_on
+ and event.value == "RELEASE"
+ and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
+ ):
self.create_walls_from_polyline(context)
+ context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
return {"FINISHED"}
- if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
- is_valid = self.recalculate_inputs(context)
- if is_valid:
- tool.Snap.insert_polyline_point(self.input_panel)
- self.is_input_on = False
- self.input_type = "OFF"
- self.number_input = []
- self.number_output = ""
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
+ self.handle_keyboard_input(context, event)
- if event.value == "PRESS" and event.type == "M":
- self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
- PolylineDecorator.set_mouse_position(event)
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- tool.Blender.update_viewport()
+ self.handle_inserting_polyline(context, event)
- if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
- return {"PASS_THROUGH"}
-
- if self.is_input_on:
- if event.value == "RELEASE" and event.type in {"ESC"}:
- self.recalculate_inputs(context)
- self.is_input_on = False
- self.input_type = "OFF"
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- 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()
- return {"CANCELLED"}
+ result = self.handle_cancelation(context, event)
+ if result is not None:
+ return result
return {"RUNNING_MODAL"}
def invoke(self, context, event):
- if context.space_data.type == "VIEW_3D":
- PolylineDecorator.install(context)
- 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:
- self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
- detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
- self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
- PolylineDecorator.set_mouse_position(event)
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- tool.Blender.update_viewport()
- context.window_manager.modal_handler_add(self)
- return {"RUNNING_MODAL"}
- else:
- self.report({"WARNING"}, "Active space must be a View3d")
- return {"CANCELLED"}
+ super().invoke(context, event)
+ self.tool_state.use_default_container = True
+ self.tool_state.plane_method = "XY"
+ return {"RUNNING_MODAL"}
class DumbWallAligner:
diff --git a/src/bonsai/bonsai/bim/module/profile/__init__.py b/src/bonsai/bonsai/bim/module/profile/__init__.py
index d06bc1c78c..a9a4895884 100644
--- a/src/bonsai/bonsai/bim/module/profile/__init__.py
+++ b/src/bonsai/bonsai/bim/module/profile/__init__.py
@@ -30,7 +30,6 @@ classes = (
operator.EnableEditingArbitraryProfile,
operator.EnableEditingProfile,
operator.LoadProfiles,
- operator.PurgeUnusedProfiles,
operator.RemoveProfileDef,
prop.Profile,
prop.BIMProfileProperties,
diff --git a/src/bonsai/bonsai/bim/module/profile/data.py b/src/bonsai/bonsai/bim/module/profile/data.py
index 2402192aad..2ea97a9a20 100644
--- a/src/bonsai/bonsai/bim/module/profile/data.py
+++ b/src/bonsai/bonsai/bim/module/profile/data.py
@@ -29,6 +29,7 @@ def refresh():
class ProfileData:
data = {}
+ failed_previews: set[int] = set()
preview_collection = bpy.utils.previews.new()
is_loaded = False
diff --git a/src/bonsai/bonsai/bim/module/profile/operator.py b/src/bonsai/bonsai/bim/module/profile/operator.py
index 2c0a7faee8..cb7cd27491 100644
--- a/src/bonsai/bonsai/bim/module/profile/operator.py
+++ b/src/bonsai/bonsai/bim/module/profile/operator.py
@@ -18,6 +18,7 @@
import bpy
import ifcopenshell.api
+import ifcopenshell.util.element
import bonsai.bim.helper
import bonsai.tool as tool
import bonsai.bim.module.model.profile as model_profile
@@ -97,17 +98,18 @@ class RemoveProfileDef(bpy.types.Operator, tool.Ifc.Operator):
props.active_profile_index = min(current_index, len(props.profiles) - 1)
-class EnableEditingProfile(bpy.types.Operator, tool.Ifc.Operator):
+class EnableEditingProfile(bpy.types.Operator):
bl_idname = "bim.enable_editing_profile"
bl_label = "Enable Editing Profile"
bl_options = {"REGISTER", "UNDO"}
profile: bpy.props.IntProperty()
- def _execute(self, context):
+ def execute(self, context):
props = context.scene.BIMProfileProperties
props.profile_attributes.clear()
bonsai.bim.helper.import_attributes2(tool.Ifc.get().by_id(self.profile), props.profile_attributes)
props.active_profile_id = self.profile
+ return {"FINISHED"}
class DisableEditingProfile(bpy.types.Operator):
@@ -149,6 +151,7 @@ class AddProfileDef(bpy.types.Operator, tool.Ifc.Operator):
profile = ifcopenshell.api.run("profile.add_arbitrary_profile", tool.Ifc.get(), profile=points)
else:
profile = ifcopenshell.api.run("profile.add_parameterized_profile", tool.Ifc.get(), ifc_class=profile_class)
+ tool.Profile.set_default_profile_attrs(profile)
profile.ProfileName = "New Profile"
bpy.ops.bim.load_profiles()
@@ -174,12 +177,12 @@ class DuplicateProfileDef(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.load_profiles()
-class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
+class EnableEditingArbitraryProfile(bpy.types.Operator):
bl_idname = "bim.enable_editing_arbitrary_profile"
bl_label = "Enable Editing Arbitrary Profile"
bl_options = {"REGISTER", "UNDO"}
- def _execute(self, context):
+ def execute(self, context):
props = context.scene.BIMProfileProperties
active_profile = props.profiles[props.active_profile_index]
profile_id = active_profile.ifc_definition_id
@@ -192,6 +195,7 @@ class EnableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.object.mode_set(mode="EDIT")
ProfileDecorator.install(context, exit_edit_mode_callback=lambda: disable_editing_arbitrary_profile(context))
tool.Blender.set_viewport_tool("bim.cad_tool")
+ return {"FINISHED"}
def disable_editing_arbitrary_profile(context):
@@ -210,13 +214,14 @@ def disable_editing_arbitrary_profile(context):
refresh()
-class DisableEditingArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
+class DisableEditingArbitraryProfile(bpy.types.Operator):
bl_idname = "bim.disable_editing_arbitrary_profile"
bl_label = "Disable Editing Arbitrary Profile"
bl_options = {"REGISTER", "UNDO"}
- def _execute(self, context):
- return disable_editing_arbitrary_profile(context)
+ def execute(self, context):
+ disable_editing_arbitrary_profile(context)
+ return {"FINISHED"}
class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
@@ -262,18 +267,3 @@ class EditArbitraryProfile(bpy.types.Operator, tool.Ifc.Operator):
props.active_arbitrary_profile_id = 0
model_profile.DumbProfileRegenerator().regenerate_from_profile_def(profile)
-
-
-class PurgeUnusedProfiles(bpy.types.Operator, tool.Ifc.Operator):
- bl_idname = "bim.purge_unused_profiles"
- bl_label = "Purge Unused Profiles"
- bl_options = {"REGISTER", "UNDO"}
-
- def _execute(self, context):
- props = context.scene.BIMProfileProperties
- purged_profiles = core.purge_unused_profiles(tool.Ifc, tool.Profile)
- self.report({"INFO"}, f"{purged_profiles} profiles were purged.")
-
- if props.is_editing:
- refresh()
- bpy.ops.bim.load_profiles()
diff --git a/src/bonsai/bonsai/bim/module/profile/prop.py b/src/bonsai/bonsai/bim/module/profile/prop.py
index afe21b1446..9fa4f5e360 100644
--- a/src/bonsai/bonsai/bim/module/profile/prop.py
+++ b/src/bonsai/bonsai/bim/module/profile/prop.py
@@ -81,14 +81,24 @@ def generate_thumbnail_for_active_profile():
if not props.profiles:
bpy.ops.bim.load_profiles()
- profile_id = props.profiles[props.active_profile_index].ifc_definition_id
+ active_profile = tool.Profile.get_active_profile_ui()
+ assert active_profile
+ profile_id = active_profile.ifc_definition_id
profile = ifc_file.by_id(profile_id)
# generate image
size = 128
img = Image.new("RGBA", (size, size))
draw = ImageDraw.Draw(img)
- tool.Profile.draw_image_for_ifc_profile(draw, profile, size)
+
+ try:
+ tool.Profile.draw_image_for_ifc_profile(draw, profile, size)
+ except RuntimeError as e:
+ print(f"Failed to generate preview image for profile '{profile}': '{e}'.")
+ ProfileData.failed_previews.add(profile_id)
+ return
+
+ ProfileData.failed_previews.discard(profile_id)
pixels = [item for sublist in img.getdata() for item in sublist]
# save generated image to preview collection
diff --git a/src/bonsai/bonsai/bim/module/profile/ui.py b/src/bonsai/bonsai/bim/module/profile/ui.py
index 56c5aefe96..23eecc0037 100644
--- a/src/bonsai/bonsai/bim/module/profile/ui.py
+++ b/src/bonsai/bonsai/bim/module/profile/ui.py
@@ -43,19 +43,22 @@ class BIM_PT_profiles(Panel):
self.props = context.scene.BIMProfileProperties
active_profile = None
- if self.props.is_editing and self.props.profiles and self.props.active_profile_index < len(self.props.profiles):
+ if self.props.is_editing and (active_profile := tool.Profile.get_active_profile_ui()):
preview_collection = ProfileData.preview_collection
box = self.layout.box()
- active_profile = self.props.profiles[self.props.active_profile_index]
profile_id = active_profile.ifc_definition_id
- profile_id_str = str(profile_id)
- if profile_id_str in preview_collection:
- preview_image = preview_collection[profile_id_str]
- else:
- preview_image = preview_collection.new(profile_id_str)
- generate_thumbnail_for_active_profile()
- box.template_icon(icon_value=preview_image.icon_id, scale=5)
+ if profile_id in ProfileData.failed_previews:
+ box.label(text="Failed to load preview (invalid profile).", icon="ERROR")
+ else:
+ profile_id_str = str(profile_id)
+ if profile_id_str in preview_collection:
+ preview_image = preview_collection[profile_id_str]
+ else:
+ preview_image = preview_collection.new(profile_id_str)
+ generate_thumbnail_for_active_profile()
+
+ box.template_icon(icon_value=preview_image.icon_id, scale=5)
row = self.layout.row(align=True)
row.label(text=f"{ProfileData.data['total_profiles']} Named Profiles", icon="ITALIC")
diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py
index f8acb4d7e1..178eaa49e0 100644
--- a/src/bonsai/bonsai/bim/module/project/operator.py
+++ b/src/bonsai/bonsai/bim/module/project/operator.py
@@ -53,6 +53,7 @@ from ifcopenshell.geom import ShapeElementType
from bonsai.bim.module.project.data import LinksData
from bonsai.bim.module.project.decorator import ProjectDecorator, ClippingPlaneDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator
+from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union
@@ -819,6 +820,7 @@ class LoadProjectElements(bpy.types.Operator):
tool.Project.set_default_context()
tool.Project.set_default_modeling_dimensions()
tool.Root.reload_grid_decorator()
+ tool.Root.reload_item_decorator()
return {"FINISHED"}
def get_decomposition_elements(self):
@@ -2268,7 +2270,7 @@ if bpy.app.version >= (4, 1, 0):
return True
-class MeasureTool(bpy.types.Operator):
+class MeasureTool(bpy.types.Operator, PolylineOperator):
bl_idname = "bim.measure_tool"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Measure Tool"
@@ -2278,271 +2280,55 @@ class MeasureTool(bpy.types.Operator):
return context.space_data.type == "VIEW_3D"
def __init__(self):
- self.mousemove_count = 0
- self.action_count = 0
- self.visible_objs = []
- self.objs_2d_bbox = []
- self.number_options = {
- "0",
- "1",
- "2",
- "3",
- "4",
- "5",
- "6",
- "7",
- "8",
- "9",
- " ",
- ".",
- "+",
- "-",
- "*",
- "/",
- "'",
- '"',
- "=",
- }
- self.number_input = []
- self.number_output = ""
- self.number_is_negative = False
- self.is_input_on = False
+ super().__init__()
+ self.input_ui = tool.Polyline.create_input_ui(init_z=True)
self.input_options = ["D", "A", "X", "Y", "Z"]
- self.input_type = None
- self.input_value_xy = [None, None]
- self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""}
- self.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:
- is_valid, self.number_output = tool.Snap.validate_input(self.number_output, self.input_type)
- self.input_panel[self.input_type] = self.number_output
- if not is_valid:
- self.report({"WARNING"}, "The number typed is not valid.")
- return is_valid
- else:
- if self.input_type in {"X", "Y", "Z"}:
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- elif self.input_type in {"D", "A"}:
- self.input_panel = PolylineDecorator.calculate_x_y_and_z(context)
- # self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- else:
- self.input_panel[self.input_type] = self.number_output
- tool.Blender.update_viewport()
- return is_valid
+ D: Distance Input
+ A: Angle Input
+ M: Modify Snap Point
+ C: Close Polyline
+ BACKSPACE: Remove Point
+ X, Y, Z: Choose Axis
+ S-X, S-Y, S-Z: Choose Plane
+ SHIFT: Lock axis
+ """
def modal(self, context, event):
+ if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
+ return {"PASS_THROUGH"}
- if not self.is_input_on:
- if event.type == "MOUSEMOVE" or event.type == "INBETWEEN_MOUSEMOVE":
- self.mousemove_count += 1
- self.is_input_on = False
- self.input_type = None
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Snap.clear_snapping_ref()
- tool.Blender.update_viewport()
- else:
- self.mousemove_count = 0
+ self.handle_instructions(context)
- if self.mousemove_count == 2:
- self.objs_2d_bbox = []
- for obj in self.visible_objs:
- self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
+ self.handle_mouse_move(context, event)
- if self.mousemove_count > 3:
- detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
- self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
- PolylineDecorator.set_mouse_position(event)
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- tool.Blender.update_viewport()
- return {"RUNNING_MODAL"}
+ self.choose_axis(event, z=True)
- if event.value == "RELEASE" and event.type == "BACK_SPACE":
- tool.Snap.remove_last_polyline_point()
- tool.Blender.update_viewport()
+ self.choose_plane(event)
- if event.value == "RELEASE" and event.type == "LEFTMOUSE":
- tool.Snap.insert_polyline_point(self.input_panel)
- tool.Blender.update_viewport()
+ self.handle_snap_selection(context, event)
- 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)
- tool.Blender.update_viewport()
-
- if self.is_input_on and event.value == "PRESS" and event.type == "TAB":
- self.recalculate_inputs(context)
- index = self.input_options.index(self.input_type)
- size = len(self.input_options)
- self.input_type = self.input_options[((index + 1) % size)]
-
- self.number_input = self.input_panel[self.input_type]
- self.number_input = list(self.number_input)
- self.number_output = "".join(self.number_input)
- if self.input_type != "A":
- self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
-
- self.input_panel[self.input_type] = self.number_output
-
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB":
- self.recalculate_inputs(context)
- self.is_input_on = True
- self.input_type = "D"
-
- self.number_input = self.input_panel[self.input_type]
- self.number_input = list(self.number_input)
- self.number_output = "".join(self.number_input)
- self.number_output = PolylineDecorator.format_input_panel_units(context, float(self.number_output))
- self.input_panel[self.input_type] = self.number_output
-
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if not self.is_input_on and event.ascii in self.number_options:
- self.recalculate_inputs(context)
- self.is_input_on = True
- self.input_type = "D"
- self.number_input = []
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if event.value == "RELEASE" and event.type in {"D", "A"}:
- self.recalculate_inputs(context)
- self.is_input_on = True
- self.input_type = event.type
- self.number_input = []
- self.input_panel[self.input_type] = ""
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if self.input_type in self.input_options:
- if (event.ascii in self.number_options) or (event.value == "RELEASE" and event.type == "BACK_SPACE"):
- if event.type == "BACK_SPACE":
- if len(self.number_input) <= 1:
- self.number_input = []
- else:
- self.number_input = self.number_input[:-1]
- self.number_output = "".join(self.number_input)
- self.input_panel[self.input_type] = self.number_output
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
- else:
- self.number_input.append(event.ascii)
- self.number_output = "".join(self.number_input)
-
- if self.number_input:
- self.input_panel[self.input_type] = self.number_output
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
-
- if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
+ if (
+ not self.tool_state.is_input_on
+ and event.value == "RELEASE"
+ and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
+ ):
+ context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall()
tool.Snap.clear_polyline()
tool.Blender.update_viewport()
return {"FINISHED"}
- if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}:
- is_valid = self.recalculate_inputs(context)
- if is_valid:
- tool.Snap.insert_polyline_point(self.input_panel)
- self.is_input_on = False
- self.input_type = None
- self.number_input = []
- self.number_output = ""
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- tool.Blender.update_viewport()
+ self.handle_keyboard_input(context, event)
- if event.value == "PRESS" and event.type == "M":
- self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points)
- PolylineDecorator.set_mouse_position(event)
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- tool.Blender.update_viewport()
+ self.handle_inserting_polyline(context, event)
- 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.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.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.cycle_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"}
-
- if self.is_input_on:
- if event.value == "RELEASE" and event.type in {"ESC"}:
- self.recalculate_inputs(context)
- self.is_input_on = False
- self.input_type = None
- PolylineDecorator.set_input_panel(self.input_panel, self.input_type)
- 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()
- return {"CANCELLED"}
+ result = self.handle_cancelation(context, event)
+ if result is not None:
+ return result
return {"RUNNING_MODAL"}
def invoke(self, context, event):
- if context.space_data.type == "VIEW_3D":
- PolylineDecorator.install(context)
- 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_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:
- self.objs_2d_bbox.append(tool.Raycast.get_on_screen_2d_bounding_boxes(context, obj))
- detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox)
- self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps)
- PolylineDecorator.set_mouse_position(event)
- self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
- tool.Blender.update_viewport()
- context.window_manager.modal_handler_add(self)
- return {"RUNNING_MODAL"}
- else:
- self.report({"WARNING"}, "Active space must be a View3d")
- return {"CANCELLED"}
+ super().invoke(context, event)
+ return {"RUNNING_MODAL"}
diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py
index 1b476ba1e4..9004f5a84d 100644
--- a/src/bonsai/bonsai/bim/module/project/ui.py
+++ b/src/bonsai/bonsai/bim/module/project/ui.py
@@ -519,5 +519,7 @@ class BIM_PT_purge(Panel):
def draw(self, context):
layout = self.layout
- layout.operator("bim.purge_unused_profiles")
- layout.operator("bim.purge_unused_types")
+ layout.operator("bim.purge_unused_objects", text="Purge Unused Profiles").object_type = "PROFILE"
+ layout.operator("bim.purge_unused_objects", text="Purge Unused Types").object_type = "TYPE"
+ layout.operator("bim.purge_unused_objects", text="Purge Unused Styles").object_type = "STYLE"
+ layout.operator("bim.purge_unused_objects", text="Purge Unused Materials").object_type = "MATERIAL"
diff --git a/src/bonsai/bonsai/bim/module/pset/data.py b/src/bonsai/bonsai/bim/module/pset/data.py
index c22ee88053..e9768ff0ab 100644
--- a/src/bonsai/bonsai/bim/module/pset/data.py
+++ b/src/bonsai/bonsai/bim/module/pset/data.py
@@ -290,9 +290,18 @@ class ProfilePsetsData(Data):
@classmethod
def load(cls):
- pprops = bpy.context.scene.BIMProfileProperties
- ifc_definition_id = pprops.profiles[pprops.active_profile_index].ifc_definition_id
- cls.data = {"psets": cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id), psets_only=True)}
+ active_profile = tool.Profile.get_active_profile_ui()
+ if active_profile:
+ ifc_definition_id = active_profile.ifc_definition_id
+ psets_data = cls.psetqtos(tool.Ifc.get().by_id(ifc_definition_id), psets_only=True)
+ else:
+ ifc_definition_id = 0
+ psets_data = []
+
+ cls.data = {
+ "ifc_definition_id": ifc_definition_id,
+ "psets": psets_data,
+ }
cls.is_loaded = True
diff --git a/src/bonsai/bonsai/bim/module/pset/operator.py b/src/bonsai/bonsai/bim/module/pset/operator.py
index 5870547bce..fe08585701 100644
--- a/src/bonsai/bonsai/bim/module/pset/operator.py
+++ b/src/bonsai/bonsai/bim/module/pset/operator.py
@@ -433,6 +433,14 @@ class BIM_OT_bulk_remove_psets(bpy.types.Operator, tool.Ifc.Operator):
class AddProposedProp(bpy.types.Operator):
bl_idname = "bim.add_proposed_prop"
bl_label = "Add Proposed Prop"
+ bl_description = (
+ "Add proposed property to the custom property set.\n\n"
+ "Property type will be deduced from the provided value. Possible types:\n"
+ "- provide an integer or a float to create integer/real property\n"
+ "- 'true', 'false' to add a boolean property\n"
+ "- 'null' or '' (empty value) to add a null property\n"
+ "- any other value will be added as a string property"
+ )
bl_options = {"REGISTER", "UNDO"}
obj: bpy.props.StringProperty()
obj_type: bpy.props.StringProperty()
@@ -440,5 +448,8 @@ class AddProposedProp(bpy.types.Operator):
prop_value: bpy.props.StringProperty()
def execute(self, context):
- core.add_proposed_prop(tool.Pset, self.obj, self.obj_type, self.prop_name, self.prop_value)
+ res = core.add_proposed_prop(tool.Pset, self.obj, self.obj_type, self.prop_name, self.prop_value)
+ if res:
+ self.report({"ERROR"}, res)
+ return {"CANCELLED"}
return {"FINISHED"}
diff --git a/src/bonsai/bonsai/bim/module/pset/prop.py b/src/bonsai/bonsai/bim/module/pset/prop.py
index 074ac3d73f..7804521221 100644
--- a/src/bonsai/bonsai/bim/module/pset/prop.py
+++ b/src/bonsai/bonsai/bim/module/pset/prop.py
@@ -243,6 +243,7 @@ class PsetProperties(PropertyGroup):
properties: CollectionProperty(name="Properties", type=IfcProperty)
pset_name: EnumProperty(items=get_pset_name, name="Pset Name")
qto_name: EnumProperty(items=get_qto_name, name="Qto Name")
+ # Proposed property.
prop_name: StringProperty(name="Property Name", default="MyProperty")
prop_value: StringProperty(name="Property Value", default="Some Value")
diff --git a/src/bonsai/bonsai/bim/module/pset/ui.py b/src/bonsai/bonsai/bim/module/pset/ui.py
index fd01a3b6af..3177a9ae31 100644
--- a/src/bonsai/bonsai/bim/module/pset/ui.py
+++ b/src/bonsai/bonsai/bim/module/pset/ui.py
@@ -664,7 +664,15 @@ class BIM_PT_profile_psets(Panel):
return False
def draw(self, context):
- if not ProfilePsetsData.is_loaded:
+ active_profile = tool.Profile.get_active_profile_ui()
+
+ if not active_profile:
+ return
+
+ if (
+ not ProfilePsetsData.is_loaded
+ or active_profile.ifc_definition_id != ProfilePsetsData.data["ifc_definition_id"]
+ ):
ProfilePsetsData.load()
props = context.scene.ProfilePsetProperties
diff --git a/src/bonsai/bonsai/bim/module/spatial/operator.py b/src/bonsai/bonsai/bim/module/spatial/operator.py
index 5352ff144c..f0c117528b 100644
--- a/src/bonsai/bonsai/bim/module/spatial/operator.py
+++ b/src/bonsai/bonsai/bim/module/spatial/operator.py
@@ -414,7 +414,7 @@ class ToggleGrids(bpy.types.Operator, tool.Ifc.Operator):
is_visible: bpy.props.BoolProperty(name="Is Visible", default=False, options={"SKIP_SAVE"})
def _execute(self, context):
- for element in (tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis")):
+ for element in tool.Ifc.get().by_type("IfcGrid") + tool.Ifc.get().by_type("IfcGridAxis"):
if obj := tool.Ifc.get_object(element):
obj.hide_set(not self.is_visible)
diff --git a/src/bonsai/bonsai/bim/module/spatial/prop.py b/src/bonsai/bonsai/bim/module/spatial/prop.py
index c2b1def5b6..161130b877 100644
--- a/src/bonsai/bonsai/bim/module/spatial/prop.py
+++ b/src/bonsai/bonsai/bim/module/spatial/prop.py
@@ -31,9 +31,11 @@ from bpy.props import (
CollectionProperty,
)
import bonsai.tool as tool
+import bonsai.bim.handler
import bonsai.core.geometry
import ifcopenshell
import ifcopenshell.util.element
+import ifcopenshell.util.unit
def get_subelement_class(self, context):
@@ -124,6 +126,16 @@ def update_spatial_is_locked(self, context):
tool.Geometry.lock_object(obj)
else:
tool.Geometry.unlock_object(obj)
+ # Need to update ViewportData.mode.
+ bonsai.bim.handler.refresh_ui_data()
+
+
+def update_spatial_is_visible(self: "BIMSpatialDecompositionProperties", context: bpy.types.Context) -> None:
+ bpy.ops.bim.toggle_spatial_elements(is_visible=self.is_visible)
+
+
+def update_grid_is_visible(self: "BIMGridProperties", context: bpy.types.Context) -> None:
+ bpy.ops.bim.toggle_grids(is_visible=self.is_visible)
def poll_container_obj(self, obj):
@@ -173,7 +185,18 @@ class Element(PropertyGroup):
class BIMSpatialDecompositionProperties(PropertyGroup):
- is_locked: BoolProperty(name="Is Locked", default=True, update=update_spatial_is_locked)
+ is_locked: BoolProperty(
+ name="Is Locked",
+ description="Prevent all spatial elements from being edited, removed, duplicated",
+ default=True,
+ update=update_spatial_is_locked,
+ )
+ is_visible: BoolProperty(
+ name="Is Visible",
+ description="Show or hide spatial elements, such as buildings, sites, etc",
+ default=True,
+ update=update_spatial_is_visible,
+ )
container_filter: StringProperty(name="Container Filter", default="", options={"TEXTEDIT_UPDATE"})
containers: CollectionProperty(name="Containers", type=BIMContainer)
contracted_containers: StringProperty(name="Contracted containers", default="[]")
@@ -210,5 +233,16 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
class BIMGridProperties(PropertyGroup):
- is_locked: BoolProperty(name="Is Locked", default=True, update=update_grid_is_locked)
+ is_locked: BoolProperty(
+ name="Is Locked",
+ description="Prevent all grids and grid axes from being edited, removed, duplicated",
+ default=True,
+ update=update_grid_is_locked,
+ )
+ is_visible: BoolProperty(
+ name="Is Visible",
+ description="Show or hide grids and grid axes",
+ default=True,
+ update=update_grid_is_visible,
+ )
grid_axes: CollectionProperty(name="Grid Axes", type=ObjProperty)
diff --git a/src/bonsai/bonsai/bim/module/spatial/ui.py b/src/bonsai/bonsai/bim/module/spatial/ui.py
index 39e810ed8d..a1d25b8c6f 100644
--- a/src/bonsai/bonsai/bim/module/spatial/ui.py
+++ b/src/bonsai/bonsai/bim/module/spatial/ui.py
@@ -103,12 +103,13 @@ class BIM_PT_spatial_decomposition(Panel):
return tool.Ifc.get()
def draw_header(self, context):
+ props = context.scene.BIMSpatialDecompositionProperties
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
- row.operator("bim.toggle_spatial_elements", text="", icon="HIDE_OFF").is_visible = True
- row.operator("bim.toggle_spatial_elements", text="", icon="HIDE_ON").is_visible = False
- icon = "VIEW_LOCKED" if context.scene.BIMSpatialDecompositionProperties.is_locked else "VIEW_UNLOCKED"
- row.prop(context.scene.BIMSpatialDecompositionProperties, "is_locked", text="", icon=icon)
+ icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
+ row.prop(props, "is_visible", text="", icon=icon)
+ icon = "VIEW_LOCKED" if props.is_locked else "VIEW_UNLOCKED"
+ row.prop(props, "is_locked", text="", icon=icon)
def draw(self, context):
if not SpatialDecompositionData.is_loaded:
@@ -218,12 +219,13 @@ class BIM_PT_grids(Panel):
self.layout.row().operator("mesh.add_grid", icon="ADD", text="Add Grids")
def draw_header(self, context):
+ props = context.scene.BIMGridProperties
row = self.layout.row(align=True)
row.label(text="") # empty text occupies the left of the row
- row.operator("bim.toggle_grids", text="", icon="HIDE_OFF").is_visible = True
- row.operator("bim.toggle_grids", text="", icon="HIDE_ON").is_visible = False
- icon = "VIEW_LOCKED" if context.scene.BIMGridProperties.is_locked else "VIEW_UNLOCKED"
- row.prop(context.scene.BIMGridProperties, "is_locked", text="", icon=icon)
+ icon = "HIDE_OFF" if props.is_visible else "HIDE_ON"
+ row.prop(props, "is_visible", text="", icon=icon)
+ icon = "VIEW_LOCKED" if props.is_locked else "VIEW_UNLOCKED"
+ row.prop(props, "is_locked", text="", icon=icon)
class BIM_UL_containers_manager(UIList):
diff --git a/src/bonsai/bonsai/bim/module/style/operator.py b/src/bonsai/bonsai/bim/module/style/operator.py
index 22dd98b7ba..0a1bc04598 100644
--- a/src/bonsai/bonsai/bim/module/style/operator.py
+++ b/src/bonsai/bonsai/bim/module/style/operator.py
@@ -356,14 +356,14 @@ class BrowseExternalStyle(bpy.types.Operator):
return {"FINISHED"}
-class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
+class ActivateExternalStyle(bpy.types.Operator):
bl_idname = "bim.activate_external_style"
bl_label = "Activate External Style"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
material_name: bpy.props.StringProperty(name="Material Name", default="")
- def _execute(self, context):
+ def execute(self, context):
if not self.material_name:
material = context.active_object.active_material
else:
@@ -403,6 +403,7 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
if material.use_nodes:
tool.Blender.copy_node_graph(material, db["data_block"])
bpy.data.materials.remove(db["data_block"])
+ return {"FINISHED"}
def copy_material_attributes(self, source, target):
ID_properties = bpy.types.ID.bl_rna.properties
@@ -444,33 +445,37 @@ class ActivateExternalStyle(bpy.types.Operator, tool.Ifc.Operator):
set_prop(prop_name)
-class DisableEditingStyles(bpy.types.Operator, tool.Ifc.Operator):
+class DisableEditingStyles(bpy.types.Operator):
bl_idname = "bim.disable_editing_styles"
bl_options = {"REGISTER", "UNDO"}
bl_label = "Disable Editing Styles"
- def _execute(self, context):
+ def execute(self, context):
core.disable_editing_styles(tool.Style)
+ return {"FINISHED"}
-class LoadStyles(bpy.types.Operator, tool.Ifc.Operator):
+class LoadStyles(bpy.types.Operator):
bl_idname = "bim.load_styles"
bl_label = "Load Styles"
bl_options = {"REGISTER", "UNDO"}
style_type: bpy.props.StringProperty()
- def _execute(self, context):
- core.load_styles(tool.Style, style_type=self.style_type)
+ def execute(self, context):
+ style_type = self.style_type if self.style_type else context.scene.BIMStylesProperties.style_type
+ core.load_styles(tool.Style, style_type=style_type)
+ return {"FINISHED"}
-class SelectByStyle(bpy.types.Operator, tool.Ifc.Operator):
+class SelectByStyle(bpy.types.Operator):
bl_idname = "bim.select_by_style"
bl_label = "Select By Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty()
- def _execute(self, context):
+ def execute(self, context):
core.select_by_style(tool.Style, tool.Spatial, style=tool.Ifc.get().by_id(self.style))
+ return {"FINISHED"}
class ChooseTextureMapPath(bpy.types.Operator):
@@ -633,14 +638,14 @@ class AddPresentationStyle(bpy.types.Operator, tool.Ifc.Operator):
core.load_styles(tool.Style, style_type=props.style_type)
-class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
+class EnableEditingSurfaceStyle(bpy.types.Operator):
bl_idname = "bim.enable_editing_surface_style"
bl_label = "Enable Editing Surface Style"
bl_options = {"REGISTER", "UNDO"}
style: bpy.props.IntProperty(default=0)
ifc_class: bpy.props.StringProperty(default="")
- def _execute(self, context):
+ def execute(self, context):
props = bpy.context.scene.BIMStylesProperties
style = tool.Ifc.get().by_id(self.style)
props.is_editing_style = self.style
@@ -678,6 +683,7 @@ class EnableEditingSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
and active_style_type != "Shading"
):
tool.Style.switch_shading(material, "Shading")
+ return {"FINISHED"}
class EditSurfaceStyle(bpy.types.Operator, tool.Ifc.Operator):
diff --git a/src/bonsai/bonsai/bim/module/type/__init__.py b/src/bonsai/bonsai/bim/module/type/__init__.py
index 445c9e1e28..f0e5546740 100644
--- a/src/bonsai/bonsai/bim/module/type/__init__.py
+++ b/src/bonsai/bonsai/bim/module/type/__init__.py
@@ -26,7 +26,6 @@ classes = (
operator.DisableEditingType,
operator.DuplicateType,
operator.EnableEditingType,
- operator.PurgeUnusedTypes,
operator.RemoveType,
operator.RenameType,
operator.SelectSimilarType,
diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py
index c72cebcf71..605ab97c25 100644
--- a/src/bonsai/bonsai/bim/module/type/operator.py
+++ b/src/bonsai/bonsai/bim/module/type/operator.py
@@ -515,12 +515,12 @@ class RenameType(bpy.types.Operator, tool.Ifc.Operator):
self.layout.prop(self, "name")
-class AutoRenameOccurrences(bpy.types.Operator, tool.Ifc.Operator):
+class AutoRenameOccurrences(bpy.types.Operator):
bl_idname = "bim.auto_rename_occurrences"
bl_label = "Auto Rename Occurrences"
bl_options = {"REGISTER", "UNDO"}
- def _execute(self, context):
+ def execute(self, context):
obj = context.active_object
element_type = tool.Ifc.get_entity(obj)
if element_type and element_type.is_a("IfcTypeObject"):
@@ -529,6 +529,7 @@ class AutoRenameOccurrences(bpy.types.Operator, tool.Ifc.Operator):
occurrence.Name = tool.Model.generate_occurrence_name(element_type, occurrence.is_a())
if obj:
tool.Root.set_object_name(obj, occurrence)
+ return {"FINISHED"}
class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
@@ -555,13 +556,3 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
context.scene.BIMModelProperties.ifc_class = new.is_a()
context.scene.BIMModelProperties.relating_type_id = str(new_obj.BIMObjectProperties.ifc_definition_id)
return {"FINISHED"}
-
-
-class PurgeUnusedTypes(bpy.types.Operator, tool.Ifc.Operator):
- bl_idname = "bim.purge_unused_types"
- bl_label = "Purge Unused Types"
- bl_options = {"REGISTER", "UNDO"}
-
- def _execute(self, context):
- purged_types = core.purge_unused_types(tool.Ifc, tool.Type, tool.Geometry)
- self.report({"INFO"}, f"{purged_types} types were purged.")
diff --git a/src/bonsai/bonsai/core/profile.py b/src/bonsai/bonsai/core/profile.py
index d8cf537345..a0ee00b6de 100644
--- a/src/bonsai/bonsai/core/profile.py
+++ b/src/bonsai/bonsai/core/profile.py
@@ -26,7 +26,7 @@ if TYPE_CHECKING:
def purge_unused_profiles(ifc: tool.Ifc, profile: tool.Profile) -> int:
- """Purge profiles that have no invserses.
+ """Purge profiles that have no inverses.
:return: Number of removed profiles.
"""
diff --git a/src/bonsai/bonsai/core/pset.py b/src/bonsai/bonsai/core/pset.py
index c2b87836df..c96e5177ff 100644
--- a/src/bonsai/bonsai/core/pset.py
+++ b/src/bonsai/bonsai/core/pset.py
@@ -89,9 +89,12 @@ def enable_pset_editing(
pset_tool.enable_proposed_pset(props, pset_name, pset_type, has_template)
-def add_proposed_prop(pset: tool.Pset, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE, name: str, value: Any) -> None:
+def add_proposed_prop(
+ pset: tool.Pset, obj_name: str, obj_type: tool.Ifc.OBJECT_TYPE, name: str, value: Any
+) -> Union[None, str]:
props = pset.get_pset_props(obj_name, obj_type)
- pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props)
+ res = pset.add_proposed_property(name, pset.cast_string_to_primitive(value), props)
+ return res
def unshare_pset(
diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py
index e3566b72cf..dc9a8a81a2 100644
--- a/src/bonsai/bonsai/core/tool.py
+++ b/src/bonsai/bonsai/core/tool.py
@@ -581,6 +581,16 @@ class Patch:
def run_migrate_patch(cls, infile, outfile, schema): pass
+@interface
+class Polyline:
+ def create_input_ui(cls, init_z=False, init_area=False): pass
+ def create_tool_state(cls): pass
+ def calculate_distance_and_angle(cls, context, input_ui, tool_state): pass
+ def calculate_area(cls, context, input_ui): pass
+ def calculate_x_y_and_z(cls, context, input_ui, tool_state): pass
+ def validate_input(cls, input_number, input_type): pass
+
+
@interface
class Owner:
def add_address_attribute(cls, name): pass
@@ -668,10 +678,12 @@ class Qto:
def get_rounded_value(cls, new_quantity): pass
def set_qto_result(cls, result): pass
+
@interface
class Raycast:
pass
+
@interface
class Resource:
def clear_productivity_data(cls, props): pass
@@ -945,10 +957,12 @@ class Spatial:
class Covering:
def get_z_from_ceiling_height(cls): pass
+
@interface
class Snap:
pass
+
@interface
class Structural:
def disable_editing_structural_analysis_model(cls): pass
diff --git a/src/bonsai/bonsai/tool/__init__.py b/src/bonsai/bonsai/tool/__init__.py
index a7d28cab44..16a64716e4 100644
--- a/src/bonsai/bonsai/tool/__init__.py
+++ b/src/bonsai/bonsai/tool/__init__.py
@@ -44,6 +44,7 @@ from bonsai.tool.model import Model
from bonsai.tool.nest import Nest
from bonsai.tool.owner import Owner
from bonsai.tool.patch import Patch
+from bonsai.tool.polyline import Polyline
from bonsai.tool.project import Project
from bonsai.tool.profile import Profile
from bonsai.tool.pset import Pset
diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py
index 5ea0cb05e8..7d15d5772c 100644
--- a/src/bonsai/bonsai/tool/blender.py
+++ b/src/bonsai/bonsai/tool/blender.py
@@ -803,7 +803,7 @@ class Blender(bonsai.core.tool.Blender):
return collections_mapping
@classmethod
- def is_editable(cls, obj):
+ def is_editable(cls, obj: bpy.types.Object) -> bool:
if obj.type not in cls.OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE:
return False
if not (element := tool.Ifc.get_entity(obj)):
diff --git a/src/bonsai/bonsai/tool/cost.py b/src/bonsai/bonsai/tool/cost.py
index 8d9945a4ad..8b38502c14 100644
--- a/src/bonsai/bonsai/tool/cost.py
+++ b/src/bonsai/bonsai/tool/cost.py
@@ -295,6 +295,14 @@ class Cost(bonsai.core.tool.Cost):
unit = cls.get_quantity_unit_symbol(quantity)
return selected_quantitites, unit
+ @classmethod
+ def get_assigned_product(cls, cost_item, quantity):
+ assigned_products = cls.get_cost_item_assignments(cost_item, filter_by_type="PRODUCT", is_deep=False)
+ for product in assigned_products:
+ assigned_quantities, _ = cls.get_assigned_quantities(cost_item, product)
+ if quantity in assigned_quantities:
+ return product
+
@classmethod
def get_products(cls, related_object_type: RELATED_OBJECT_TYPE) -> list[ifcopenshell.entity_instance]:
if related_object_type == "PRODUCT":
@@ -859,7 +867,32 @@ class Cost(bonsai.core.tool.Cost):
bpy.ops.bim.connect_websocket_server(page="costing")
cost_schedule_data = cls.create_cost_schedule_json(cost_chedule)
tool.Web.send_webui_data(
- data={"cost_items": cost_schedule_data, "cost_schedule_id": cost_chedule.id()},
+ data={"cost_items": cost_schedule_data, "cost_schedule_id": cost_chedule.id(), "currency": cls.currency()},
data_key="cost_items",
event="cost_items",
)
+
+ @classmethod
+ def get_cost_quantities(cls, cost_item: ifcopenshell.entity_instance) -> dict:
+ results = {
+ "quantities": [],
+ "unit_symbol": None,
+ }
+ if not cost_item:
+ return results
+ results["quantity_type"] = cost_item.CostQuantities[0].is_a() if cost_item.CostQuantities else None
+ unit = (
+ ifcopenshell.util.unit.get_property_unit(cost_item.CostQuantities[0], tool.Ifc.get())
+ if cost_item.CostQuantities
+ else None
+ )
+ if unit:
+ results["unit_symbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
+ for quantity in cost_item.CostQuantities or []:
+ assigned_product = cls.get_assigned_product(cost_item, quantity)
+ info = quantity.get_info()
+ info["fromProduct"] = assigned_product.get_info(recursive=True) if assigned_product else None
+ results["quantities"].append(info)
+ if results["quantity_type"] == "IfcQuantityCount":
+ results["unit_symbol"] = "U"
+ return results
diff --git a/src/bonsai/bonsai/tool/debug.py b/src/bonsai/bonsai/tool/debug.py
index 99bf884aa9..5451cae5b6 100644
--- a/src/bonsai/bonsai/tool/debug.py
+++ b/src/bonsai/bonsai/tool/debug.py
@@ -97,3 +97,15 @@ class Debug(bonsai.core.tool.Debug):
print(f"{class_string: <50} {unused[ifc_class]: >5}")
return sum(unused.values())
+
+ @classmethod
+ def purge_unused_class(cls, ifc_class: str) -> int:
+ ifc_file = tool.Ifc.get()
+ elements = ifc_file.by_type(ifc_class)
+ i = 0
+ for element in elements:
+ if ifc_file.get_total_inverses(element) != 0:
+ continue
+ ifcopenshell.util.element.remove_deep(ifc_file, element)
+ i += 1
+ return i
diff --git a/src/bonsai/bonsai/tool/drawing.py b/src/bonsai/bonsai/tool/drawing.py
index de4c983495..cdf5fbc49d 100644
--- a/src/bonsai/bonsai/tool/drawing.py
+++ b/src/bonsai/bonsai/tool/drawing.py
@@ -1855,6 +1855,8 @@ class Drawing(bonsai.core.tool.Drawing):
element_obj_names = set()
for element in filtered_elements:
obj = tool.Ifc.get_object(element)
+ if not obj:
+ continue
current_representation = tool.Geometry.get_active_representation(obj)
if current_representation:
subcontext = current_representation.ContextOfItems
diff --git a/src/bonsai/bonsai/tool/polyline.py b/src/bonsai/bonsai/tool/polyline.py
new file mode 100644
index 0000000000..4c66316bd4
--- /dev/null
+++ b/src/bonsai/bonsai/tool/polyline.py
@@ -0,0 +1,406 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2024 Bruno Perdigão
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
+
+import bpy
+import bonsai.core.tool
+import bonsai.tool as tool
+from bonsai.bim.module.drawing.helper import format_distance
+from dataclasses import dataclass
+from lark import Lark, Transformer
+from math import radians
+from mathutils import Vector
+from typing import Optional
+
+
+class Polyline(bonsai.core.tool.Polyline):
+
+ @dataclass
+ class PolylineUI:
+ _D: str = ""
+ _A: str = ""
+ _X: str = ""
+ _Y: str = ""
+ _Z: Optional[str] = None
+ _AREA: Optional[str] = None
+ init_z: bool = False
+ init_area: bool = False
+
+ def __post_init__(self):
+ if self.init_z:
+ self._Z = ""
+ if self.init_area:
+ self._AREA = ""
+
+ def set_value(self, attribute_name, value):
+ value = str(value)
+ setattr(self, f"_{attribute_name}", value)
+
+ def get_text_value(self, attribute_name):
+ value = getattr(self, f"_{attribute_name}")
+ return value
+
+ def get_number_value(self, attribute_name):
+ value = getattr(self, f"_{attribute_name}")
+ if value:
+ return float(value)
+ else:
+ return value
+
+ def get_formatted_value(self, attribute_name):
+ value = self.get_number_value(attribute_name)
+ context = bpy.context
+ if value is None:
+ return None
+ if attribute_name == "A":
+ value = float(self.get_text_value(attribute_name))
+ return f"{value:.2f}"
+ else:
+ return self.format_input_ui_units(context, value)
+
+ def format_input_ui_units(cls, context, value):
+ unit_system = tool.Drawing.get_unit_system()
+ if unit_system == "IMPERIAL":
+ precision = context.scene.DocProperties.imperial_precision
+ factor = 3.28084
+ else:
+ precision = None
+ factor = 1
+ if context.scene.unit_settings.length_unit == "MILLIMETERS":
+ factor = 1000
+
+ return format_distance(value * factor, precision=precision, suppress_zero_inches=True, in_unit_length=True)
+
+ @dataclass
+ class ToolState:
+ use_default_container: bool = None
+ snap_angle: float = None
+ is_input_on: bool = None
+ # angle_axis_start: Vector
+ # angle_axis_end: Vector
+ axis_method: str = None
+ plane_method: str = None
+ instructions: str = """TAB: Cycle Input
+ M: Modify Snap Point
+ C: Close
+ Backspace: Remove
+ X Y: Axis
+ Shift: Lock axis
+ """
+ snap_info: str = None
+ mode: str = None
+ input_type: str = None
+
+ @classmethod
+ def create_input_ui(cls, init_z=False, init_area=False):
+ return cls.PolylineUI(init_z=init_z, init_area=init_area)
+
+ @classmethod
+ def create_tool_state(cls):
+ return cls.ToolState()
+
+ @classmethod
+ def calculate_distance_and_angle(cls, context, input_ui, tool_state):
+
+ try:
+ polyline_data = context.scene.BIMModelProperties.polyline_point
+ default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
+ last_point_data = polyline_data[len(polyline_data) - 1]
+ except:
+ default_container_elevation = 0
+ last_point_data = None
+
+ snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
+
+ if last_point_data:
+ last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
+ else:
+ last_point = Vector((0, 0, 0))
+
+ if tool_state.is_input_on:
+ if tool_state.use_default_container:
+ snap_vector = Vector(
+ (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), default_container_elevation)
+ )
+ else:
+ snap_vector = Vector(
+ (input_ui.get_number_value("X"), input_ui.get_number_value("Y"), input_ui.get_number_value("Z"))
+ )
+ else:
+ if tool_state.use_default_container:
+ snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
+ else:
+ snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
+
+ second_to_last_point = None
+ if len(polyline_data) > 1:
+ second_to_last_point_data = polyline_data[len(polyline_data) - 2]
+ second_to_last_point = Vector(
+ (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
+ )
+ else:
+ # Creates a fake "second to last" point away from the first point but in the same x axis
+ # this allows to calculate the angle relative to x axis when there is only one point
+ second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
+
+ distance = (snap_vector - last_point).length
+ if distance > 0:
+ angle = tool.Cad.angle_3_vectors(
+ second_to_last_point, last_point, snap_vector, new_angle=None, degrees=True
+ )
+
+ # Round angle to the nearest 0.05
+ angle = round(angle / 0.05) * 0.05
+
+ if input_ui:
+ input_ui.set_value("X", snap_vector.x)
+ input_ui.set_value("Y", snap_vector.y)
+ if input_ui.get_number_value("Z") is not None:
+ input_ui.set_value("Z", snap_vector.z)
+
+ input_ui.set_value("D", distance)
+ input_ui.set_value("A", angle)
+ return
+
+ return
+
+ @classmethod
+ def calculate_area(cls, context, input_ui):
+ try:
+ polyline_data = context.scene.BIMModelProperties.polyline_point
+ except:
+ return input_ui
+
+ if len(polyline_data) < 3:
+ return input_ui
+
+ points = []
+ for data in polyline_data:
+ points.append(Vector((data.x, data.y, data.z)))
+
+ if points[0] == points[-1]:
+ points = points[1:]
+
+ # TODO move this to CAD
+ # Calculate the normal vector of the plane formed by the first three vertices
+ v1, v2, v3 = points[:3]
+ normal = (v2 - v1).cross(v3 - v1).normalized()
+
+ # Check if all points are coplanar
+ is_coplanar = True
+ tolerance = 1e-6 # Adjust this value as needed
+ for v in points:
+ if abs((v - v1).dot(normal)) > tolerance:
+ is_coplanar = False
+
+ if is_coplanar:
+ area = 0
+ for i in range(len(points)):
+ j = (i + 1) % len(points)
+ area += points[i].cross(points[j]).dot(normal)
+
+ area = abs(area) / 2
+ else:
+ area = 0
+
+ if input_ui.get_text_value("A") is not None:
+ input_ui.set_value("A", area)
+ return
+
+ @classmethod
+ def calculate_x_y_and_z(cls, context, input_ui, tool_state):
+ try:
+ polyline_data = context.scene.BIMModelProperties.polyline_point
+ default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
+ last_point_data = polyline_data[len(polyline_data) - 1]
+ last_point = Vector((last_point_data.x, last_point_data.y, last_point_data.z))
+ except:
+ default_container_elevation = 0
+ last_point = Vector((0, 0, 0))
+
+ snap_prop = context.scene.BIMModelProperties.snap_mouse_point[0]
+ snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
+
+ if tool_state.use_default_container:
+ snap_vector = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
+ else:
+ snap_vector = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
+
+ if len(polyline_data) > 1:
+ second_to_last_point_data = polyline_data[len(polyline_data) - 2]
+ second_to_last_point = Vector(
+ (second_to_last_point_data.x, second_to_last_point_data.y, second_to_last_point_data.z)
+ )
+ else:
+ # Creates a fake "second to last" point away from the first point but in the same x axis
+ # this allows to calculate the angle relative to x axis when there is only one point
+ second_to_last_point = Vector((last_point.x + 1000, last_point.y, last_point.z))
+
+ distance = input_ui.get_number_value("D")
+
+ if distance < 0 or distance > 0:
+ angle = radians(input_ui.get_number_value("A"))
+
+ rot_vector = tool.Cad.angle_3_vectors(second_to_last_point, last_point, snap_vector, angle, degrees=True)
+
+ coords = rot_vector * distance + last_point
+
+ x = coords[0]
+ y = coords[1]
+ z = coords[2]
+ if input_ui:
+ input_ui.set_value("X", x)
+ input_ui.set_value("Y", y)
+ if input_ui.get_number_value("Z") is not None:
+ input_ui.set_value("Z", z)
+
+ return
+
+ input_ui.set_value("X", last_point.x)
+ input_ui.set_value("Y", last_point.y)
+ if input_ui.get_number_value("Z") is not None:
+ input_ui.set_value("Z", last_point.z)
+
+ return
+
+ @classmethod
+ def validate_input(cls, input_number, input_type):
+
+ grammar_imperial = """
+ start: (FORMULA dim expr) | dim
+ dim: imperial
+
+ FORMULA: "="
+
+ imperial: feet? "-"? inches?
+ feet: NUMBER? "-"? fraction? "'"
+ inches: NUMBER? "-"? fraction? "\\""
+ fraction: NUMBER "/" NUMBER
+
+ expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
+
+ NUMBER: /-?\\d+(?:\\.\\d+)?/
+ ADD: "+"
+ SUB: "-"
+ MUL: "*"
+ DIV: "/"
+
+ %ignore " "
+ """
+
+ grammar_metric = """
+ start: FORMULA? dim expr?
+ dim: metric
+
+ FORMULA: "="
+
+ metric: NUMBER
+
+ expr: (ADD | SUB | MUL | DIV) dim
+
+ NUMBER: /-?\\d+(?:\\.\\d+)?/
+ ADD: "+"
+ SUB: "-"
+ MUL: "*"
+ DIV: "/"
+
+ %ignore " "
+ """
+
+ class InputTransform(Transformer):
+ def NUMBER(self, n):
+ return float(n)
+
+ def fraction(self, numbers):
+ return numbers[0] / numbers[1]
+
+ def inches(self, args):
+ if len(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:
+ if args[0] <= 0:
+ result = args[0] - args[1]
+ else:
+ result = args[0] + args[1]
+ else:
+ result = args[0]
+ return result
+
+ def metric(self, args):
+ return args[0]
+
+ def dim(self, args):
+ return args[0]
+
+ def expr(self, args):
+ op = args[0]
+ value = float(args[1])
+ if op == "+":
+ return lambda x: x + value
+ elif op == "-":
+ return lambda x: x - value
+ elif op == "*":
+ return lambda x: x * value
+ elif op == "/":
+ return lambda x: x / value
+
+ def FORMULA(cls, args):
+ return args[0]
+
+ def start(self, args):
+ i = 0
+ if args[0] == "=":
+ i += 1
+ else:
+ if len(args) > 1:
+ raise ValueError("Invalid input.")
+ dimension = args[i]
+ 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":
+ parser = Lark(grammar_imperial)
+ factor = 0.3048
+ else:
+ parser = Lark(grammar_metric)
+ factor = 1
+ if bpy.context.scene.unit_settings.length_unit == "MILLIMETERS":
+ factor = 0.001
+
+ if input_type == "A":
+ parser = Lark(grammar_metric)
+ factor = 1
+
+ parse_tree = parser.parse(input_number)
+
+ transformer = InputTransform()
+ result = transformer.transform(parse_tree)
+ return True, str(result)
+ except:
+ return False, "0"
diff --git a/src/bonsai/bonsai/tool/profile.py b/src/bonsai/bonsai/tool/profile.py
index 859f8e4a3b..6a8d0a3b6d 100644
--- a/src/bonsai/bonsai/tool/profile.py
+++ b/src/bonsai/bonsai/tool/profile.py
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
+import bpy
import ifcopenshell
import ifcopenshell.api.profile
import ifcopenshell.geom
@@ -39,7 +40,11 @@ class Profile(bonsai.core.tool.Profile):
settings = ifcopenshell.geom.settings()
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
shape = ifcopenshell.geom.create_shape(settings, profile)
+
verts = shape.verts
+ if not verts:
+ raise RuntimeError("Profile shape has no vertices, it probably is invalid.")
+
edges = shape.edges
grouped_verts = [[verts[i], verts[i + 1]] for i in range(0, len(verts), 3)]
@@ -104,3 +109,111 @@ class Profile(bonsai.core.tool.Profile):
# In UI unnamed profiles are not available, so we don't handle them.
new_profile.ProfileName = profile.ProfileName + "_copy"
return new_profile
+
+ @classmethod
+ def get_active_profile_ui(cls) -> Union[bpy.types.PropertyGroup, None]:
+ props = bpy.context.scene.BIMProfileProperties
+ index = props.active_profile_index
+ if len(props.profiles) > index >= 0:
+ return props.profiles[index]
+
+ # Lengths are in meters.
+ DEFAULT_PROFILE_ATTRS = {
+ "IfcCircleProfileDef": {
+ "Radius": 0.05,
+ },
+ # TODO: test after debug
+ "IfcAsymmetricIShapeProfileDef": {
+ "BottomFlangeWidth": 0.1,
+ "BottomFlangeThickness": 0.01,
+ "BottomFlangeFilletRadius": 0.01,
+ "OverallDepth": 0.1,
+ "WebThickness": 0.005,
+ "TopFlangeWidth": 0.075,
+ "TopFlangeThickness": 0.01,
+ "TopFlangeFilletRadius": 0.01,
+ },
+ "IfcCShapeProfileDef": {
+ "Depth": 0.1,
+ "Width": 0.05,
+ "WallThickness": 0.01,
+ "Girth": 0.01,
+ },
+ # 101.6-10.0
+ "IfcCircleHollowProfileDef": {
+ "WallThickness": 0.01,
+ },
+ # TODO: check shape after fixing crash
+ # "IfcEllipseProfileDef": {
+ # "SemiAxis1": 0.1,
+ # "SemiAxis2": 0.1,
+ # },
+ # HEA100
+ "IfcIShapeProfileDef": {
+ "OverallWidth": 0.1,
+ "OverallDepth": 0.1,
+ "WebThickness": 0.005,
+ "FlangeThickness": 0.01,
+ "FilletRadius": 0.01,
+ },
+ # LNP100x10
+ "IfcLShapeProfileDef": {
+ "Depth": 0.1,
+ "Thickness": 0.01,
+ "FilletRadius": 0.012,
+ "EdgeRadius": 0.01,
+ },
+ "IfcRectangleProfileDef": {
+ "XDim": 0.1,
+ "YDim": 0.1,
+ },
+ "IfcRoundedRectangleProfileDef": {
+ "RoundingRadius": 0.01,
+ },
+ # 100-10.0
+ "IfcRectangleHollowProfileDef": {
+ "WallThickness": 0.01,
+ "InnerFilletRadius": 0.01,
+ "OuterFilletRadius": 0.01,
+ },
+ "IfcTShapeProfileDef": {
+ "Depth": 0.1,
+ "FlangeWidth": 0.05,
+ "WebThickness": 0.005,
+ "FlangeThickness": 0.009,
+ },
+ "IfcTrapeziumProfileDef": {
+ "BottomXDim": 0.1,
+ "TopXDim": 0.08,
+ "YDim": 0.05,
+ "TopXOffset": 0.01,
+ },
+ # UAP100
+ "IfcUShapeProfileDef": {
+ "Depth": 0.1,
+ "FlangeWidth": 0.05,
+ "WebThickness": 0.005,
+ "FlangeThickness": 0.009,
+ },
+ # ZNP100
+ "IfcZShapeProfileDef": {
+ "Depth": 0.1,
+ "FlangeWidth": 0.05,
+ "WebThickness": 0.007,
+ "FlangeThickness": 0.01,
+ },
+ }
+
+ @classmethod
+ def set_default_profile_attrs(cls, profile: ifcopenshell.entity_instance) -> None:
+ """Set default profile attributes to keep profile valid."""
+ class_match = False
+ si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
+ for ifc_class, params in cls.DEFAULT_PROFILE_ATTRS.items():
+ if profile.is_a(ifc_class):
+ class_match = True
+ for key, value in params.items():
+ setattr(profile, key, value / si_conversion)
+
+ if not class_match:
+ raise ValueError(f"Unable to set default profile parameters for {profile.is_a()}.")
diff --git a/src/bonsai/bonsai/tool/pset.py b/src/bonsai/bonsai/tool/pset.py
index b4d9e936d6..dcfa111872 100644
--- a/src/bonsai/bonsai/tool/pset.py
+++ b/src/bonsai/bonsai/tool/pset.py
@@ -297,9 +297,9 @@ class Pset(bonsai.core.tool.Pset):
return bonsai.bim.schema.ifc.psetqto.get_by_name(name)
@classmethod
- def add_proposed_property(cls, name: str, value: Any, props: bpy.types.PropertyGroup) -> None:
+ def add_proposed_property(cls, name: str, value: Any, props: bpy.types.PropertyGroup) -> Union[None, str]:
if props.properties.get(name):
- return
+ return f"Property '{name}' already exists."
prop = props.properties.add()
prop.name = name
metadata = prop.metadata
@@ -327,4 +327,3 @@ class Pset(bonsai.core.tool.Pset):
return value
except:
return value
- return value
diff --git a/src/bonsai/bonsai/tool/root.py b/src/bonsai/bonsai/tool/root.py
index 13a47b386c..0522ab6ea0 100644
--- a/src/bonsai/bonsai/tool/root.py
+++ b/src/bonsai/bonsai/tool/root.py
@@ -28,6 +28,7 @@ import bonsai.core.geometry
import bonsai.tool as tool
from typing import Union, Optional, Any
from bonsai.bim.module.spatial.decorator import GridDecorator
+from bonsai.bim.module.geometry.decorator import ItemDecorator
class Root(bonsai.core.tool.Root):
@@ -214,6 +215,12 @@ class Root(bonsai.core.tool.Root):
new.obj = obj
GridDecorator.install(bpy.context)
+ @classmethod
+ def reload_item_decorator(cls) -> None:
+ item_objs = bpy.context.scene.BIMGeometryProperties.item_objs
+ item_objs.clear()
+ ItemDecorator.install(bpy.context)
+
@classmethod
def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
destination_obj.data = source_obj.data
diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py
index 4f51e85ca6..0249b6ece6 100644
--- a/src/bonsai/bonsai/tool/snap.py
+++ b/src/bonsai/bonsai/tool/snap.py
@@ -27,15 +27,12 @@ from lark import Lark, Transformer
class Snap(bonsai.core.tool.Snap):
- mouse_pos = None
- snap_angle = None
- use_default_container = False
+ tool_state = None
snap_plane_method = None
- snap_axis_method = None
@classmethod
- def set_use_default_container(cls, value=True):
- cls.use_default_container = value
+ def set_tool_state(cls, tool_state):
+ cls.tool_state = tool_state
@classmethod
def set_snap_plane_method(cls, value=True):
@@ -48,13 +45,6 @@ class Snap(bonsai.core.tool.Snap):
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()
@@ -70,6 +60,7 @@ class Snap(bonsai.core.tool.Snap):
return snap_point
+ # TODO Remove this function
@classmethod
def select_snap_point(cls, snap_points, hit, threshold):
shortest_distance = None
@@ -98,16 +89,15 @@ 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]
snap_vertex.snap_type = snap_type
+ @classmethod
+ def clear_snapping_point(cls):
+ bpy.context.scene.BIMModelProperties.snap_mouse_point.clear()
+
@classmethod
def update_snapping_ref(cls, snap_point, snap_type):
try:
@@ -125,18 +115,18 @@ class Snap(bonsai.core.tool.Snap):
bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear()
@classmethod
- def insert_polyline_point(cls, input_panel):
- x = float(input_panel["X"])
- y = float(input_panel["Y"])
- try:
- z = float(input_panel["Z"])
- except:
- z = Vector((0, 0, 0))
- d = input_panel["D"]
- a = input_panel["A"]
+ def insert_polyline_point(cls, input_ui):
+ x = input_ui.get_number_value("X")
+ y = input_ui.get_number_value("Y")
+ if input_ui.get_number_value("Z") is not None:
+ z = input_ui.get_number_value("Z")
+ else:
+ z = 0
+ d = input_ui.get_formatted_value("D")
+ a = input_ui.get_formatted_value("A")
snap_vertex = bpy.context.scene.BIMModelProperties.snap_mouse_point[0]
- if cls.use_default_container:
+ if cls.tool_state.use_default_container:
z = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
if x is None and y is None:
@@ -186,12 +176,12 @@ class Snap(bonsai.core.tool.Snap):
polyline_measurement.remove(len(polyline_measurement) - 1)
@classmethod
- def snap_on_axis(cls, intersection, lock_axis=None):
+ def snap_on_axis(cls, intersection, tool_state, lock_angle=False):
def create_axis_line_data(rot_mat, origin):
length = 1000
direction = Vector((1, 0, 0))
- if cls.snap_plane_method == "YZ" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"):
+ if tool_state.plane_method == "YZ" or (not tool_state.plane_method and tool_state.axis_method == "Z"):
direction = Vector((0, 0, 1))
rot_dir = rot_mat.inverted() @ direction
start = origin + rot_dir * length
@@ -202,13 +192,13 @@ class Snap(bonsai.core.tool.Snap):
def create_axis_rectangle_data(origin):
size = 0.5
direction = Vector((1, 0, 0))
- if cls.snap_plane_method == "YZ":
+ if tool_state.plane_method == "YZ":
direction = Vector((0, 0, 1))
rot_mat = Matrix.Rotation(math.radians(360), 3, pivot_axis)
rot_dir = rot_mat.inverted() @ direction
v1 = origin + rot_dir * 0
v2 = origin + rot_dir * size
- if cls.snap_plane_method == "XY":
+ if tool_state.plane_method == "XY":
angle = 270
else:
angle = 90
@@ -230,17 +220,17 @@ class Snap(bonsai.core.tool.Snap):
# Translates intersection point based on last_point
translated_intersection = intersection - last_point
snap_axis = []
- if not lock_axis:
- for i in range(1, 13):
- angle = 30 * i
+ if not tool_state.snap_angle:
+ for i in range(1, 25):
+ angle = 15 * i
snap_axis.append(angle)
else:
- snap_axis = [lock_axis]
+ snap_axis = [tool_state.snap_angle]
pivot_axis = "Z"
- if cls.snap_plane_method == "XZ":
+ if tool_state.plane_method == "XZ":
pivot_axis = "Y"
- if cls.snap_plane_method == "YZ":
+ if tool_state.plane_method == "YZ":
pivot_axis = "X"
for axis in snap_axis:
@@ -248,10 +238,10 @@ class Snap(bonsai.core.tool.Snap):
start, end = create_axis_line_data(rot_mat, last_point)
rot_intersection = rot_mat @ translated_intersection
proximity = rot_intersection.y
- if cls.snap_plane_method == "XZ":
+ if tool_state.plane_method == "XZ":
proximity = rot_intersection.z
PolylineDecorator.set_angle_axis_line(start, end)
- if lock_axis:
+ if lock_angle:
is_on_rot_axis = True
else:
is_on_rot_axis = abs(proximity) <= 0.15
@@ -259,7 +249,7 @@ class Snap(bonsai.core.tool.Snap):
if is_on_rot_axis:
# Snap to axis
rot_intersection = Vector((rot_intersection.x, 0, rot_intersection.z))
- if cls.snap_plane_method == "XZ":
+ if tool_state.plane_method == "XZ":
rot_intersection = Vector((rot_intersection.x, rot_intersection.y, 0))
# Convert it back
snap_intersection = rot_mat.inverted() @ rot_intersection + last_point
@@ -280,11 +270,10 @@ class Snap(bonsai.core.tool.Snap):
return sorted_intersections[0], "Mix"
@classmethod
- def detect_snapping_points(cls, context, event, objs_2d_bbox):
- region = context.region
+ def detect_snapping_points(cls, context, event, objs_2d_bbox, tool_state):
rv3d = context.region_data
space = context.space_data
- cls.mouse_pos = event.mouse_region_x, event.mouse_region_y
+ mouse_pos = event.mouse_region_x, event.mouse_region_y
detected_snaps = []
snap_threshold = 0.3
@@ -306,14 +295,16 @@ class Snap(bonsai.core.tool.Snap):
plane_origin = Vector((0, 0, 0))
plane_normal = Vector((0, 0, 1))
- if not cls.snap_plane_method:
+ if not tool_state.plane_method:
camera_rotation = rv3d.view_rotation
plane_origin = Vector((0, 0, 0))
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:
+ if tool_state.plane_method == "XY" or (
+ not tool_state.plane_method and tool_state.axis_method in {"X", "Y"}
+ ):
+ if cls.tool_state.use_default_container:
plane_origin = Vector((0, 0, elevation))
elif not last_polyline_point:
plane_origin = Vector((0, 0, 0))
@@ -321,19 +312,19 @@ 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" or (not cls.snap_plane_method and cls.snap_axis_method == "Z"):
+ elif tool_state.plane_method == "XZ" or (not tool_state.plane_method and tool_state.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))
- elif cls.snap_plane_method == "YZ":
+ elif tool_state.plane_method == "YZ":
if last_polyline_point:
plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
plane_normal = Vector((1, 0, 0))
return plane_origin, plane_normal
- def cast_rays_and_get_best_object(objs_to_raycast):
+ def cast_rays_and_get_best_object(objs_to_raycast, mouse_pos):
best_length_squared = 1.0
best_obj = None
best_hit = None
@@ -343,13 +334,13 @@ class Snap(bonsai.core.tool.Snap):
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
if hit is None:
# Tried original mouse position. Now it will try the offsets.
- original_mouse_pos = cls.mouse_pos
+ original_mouse_pos = mouse_pos
for value in mouse_offset:
- cls.mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value))
- hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, cls.mouse_pos)
+ mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value))
+ hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj, mouse_pos)
if hit:
break
- cls.mouse_pos = original_mouse_pos
+ mouse_pos = original_mouse_pos
if hit is not None:
hit_world = obj.original.matrix_world @ hit
@@ -371,14 +362,14 @@ class Snap(bonsai.core.tool.Snap):
objs_to_raycast = []
for obj, bbox_2d in objs_2d_bbox:
if obj.type == "MESH" and bbox_2d:
- if tool.Raycast.intersect_mouse_2d_bounding_box(cls.mouse_pos, bbox_2d, offset):
+ if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d, offset):
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)
+ snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast, mouse_pos)
if hit is not None:
detected_snaps.append({"Object": (snap_obj, hit, face_index)})
@@ -405,7 +396,6 @@ class Snap(bonsai.core.tool.Snap):
elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
plane_origin, plane_normal = select_plane_method()
- PolylineDecorator.set_plane(plane_origin, plane_normal)
intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal)
axis_start = None
@@ -413,30 +403,33 @@ class Snap(bonsai.core.tool.Snap):
# 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:
+ if not tool_state.plane_method:
+ if tool_state.axis_method == "X":
+ tool_state.snap_angle = 180
+ if tool_state.axis_method == "Y":
+ tool_state.snap_angle = 90
+ if tool_state.axis_method == "Z":
+ tool_state.snap_angle = 90
+ if tool_state.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)
+ rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, tool_state, True)
+
+ if tool_state.plane_method:
+ if tool_state.plane_method in {"XY", "XZ"} and tool_state.axis_method == "X":
+ tool_state.snap_angle = 180
+ if tool_state.plane_method in {"XY", "YZ"} and tool_state.axis_method == "Y":
+ tool_state.snap_angle = 90
+ if tool_state.plane_method in {"YZ"} and tool_state.axis_method == "Z":
+ tool_state.snap_angle = 180
+ if tool_state.plane_method in {"XZ"} and tool_state.axis_method == "Z":
+ tool_state.snap_angle = 90
+ if event.shift or tool_state.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, tool_state, True)
else:
- rot_intersection, cls.snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, None)
+ rot_intersection, tool_state.snap_angle, axis_start, axis_end = cls.snap_on_axis(
+ intersection, tool_state, False
+ )
if rot_intersection:
detected_snaps.append({"Axis": (rot_intersection, axis_start, axis_end)})
@@ -445,7 +438,7 @@ class Snap(bonsai.core.tool.Snap):
return detected_snaps
@classmethod
- def select_snapping_points(cls, context, event, detected_snaps):
+ def select_snapping_points(cls, context, event, tool_state, detected_snaps):
snapping_points = []
for origin in detected_snaps:
if "Object" in list(origin.keys()):
@@ -477,10 +470,6 @@ class Snap(bonsai.core.tool.Snap):
snapping_points.append(op)
break
- if "Plane" in list(origin.keys()):
- intersection = origin["Plane"]
- snapping_points.append((intersection, "Plane"))
-
for origin in detected_snaps:
if "Axis" in list(origin.keys()):
intersection = origin["Axis"]
@@ -488,8 +477,12 @@ class Snap(bonsai.core.tool.Snap):
axis_end = intersection[2]
snapping_points.append((intersection[0], "Axis"))
+ if "Plane" in list(origin.keys()):
+ intersection = origin["Plane"]
+ snapping_points.append((intersection, "Plane"))
+
# Make Axis first priority
- if event.shift or cls.snap_axis_method in {"X", "Y", "Z"}:
+ if event.shift or tool_state.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":
@@ -508,131 +501,3 @@ class Snap(bonsai.core.tool.Snap):
shifted_list = snapping_points[1:] + snapping_points[: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
- dim: imperial
-
- FORMULA: "="
-
- imperial: feet? "-"? inches?
- feet: NUMBER? "-"? fraction? "'"
- inches: NUMBER? "-"? fraction? "\\""
- fraction: NUMBER "/" NUMBER
-
- expr: (ADD | SUB) dim | (MUL | DIV) NUMBER
-
- NUMBER: /-?\\d+(?:\\.\\d+)?/
- ADD: "+"
- SUB: "-"
- MUL: "*"
- DIV: "/"
-
- %ignore " "
- """
-
- grammar_metric = """
- start: FORMULA? dim expr?
- dim: metric
-
- FORMULA: "="
-
- metric: NUMBER
-
- expr: (ADD | SUB | MUL | DIV) dim
-
- NUMBER: /-?\\d+(?:\\.\\d+)?/
- ADD: "+"
- SUB: "-"
- MUL: "*"
- DIV: "/"
-
- %ignore " "
- """
-
- class InputTransform(Transformer):
- def NUMBER(self, n):
- return float(n)
-
- def fraction(self, numbers):
- return numbers[0] / numbers[1]
-
- def inches(self, args):
- if len(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:
- if args[0] <= 0:
- result = args[0] - args[1]
- else:
- result = args[0] + args[1]
- else:
- result = args[0]
- return result
-
- def metric(self, args):
- return args[0]
-
- def dim(self, args):
- return args[0]
-
- def expr(self, args):
- op = args[0]
- value = float(args[1])
- if op == "+":
- return lambda x: x + value
- elif op == "-":
- return lambda x: x - value
- elif op == "*":
- return lambda x: x * value
- elif op == "/":
- return lambda x: x / value
-
- def FORMULA(cls, args):
- return args[0]
-
- def start(self, args):
- i = 0
- if args[0] == "=":
- i += 1
- else:
- if len(args) > 1:
- raise ValueError("Invalid input.")
- dimension = args[i]
- 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":
- parser = Lark(grammar_imperial)
- factor = 0.3048
- else:
- parser = Lark(grammar_metric)
- factor = 1
- if bpy.context.scene.unit_settings.length_unit == "MILLIMETERS":
- factor = 0.001
-
- if input_type == "A":
- parser = Lark(grammar_metric)
- factor = 1
-
- parse_tree = parser.parse(input_number)
-
- transformer = InputTransform()
- result = transformer.transform(parse_tree)
- return True, str(result)
- except:
- return False, "0"
diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py
index e4147c61bc..d9442bd356 100644
--- a/src/bonsai/bonsai/tool/spatial.py
+++ b/src/bonsai/bonsai/tool/spatial.py
@@ -48,8 +48,7 @@ from natsort import natsorted
class Spatial(bonsai.core.tool.Spatial):
@classmethod
def can_contain(cls, container: ifcopenshell.entity_instance, element_obj: Union[bpy.types.Object, None]) -> bool:
- element = tool.Ifc.get_entity(element_obj)
- if not element:
+ if not (element := tool.Ifc.get_entity(element_obj)):
return False
if tool.Ifc.get_schema() == "IFC2X3":
if not container.is_a("IfcSpatialStructureElement"):
diff --git a/src/bonsai/bonsai/tool/web.py b/src/bonsai/bonsai/tool/web.py
index 5408efd898..d459ac1e3c 100644
--- a/src/bonsai/bonsai/tool/web.py
+++ b/src/bonsai/bonsai/tool/web.py
@@ -344,52 +344,6 @@ class Web(bonsai.core.tool.Web):
operator_data (dict): A dictionary containing the operator data.
"""
- def selection_data(cost_item, elements):
- print(cost_item, elements)
- if not elements:
- return []
- if not cost_item:
- return [
- {
- "info": {
- "id": element.id(),
- "name": element.Name,
- "class": element.is_a(),
- "type": get_type(element).Name if get_type(element) else None,
- },
- "qtos": get_psets(element, qtos_only=True),
- }
- for element in elements or []
- ]
- data = []
- for element in elements or []:
- psets = get_psets(element, qtos_only=True)
- element_type = get_type(element)
- quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element)
- data.append(
- {
- "info": {
- "id": element.id(),
- "name": element.Name,
- "class": element.is_a(),
- "type": element_type.Name if element_type else None,
- },
- "qtos": psets,
- "assigned_quantities": [
- {
- "cost_item_id": cost_item.id(),
- "product_id": element.id(),
- "name": q.Name,
- "value": q[3],
- "type": q.Unit,
- }
- for q in quantities or []
- ],
- "unit": unit,
- }
- )
- return data
-
ifc_file = tool.Ifc.get()
if operator_data["type"] == "getPredefinedTypes":
print("getting predefined types")
@@ -425,31 +379,6 @@ class Web(bonsai.core.tool.Web):
cost_item = tool.Ifc.get().by_id(operator_data["costItemId"])
products = tool.Cost.get_cost_item_products(cost_item, is_deep=True)
tool.Spatial.select_products(products, unhide=True)
- if operator_data["type"] == "getSelectedProducts":
- cost_item_id = operator_data["costItemId"]
- cost_item = ifc_file.by_id(cost_item_id)
- if not cost_item:
- print("---> cost item not found")
- return
-
- selected_products = list(tool.Spatial.get_selected_products())
- selected_products_data = selection_data(cost_item, selected_products)
-
- assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False)
- assigned_products_data = selection_data(cost_item, assigned_products)
-
- names = ifcopenshell.util.cost.get_product_quantity_names(selected_products)
- cls.send_webui_data(
- data={
- "selected_products": selected_products_data,
- "assigned_products": assigned_products_data,
- "product_quantity_names": names,
- "cost_item_id": cost_item_id,
- },
- data_key="selected_products",
- event="selected_products",
- )
-
if operator_data["type"] == "addSummaryCostItem":
cost_schedule = ifc_file.by_id(operator_data["costScheduleId"])
bonsai.core.cost.add_summary_cost_item(tool.Ifc, tool.Cost, cost_schedule=cost_schedule)
@@ -520,18 +449,120 @@ class Web(bonsai.core.tool.Web):
prop_name = operator_data["propName"]
if prop_name == "count":
prop_name = ""
- print(prop_name)
bpy.ops.bim.assign_cost_item_quantity(
cost_item=operator_data["costItemId"], related_object_type="PRODUCT", prop_name=prop_name
)
cost_schedule = tool.Cost.get_cost_schedule(cost_item=tool.Ifc.get().by_id(operator_data["costItemId"]))
cls.load_cost_schedule_web_ui(cost_schedule)
+ if operator_data["type"] == "enableEditingQuantities":
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cls.enableEditingCostItemQuantities(cost_item)
+ if operator_data["type"] == "AddCostItemQuantity":
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cost_schedule = tool.Cost.get_cost_schedule(cost_item)
+ bpy.ops.bim.add_cost_item_quantity(cost_item=cost_item_id, ifc_class=operator_data["ifcClass"])
+ cls.load_cost_schedule_web_ui(cost_schedule)
+ cls.enableEditingCostItemQuantities(cost_item)
+ if operator_data["type"] == "editCostItemQuantity":
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cost_schedule = tool.Cost.get_cost_schedule(cost_item)
+ physical_quantity = tool.Ifc.get().by_id(operator_data["quantityId"])
+ attributes = operator_data["attributes"]
+ tool.Ifc.run("cost.edit_cost_item_quantity", physical_quantity=physical_quantity, attributes=attributes)
+ tool.Cost.load_cost_item_quantities(ifc_file.by_id(operator_data["costItemId"]))
+ cls.load_cost_schedule_web_ui(cost_schedule)
+ cls.enableEditingCostItemQuantities(cost_item)
+ if operator_data["type"] == "deleteCostItemQuantity":
+ bpy.ops.bim.remove_cost_item_quantity(
+ cost_item=operator_data["costItemId"], physical_quantity=operator_data["quantityId"]
+ )
+ cost_item_id = operator_data["costItemId"]
+ cost_item = ifc_file.by_id(cost_item_id)
+ cost_schedule = tool.Cost.get_cost_schedule(cost_item)
+ cls.load_cost_schedule_web_ui(cost_schedule)
+ cls.enableEditingCostItemQuantities(cost_item)
+
+ @classmethod
+ def enableEditingCostItemQuantities(cls, cost_item):
+ if not cost_item:
+ return
+ selected_products = list(tool.Spatial.get_selected_products())
+ selected_products_data = cls.selection_data(cost_item, selected_products)
+
+ assigned_products = tool.Cost.get_cost_item_products(cost_item, is_deep=False)
+ assigned_products_data = cls.selection_data(cost_item, assigned_products)
+
+ names = ifcopenshell.util.cost.get_product_quantity_names(selected_products)
+ cls.send_webui_data(
+ data={
+ "selected_products": selected_products_data,
+ "assigned_products": assigned_products_data,
+ "product_quantity_names": names,
+ "cost_item_id": cost_item.id(),
+ "cost_quantities": tool.Cost.get_cost_quantities(cost_item),
+ },
+ data_key="quantities",
+ event="quantities",
+ )
+
+ @classmethod
+ def selection_data(cls, cost_item, elements):
+ if not elements:
+ return []
+ if not cost_item:
+ return [
+ {
+ "info": {
+ "id": element.id(),
+ "name": element.Name,
+ "class": element.is_a(),
+ "type": get_type(element).Name if get_type(element) else None,
+ },
+ "qtos": get_psets(element, qtos_only=True),
+ }
+ for element in elements or []
+ ]
+ data = []
+ for element in elements or []:
+ psets = get_psets(element, qtos_only=True)
+ element_type = get_type(element)
+ quantities, unit = tool.Cost.get_assigned_quantities(cost_item, element)
+ data.append(
+ {
+ "info": {
+ "id": element.id(),
+ "name": element.Name,
+ "class": element.is_a(),
+ "type": element_type.Name if element_type else None,
+ },
+ "qtos": psets,
+ "assigned_quantities": [
+ {
+ "cost_item_id": cost_item.id(),
+ "product_id": element.id(),
+ "name": q.Name,
+ "value": q[3],
+ "type": q.Unit,
+ }
+ for q in quantities or []
+ ],
+ "unit": unit,
+ }
+ )
+ return data
@classmethod
def load_cost_schedule_web_ui(cls, cost_schedule):
json_data = tool.Cost.create_cost_schedule_json(cost_schedule)
cls.send_webui_data(
- data={"cost_items": json_data, "cost_schedule_id": cost_schedule.id()},
+ data={
+ "cost_items": json_data,
+ "cost_schedule_id": cost_schedule.id(),
+ "currency": tool.Cost.currency(),
+ },
data_key="cost_items",
event="cost_items",
)
diff --git a/src/ifc5d/ifc5d/qto.py b/src/ifc5d/ifc5d/qto.py
index f73d5870c1..6673997ce2 100644
--- a/src/ifc5d/ifc5d/qto.py
+++ b/src/ifc5d/ifc5d/qto.py
@@ -24,22 +24,29 @@ import ifcopenshell.api.pset
import ifcopenshell.util.unit
import ifcopenshell.util.element
import ifcopenshell.util.selector
+import ifcopenshell.util.shape
import ifcopenshell.util.representation
import multiprocessing
from collections import namedtuple
-from typing import Any
+from typing import Any, Literal, get_args
Function = namedtuple("Function", ["measure", "name", "description"])
-rules = {}
+RULE_SET = Literal["IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender"]
+rules: dict[RULE_SET, dict[str, Any]] = {}
cwd = os.path.dirname(os.path.realpath(__file__))
-for name in ("IFC4QtoBaseQuantities", "IFC4QtoBaseQuantitiesBlender"):
+for name in get_args(RULE_SET):
with open(os.path.join(cwd, name + ".json"), "r") as f:
rules[name] = json.load(f)
def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> dict:
+ """
+
+ :param rules: Set of rules from `ifc5d.qto.rules`.
+
+ """
results = {}
for calculator, queries in rules["calculators"].items():
calculator = calculators[calculator]
@@ -51,6 +58,11 @@ def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_inst
def edit_qtos(ifc_file: ifcopenshell.file, results: dict[ifcopenshell.entity_instance, Any]) -> None:
+ """
+
+ :param results: Results from `ifc5d.qto.quantify`.
+
+ """
for element, qtos in results.items():
for name, quantities in qtos.items():
qto = ifcopenshell.util.element.get_pset(element, name, should_inherit=False)
diff --git a/src/ifcgeom/AbstractKernel.cpp b/src/ifcgeom/AbstractKernel.cpp
index 23d7bc1e21..f785114420 100644
--- a/src/ifcgeom/AbstractKernel.cpp
+++ b/src/ifcgeom/AbstractKernel.cpp
@@ -3,6 +3,7 @@
#include "../ifcgeom/IfcGeomElement.h"
#include "../ifcgeom/ConversionSettings.h"
#include "../ifcgeom/abstract_mapping.h"
+#include "../ifcgeom/piecewise_function_evaluator.h"
#ifdef IFOPSH_WITH_OPENCASCADE
#include "../ifcgeom/kernels/opencascade/OpenCascadeKernel.h"
@@ -227,7 +228,8 @@ bool ifcopenshell::geometry::kernels::AbstractKernel::convert_impl(const taxonom
}
bool ifcopenshell::geometry::kernels::AbstractKernel::convert_impl(const taxonomy::piecewise_function::ptr item, IfcGeom::ConversionResults& cs) {
- auto expl = item->evaluate();
+ piecewise_function_evaluator evaluator(item);
+ auto expl = evaluator.evaluate();
expl->instance = item->instance;
return convert(expl, cs);
}
diff --git a/src/ifcgeom/Iterator.h b/src/ifcgeom/Iterator.h
index 8bf862def1..242cdb7418 100644
--- a/src/ifcgeom/Iterator.h
+++ b/src/ifcgeom/Iterator.h
@@ -701,6 +701,10 @@ namespace IfcGeom {
/// Gets the representation of the current geometrical entity.
Element* get()
{
+ if (!initialization_outcome_) {
+ throw std::runtime_error("Iterator not initialized");
+ }
+
auto ret = *task_result_iterator_;
// If we want to organize the element considering their hierarchy
diff --git a/src/ifcgeom/infra_sweep_helper.cpp b/src/ifcgeom/infra_sweep_helper.cpp
new file mode 100644
index 0000000000..5456a14fc9
--- /dev/null
+++ b/src/ifcgeom/infra_sweep_helper.cpp
@@ -0,0 +1,203 @@
+#include "profile_helper.h"
+#include "infra_sweep_helper.h"
+#include "piecewise_function_evaluator.h"
+
+#include
+
+using namespace ifcopenshell::geometry;
+
+namespace {
+ // std::lerp when upgrading to C++ 20
+ template
+ T lerp(const T& a, const T& b, double t) {
+ return a + t * (b - a);
+ }
+}
+
+taxonomy::loft::ptr ifcopenshell::geometry::make_loft(const Settings& settings_, const IfcUtil::IfcBaseClass* inst, const taxonomy::piecewise_function::ptr& pwf, std::vector& cross_sections)
+{
+ std::sort(cross_sections.begin(), cross_sections.end());
+
+ auto loft = taxonomy::make();
+ // @todo intialize as default
+ loft->axis = nullptr;
+
+ // @todo currently only the case is handled where directrix returns a piecewise_function
+ // @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a piecewise function
+ if (pwf) {
+ piecewise_function_evaluator evaluator(pwf, &settings_);
+ double start = std::max(0., cross_sections.front().dist_along);
+ double end = std::min(pwf->length(), cross_sections.back().dist_along);
+
+ if (end - start < 1.e-9) {
+ Logger::Warning("Empty sweep domain with start at " + std::to_string(cross_sections.front().dist_along) + " end at " + std::to_string(cross_sections.back().dist_along) + " and curve domain length " + std::to_string(pwf->length()), inst);
+ return nullptr;
+ }
+
+ auto curve_length = end - start;
+ auto param_type = settings_.get().get();
+ auto param = settings_.get().get();
+ size_t num_steps = 0;
+ if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) {
+ // parameter is max step size
+ num_steps = (size_t)std::ceil(curve_length / param);
+ } else {
+ // parameter is minimum number of steps
+ num_steps = (size_t)std::ceil(param);
+ }
+ std::vector longitudes;
+ for (auto& x : cross_sections) {
+ longitudes.push_back(x.dist_along);
+ }
+ longitudes.push_back(std::numeric_limits::infinity());
+ auto profile_index = longitudes.begin();
+ for (size_t i = 0; i <= num_steps; ++i) {
+ auto dist_along = start + curve_length / num_steps * i;
+ while (dist_along > *(profile_index + 1)) {
+ profile_index++;
+ if (profile_index == longitudes.end()) {
+ // @todo handle this?
+ }
+ }
+
+ auto relative_dist_along = (dist_along - *profile_index) / (*(profile_index + 1) - *profile_index);
+ const auto& profile_a = cross_sections[std::distance(longitudes.begin(), profile_index)].section_geometry;
+ const auto& offset_a = cross_sections[std::distance(longitudes.begin(), profile_index)].offset;
+
+ taxonomy::geom_item::ptr interpolated = nullptr;
+
+ // Only interpolate if:
+ // - there is a profile ahead of us, and
+ // - we're not exactly at the location of the current profile or whether there is an offset involved.
+ bool should_interpolate =
+ (profile_index + 1 < longitudes.end()) &&
+ (relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0.);
+
+ if (should_interpolate) {
+ taxonomy::geom_item::ptr profile_b;
+ Eigen::Vector3d offset_b;
+ if ((profile_index + 1 < longitudes.end())) {
+ profile_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].section_geometry;
+ offset_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].offset;
+ } else {
+ profile_b = profile_a;
+ offset_b = offset_a;
+ }
+
+ // Only interpolate if the profiles are different or either of the offsets is non-zero
+ bool should_interpolate2 =
+ (profile_a->instance != profile_b->instance) ||
+ (offset_a.cwiseAbs().maxCoeff() > 0. || offset_b.cwiseAbs().maxCoeff() > 0.);
+
+ if (should_interpolate2) {
+
+ std::vector loops_a, loops_b;
+
+ if (profile_a->kind() == taxonomy::FACE) {
+ interpolated = taxonomy::make();
+
+ auto profile_a_f = std::static_pointer_cast(profile_a);
+ auto profile_b_f = std::static_pointer_cast(profile_b);
+
+ if (profile_a_f->children.size() != profile_b_f->children.size()) {
+ Logger::Warning("Mismatching number of face boundaries: " +
+ std::to_string(profile_a_f->children.size()) + " vs " +
+ std::to_string(profile_b_f->children.size()),
+ inst
+ );
+ return nullptr;
+ }
+ loops_a = profile_a_f->children;
+ loops_b = profile_b_f->children;
+ } else {
+ loops_a = { std::static_pointer_cast(profile_a) };
+ loops_b = { std::static_pointer_cast(profile_b) };
+ interpolated = taxonomy::make();
+ }
+
+ // @todo should_interpolate should also be informed based by different face matrices.
+ if (profile_a->matrix || profile_b->matrix) {
+ interpolated->matrix = taxonomy::make();
+ Eigen::Matrix4d m4a = Eigen::Matrix4d::Identity();
+ Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity();
+ if (profile_a->matrix) {
+ m4a = profile_a->matrix->ccomponents();
+ }
+ if (profile_b->matrix) {
+ m4b = profile_b->matrix->ccomponents();
+ }
+ interpolated->matrix->components() = lerp(m4a, m4b, relative_dist_along);
+ }
+
+ auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along);
+ taxonomy::loop::ptr w1, w2;
+ taxonomy::edge::ptr e1, e2;
+ for (auto tmp_ : boost::combine(loops_a, loops_b)) {
+ boost::tie(w1, w2) = tmp_;
+ if (w1->children.size() != w2->children.size()) {
+ Logger::Warning("Mismatching number of edges: " +
+ std::to_string(w1->children.size()) + " vs " +
+ std::to_string(w2->children.size()),
+ inst
+ );
+ return nullptr;
+ }
+ std::vector points;
+ for (auto tmp__ : boost::combine(w1->children, w2->children)) {
+ boost::tie(e1, e2) = tmp__;
+ auto& p1 = boost::get(e1->start);
+ auto& p2 = boost::get(e2->start);
+
+ auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval();
+ points.push_back(taxonomy::make(p3));
+ }
+ if (!points.empty()) {
+ // close polygon by referencing first point
+ // @todo add a closed=true|false to polygon_from_points()?
+ points.push_back(points.front());
+ }
+
+ auto interpolated_loop = polygon_from_points(points);
+ if (interpolated->kind() == taxonomy::FACE) {
+ std::static_pointer_cast(interpolated)->children.push_back(interpolated_loop);
+ } else {
+ std::static_pointer_cast(interpolated)->children = interpolated_loop->children;
+ }
+ }
+ }
+ }
+
+ auto m4 = evaluator.evaluate(dist_along);
+ /* {
+ std::wcout << "#" << pwf->instance->data().id() << " " << dist_along << ": " << m4.col(3).row(2).value() << std::endl;
+ }*/
+
+ Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity();
+ m4b.col(0).head<3>() = m4.col(1).head<3>().normalized();
+ m4b.col(1).head<3>() = m4.col(2).head<3>().normalized();
+ m4b.col(2).head<3>() = m4.col(0).head<3>().normalized();
+ m4b.col(3).head<3>() = m4.col(3).head<3>();
+
+ if (interpolated) {
+ loft->children.push_back(interpolated);
+ } else {
+ if (profile_a->kind() == taxonomy::FACE) {
+ loft->children.push_back(std::static_pointer_cast(taxonomy::item::ptr(profile_a->clone_())));
+ } else {
+ loft->children.push_back(std::static_pointer_cast(taxonomy::item::ptr(profile_a->clone_())));
+ }
+ if (profile_a->matrix) {
+ loft->children.back()->matrix = taxonomy::matrix4::ptr(profile_a->matrix->clone_());
+ }
+ }
+ if (!loft->children.back()->matrix) {
+ // @todo should this not be initialized by default? matrix4 already has a 'lazy identity' mechanism.
+ loft->children.back()->matrix = taxonomy::make();
+ }
+ auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval();
+ loft->children.back()->matrix->components() = m;
+ }
+ }
+
+ return loft;
+}
diff --git a/src/ifcgeom/infra_sweep_helper.h b/src/ifcgeom/infra_sweep_helper.h
new file mode 100644
index 0000000000..41b38e6c9a
--- /dev/null
+++ b/src/ifcgeom/infra_sweep_helper.h
@@ -0,0 +1,26 @@
+#ifndef LINEAR_SWEEP_HELPER_H
+#define LINEAR_SWEEP_HELPER_H
+
+#include "taxonomy.h"
+#include "ConversionSettings.h"
+
+namespace ifcopenshell {
+
+ namespace geometry {
+
+ struct cross_section {
+ double dist_along;
+ taxonomy::geom_item::ptr section_geometry;
+ Eigen::Vector3d offset;
+
+ bool operator <(const cross_section& other) const {
+ return dist_along < other.dist_along;
+ }
+ };
+
+ taxonomy::loft::ptr make_loft(const Settings& settings_, const IfcUtil::IfcBaseClass* inst, const taxonomy::piecewise_function::ptr& directrix, std::vector& cross_sections);
+ }
+
+}
+
+#endif
\ No newline at end of file
diff --git a/src/ifcgeom/kernels/opencascade/loft.cpp b/src/ifcgeom/kernels/opencascade/loft.cpp
index b418675fbd..70e174fe01 100644
--- a/src/ifcgeom/kernels/opencascade/loft.cpp
+++ b/src/ifcgeom/kernels/opencascade/loft.cpp
@@ -48,26 +48,43 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
for (auto it = loft->children.begin(); it < loft->children.end() - 1; ++it) {
auto jt = it + 1;
- std::array fa = { *it, *jt };
+ std::array fa = { *it, *jt };
std::array shps;
std::array ws;
for (int i = 0; i < 2; ++i) {
- if (!convert(fa[i], shps[i])) {
- return false;
+ if (fa[i]->kind() == taxonomy::FACE) {
+ if (!convert(std::static_pointer_cast(fa[i]), shps[i])) {
+ return false;
+ }
}
- if (shps[i].ShapeType() != TopAbs_FACE) {
+ if (fa[i]->kind() == taxonomy::LOOP) {
+ TopoDS_Wire w;
+ if (!convert(std::static_pointer_cast(fa[i]), w)) {
+ return false;
+ }
+ shps[i] = w;
+ }
+ if (shps[i].ShapeType() != TopAbs_FACE && shps[i].ShapeType() != TopAbs_WIRE) {
return false;
}
// @todo this is only outer wire
- ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
+ if (shps[i].ShapeType() == TopAbs_FACE) {
+ ws[i] = BRepTools::OuterWire(TopoDS::Face(shps[i]));
+ } else {
+ ws[i] = TopoDS::Wire(shps[i]);
+ }
}
- if (it == loft->children.begin()) {
- // faces.Append(shps[0]);
- BB.Add(comp, shps[0]);
- }
- if (jt == loft->children.end() - 1) {
- // faces.Append(shps[1]);
- BB.Add(comp, shps[1]);
+ if (shps[0].ShapeType() == TopAbs_FACE) {
+ // When processing a sectioned *surface* there are no
+ // begin and end caps that need to be added.
+ if (it == loft->children.begin()) {
+ // faces.Append(shps[0]);
+ BB.Add(comp, shps[0]);
+ }
+ if (jt == loft->children.end() - 1) {
+ // faces.Append(shps[1]);
+ BB.Add(comp, shps[1]);
+ }
}
BRepTools_WireExplorer a(ws[0]);
BRepTools_WireExplorer b(ws[1]);
diff --git a/src/ifcgeom/mapping/IfcCompositeCurve.cpp b/src/ifcgeom/mapping/IfcCompositeCurve.cpp
index b9f2aaaf4d..568b861ad7 100644
--- a/src/ifcgeom/mapping/IfcCompositeCurve.cpp
+++ b/src/ifcgeom/mapping/IfcCompositeCurve.cpp
@@ -92,7 +92,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCompositeCurve* inst) {
return loop;
}
else {
- auto pwf = taxonomy::make(0.0,pwfs,&settings_,inst);
+ auto pwf = taxonomy::make(0.0,pwfs,inst);
return pwf;
}
}
diff --git a/src/ifcgeom/mapping/IfcCurveSegment.cpp b/src/ifcgeom/mapping/IfcCurveSegment.cpp
index 343cb8300d..e2f38135e3 100644
--- a/src/ifcgeom/mapping/IfcCurveSegment.cpp
+++ b/src/ifcgeom/mapping/IfcCurveSegment.cpp
@@ -961,7 +961,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCurveSegment* inst) {
taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(fabs(length), fn);
- auto pwf = taxonomy::make(0.0, spans,&settings_,inst);
+ auto pwf = taxonomy::make(0.0, spans,inst);
return pwf;
}
diff --git a/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp b/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp
index d07e3e80fb..5608b01cdf 100644
--- a/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp
+++ b/src/ifcgeom/mapping/IfcEllipseProfileDef.cpp
@@ -39,6 +39,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) {
#endif
if (has_position) {
m4 = taxonomy::cast(map(inst->Position()));
+ } else {
+ // matrix needs to be set on elementary curves.
+ m4 = taxonomy::make();
}
if (ry > rx) {
@@ -58,9 +61,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) {
auto el = taxonomy::make();
el->radius = rx;
el->radius2 = ry;
+ el->matrix = m4;
ed->basis = el;
lp->children.push_back(ed);
fc->children.push_back(lp);
- fc->matrix = m4;
return fc;
}
diff --git a/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp b/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp
index 7267f61b2a..79632b9f01 100644
--- a/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp
+++ b/src/ifcgeom/mapping/IfcFixedReferenceSweptAreaSolid.cpp
@@ -18,6 +18,7 @@
********************************************************************************/
#include "mapping.h"
+#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
@@ -34,6 +35,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcFixedReferenceSweptAreaSolid
// @todo currently only the case is handled where directrix returns a piecewise_function
if (auto pwf = taxonomy::dcast(dir)) {
+ piecewise_function_evaluator evaluator(pwf,&settings_);
double start = 0;
double end = pwf->length();
#ifdef SCHEMA_HAS_IfcDirectrixCurveSweptAreaSolid
@@ -53,20 +55,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcFixedReferenceSweptAreaSolid
}
}
#endif
- auto curve_length = end - start;
- auto param_type = settings_.get().get();
- auto param = settings_.get().get();
- size_t num_steps = 0;
- if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) {
- // parameter is max step size
- num_steps = (size_t) std::ceil(curve_length / param);
- } else {
- // parameter is minimum number of steps
- num_steps = (size_t) std::ceil(param);
- }
- for (size_t i = 0; i <= num_steps; ++i) {
- auto distalong = start + curve_length / num_steps * i;
- auto m4 = pwf->evaluate(distalong);
+ auto evaluation_points = evaluator.evaluation_points();
+ for (const auto& dist_along : evaluation_points) {
+ auto m4 = evaluator.evaluate(dist_along);
/*
std::stringstream ss;
diff --git a/src/ifcgeom/mapping/IfcGradientCurve.cpp b/src/ifcgeom/mapping/IfcGradientCurve.cpp
index 3c5b95635a..6217b81a0e 100644
--- a/src/ifcgeom/mapping/IfcGradientCurve.cpp
+++ b/src/ifcgeom/mapping/IfcGradientCurve.cpp
@@ -18,6 +18,7 @@
********************************************************************************/
#include "mapping.h"
+#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
@@ -55,7 +56,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) {
double gradient_start = m(0, 3); // start of vertical (row 0, col 3) - "Distance Along" horizontal curve
// create the vertical pwf
- auto vertical = taxonomy::make(gradient_start, pwfs, &settings_);
+ auto vertical = taxonomy::make(gradient_start, pwfs);
// Determine the valid domain of the PWF... the valid domain is where both
// the base curve and gradient curves are defined
@@ -69,11 +70,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) {
}
// define the callback function for the gradient curve
- auto composition = [horizontal, vertical](double u)->Eigen::Matrix4d {
+ piecewise_function_evaluator horizontal_evaluator(horizontal, &settings_), vertical_evaluator(vertical, &settings_);
+ auto composition = [horizontal_evaluator, vertical_evaluator,start=vertical->start()](double u) -> Eigen::Matrix4d {
// u is distance from start of gradient curve (vertical)
// add vertical->start() to u to get distance from start of horizontal
- auto xy = horizontal->evaluate(u + vertical->start());
- auto uz = vertical->evaluate(u);
+ auto xy = horizontal_evaluator.evaluate(u + start);
+ auto uz = vertical_evaluator.evaluate(u);
uz.col(3)(0) = 0.0; // x is distance along. zero it out so it doesn't add to the x from horizontal
uz.col(1).swap(uz.col(2)); // uz is 2D in distance along - y plane, swap y and z so elevations become z
@@ -86,7 +88,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcGradientCurve* inst) {
taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(length, composition);
- auto pwf = taxonomy::make(start, spans, &settings_, inst);
+ auto pwf = taxonomy::make(start, spans, inst);
return pwf;
}
diff --git a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp
index 7af8b42bd8..1ed4d05e8b 100644
--- a/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp
+++ b/src/ifcgeom/mapping/IfcOffsetCurveByDistance.cpp
@@ -19,6 +19,7 @@
#include "mapping.h"
#include "../profile_helper.h"
+#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
@@ -144,11 +145,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst
offset_spans.emplace_back(l, fn);
}
- auto offsets = taxonomy::make(start,offset_spans,&settings_);
+ auto offsets = taxonomy::make(start,offset_spans);
- auto composition = [pw_curve, offsets](double u) -> Eigen::Matrix4d {
- auto p = pw_curve->evaluate(u);
- auto offset = offsets->evaluate(u);
+ piecewise_function_evaluator pw_evaluator(pw_curve, &settings_), offsets_evaluator(offsets, &settings_);
+ auto composition = [pw_evaluator, offsets_evaluator](double u) -> Eigen::Matrix4d {
+ auto p = pw_evaluator.evaluate(u);
+ auto offset = offsets_evaluator.evaluate(u);
Eigen::Matrix4d m = p * offset;
return m;
};
@@ -157,7 +159,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcOffsetCurveByDistances* inst
// this may change depending on decisions in the bSI-IF
taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(basis_curve_length, composition);
- auto pwf = taxonomy::make(start,spans,&settings_,inst);
+ auto pwf = taxonomy::make(start,spans,inst);
return pwf;
}
diff --git a/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp b/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp
index 601765e1a3..6097344eb3 100644
--- a/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp
+++ b/src/ifcgeom/mapping/IfcPointByDistanceExpression.cpp
@@ -19,6 +19,7 @@
#include "mapping.h"
#include "../profile_helper.h"
+#include "../piecewise_function_evaluator.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
@@ -31,7 +32,8 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPointByDistanceExpression* i
//auto item = map(basis_curve);
//auto pw_curve = ifcopenshell::geometry::piecewise_from_item(item);
auto pw_curve = taxonomy::dcast(map(inst->BasisCurve()));
- auto m = pw_curve->evaluate(u);
+ piecewise_function_evaluator evaluator(pw_curve,&settings_);
+ auto m = evaluator.evaluate(u);
auto o = m.col(3).head<3>();
auto z = m.col(2).head<3>();
diff --git a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
index 74e6e4cd89..14ee9899ad 100644
--- a/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
+++ b/src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp
@@ -22,28 +22,10 @@
using namespace ifcopenshell::geometry;
#include "../../ifcgeom/profile_helper.h"
-
-#include
+#include "../../ifcgeom/infra_sweep_helper.h"
#ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal
-namespace {
- // std::lerp when upgrading to C++ 20
- template
- T lerp(const T& a, const T& b, double t) {
- return a + t * (b - a);
- }
-
- struct cross_section {
- double dist_along;
- taxonomy::face::ptr section_geometry;
- Eigen::Vector3d offset;
-
- bool operator <(const cross_section& other) const {
- return dist_along < other.dist_along;
- }
- };
-}
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* inst) {
std::vector cross_sections;
@@ -105,162 +87,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSolidHorizontal* in
}
}
- std::sort(cross_sections.begin(), cross_sections.end());
-
- auto loft = taxonomy::make();
- // @todo intialize as default
- loft->axis = nullptr;
-
- // @todo currently only the case is handled where directrix returns a piecewise_function
- // @todo this "if" statement is not really required because the function returns at the start if the Directrix is not a piecewise function
- if (pwf) {
- double start = std::max(0., cross_sections.front().dist_along);
- double end = std::min(pwf->length(), cross_sections.back().dist_along);
-
- if (end - start < 1.e-9) {
- Logger::Warning("Empty sweep domain with start at " + std::to_string(cross_sections.front().dist_along) + " end at " + std::to_string(cross_sections.back().dist_along) + " and curve domain length " + std::to_string(pwf->length()), inst);
- return nullptr;
- }
-
- auto curve_length = end - start;
- auto param_type = settings_.get().get();
- auto param = settings_.get().get();
- size_t num_steps = 0;
- if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) {
- // parameter is max step size
- num_steps = (size_t) std::ceil(curve_length / param);
- } else {
- // parameter is minimum number of steps
- num_steps = (size_t) std::ceil(param);
- }
- std::vector longitudes;
- for (auto& x : cross_sections) {
- longitudes.push_back(x.dist_along);
- }
- longitudes.push_back(std::numeric_limits::infinity());
- auto profile_index = longitudes.begin();
- for (size_t i = 0; i <= num_steps; ++i) {
- auto dist_along = start + curve_length / num_steps * i;
- while (dist_along > *(profile_index+1)) {
- profile_index++;
- if (profile_index == longitudes.end()) {
- // @todo handle this?
- }
- }
-
- auto relative_dist_along = (dist_along - *profile_index) / (*(profile_index+1) - *profile_index);
- const auto& profile_a = cross_sections[std::distance(longitudes.begin(), profile_index)].section_geometry;
- const auto& offset_a = cross_sections[std::distance(longitudes.begin(), profile_index)].offset;
-
- taxonomy::face::ptr interpolated = nullptr;
-
- // Only interpolate if:
- // - there is a profile ahead of us, and
- // - we're not exactly at the location of the current profile or whether there is an offset involved.
- bool should_interpolate =
- (profile_index + 1 < longitudes.end()) &&
- (relative_dist_along >= 1.e-9 || offset_a.cwiseAbs().maxCoeff() > 0.);
-
- if (should_interpolate) {
- taxonomy::face::ptr profile_b;
- Eigen::Vector3d offset_b;
- if ((profile_index + 1 < longitudes.end())) {
- profile_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].section_geometry;
- offset_b = cross_sections[std::distance(longitudes.begin(), profile_index) + 1].offset;
- } else {
- profile_b = profile_a;
- offset_b = offset_a;
- }
-
- // Only interpolate if the profiles are different or either of the offsets is non-zero
- bool should_interpolate2 =
- (profile_a->instance != profile_b->instance) ||
- (offset_a.cwiseAbs().maxCoeff() > 0. || offset_b.cwiseAbs().maxCoeff() > 0.);
-
- if (should_interpolate2) {
- if (profile_a->children.size() != profile_b->children.size()) {
- Logger::Warning("Mismatching number of face boundaries: " +
- std::to_string(profile_a->children.size()) + " vs " +
- std::to_string(profile_b->children.size()),
- inst
- );
- return nullptr;
- }
- interpolated = taxonomy::make();
- // @todo should_interpolate should also be informed based by different face matrices.
- if (profile_a->matrix || profile_b->matrix) {
- interpolated->matrix = taxonomy::make();
- Eigen::Matrix4d m4a = Eigen::Matrix4d::Identity();
- Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity();
- if (profile_a->matrix) {
- m4a = profile_a->matrix->ccomponents();
- }
- if (profile_b->matrix) {
- m4b = profile_b->matrix->ccomponents();
- }
- interpolated->matrix->components() = lerp(m4a, m4b, relative_dist_along);
- }
- auto interpolated_offset = lerp(offset_a, offset_b, relative_dist_along);
- taxonomy::loop::ptr w1, w2;
- taxonomy::edge::ptr e1, e2;
- for (auto tmp_ : boost::combine(profile_a->children, profile_b->children)) {
- boost::tie(w1, w2) = tmp_;
- if (w1->children.size() != w2->children.size()) {
- Logger::Warning("Mismatching number of edges for face boundary: " +
- std::to_string(w1->children.size()) + " vs " +
- std::to_string(w2->children.size()),
- inst
- );
- return nullptr;
- }
- std::vector points;
- for (auto tmp__ : boost::combine(w1->children, w2->children)) {
- boost::tie(e1, e2) = tmp__;
- auto& p1 = boost::get(e1->start);
- auto& p2 = boost::get(e2->start);
-
- auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval();
- points.push_back(taxonomy::make(p3));
- }
- if (!points.empty()) {
- // close polygon by referencing first point
- // @todo add a closed=true|false to polygon_from_points()?
- points.push_back(points.front());
- }
- interpolated->children.push_back(polygon_from_points(points));
- }
- }
- }
-
- auto m4 = pwf->evaluate(dist_along);
- /* {
- std::wcout << "#" << pwf->instance->data().id() << " " << dist_along << ": " << m4.col(3).row(2).value() << std::endl;
- }*/
-
- Eigen::Matrix4d m4b = Eigen::Matrix4d::Identity();
- m4b.col(0).head<3>() = m4.col(1).head<3>().normalized();
- m4b.col(1).head<3>() = m4.col(2).head<3>().normalized();
- m4b.col(2).head<3>() = m4.col(0).head<3>().normalized();
- m4b.col(3).head<3>() = m4.col(3).head<3>();
-
- if (interpolated) {
- loft->children.push_back(interpolated);
- } else {
- loft->children.push_back(taxonomy::face::ptr(profile_a->clone_()));
- if (profile_a->matrix) {
- loft->children.back()->matrix = taxonomy::matrix4::ptr(profile_a->matrix->clone_());
- }
- }
- if (!loft->children.back()->matrix) {
- // @todo should this not be initialized by default? matrix4 already has a 'lazy identity' mechanism.
- loft->children.back()->matrix = taxonomy::make();
- }
- auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval();
- loft->children.back()->matrix->components() = m;
- }
- }
-
- return loft;
+ return make_loft(settings_, inst, pwf, cross_sections);
}
#endif
diff --git a/src/ifcgeom/mapping/IfcSectionedSurface.cpp b/src/ifcgeom/mapping/IfcSectionedSurface.cpp
new file mode 100644
index 0000000000..3edcde7946
--- /dev/null
+++ b/src/ifcgeom/mapping/IfcSectionedSurface.cpp
@@ -0,0 +1,93 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include "mapping.h"
+#define mapping POSTFIX_SCHEMA(mapping)
+using namespace ifcopenshell::geometry;
+
+#include "../../ifcgeom/profile_helper.h"
+#include "../../ifcgeom/infra_sweep_helper.h"
+
+#ifdef SCHEMA_HAS_IfcSectionedSurface
+
+
+taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSectionedSurface* inst) {
+ std::vector cross_sections;
+
+ auto dir = map(inst->Directrix());
+ auto pwf = taxonomy::dcast(dir);
+ if (!pwf) {
+ // Only implement on alignment curves
+ Logger::Warning("IfcSectionedSurface is only implemented for piecewise function Directrix curves", inst);
+ return nullptr;
+ }
+
+ {
+ auto css = inst->CrossSections();
+ auto csps = inst->CrossSectionPositions();
+ std::vector faces;
+
+ // The PointByDistanceExpressesions are factored out into (a) a cartesian offset relative to the
+ // reference frame along a certain curve location (b) the longitude.
+
+ // The longitudes determine the range of the sweep and the offsets are interpolated in between
+ // sweep segments.
+ std::vector profile_offsets;
+ std::vector longitudes;
+
+ for (auto& cs : *css) {
+ faces.push_back(std::move(taxonomy::cast(map(cs))));
+ }
+#ifdef SCHEMA_HAS_IfcPointByDistanceExpression
+ for (auto& csp : *csps) {
+ auto pbde = csp->Location()->as(true);
+
+ longitudes.push_back(*pbde->DistanceAlong()->as(true) * length_unit_);
+
+ // Corresponds to the profile X, Y directions (hopefully).
+ Eigen::Vector3d po(
+ pbde->OffsetLateral().get_value_or(0.),
+ // @todo I don't understand whether vertical is an offset relative to the tangent plane or to the global XY plane
+ pbde->OffsetVertical().get_value_or(0.),
+ 0.
+ );
+
+ profile_offsets.push_back(po);
+ }
+#else
+ return nullptr;
+#endif
+ if (faces.size() != profile_offsets.size()) {
+ Logger::Warning("Expected CrossSections and CrossSectionPositions to be equal length, but got " + std::to_string(faces.size()) + " and " + std::to_string(profile_offsets.size()) + " respectively", inst);
+ return nullptr;
+ }
+ if (faces.size() < 2) {
+ Logger::Warning("Expected at least two cross sections, but got " + std::to_string(faces.size()), inst);
+ return nullptr;
+ }
+
+ for (size_t i = 0; i < faces.size(); ++i) {
+ cross_sections.push_back({ longitudes[i], faces[i], profile_offsets[i] });
+ }
+ }
+
+ return make_loft(settings_, inst, pwf, cross_sections);
+}
+
+#endif
diff --git a/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp b/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp
index 2ab56482c1..d1ab88fbe5 100644
--- a/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp
+++ b/src/ifcgeom/mapping/IfcSegmentedReferenceCurve.cpp
@@ -21,6 +21,8 @@
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
+#include "../piecewise_function_evaluator.h"
+
#ifdef SCHEMA_HAS_IfcSegmentedReferenceCurve
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* inst) {
@@ -53,7 +55,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins
const Eigen::Matrix4d& m = p->ccomponents();
double cant_start = m(0, 3); // start of cant curve
- auto cant = taxonomy::make(cant_start,pwfs,&settings_);
+ auto cant = taxonomy::make(cant_start,pwfs);
// Determine the valid domain of the PWF... the valid domain is where
// horizontal, gradient and cant curves are defined
@@ -68,11 +70,12 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins
}
// define the callback function for the segmented reference curve
- auto composition = [gradient, cant](double u)->Eigen::Matrix4d {
+ piecewise_function_evaluator gradient_evaluator(gradient, &settings_), cant_evaluator(cant, &settings_);
+ auto composition = [gradient_evaluator, cant_evaluator, start = cant->start()](double u) -> Eigen::Matrix4d {
// u is distance from start of cant curve
// add cant->start() to u to get the distance from start of gradient curve
- auto g = gradient->evaluate(u+cant->start());
- auto c = cant->evaluate(u);
+ auto g = gradient_evaluator.evaluate(u + start);
+ auto c = cant_evaluator.evaluate(u);
// Need to multiply g and c so the axis vectors
// from cant have the correct rotation applied so
@@ -105,7 +108,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcSegmentedReferenceCurve* ins
taxonomy::piecewise_function::spans_t spans;
spans.emplace_back(length, composition);
- auto pwf = taxonomy::make(start, spans, &settings_, inst);
+ auto pwf = taxonomy::make(start, spans, inst);
return pwf;
}
diff --git a/src/ifcgeom/mapping/mapping.i b/src/ifcgeom/mapping/mapping.i
index c6ed7a11f9..51e001f05d 100644
--- a/src/ifcgeom/mapping/mapping.i
+++ b/src/ifcgeom/mapping/mapping.i
@@ -135,6 +135,9 @@ BIND(IfcFixedReferenceSweptAreaSolid)
#ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal
BIND(IfcSectionedSolidHorizontal)
#endif
+#ifdef SCHEMA_HAS_IfcSectionedSurface
+BIND(IfcSectionedSurface)
+#endif
BIND(IfcCircle);
BIND(IfcEllipse);
diff --git a/src/ifcgeom/piecewise_function_evaluator.cpp b/src/ifcgeom/piecewise_function_evaluator.cpp
new file mode 100644
index 0000000000..b85b165f21
--- /dev/null
+++ b/src/ifcgeom/piecewise_function_evaluator.cpp
@@ -0,0 +1,103 @@
+#include "piecewise_function_evaluator.h"
+#include "profile_helper.h"
+
+using namespace ifcopenshell::geometry;
+
+
+piecewise_function_evaluator::piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, const ifcopenshell::geometry::Settings* settings) : pwf_(pwf) {
+ if (settings) {
+ settings_ = *settings;
+ }
+}
+
+std::vector piecewise_function_evaluator::evaluation_points() const {
+ if (!eval_points_.has_value()) {
+ double curve_length = pwf_->length();
+
+ auto param_type = settings_.get().get();
+ auto param = settings_.get().get();
+ unsigned num_steps = 0;
+ if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) {
+ // parameter is max step size
+ num_steps = (unsigned)std::ceil(curve_length / param);
+ } else {
+ // parameter is minimum number of steps
+ num_steps = (unsigned)std::ceil(param);
+ }
+
+ eval_points_ = evaluation_points(pwf_->start(), pwf_->start() + curve_length, num_steps);
+ }
+ return *eval_points_;
+}
+
+std::vector piecewise_function_evaluator::evaluation_points(double ustart, double uend, unsigned nsteps) const {
+ double curve_length = pwf_->length();
+ ustart = std::max(pwf_->start(), ustart);
+ uend = std::min(uend, pwf_->start() + curve_length);
+
+ nsteps = std::max(1u, nsteps); // never have fewer than 1 step
+
+ auto resolution = (uend - ustart) / nsteps;
+
+ std::vector u_values;
+ u_values.reserve(nsteps);
+
+ for (unsigned i = 0; i <= nsteps; ++i) {
+ auto u = resolution * i + ustart;
+ u_values.push_back(u);
+ }
+
+ return u_values;
+}
+
+taxonomy::item::ptr piecewise_function_evaluator::evaluate() const {
+ return evaluate(evaluation_points());
+}
+
+taxonomy::item::ptr piecewise_function_evaluator::evaluate(double ustart, double uend, unsigned nsteps) const {
+ return evaluate(evaluation_points(ustart, uend, nsteps));
+}
+
+Eigen::Matrix4d piecewise_function_evaluator::evaluate(double u) const {
+ // assume monotonic evaluation and store last evaluated segment
+ if (current_span_fn_ == nullptr || (u < current_span_start_ || current_span_end_ < u)) {
+ // there isn't a current span or u is outside the range of the current span
+ // get a new "current span"
+ std::tie(current_span_start_, current_span_end_, current_span_fn_) = get_span(u);
+ }
+
+ u -= current_span_start_; // make u relative to start of span
+ return (*current_span_fn_)(u);
+}
+
+taxonomy::item::ptr piecewise_function_evaluator::evaluate(const std::vector& dist) const {
+ std::vector polygon;
+ polygon.reserve(dist.size());
+ for (auto& u : dist) {
+ Eigen::Matrix4d m = evaluate(u);
+ polygon.push_back(taxonomy::make(m(0, 3), m(1, 3), m(2, 3)));
+ }
+
+ return polygon_from_points(polygon);
+}
+
+std::tuple*> piecewise_function_evaluator::get_span(double u) const {
+ // force u to be within bounds of the curve
+ double s = pwf_->start();
+ double e = pwf_->end();
+ u = std::max(s, u);
+ u = std::min(u, e);
+
+ double span_start = s;
+ for (auto& [length, fn] : pwf_->spans()) {
+ double span_end = span_start + length;
+ auto tolerance = settings_.get().get();
+ if (span_start <= u && u < span_end + tolerance) {
+ return {span_start, span_end, &fn};
+ }
+ span_start += length;
+ }
+
+ Logger::Error("piecewise_function_impl::get_span span not found.");
+ return {0, 0, nullptr};
+}
diff --git a/src/ifcgeom/piecewise_function_evaluator.h b/src/ifcgeom/piecewise_function_evaluator.h
new file mode 100644
index 0000000000..3d91547268
--- /dev/null
+++ b/src/ifcgeom/piecewise_function_evaluator.h
@@ -0,0 +1,58 @@
+#ifndef ITERATOR_PWF_EVALUATOR_H
+#define ITERATOR_PWF_EVALUATOR_H
+
+#include "../ifcgeom/taxonomy.h"
+
+#include
+
+namespace ifcopenshell { namespace geometry {
+
+/// @brief utility class to evaluate piecewise_function objects
+class piecewise_function_evaluator {
+ public:
+ piecewise_function_evaluator(taxonomy::piecewise_function::const_ptr pwf, const ifcopenshell::geometry::Settings* settings=nullptr);
+
+ /// @brief returns a vector of "distance along" points where the evaluate function computes loop points
+ std::vector evaluation_points() const;
+
+ /// @brief returns a vector of "distance along" points between ustart and uend
+ /// @param ustart starting location
+ /// @param uend ending location
+ /// @param nsteps number of steps to evaluate
+ std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const;
+
+ /// @brief evaluates the piecewise function between start and end
+ /// evaluation point step size is taken from the settings object
+ taxonomy::item::ptr evaluate() const;
+
+ /// @brief evaluates the piecewise function between ustart and uend
+ /// if ustart and uend are out of range, the range of values evaluated
+ /// are constrained to start_ and start_+length_
+ /// @param ustart starting location
+ /// @param uend ending location
+ /// @param nsteps number of steps to evaluate
+ /// @return taxonomy::loop::ptr
+ taxonomy::item::ptr evaluate(double ustart, double uend, unsigned nsteps) const;
+
+ /// @brief evaluates the piecewise function at u
+ /// @param u u is constrained to be between start_ and start_+length
+ /// @return 4x4 placement matrix
+ Eigen::Matrix4d evaluate(double u) const;
+
+ private:
+ taxonomy::item::ptr evaluate(const std::vector& dist) const;
+ std::tuple*> get_span(double u) const;
+
+ taxonomy::piecewise_function::const_ptr pwf_;
+
+ ifcopenshell::geometry::Settings settings_;
+
+ mutable double current_span_start_ = 0;
+ mutable double current_span_end_ = 0;
+ mutable const std::function* current_span_fn_ = nullptr;
+ mutable boost::optional> eval_points_;
+};
+
+}}
+
+#endif
\ No newline at end of file
diff --git a/src/ifcgeom/piecewise_function_impl.cpp b/src/ifcgeom/piecewise_function_impl.cpp
index ecd2762057..6fa29fb649 100644
--- a/src/ifcgeom/piecewise_function_impl.cpp
+++ b/src/ifcgeom/piecewise_function_impl.cpp
@@ -7,97 +7,40 @@ namespace geometry {
namespace taxonomy {
-std::vector ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points() const {
- if (!eval_points_.has_value()) {
- double curve_length = length();
+piecewise_function_impl::piecewise_function_impl(double start, const spans_t& s) : start_(start), spans_(s) {
+}
- auto param_type = settings_ ? settings_->get().get() : ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE;
- auto param = settings_ ? settings_->get().get() : 0.5;
- unsigned num_steps = 0;
- if (param_type == ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE) {
- // parameter is max step size
- num_steps = (unsigned)std::ceil(curve_length / param);
- } else {
- // parameter is minimum number of steps
- num_steps = (unsigned)std::ceil(param);
- }
-
- eval_points_ = evaluation_points(start_, start_ + curve_length, num_steps);
+piecewise_function_impl::piecewise_function_impl(double start, const std::vector& pwfs) : start_(start) {
+ for (auto& pwf : pwfs) {
+ spans_.insert(spans_.end(), pwf->spans().begin(), pwf->spans().end());
}
- return *eval_points_;
}
-std::vector ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluation_points(double ustart, double uend, unsigned nsteps) const {
- double curve_length = length();
- ustart = std::max(start_, ustart);
- uend = std::min(uend, start_ + curve_length);
+const piecewise_function_impl::spans_t& piecewise_function_impl::spans() const { return spans_; }
- nsteps = std::max(1u, nsteps); // never have fewer than 1 step
+bool piecewise_function_impl::is_empty() const { return spans_.empty(); }
- auto resolution = (uend - ustart) / nsteps;
-
- std::vector u_values;
- u_values.reserve(nsteps);
-
- for (unsigned i = 0; i <= nsteps; ++i) {
- auto u = resolution * i + ustart;
- u_values.push_back(u);
- }
-
- return u_values;
+double piecewise_function_impl::start() const {
+ return start_;
}
-ifcopenshell::geometry::taxonomy::item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate() const {
- return evaluate(evaluation_points());
+double piecewise_function_impl::end() const {
+ return start_ + length();
}
-item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(double ustart, double uend, unsigned nsteps) const {
- return evaluate(evaluation_points(ustart, uend, nsteps));
+double piecewise_function_impl::length() const {
+ return std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; });
+
+ // this is a secondary option where we only compute length once and cache it.
+ // mutex is needed to prevent interruption of the accumulation if there is multi-threading
+ // skipping this detail for now and just adding up the span lengths every time
+ //if (!length_.has_value()) {
+ // length_ = std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; });
+ //}
+ //return *length_;
}
-item::ptr ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(const std::vector& dist) const {
- std::vector polygon;
- polygon.reserve(dist.size());
- for (auto& u : dist) {
- Eigen::Matrix4d m = evaluate(u);
- polygon.push_back(taxonomy::make(m.col(3)(0), m.col(3)(1), m.col(3)(2)));
- }
-
- return polygon_from_points(polygon);
-}
-
-Eigen::Matrix4d ifcopenshell::geometry::taxonomy::piecewise_function_impl::evaluate(double u) const {
- // assume monotonic evaluation and store last evaluated segment
- if (current_span_fn_ == nullptr || (u < current_span_start_ || current_span_end_ < u)) {
- // there isn't a current span or u is outside the range of the current span
- // get a new "current span"
- std::tie(current_span_start_, current_span_end_, current_span_fn_) = get_span(u);
- }
-
- u -= current_span_start_; // make u relative to start of span
- return (*current_span_fn_)(u);
-}
-
-std::tuple*> ifcopenshell::geometry::taxonomy::piecewise_function_impl::get_span(double u) const {
- // force u to be within bounds of the curve
- double s = start();
- double e = end();
- u = std::max(s, u);
- u = std::min(u, e);
-
- double span_start = s;
- for (auto& [length, fn] : spans_) {
- double span_end = span_start + length;
- auto tolerance = settings_ ? settings_->get().get() : 0.001;
- if (span_start <= u && u < span_end + tolerance) {
- return {span_start, span_end, &fn};
- }
- span_start += length;
- }
-
- Logger::Error("piecewise_function_impl::get_span span not found.");
- return {0, 0, nullptr};
-}
+piecewise_function_impl* piecewise_function_impl::clone_() const { return new piecewise_function_impl(*this); }
} // namespace taxonomy
diff --git a/src/ifcgeom/piecewise_function_impl.h b/src/ifcgeom/piecewise_function_impl.h
index 0c2f306ac5..daf8636feb 100644
--- a/src/ifcgeom/piecewise_function_impl.h
+++ b/src/ifcgeom/piecewise_function_impl.h
@@ -12,79 +12,23 @@ namespace taxonomy {
struct piecewise_function_impl {
using spans_t = std::vector>>;
- piecewise_function_impl(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings = nullptr) : start_(start),
- settings_(settings),
- spans_(s){};
- piecewise_function_impl(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings = nullptr) : start_(start),
- settings_(settings) {
- for (auto& pwf : pwfs) {
- spans_.insert(spans_.end(), pwf->spans().begin(), pwf->spans().end());
- }
- };
+ piecewise_function_impl(double start, const spans_t& s);
+ piecewise_function_impl(double start, const std::vector& pwfs);
piecewise_function_impl(piecewise_function_impl&&) = default;
piecewise_function_impl(const piecewise_function_impl&) = default;
- const ifcopenshell::geometry::Settings* settings_ = nullptr;
-
- const spans_t& spans() const { return spans_; }
-
- bool is_empty() const { return spans_.empty(); }
-
- double start() const {
- return start_;
- }
-
- double end() const {
- return start_ + length();
- }
-
- double length() const {
- if (!length_.has_value()) {
- length_ = std::accumulate(spans_.begin(), spans_.end(), 0.0, [](const auto& v, const auto& s) { return v + s.first; });
- }
- return *length_;
- }
-
- piecewise_function_impl* clone_() const { return new piecewise_function_impl(*this); }
-
- /// @brief returns a vector of "distance along" points where the evaluate function computes loop points
- std::vector evaluation_points() const;
-
- /// @brief returns a vector of "distance along" points between ustart and uend
- /// @param ustart starting location
- /// @param uend ending location
- /// @param nsteps number of steps to evaluate
- std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const;
-
- /// @brief evaluates the piecewise function between start and end
- /// evaluation point step size is taken from the settings object
- item::ptr evaluate() const;
-
- /// @brief evaluates the piecewise function between ustart and uend
- /// if ustart and uend are out of range, the range of values evaluated
- /// are constrained to start_ and start_+length_
- /// @param ustart starting location
- /// @param uend ending location
- /// @param nsteps number of steps to evaluate
- /// @return taxonomy::loop::ptr
- item::ptr evaluate(double ustart, double uend, unsigned nsteps) const;
-
- /// @brief evaluates the piecewise function at u
- /// @param u u is constrained to be between start_ and start_+length
- /// @return 4x4 placement matrix
- Eigen::Matrix4d evaluate(double u) const;
+ const spans_t& spans() const;
+ bool is_empty() const;
+ double start() const;
+ double end() const;
+ double length() const;
+ piecewise_function_impl* clone_() const;
private:
- item::ptr evaluate(const std::vector& dist) const;
- std::tuple*> get_span(double u) const;
double start_ = 0.0; // starting value of the pwf
spans_t spans_;
- mutable double current_span_start_ = 0;
- mutable double current_span_end_ = 0;
- mutable const std::function* current_span_fn_ = nullptr;
- mutable boost::optional length_;
- mutable boost::optional> eval_points_;
+ //mutable boost::optional length_; // used for length() method
};
} // namespace taxonomy
diff --git a/src/ifcgeom/taxonomy.cpp b/src/ifcgeom/taxonomy.cpp
index 818802dd2d..6f5e6249c7 100644
--- a/src/ifcgeom/taxonomy.cpp
+++ b/src/ifcgeom/taxonomy.cpp
@@ -313,7 +313,7 @@ namespace {
}
bool compare(const loft& a, const loft& b) {
- return compare_collection(a, b);
+ return compare_collection(a, b);
}
bool compare(const collection& a, const collection& b) {
@@ -464,12 +464,12 @@ ifcopenshell::geometry::taxonomy::solid::ptr ifcopenshell::geometry::create_box(
}
///////////////////
-piecewise_function::piecewise_function(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) {
- impl_ = new piecewise_function_impl(start, s, settings);
+piecewise_function::piecewise_function(double start, const spans_t& s, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) {
+ impl_ = new piecewise_function_impl(start, s);
}
-piecewise_function::piecewise_function(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) {
- impl_ = new piecewise_function_impl(start, pwfs, settings);
+piecewise_function::piecewise_function(double start, const std::vector& pwfs, const IfcUtil::IfcBaseInterface* instance) : implicit_item(instance) {
+ impl_ = new piecewise_function_impl(start, pwfs);
};
piecewise_function::piecewise_function(const piecewise_function& other) : implicit_item(other) {
@@ -486,11 +486,6 @@ double piecewise_function::start() const { return impl_->start(); }
double piecewise_function::end() const { return impl_->end(); }
double piecewise_function::length() const { return impl_->length(); }
-std::vector piecewise_function::evaluation_points() const { return impl_->evaluation_points(); }
-std::vector piecewise_function::evaluation_points(double ustart, double uend, unsigned nsteps) const { return impl_->evaluation_points(ustart, uend, nsteps); }
-item::ptr piecewise_function::evaluate() const { return impl_->evaluate(); }
-item::ptr piecewise_function::evaluate(double ustart, double uend, unsigned nsteps) const { return impl_->evaluate(ustart, uend, nsteps); }
-Eigen::Matrix4d piecewise_function::evaluate(double u) const { return impl_->evaluate(u); }
ifcopenshell::geometry::taxonomy::collection::ptr ifcopenshell::geometry::flatten(const taxonomy::collection::ptr& deep) {
auto flat = make();
diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h
index 07385be746..13164daebf 100644
--- a/src/ifcgeom/taxonomy.h
+++ b/src/ifcgeom/taxonomy.h
@@ -347,8 +347,6 @@ typedef item const* ptr;
struct implicit_item : public geom_item {
DECLARE_PTR(implicit_item)
using geom_item::geom_item;
-
- virtual item::ptr evaluate() const = 0;
};
struct piecewise_function_impl; // forward declaration
@@ -357,14 +355,12 @@ typedef item const* ptr;
using spans_t = std::vector>>;
- piecewise_function(double start, const spans_t& s, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr);
- piecewise_function(double start, const std::vector& pwfs, ifcopenshell::geometry::Settings* settings = nullptr, const IfcUtil::IfcBaseInterface* instance = nullptr);
+ piecewise_function(double start, const spans_t& s, const IfcUtil::IfcBaseInterface* instance = nullptr);
+ piecewise_function(double start, const std::vector& pwfs, const IfcUtil::IfcBaseInterface* instance = nullptr);
piecewise_function(piecewise_function&&) = default;
piecewise_function(const piecewise_function&);
virtual ~piecewise_function();
- const ifcopenshell::geometry::Settings* settings_ = nullptr;
-
const spans_t& spans() const;
bool is_empty() const;
double start() const;
@@ -379,33 +375,6 @@ typedef item const* ptr;
return boost::hash{}(v);
}
- /// @brief returns a vector of "distance along" points where the evaluate function computes loop points
- std::vector evaluation_points() const;
-
- /// @brief returns a vector of "distance along" points between ustart and uend
- /// @param ustart starting location
- /// @param uend ending location
- /// @param nsteps number of steps to evaluate
- std::vector evaluation_points(double ustart, double uend, unsigned nsteps) const;
-
- /// @brief evaluates the piecewise function between start and end
- /// evaluation point step size is taken from the settings object
- item::ptr evaluate() const override;
-
- /// @brief evaluates the piecewise function between ustart and uend
- /// if ustart and uend are out of range, the range of values evaluated
- /// are constrained to start_ and start_+length_
- /// @param ustart starting location
- /// @param uend ending location
- /// @param nsteps number of steps to evaluate
- /// @return taxonomy::loop::ptr
- item::ptr evaluate(double ustart, double uend, unsigned nsteps) const;
-
- /// @brief evaluates the piecewise function at u
- /// @param u u is constrained to be between start_ and start_+length
- /// @return 4x4 placement matrix
- Eigen::Matrix4d evaluate(double u) const;
-
private:
// note: it would be better if this were a std::unique_ptr, but that requires having the full definition
// of piecewise_function_impl in this header file, which defeats the purpose of the PIMPL idiom.
@@ -841,7 +810,7 @@ typedef item const* ptr;
}
};
- struct loft : public collection_base {
+ struct loft : public collection_base {
DECLARE_PTR(loft)
item::ptr axis;
diff --git a/src/ifcopenshell-python/Makefile b/src/ifcopenshell-python/Makefile
index 7cfadb02dd..6239a44476 100644
--- a/src/ifcopenshell-python/Makefile
+++ b/src/ifcopenshell-python/Makefile
@@ -52,8 +52,8 @@ ifeq ($(PLATFORM), win64)
PLATFORMTAG:=win_amd64
endif
-IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v0.7.11-03935a9-$(PLATFORM).zip
-IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v0.7.11-03935a9-$(PLATFORM).zip
+IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(VERSION)-f5e02d1-$(PLATFORM).zip
+IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(VERSION)-f5e02d1-$(PLATFORM).zip
.PHONY: test
test:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py
index a1c0b9a706..542347fa11 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/assign_cost_item_quantity.py
@@ -104,12 +104,14 @@ class Usecase:
if self.settings["prop_name"]:
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
for product in self.settings["products"]:
+ if not product.is_a("IfcElement"):
+ continue
self.assign_cost_control(related_object=product, cost_item=self.settings["cost_item"])
if self.settings["prop_name"]:
if (
self.settings["cost_item"].CostQuantities
and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower()
- ) or not product.is_a("IfcObject"):
+ ):
continue
self.add_quantity_from_related_object(product)
if self.settings["prop_name"]:
diff --git a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py
index c58bd7e657..b160803d0a 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/cost/calculate_cost_item_resource_value.py
@@ -102,9 +102,10 @@ def calculate_cost_item_resource_value(file: ifcopenshell.file, cost_item: ifcop
for resource in resources:
cost, unit = ifcopenshell.util.resource.get_cost(resource)
if not cost:
- cost, unit = ifcopenshell.util.resource.get_parent_cost(
- resource
- ) # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data.
+ # Concept to standardise - Not defined in schema, but this makes manual scheduling of resources 10x faster and less duplicate data.
+ parent_cost = ifcopenshell.util.resource.get_parent_cost(resource)
+ assert parent_cost
+ cost, unit = parent_cost
quantity = ifcopenshell.util.resource.get_quantity(resource)
if not cost or not quantity:
continue
diff --git a/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py b/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py
index 61d3c7d6cc..0da06f2beb 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/profile/copy_profile.py
@@ -42,6 +42,6 @@ def copy_profile(file: ifcopenshell.file, profile: ifcopenshell.entity_instance)
inverses = file.get_inverse(profile)
psets = [i for i in inverses if i.is_a("IfcProfileProperties")]
for pset in psets:
- new_pset = ifcopenshell.util.element.copy_deep(file, pset, exclude=["IfcProfileDef"])
+ new_pset = ifcopenshell.util.element.copy(file, pset)
new_pset.ProfileDefinition = new_profile
return new_profile
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py
index 797f0afba3..6de7ca5f44 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset/edit_pset.py
@@ -167,6 +167,9 @@ def edit_pset(
class Usecase:
+ file: ifcopenshell.file
+ settings: dict[str, Any]
+
def execute(self) -> None:
self.update_pset_name()
self.load_pset_template()
@@ -378,7 +381,7 @@ class Usecase:
properties.append(self.file.create_entity("IfcPropertySingleValue", **args))
return properties
- def assign_new_properties(self, props: ifcopenshell.entity_instance) -> None:
+ def assign_new_properties(self, props: list[ifcopenshell.entity_instance]) -> None:
if hasattr(self.settings["pset"], "HasProperties"):
self.settings["pset"].HasProperties = props
diff --git a/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py b/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py
index acf955879a..051905b2b7 100644
--- a/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py
+++ b/src/ifcopenshell-python/ifcopenshell/api/pset/unshare_pset.py
@@ -87,7 +87,8 @@ def unshare_pset(
for product in products:
# No need to consider about profile/material properties since
# they are assigned to 1 element directly and therefore cannot be shared.
- pset_copy = ifcopenshell.util.element.copy_deep(file, pset)
+ # Don't copy_deep to keep it light - edit_pset supports unsharing shared props.
+ pset_copy = ifcopenshell.util.element.copy(file, pset)
pset_copies.append(pset_copy)
ifcopenshell.api.pset.assign_pset(file, [product], pset_copy)
diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py
index 8804586acd..61d20457fd 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/element.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/element.py
@@ -445,7 +445,7 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell
"""Retrieve the elements (or element types) that are using the provided property set."""
is_ifc2x3 = pset.file.schema == "IFC2X3"
elements = set()
- if pset.is_a("IfcPropertySet"):
+ if pset.is_a("IfcPropertySet") or pset.is_a("IfcQuantitySet"):
rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence
for rel in rels:
elements.update(rel.RelatedObjects)
diff --git a/src/ifcopenshell-python/ifcopenshell/util/resource.py b/src/ifcopenshell-python/ifcopenshell/util/resource.py
index adce5bfa3f..8a444e6371 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/resource.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/resource.py
@@ -166,9 +166,9 @@ def get_quantity(resource: ifcopenshell.entity_instance) -> float:
return duration.total_seconds() / 3600
-def get_parent_cost(resource: ifcopenshell.entity_instance) -> Union[None, tuple[float, Union[str, None]]]:
- if not resource.Nests:
+def get_parent_cost(resource: ifcopenshell.entity_instance) -> Union[tuple[float, Union[str, None]], None]:
+ if not (nests := resource.Nests):
return
else:
- cost = get_cost(resource.Nests[0].RelatingObject)
+ cost = get_cost(nests[0].RelatingObject)
return cost
diff --git a/src/ifcopenshell-python/ifcopenshell/util/sequence.py b/src/ifcopenshell-python/ifcopenshell/util/sequence.py
index f476df5792..720bebfb22 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/sequence.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/sequence.py
@@ -36,7 +36,18 @@ RECURRENCE_TYPE = Literal[
]
-def derive_date(task, attribute_name, date=None, is_earliest=False, is_latest=False):
+def derive_date(
+ task: ifcopenshell.entity_instance,
+ attribute_name: str,
+ date=None,
+ is_earliest: bool = False,
+ is_latest: bool = False,
+):
+ """
+
+ :param task: IfcTask.
+
+ """
if task.TaskTime:
current_date = (
ifcopenshell.util.date.ifc2datetime(getattr(task.TaskTime, attribute_name))
@@ -268,7 +279,7 @@ def get_task_work_schedule(task: ifcopenshell.entity_instance) -> Union[ifcopens
def get_nested_tasks(task: ifcopenshell.entity_instance) -> list[ifcopenshell.entity_instance]:
- return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects]
+ return [object for rel in task.IsNestedBy or [] for object in rel.RelatedObjects if object.is_a("IfcTask")]
def get_parent_task(task: ifcopenshell.entity_instance) -> Union[ifcopenshell.entity_instance, None]:
@@ -448,12 +459,9 @@ def get_related_products(
"""Gets the related products being output by a task
:param relating_product: One of the products already output by the task.
- :type relating_product: ifcopenshell.entity_instance, optional
:param related_object: The IfcTask that you want to get all the related
products for.
- :type related_object: ifcopenshell.entity_instance, optional
:return: A set of IfcProducts output by the IfcTask.
- :rtype: set[ifcopenshell.entity_instance]
Example:
@@ -478,17 +486,18 @@ def get_related_products(
products = ifcopenshell.util.sequence.get_related_products(related_object=task)
"""
+ assert relating_product or related_object, "Either relating_product or related_object must be provided."
+
products = set()
- related_object = None
- if related_object:
- related_object = related_object
- elif relating_product:
+ if not related_object and relating_product:
for reference in relating_product.ReferencedBy:
if reference.is_a("IfcRelAssignsToProduct"):
related_object = reference.RelatedObjects[0]
+
if related_object:
assignments = related_object.HasAssignments
for assignment in assignments:
if assignment.is_a("IfcRelAssignsToProduct"):
products.add(assignment.RelatingProduct.id())
+
return products
diff --git a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
index 822f012925..05cae93995 100644
--- a/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
+++ b/src/ifcopenshell-python/ifcopenshell/util/shape_builder.py
@@ -368,6 +368,7 @@ class ShapeBuilder:
"ProfileType": profile_type,
"OuterCurve": outer_curve,
}
+
if self.file.schema == "IFC2X3":
kwargs["Position"] = self.create_axis2_placement_2d()
diff --git a/src/ifcparse/IfcFile.cpp b/src/ifcparse/IfcFile.cpp
index 830754444f..79c3d25f6f 100644
--- a/src/ifcparse/IfcFile.cpp
+++ b/src/ifcparse/IfcFile.cpp
@@ -130,10 +130,48 @@ namespace {
if (aggregate_storage.which() == 0) {
aggregate_storage = std::vector>{ v };
} else {
- auto* vec_ptr = boost::get>>(&aggregate_storage);
- if (vec_ptr) {
+ if (auto* vec_ptr = boost::get>>(&aggregate_storage)) {
vec_ptr->push_back(v);
} else {
+ if constexpr (std::is_same_v, int>) {
+ auto* vec_ptr2 = boost::get>(&aggregate_storage);
+ if (vec_ptr2) {
+ // double[] + int
+ vec_ptr2->push_back((double) v);
+ }
+ }
+ if constexpr (std::is_same_v, double>) {
+ auto* vec_ptr2 = boost::get>(&aggregate_storage);
+ if (vec_ptr2) {
+ // int[] -> double[] + double
+ std::vector ps(vec_ptr2->begin(), vec_ptr2->end());
+ ps.push_back(v);
+ aggregate_storage = ps;
+ }
+ }
+
+ if constexpr (std::is_same_v, std::vector>) {
+ auto* vec_ptr2 = boost::get>>(&aggregate_storage);
+ if (vec_ptr2) {
+ // double[][] + int[]
+ std::vector vd(v.begin(), v.end());
+ vec_ptr2->push_back(vd);
+ }
+ }
+ if constexpr (std::is_same_v, std::vector>) {
+ auto* vec_ptr2 = boost::get>>(&aggregate_storage);
+ if (vec_ptr2) {
+ // int[][] -> double[][] + double[]
+ std::vector> vvd;
+ for (auto& vv : *vec_ptr2) {
+ std::vector vd(vv.begin(), vv.end());
+ vvd.push_back(vd);
+ }
+ vvd.push_back(v);
+ aggregate_storage = vvd;
+ }
+ }
+
// @todo would be cool if we can trace this back to file offset
auto current = boost::apply_visitor([](auto v) {
if constexpr (!std::is_same_v) {
@@ -145,6 +183,7 @@ namespace {
return std::string{};
}
}, aggregate_storage);
+
Logger::Error("Inconsistent aggregate valuation while attempting to append " + std::string(typeid(decltype(v)).name()) + " to an aggregate of " + current);
// @todo boolean -> logical upgrade
diff --git a/src/ifcparse/IfcParse.cpp b/src/ifcparse/IfcParse.cpp
index 0ea64c2f78..a922775977 100644
--- a/src/ifcparse/IfcParse.cpp
+++ b/src/ifcparse/IfcParse.cpp
@@ -709,7 +709,7 @@ void IfcParse::IfcFile::load(unsigned entity_instance_name, const IfcParse::enti
load(entity_instance_name, entity, context.push(), attribute_index == -1 ? (int) attribute_index_within_data : attribute_index);
} else {
return_value++;
- if (TokenFunc::isIdentifier(next)) {
+ if (TokenFunc::isIdentifier(next) && entity) {
register_inverse(entity_instance_name, entity, next, attribute_index == -1 ? attribute_index_within_data : attribute_index);
}
diff --git a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py
index 0769e2027e..06929cea59 100644
--- a/src/ifcpatch/ifcpatch/recipes/TessellateElements.py
+++ b/src/ifcpatch/ifcpatch/recipes/TessellateElements.py
@@ -92,7 +92,7 @@ class Patcher:
) -> None:
geometry = getattr(shape, "geometry", shape)
v = [[x.tolist() for x in ifcopenshell.util.shape.get_vertices(geometry)]]
- f = [ifcopenshell.util.shape.get_faces(geometry)]
+ f = [ifcopenshell.util.shape.get_faces(geometry).tolist()]
replacements[element] = (v, f)
iterator = ifcopenshell.geom.iterator(settings, self.file, multiprocessing.cpu_count(), include=products)
@@ -112,6 +112,8 @@ class Patcher:
# Do the replacements outside the iterator to prevent messing up iterator state.
for element, geometry in replacements.items():
v, f = geometry
+ if not v or not f:
+ continue
mesh = ifcopenshell.api.run(
"geometry.add_mesh_representation",
self.file,
diff --git a/src/ifcpatch/test/test_TesselateElements.py b/src/ifcpatch/test/test_TesselateElements.py
new file mode 100644
index 0000000000..83ea349137
--- /dev/null
+++ b/src/ifcpatch/test/test_TesselateElements.py
@@ -0,0 +1,84 @@
+# IfcOpenShell - IFC toolkit and geometry engine
+# Copyright (C) 2022 Dion Moult
+#
+# This file is part of IfcOpenShell.
+#
+# IfcOpenShell is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# IfcOpenShell is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with IfcOpenShell. If not, see .
+
+import ifcopenshell.util.representation
+import ifcpatch
+import ifcopenshell
+import ifcopenshell.api.context
+import ifcopenshell.api.geometry
+import ifcopenshell.api.profile
+import ifcopenshell.api.root
+import ifcopenshell.api.unit
+import test.bootstrap
+import ifcopenshell.geom
+import ifcopenshell.util.element
+from ifcopenshell.util.shape_builder import ShapeBuilder
+
+
+class TestTesselateElements(test.bootstrap.IFC4):
+ def test_run(self):
+ is_ifc2x3 = self.file.schema == "IFC2X3"
+ ifcopenshell.api.root.create_entity(self.file, "IfcProject")
+ wall = ifcopenshell.api.root.create_entity(self.file, "IfcWall")
+ model = ifcopenshell.api.context.add_context(self.file, "Model")
+ body = ifcopenshell.api.context.add_context(
+ self.file,
+ context_type="Model",
+ context_identifier="Body",
+ target_view="MODEL_VIEW",
+ parent=model,
+ )
+
+ # Add a length unit just for tests.
+ unit = ifcopenshell.api.unit.add_si_unit(self.file, unit_type="LENGTHUNIT")
+ ifcopenshell.api.unit.assign_unit(self.file, units=[unit])
+
+ builder = ShapeBuilder(self.file)
+ rect = builder.polyline(builder.get_rectangle_coords(), closed=True)
+ extrusion = builder.extrude(rect, 1.0)
+ rep = builder.get_representation(body, extrusion)
+ ifcopenshell.api.geometry.assign_representation(self.file, wall, rep)
+ original_rep_id = rep.id()
+
+ ifcpatch.execute({"file": self.file, "recipe": "TessellateElements", "arguments": ["IfcWall"]})
+
+ # Original representation still exists.
+ assert self.file.by_id(original_rep_id)
+
+ other_reps = [r for r in ifcopenshell.util.representation.get_representations_iter(wall) if r != rep]
+ assert len(other_reps) == 1
+ new_rep = other_reps[0]
+ new_rep.ContextOfItems = body
+ new_rep.RepresentationIdentifier = "Body"
+ new_rep.RepresentationType = "Tesselation"
+ assert len(items := new_rep.Items) == 1
+ item = items[0]
+ if is_ifc2x3:
+ assert item.is_a("IfcFacetedBrep")
+ assert len(faces := item.Outer.CfsFaces) == 12
+ for face in faces:
+ assert len(face.Bounds[0].Bound.Polygon) == 3
+ else:
+ assert item.is_a("IfcPolygonalFaceSet")
+ assert len(faces := item.Faces) == 12
+ for face in faces:
+ assert len(face.CoordIndex) == 3
+
+
+class TestTesselateElementsIFC2X3(test.bootstrap.IFC2X3, TestTesselateElements):
+ pass
diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i
index 0f26e28db7..ca0851324f 100644
--- a/src/ifcwrap/IfcGeomWrapper.i
+++ b/src/ifcwrap/IfcGeomWrapper.i
@@ -246,6 +246,7 @@ namespace {
%include "../ifcgeom/Iterator.h"
%include "../ifcgeom/GeometrySerializer.h"
%include "../ifcgeom/taxonomy.h"
+%include "../ifcgeom/piecewise_function_evaluator.h"
%include "../serializers/SvgSerializer.h"
%include "../serializers/HdfSerializer.h"
@@ -315,7 +316,7 @@ assign_children_access(loop, edge);
assign_children_access(face, loop);
assign_children_access(shell, face);
assign_children_access(solid, shell);
-assign_children_access(loft, face);
+assign_children_access(loft, geom_item);
assign_children_access(boolean_result, geom_item);
%define assign_matrix_access(item_name)
diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i
index b3b6cc3a85..f70388663d 100644
--- a/src/ifcwrap/IfcPython.i
+++ b/src/ifcwrap/IfcPython.i
@@ -84,6 +84,7 @@
%{
#include "../ifcgeom/Iterator.h"
#include "../ifcgeom/taxonomy.h"
+ #include "../ifcgeom/piecewise_function_evaluator.h"
#ifdef IFOPSH_WITH_OPENCASCADE
#include "../ifcgeom/Serialization/Serialization.h"
#include "../ifcgeom/kernels/opencascade/IfcGeomTree.h"
@@ -159,6 +160,7 @@
%module ifcopenshell_wrapper %{
#include "../ifcgeom/Converter.h"
#include "../ifcgeom/taxonomy.h"
+ #include "../ifcgeom/piecewise_function_evaluator.h"
#ifdef IFOPSH_WITH_OPENCASCADE
#include "../ifcgeom/Serialization/Serialization.h"
#include "../ifcgeom/kernels/opencascade/IfcGeomTree.h"