Merge branch 'v0.8.0' into fix-ifccircle-ifcellipse-processing

This commit is contained in:
Thomas Krijnen
2024-09-12 20:18:57 +02:00
committed by GitHub
91 changed files with 3282 additions and 1718 deletions
+4
View File
@@ -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)
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
@@ -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:
@@ -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;
}
@@ -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) {
@@ -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 = `
<button class="action-button" id="selected-products-btn">Selected Products</button>
<button class="action-button" id="assigned-products-btn">Assigned Products</button>
<button class="action-button" id="manual-quantities-btn">Manual Quantities</button>
`;
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;
}
}
@@ -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
@@ -36,6 +36,7 @@ classes = (
operator.ProfileImportIFC,
operator.PurgeHdf5Cache,
operator.PurgeUnusedElementsByClass,
operator.PurgeUnusedObjects,
operator.RestartBlender,
operator.RewindInspector,
operator.SelectExpressFile,
@@ -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"
@@ -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))
@@ -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,
@@ -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
@@ -0,0 +1,138 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2024 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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)
@@ -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
+12 -1
View File
@@ -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)
@@ -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):
+37 -253
View File
@@ -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"]:
@@ -0,0 +1,353 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2024 Bruno Perdigão <contact@brunopo.com>
#
# 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 <http://www.gnu.org/licenses/>.
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"}
+24 -225
View File
@@ -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:
@@ -30,7 +30,6 @@ classes = (
operator.EnableEditingArbitraryProfile,
operator.EnableEditingProfile,
operator.LoadProfiles,
operator.PurgeUnusedProfiles,
operator.RemoveProfileDef,
prop.Profile,
prop.BIMProfileProperties,
@@ -29,6 +29,7 @@ def refresh():
class ProfileData:
data = {}
failed_previews: set[int] = set()
preview_collection = bpy.utils.previews.new()
is_loaded = False
@@ -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()
+12 -2
View File
@@ -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
+12 -9
View File
@@ -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")
+34 -248
View File
@@ -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"}
+4 -2
View File
@@ -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"
+12 -3
View File
@@ -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
+12 -1
View File
@@ -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"}
@@ -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")
+9 -1
View File
@@ -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
@@ -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)
+36 -2
View File
@@ -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)
+10 -8
View File
@@ -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):
+17 -11
View File
@@ -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):
@@ -26,7 +26,6 @@ classes = (
operator.DisableEditingType,
operator.DuplicateType,
operator.EnableEditingType,
operator.PurgeUnusedTypes,
operator.RemoveType,
operator.RenameType,
operator.SelectSimilarType,
+3 -12
View File
@@ -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.")
+1 -1
View File
@@ -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.
"""
+5 -2
View File
@@ -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(
+14
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -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)):
+34 -1
View File
@@ -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
+12
View File
@@ -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
+2
View File
@@ -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
+406
View File
@@ -0,0 +1,406 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2024 Bruno Perdigão <contact@brunopo.com>
#
# 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 <http://www.gnu.org/licenses/>.
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"
+113
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
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()}.")
+2 -3
View File
@@ -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
+7
View File
@@ -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
+78 -213
View File
@@ -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"
+1 -2
View File
@@ -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"):
+104 -73
View File
@@ -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",
)
+15 -3
View File
@@ -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)
+3 -1
View File
@@ -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);
}
+4
View File
@@ -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
+203
View File
@@ -0,0 +1,203 @@
#include "profile_helper.h"
#include "infra_sweep_helper.h"
#include "piecewise_function_evaluator.h"
#include <boost/range/combine.hpp>
using namespace ifcopenshell::geometry;
namespace {
// std::lerp when upgrading to C++ 20
template <typename T>
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_section>& cross_sections)
{
std::sort(cross_sections.begin(), cross_sections.end());
auto loft = taxonomy::make<taxonomy::loft>();
// @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<ifcopenshell::geometry::settings::PiecewiseStepType>().get();
auto param = settings_.get<ifcopenshell::geometry::settings::PiecewiseStepParam>().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<double> longitudes;
for (auto& x : cross_sections) {
longitudes.push_back(x.dist_along);
}
longitudes.push_back(std::numeric_limits<double>::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<taxonomy::loop::ptr> loops_a, loops_b;
if (profile_a->kind() == taxonomy::FACE) {
interpolated = taxonomy::make<taxonomy::face>();
auto profile_a_f = std::static_pointer_cast<taxonomy::face>(profile_a);
auto profile_b_f = std::static_pointer_cast<taxonomy::face>(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<taxonomy::loop>(profile_a) };
loops_b = { std::static_pointer_cast<taxonomy::loop>(profile_b) };
interpolated = taxonomy::make<taxonomy::loop>();
}
// @todo should_interpolate should also be informed based by different face matrices.
if (profile_a->matrix || profile_b->matrix) {
interpolated->matrix = taxonomy::make<taxonomy::matrix4>();
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<taxonomy::point3::ptr> points;
for (auto tmp__ : boost::combine(w1->children, w2->children)) {
boost::tie(e1, e2) = tmp__;
auto& p1 = boost::get<taxonomy::point3::ptr>(e1->start);
auto& p2 = boost::get<taxonomy::point3::ptr>(e2->start);
auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval();
points.push_back(taxonomy::make<taxonomy::point3>(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<taxonomy::face>(interpolated)->children.push_back(interpolated_loop);
} else {
std::static_pointer_cast<taxonomy::loop>(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::face>(taxonomy::item::ptr(profile_a->clone_())));
} else {
loft->children.push_back(std::static_pointer_cast<taxonomy::loop>(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<taxonomy::matrix4>();
}
auto m = (m4b * loft->children.back()->matrix->ccomponents()).eval();
loft->children.back()->matrix->components() = m;
}
}
return loft;
}
+26
View File
@@ -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_section>& cross_sections);
}
}
#endif
+29 -12
View File
@@ -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<taxonomy::face::ptr, 2> fa = { *it, *jt };
std::array<taxonomy::item::ptr, 2> fa = { *it, *jt };
std::array<TopoDS_Shape, 2> shps;
std::array<TopoDS_Wire, 2> 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<taxonomy::face>(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<taxonomy::loop>(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]);
+1 -1
View File
@@ -92,7 +92,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCompositeCurve* inst) {
return loop;
}
else {
auto pwf = taxonomy::make<taxonomy::piecewise_function>(0.0,pwfs,&settings_,inst);
auto pwf = taxonomy::make<taxonomy::piecewise_function>(0.0,pwfs,inst);
return pwf;
}
}
+1 -1
View File
@@ -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<taxonomy::piecewise_function>(0.0, spans,&settings_,inst);
auto pwf = taxonomy::make<taxonomy::piecewise_function>(0.0, spans,inst);
return pwf;
}
+4 -1
View File
@@ -39,6 +39,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) {
#endif
if (has_position) {
m4 = taxonomy::cast<taxonomy::matrix4>(map(inst->Position()));
} else {
// matrix needs to be set on elementary curves.
m4 = taxonomy::make<taxonomy::matrix4>();
}
if (ry > rx) {
@@ -58,9 +61,9 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcEllipseProfileDef* inst) {
auto el = taxonomy::make<taxonomy::ellipse>();
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;
}
@@ -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<taxonomy::piecewise_function>(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<ifcopenshell::geometry::settings::PiecewiseStepType>().get();
auto param = settings_.get<ifcopenshell::geometry::settings::PiecewiseStepParam>().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;
+7 -5
View File
@@ -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<taxonomy::piecewise_function>(gradient_start, pwfs, &settings_);
auto vertical = taxonomy::make<taxonomy::piecewise_function>(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<taxonomy::piecewise_function>(start, spans, &settings_, inst);
auto pwf = taxonomy::make<taxonomy::piecewise_function>(start, spans, inst);
return pwf;
}
@@ -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<taxonomy::piecewise_function>(start,offset_spans,&settings_);
auto offsets = taxonomy::make<taxonomy::piecewise_function>(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<taxonomy::piecewise_function>(start,spans,&settings_,inst);
auto pwf = taxonomy::make<taxonomy::piecewise_function>(start,spans,inst);
return pwf;
}
@@ -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<taxonomy::piecewise_function>(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>();
@@ -22,28 +22,10 @@
using namespace ifcopenshell::geometry;
#include "../../ifcgeom/profile_helper.h"
#include <boost/range/combine.hpp>
#include "../../ifcgeom/infra_sweep_helper.h"
#ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal
namespace {
// std::lerp when upgrading to C++ 20
template <typename T>
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_section> 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<taxonomy::loft>();
// @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<ifcopenshell::geometry::settings::PiecewiseStepType>().get();
auto param = settings_.get<ifcopenshell::geometry::settings::PiecewiseStepParam>().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<double> longitudes;
for (auto& x : cross_sections) {
longitudes.push_back(x.dist_along);
}
longitudes.push_back(std::numeric_limits<double>::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<taxonomy::face>();
// @todo should_interpolate should also be informed based by different face matrices.
if (profile_a->matrix || profile_b->matrix) {
interpolated->matrix = taxonomy::make<taxonomy::matrix4>();
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<taxonomy::point3::ptr> points;
for (auto tmp__ : boost::combine(w1->children, w2->children)) {
boost::tie(e1, e2) = tmp__;
auto& p1 = boost::get<taxonomy::point3::ptr>(e1->start);
auto& p2 = boost::get<taxonomy::point3::ptr>(e2->start);
auto p3 = (lerp(p1->ccomponents(), p2->ccomponents(), relative_dist_along) + interpolated_offset).eval();
points.push_back(taxonomy::make<taxonomy::point3>(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<taxonomy::matrix4>();
}
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
@@ -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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#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_section> cross_sections;
auto dir = map(inst->Directrix());
auto pwf = taxonomy::dcast<taxonomy::piecewise_function>(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<taxonomy::geom_item::ptr> 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<Eigen::Vector3d> profile_offsets;
std::vector<double> longitudes;
for (auto& cs : *css) {
faces.push_back(std::move(taxonomy::cast<taxonomy::geom_item>(map(cs))));
}
#ifdef SCHEMA_HAS_IfcPointByDistanceExpression
for (auto& csp : *csps) {
auto pbde = csp->Location()->as<IfcSchema::IfcPointByDistanceExpression>(true);
longitudes.push_back(*pbde->DistanceAlong()->as<IfcSchema::IfcLengthMeasure>(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
@@ -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<taxonomy::piecewise_function>(cant_start,pwfs,&settings_);
auto cant = taxonomy::make<taxonomy::piecewise_function>(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<taxonomy::piecewise_function>(start, spans, &settings_, inst);
auto pwf = taxonomy::make<taxonomy::piecewise_function>(start, spans, inst);
return pwf;
}
+3
View File
@@ -135,6 +135,9 @@ BIND(IfcFixedReferenceSweptAreaSolid)
#ifdef SCHEMA_HAS_IfcSectionedSolidHorizontal
BIND(IfcSectionedSolidHorizontal)
#endif
#ifdef SCHEMA_HAS_IfcSectionedSurface
BIND(IfcSectionedSurface)
#endif
BIND(IfcCircle);
BIND(IfcEllipse);
@@ -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<double> piecewise_function_evaluator::evaluation_points() const {
if (!eval_points_.has_value()) {
double curve_length = pwf_->length();
auto param_type = settings_.get<ifcopenshell::geometry::settings::PiecewiseStepType>().get();
auto param = settings_.get<ifcopenshell::geometry::settings::PiecewiseStepParam>().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<double> 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<double> 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<double>& dist) const {
std::vector<taxonomy::point3::ptr> polygon;
polygon.reserve(dist.size());
for (auto& u : dist) {
Eigen::Matrix4d m = evaluate(u);
polygon.push_back(taxonomy::make<taxonomy::point3>(m(0, 3), m(1, 3), m(2, 3)));
}
return polygon_from_points(polygon);
}
std::tuple<double, double, const std::function<Eigen::Matrix4d(double u)>*> 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<ifcopenshell::geometry::settings::Precision>().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};
}
@@ -0,0 +1,58 @@
#ifndef ITERATOR_PWF_EVALUATOR_H
#define ITERATOR_PWF_EVALUATOR_H
#include "../ifcgeom/taxonomy.h"
#include <boost/function.hpp>
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<double> 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<double> 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<double>& dist) const;
std::tuple<double, double, const std::function<Eigen::Matrix4d(double u)>*> 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<Eigen::Matrix4d(double u)>* current_span_fn_ = nullptr;
mutable boost::optional<std::vector<double>> eval_points_;
};
}}
#endif
+22 -79
View File
@@ -7,97 +7,40 @@ namespace geometry {
namespace taxonomy {
std::vector<double> 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<ifcopenshell::geometry::settings::PiecewiseStepType>().get() : ifcopenshell::geometry::settings::PiecewiseStepMethod::MAXSTEPSIZE;
auto param = settings_ ? settings_->get<ifcopenshell::geometry::settings::PiecewiseStepParam>().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<piecewise_function::ptr>& pwfs) : start_(start) {
for (auto& pwf : pwfs) {
spans_.insert(spans_.end(), pwf->spans().begin(), pwf->spans().end());
}
return *eval_points_;
}
std::vector<double> 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<double> 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<double>& dist) const {
std::vector<taxonomy::point3::ptr> polygon;
polygon.reserve(dist.size());
for (auto& u : dist) {
Eigen::Matrix4d m = evaluate(u);
polygon.push_back(taxonomy::make<taxonomy::point3>(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<double, double, const std::function<Eigen::Matrix4d(double u)>*> 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<ifcopenshell::geometry::settings::Precision>().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
+9 -65
View File
@@ -12,79 +12,23 @@ namespace taxonomy {
struct piecewise_function_impl {
using spans_t = std::vector<std::pair<double, std::function<Eigen::Matrix4d(double u)>>>;
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<piecewise_function::ptr>& 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<piecewise_function::ptr>& 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<double> 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<double> 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<double>& dist) const;
std::tuple<double, double, const std::function<Eigen::Matrix4d(double u)>*> 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<Eigen::Matrix4d(double u)>* current_span_fn_ = nullptr;
mutable boost::optional<double> length_;
mutable boost::optional<std::vector<double>> eval_points_;
//mutable boost::optional<double> length_; // used for length() method
};
} // namespace taxonomy
+5 -10
View File
@@ -313,7 +313,7 @@ namespace {
}
bool compare(const loft& a, const loft& b) {
return compare_collection<face>(a, b);
return compare_collection<geom_item>(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<piecewise_function::ptr>& 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<piecewise_function::ptr>& 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<double> piecewise_function::evaluation_points() const { return impl_->evaluation_points(); }
std::vector<double> 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<taxonomy::collection>();
+3 -34
View File
@@ -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<std::pair<double, std::function<Eigen::Matrix4d(double u)>>>;
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<piecewise_function::ptr>& 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<piecewise_function::ptr>& 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<decltype(v)>{}(v);
}
/// @brief returns a vector of "distance along" points where the evaluate function computes loop points
std::vector<double> 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<double> 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<face> {
struct loft : public collection_base<geom_item> {
DECLARE_PTR(loft)
item::ptr axis;
+2 -2
View File
@@ -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:
@@ -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"]:
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -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
@@ -368,6 +368,7 @@ class ShapeBuilder:
"ProfileType": profile_type,
"OuterCurve": outer_curve,
}
if self.file.schema == "IFC2X3":
kwargs["Position"] = self.create_axis2_placement_2d()
+41 -2
View File
@@ -130,10 +130,48 @@ namespace {
if (aggregate_storage.which() == 0) {
aggregate_storage = std::vector<std::decay_t<decltype(v)>>{ v };
} else {
auto* vec_ptr = boost::get<std::vector<std::decay_t<decltype(v)>>>(&aggregate_storage);
if (vec_ptr) {
if (auto* vec_ptr = boost::get<std::vector<std::decay_t<decltype(v)>>>(&aggregate_storage)) {
vec_ptr->push_back(v);
} else {
if constexpr (std::is_same_v<std::decay_t<decltype(v)>, int>) {
auto* vec_ptr2 = boost::get<std::vector<double>>(&aggregate_storage);
if (vec_ptr2) {
// double[] + int
vec_ptr2->push_back((double) v);
}
}
if constexpr (std::is_same_v<std::decay_t<decltype(v)>, double>) {
auto* vec_ptr2 = boost::get<std::vector<int>>(&aggregate_storage);
if (vec_ptr2) {
// int[] -> double[] + double
std::vector<double> ps(vec_ptr2->begin(), vec_ptr2->end());
ps.push_back(v);
aggregate_storage = ps;
}
}
if constexpr (std::is_same_v<std::decay_t<decltype(v)>, std::vector<int>>) {
auto* vec_ptr2 = boost::get<std::vector<std::vector<double>>>(&aggregate_storage);
if (vec_ptr2) {
// double[][] + int[]
std::vector<double> vd(v.begin(), v.end());
vec_ptr2->push_back(vd);
}
}
if constexpr (std::is_same_v<std::decay_t<decltype(v)>, std::vector<double>>) {
auto* vec_ptr2 = boost::get<std::vector<std::vector<int>>>(&aggregate_storage);
if (vec_ptr2) {
// int[][] -> double[][] + double[]
std::vector<std::vector<double>> vvd;
for (auto& vv : *vec_ptr2) {
std::vector<double> 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<decltype(v), Blank>) {
@@ -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
+1 -1
View File
@@ -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);
}
@@ -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,
@@ -0,0 +1,84 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2022 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
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
+2 -1
View File
@@ -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)
+2
View File
@@ -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"